Add user and page statistics, add sitemap and robots.txt generation

This commit is contained in:
Peaceultime 2024-11-27 17:07:32 +01:00
parent 5fb708051b
commit e99a5f15b4
26 changed files with 520 additions and 81 deletions

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden"> <div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
<NuxtRouteAnnouncer/> <NuxtRouteAnnouncer/>
<NuxtLoadingIndicator />
<TooltipProvider> <TooltipProvider>
<NuxtLayout> <NuxtLayout>
<div class="xl:ps-12 xl:pe-12 ps-6 pe-4 flex flex-1 justify-center overflow-auto max-h-full relative"> <div class="xl:ps-12 xl:pe-12 ps-6 pe-4 flex flex-1 justify-center overflow-auto max-h-full relative">

BIN
bun.lockb

Binary file not shown.

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -12,6 +12,8 @@ export const usersTable = sqliteTable("users", {
export const usersDataTable = sqliteTable("users_data", { export const usersDataTable = sqliteTable("users_data", {
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
logCount: int().notNull().default(0),
}); });
export const userSessionsTable = sqliteTable("user_sessions", { export const userSessionsTable = sqliteTable("user_sessions", {
@ -41,7 +43,9 @@ export const explorerContentTable = sqliteTable("explorer_content", {
content: blob({ mode: 'buffer' }), content: blob({ mode: 'buffer' }),
navigable: int({ mode: 'boolean' }).notNull().default(true), navigable: int({ mode: 'boolean' }).notNull().default(true),
private: int({ mode: 'boolean' }).notNull().default(false), private: int({ mode: 'boolean' }).notNull().default(false),
order: int().unique('order').notNull(), order: int().notNull(),
visit: int().notNull().default(0),
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}); });
export const usersRelation = relations(usersTable, ({ one, many }) => ({ export const usersRelation = relations(usersTable, ({ one, many }) => ({

View File

@ -0,0 +1,21 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_explorer_content` (
`path` text PRIMARY KEY NOT NULL,
`owner` integer NOT NULL,
`title` text NOT NULL,
`type` text NOT NULL,
`content` blob,
`navigable` integer DEFAULT true NOT NULL,
`private` integer DEFAULT false NOT NULL,
`order` integer NOT NULL,
`visit` integer DEFAULT 0 NOT NULL,
`timestamp` integer NOT NULL,
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_explorer_content`("path", "owner", "title", "type", "content", "navigable", "private", "order", "visit", "timestamp") SELECT "path", "owner", "title", "type", "content", "navigable", "private", "order", "visit", "timestamp" FROM `explorer_content`;--> statement-breakpoint
DROP TABLE `explorer_content`;--> statement-breakpoint
ALTER TABLE `__new_explorer_content` RENAME TO `explorer_content`;--> statement-breakpoint
PRAGMA foreign_keys=ON;--> statement-breakpoint
ALTER TABLE `users_data` ADD `lastTimestamp` integer NOT NULL;--> statement-breakpoint
ALTER TABLE `users_data` ADD `logCount` integer DEFAULT 0 NOT NULL;

View File

@ -0,0 +1,335 @@
{
"version": "6",
"dialect": "sqlite",
"id": "b6acf5d6-d8df-4308-8d4d-55c25741cc4f",
"prevId": "a1a7b478-d0c3-4fc6-b74a-1a010c1d8ca1",
"tables": {
"explorer_content": {
"name": "explorer_content",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visit": {
"name": "visit",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"explorer_content_owner_users_id_fk": {
"name": "explorer_content_owner_users_id_fk",
"tableFrom": "explorer_content",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logCount": {
"name": "logCount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -29,6 +29,13 @@
"when": 1731344368953, "when": 1731344368953,
"tag": "0003_cultured_skaar", "tag": "0003_cultured_skaar",
"breakpoints": true "breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1732722840534,
"tag": "0004_ancient_thunderball",
"breakpoints": true
} }
] ]
} }

View File

@ -10,6 +10,7 @@ export default defineNuxtConfig({
'@nuxtjs/tailwindcss', '@nuxtjs/tailwindcss',
'@vueuse/nuxt', '@vueuse/nuxt',
'radix-vue/nuxt', 'radix-vue/nuxt',
'@nuxtjs/sitemap',
], ],
tailwindcss: { tailwindcss: {
viewer: false, viewer: false,
@ -164,4 +165,8 @@ export default defineNuxtConfig({
}, },
xssValidator: false, xssValidator: false,
}, },
sitemap: {
exclude: ['/admin/**', '/explore/edit/**', '/user/mailvalidated'],
sources: ['/api/__sitemap__/urls']
}
}) })

View File

@ -11,6 +11,7 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/sitemap": "^7.0.0",
"@nuxtjs/tailwindcss": "^6.12.2", "@nuxtjs/tailwindcss": "^6.12.2",
"@vueuse/gesture": "^2.0.0", "@vueuse/gesture": "^2.0.0",
"@vueuse/math": "^11.2.0", "@vueuse/math": "^11.2.0",

View File

@ -1,89 +1,23 @@
<script lang="ts">
const mailSchema = z.object({
to: z.string().email(),
template: z.string(),
data: z.string(),
});
const schemaList: Record<string, z.ZodObject<any> | null> = {
'pull': null,
'push': null,
'mail': mailSchema,
}
</script>
<script setup lang="ts"> <script setup lang="ts">
import { z } from 'zod';
definePageMeta({ definePageMeta({
rights: ['admin'], rights: ['admin'],
})
const job = ref<string>('');
const toaster = useToast();
const payload = reactive<Record<string, any>>({
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
to: 'clem31470@gmail.com',
}); });
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), error = ref<Error | null>();
async function fetch()
{
status.value = 'pending';
data.value = null;
error.value = null;
success.value = false;
try const { data: users } = useFetch('/api/admin/users');
{ const { data: pages } = useFetch('/api/admin/pages');
const schema = schemaList[job.value];
if(schema)
{
console.log(payload);
const parsedPayload = schema.parse(payload);
}
data.value = await $fetch(`/api/admin/jobs/${job.value}`, {
method: 'POST',
body: payload,
});
status.value = 'success';
error.value = null;
success.value = true;
toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
}
catch(e)
{
status.value = 'error';
error.value = e as Error;
success.value = false;
toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
}
}
</script> </script>
<template> <template>
<Head> <Head>
<Title>d[any] - Administration</Title> <Title>d[any] - Administration</Title>
</Head> </Head>
<div class="flex flex-col justify-start items-center"> <div class="flex flex-1 flex-col p-4 justify-start">
<ProseH2>Administration</ProseH2> <div class="flex flex-row justify-between items-center">
<div class="flex flex-row w-full gap-8"> <ProseH2 class="text-center flex-1">Administration</ProseH2>
<Select label="Job" v-model="job"> <Button><NuxtLink :to="{ name: 'admin-jobs' }">Jobs</NuxtLink></Button>
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
<SelectItem label="Envoyer un mail de test" value="mail" />
</Select>
<Select v-if="job === 'mail'" v-model="payload.template" label="Modèle" class="w-full" ><SelectItem label="Inscription" value="registration" /></Select>
</div> </div>
<div v-if="job === 'mail'" class="flex justify-center items-center flex-col"> <div>
<TextInput label="Destinataire" class="w-full" v-model="payload.to" /> <div>Utilisateurs ({{ users.length }})</div>
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none m-2 px-2"></textarea>
</div> </div>
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
<span>Executer</span>
</Button>
</div> </div>
</template> </template>

93
pages/admin/jobs.vue Normal file
View File

@ -0,0 +1,93 @@
<script lang="ts">
const mailSchema = z.object({
to: z.string().email(),
template: z.string(),
data: z.string(),
});
const schemaList: Record<string, z.ZodObject<any> | null> = {
'pull': null,
'push': null,
'mail': mailSchema,
}
</script>
<script setup lang="ts">
import { z } from 'zod';
import { Icon } from '@iconify/vue/dist/iconify.js';
definePageMeta({
rights: ['admin'],
})
const job = ref<string>('');
const toaster = useToast();
const payload = reactive<Record<string, any>>({
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
to: 'clem31470@gmail.com',
});
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), error = ref<Error | null>();
async function fetch()
{
status.value = 'pending';
data.value = null;
error.value = null;
success.value = false;
try
{
const schema = schemaList[job.value];
if(schema)
{
console.log(payload);
const parsedPayload = schema.parse(payload);
}
data.value = await $fetch(`/api/admin/jobs/${job.value}`, {
method: 'POST',
body: payload,
});
status.value = 'success';
error.value = null;
success.value = true;
toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
}
catch(e)
{
status.value = 'error';
error.value = e as Error;
success.value = false;
toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
}
}
</script>
<template>
<Head>
<Title>d[any] - Administration</Title>
</Head>
<div class="flex flex-col justify-start items-center p-4">
<div class="flex flex-row justify-between items-center gap-8">
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
<ProseH2 class="text-center flex-1">Administration</ProseH2>
</div>
<div class="flex flex-row w-full gap-8">
<Select label="Job" v-model="job">
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
<SelectItem label="Envoyer un mail de test" value="mail" />
</Select>
<Select v-if="job === 'mail'" v-model="payload.template" label="Modèle" class="w-full" ><SelectItem label="Inscription" value="registration" /></Select>
</div>
<div v-if="job === 'mail'" class="flex justify-center items-center flex-col">
<TextInput label="Destinataire" class="w-full" v-model="payload.to" />
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none m-2 px-2"></textarea>
</div>
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
<span>Executer</span>
</Button>
</div>
</template>

View File

@ -12,6 +12,8 @@
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Dashboard de statistiques</span></Label> <Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Dashboard de statistiques</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Gestion de droits</span><ProseTag>prioritaire</ProseTag></Label> <Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Gestion de droits</span><ProseTag>prioritaire</ProseTag></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Synchro project <-> GIT</span><ProseTag>prioritaire</ProseTag></Label> <Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Synchro project <-> GIT</span><ProseTag>prioritaire</ProseTag></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Génération du fichier robots.txt</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Génération dynamique de la sitemap</span></Label>
</div> </div>
<div class="flex flex-col gap-2 justify-start"> <div class="flex flex-col gap-2 justify-start">
<ProseH3>Editeur</ProseH3> <ProseH3>Editeur</ProseH3>

View File

@ -13,6 +13,6 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'login' layout: 'login',
}) })
</script> </script>

View File

@ -1 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://obsidian.peaceultime.com/sitemap.xml

View File

@ -29,7 +29,7 @@ function securePassword(password: string, ctx: z.RefinementCtx): void {
{ {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: "Votre mot de passe doit contenir au moins un symbole", message: "Votre mot de passe doit contenir au moins un caractère spécial",
}); });
} }
} }

View File

@ -0,0 +1,14 @@
import type { SitemapUrlInput } from '#sitemap/types'
import { and, eq } from 'drizzle-orm';
import { explorerContentTable } from '~/db/schema';
import useDatabase from '~/composables/useDatabase';
export default defineSitemapEventHandler(() => {
const db = useDatabase();
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp }).from(explorerContentTable).where(and(eq(explorerContentTable.private, false), eq(explorerContentTable.navigable, true))).all();
return pages.map(e => ({
loc: `/explore/${e.path}`,
lastmod: e.lastMod,
})) satisfies SitemapUrlInput[];
})

View File

@ -0,0 +1,3 @@
export default defineEventHandler((e) => {
return [];
})

View File

@ -0,0 +1,3 @@
export default defineEventHandler((e) => {
return [];
})

View File

@ -3,7 +3,7 @@ import { schema } from '~/schemas/login';
import type { UserSession, UserSessionRequired } from '~/types/auth'; import type { UserSession, UserSessionRequired } from '~/types/auth';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { checkSession, logSession } from '~/server/utils/user'; import { checkSession, logSession } from '~/server/utils/user';
import { usersTable } from '~/db/schema'; import { usersDataTable, usersTable } from '~/db/schema';
import { eq, or, sql } from 'drizzle-orm'; import { eq, or, sql } from 'drizzle-orm';
interface SuccessHandler interface SuccessHandler
@ -93,6 +93,8 @@ export default defineEventHandler(async (e): Promise<Return> => {
} }
}) as UserSessionRequired); }) as UserSessionRequired);
db.update(usersDataTable).set({ logCount: user.data.logCount + 1 }).where(eq(usersDataTable.id, user.id)).run();
setResponseStatus(e, 201); setResponseStatus(e, 201);
return { success: true, session: data }; return { success: true, session: data };
} }

View File

@ -13,7 +13,7 @@ export default defineEventHandler(async (e) => {
const buffer = Buffer.from(body.data.content, 'utf-8'); const buffer = Buffer.from(body.data.content, 'utf-8');
const db = useDatabase(); const db = useDatabase();
const content = db.insert(explorerContentTable).values({ ...body.data, content: buffer }).onConflictDoUpdate({ target: explorerContentTable.path, set: { ...body.data, content: buffer } }); const content = db.insert(explorerContentTable).values({ ...body.data, content: buffer }).onConflictDoUpdate({ target: explorerContentTable.path, set: { ...body.data, content: buffer, timestamp: new Date() } });
if(content !== undefined) if(content !== undefined)
{ {

View File

@ -22,10 +22,13 @@ export default defineEventHandler(async (e) => {
'navigable': explorerContentTable.navigable, 'navigable': explorerContentTable.navigable,
'private': explorerContentTable.private, 'private': explorerContentTable.private,
'order': explorerContentTable.order, 'order': explorerContentTable.order,
'visit': explorerContentTable.visit,
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path }); }).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
if(content !== undefined) if(content !== undefined)
{ {
db.update(explorerContentTable).set({ visit: content.visit + 1 }).where(eq(explorerContentTable.path, content.path)).run();
return content; return content;
} }

View File

@ -54,6 +54,7 @@ export default defineEventHandler(async (e) => {
navigable: item.navigable, navigable: item.navigable,
private: item.private, private: item.private,
order: item.order, order: item.order,
timestamp: new Date(),
}, },
target: explorerContentTable.path, target: explorerContentTable.path,
}).run(); }).run();

View File

@ -1,5 +1,5 @@
import useDatabase from "~/composables/useDatabase"; import useDatabase from "~/composables/useDatabase";
import { userSessionsTable } from "~/db/schema"; import { usersDataTable, userSessionsTable } from "~/db/schema";
import { eq, and, sql, lte } from "drizzle-orm"; import { eq, and, sql, lte } from "drizzle-orm";
import { refreshSessionFromDB } from "../utils/user"; import { refreshSessionFromDB } from "../utils/user";
@ -19,9 +19,14 @@ export default defineNitroPlugin(() => {
} }
else else
{ {
await db.update(userSessionsTable).set({ db.update(userSessionsTable).set({
timestamp: new Date(), timestamp: new Date(),
}).where(and(eq(userSessionsTable.id, sql.placeholder('id')), eq(userSessionsTable.user_id, sql.placeholder('user_id')))).prepare().run({ id: session.id, user_id: session.user.id }); }).where(and(eq(userSessionsTable.id, sql.placeholder('id')), eq(userSessionsTable.user_id, sql.placeholder('user_id')))).prepare().run({ id: session.id, user_id: session.user.id });
db.update(usersDataTable).set({
lastTimestamp: new Date(),
}).where(eq(usersDataTable.id, sql.placeholder('user_id'))).prepare().run({ id: session.id, user_id: session.user.id });
await refreshSessionFromDB(event, session.id); await refreshSessionFromDB(event, session.id);
} }
}); });

2
types/auth.d.ts vendored
View File

@ -31,6 +31,8 @@ export interface UserRawData {
export interface UserExtendedData { export interface UserExtendedData {
signin: Date; signin: Date;
lastTimestamp: Date;
logCount: number;
} }
export type Permissions = { permissions: string[] }; export type Permissions = { permissions: string[] };