Compare commits
6 Commits
5fb708051b
...
429f1d4b38
| Author | SHA1 | Date |
|---|---|---|
|
|
429f1d4b38 | |
|
|
5062d52667 | |
|
|
c4bf95e48b | |
|
|
7fc7998a4b | |
|
|
fdaf765e2d | |
|
|
e99a5f15b4 |
|
|
@ -22,3 +22,7 @@ logs
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-wal
|
||||||
|
*.sqlite-shm
|
||||||
1
app.vue
1
app.vue
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<CollapsibleRoot v-model:open="model" :disabled="disabled">
|
<CollapsibleRoot v-model:open="model" :disabled="disabled" :defaultOpen="defaultOpen">
|
||||||
<div class="flex flex-row justify-center items-center">
|
<div class="flex flex-row justify-center items-center">
|
||||||
<span v-if="!!label">{{ label }}</span>
|
<span v-if="!!label">{{ label }}</span>
|
||||||
<CollapsibleTrigger class="ms-4" asChild>
|
<CollapsibleTrigger class="ms-4" asChild>
|
||||||
|
|
@ -18,9 +18,10 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
const { label, disabled = false } = defineProps<{
|
const { label, disabled = false, defaultOpen = false } = defineProps<{
|
||||||
label?: string
|
label?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
defaultOpen?: boolean
|
||||||
}>();
|
}>();
|
||||||
const model = defineModel<boolean>();
|
const model = defineModel<boolean>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<TagsInputRoot v-model="model" addOnPaste class="flex gap-2 items-center border p-2 w-full flex-wrap border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10" >
|
||||||
|
<TagsInputItem v-for="item in model" :key="item" :value="item" class="text-light-100 dark:text-dark-100 flex items-center justify-center gap-2 bg-light-20 dark:bg-dark-20 hover:bg-light-35 dark:hover:bg-dark-35 p-1 border border-light-35 dark:border-dark-35">
|
||||||
|
<TagsInputItemText class="text-sm pl-1" />
|
||||||
|
<TagsInputItemDelete asChild>
|
||||||
|
<Icon icon="radix-icons:cross-2" class="w-4 h-4 cursor-pointer" />
|
||||||
|
</TagsInputItemDelete>
|
||||||
|
</TagsInputItem>
|
||||||
|
|
||||||
|
<TagsInputInput :placeholder="placeholder" class="text-sm focus:outline-none flex-1 rounded text-green9 bg-transparent placeholder:text-mauve9 px-1" />
|
||||||
|
</TagsInputRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const { placeholder } = defineProps<{
|
||||||
|
placeholder?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string[]>();
|
||||||
|
</script>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink no-prefetch class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
<NuxtLink class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
||||||
:to="{ name: 'explore-path', params: { path: data[0].path }, hash: hash }" :class="class">
|
:to="{ name: 'explore-path', params: { path: data[0].path }, hash: hash }" :class="class">
|
||||||
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': data[0].type === 'canvas'}">
|
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': data[0].type === 'canvas'}">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
</template>
|
</template>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
<NuxtLink v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :height="20" :width="20"
|
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :height="20" :width="20"
|
||||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
||||||
|
|
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -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 }) => ({
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -18,17 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip v-else :message="'Mon profil'" side="right">
|
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||||
<DropdownMenu :options="[{
|
<DropdownMenu :options="options" side="bottom" align="end">
|
||||||
type: 'item',
|
|
||||||
label: 'Mon profil',
|
|
||||||
icon: 'radix-icons:avatar',
|
|
||||||
select: () => useRouter().push({ name: 'user-profile' }),
|
|
||||||
}, {
|
|
||||||
type: 'item',
|
|
||||||
label: 'Deconnexion',
|
|
||||||
icon: 'radix-icons:close',
|
|
||||||
select: () => clear(),
|
|
||||||
}]" side="right" align="start">
|
|
||||||
<div class="hover:border-opacity-70 flex">
|
<div class="hover:border-opacity-70 flex">
|
||||||
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -55,15 +45,7 @@
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip v-else :message="'Mon profil'" side="right">
|
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||||
<DropdownMenu :options="[{
|
<DropdownMenu :options="options" side="right" align="start">
|
||||||
type: 'item',
|
|
||||||
label: 'Mon profil',
|
|
||||||
select: () => useRouter().push({ name: 'user-profile' }),
|
|
||||||
}, {
|
|
||||||
type: 'item',
|
|
||||||
label: 'Deconnexion',
|
|
||||||
select: () => clear(),
|
|
||||||
}]" side="right" align="start">
|
|
||||||
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
||||||
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,12 +55,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
|
<div class="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
<NuxtLink :href="{ name: 'explore' }" no-prefetch class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
<NuxtLink :href="{ name: 'explore' }" class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||||
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
|
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path">
|
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path">
|
||||||
<template #default="{ item, isExpanded }">
|
<template #default="{ item, isExpanded }">
|
||||||
<NuxtLink :href="item.value.path && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.path } } : undefined" no-prefetch class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
|
<NuxtLink :href="item.value.path && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.path } } : undefined" class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
|
||||||
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
|
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
|
||||||
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" />
|
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" />
|
||||||
<div class="pl-3 py-1 flex-1 truncate">
|
<div class="pl-3 py-1 flex-1 truncate">
|
||||||
|
|
@ -104,9 +86,20 @@
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import type { NavigationTreeItem } from '~/server/api/navigation.get';
|
import type { NavigationTreeItem } from '~/server/api/navigation.get';
|
||||||
import { iconByType } from '#shared/general.utils';
|
import { iconByType } from '#shared/general.utils';
|
||||||
|
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
|
||||||
|
|
||||||
|
const options = ref<DropdownOption[]>([{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Mon profil',
|
||||||
|
select: () => useRouter().push({ name: 'user-profile' }),
|
||||||
|
}, {
|
||||||
|
type: 'item',
|
||||||
|
label: 'Deconnexion',
|
||||||
|
select: () => clear(),
|
||||||
|
}]);
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const { loggedIn, clear } = useUserSession();
|
const { loggedIn, user, clear } = useUserSession();
|
||||||
|
|
||||||
const route = useRouter().currentRoute;
|
const route = useRouter().currentRoute;
|
||||||
const path = computed(() => route.value.params.path ? Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path : undefined);
|
const path = computed(() => route.value.params.path ? Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path : undefined);
|
||||||
|
|
|
||||||
|
|
@ -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,18 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
xssValidator: false,
|
xssValidator: false,
|
||||||
},
|
},
|
||||||
|
sitemap: {
|
||||||
|
exclude: ['/admin/**', '/explore/edit/**', '/user/mailvalidated'],
|
||||||
|
sources: ['/api/__sitemap__/urls']
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
defaults: {
|
||||||
|
nuxtLink: {
|
||||||
|
prefetchOn: {
|
||||||
|
interaction: true,
|
||||||
|
visibility: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,138 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
const mailSchema = z.object({
|
/**
|
||||||
to: z.string().email(),
|
* Format bytes as human-readable text.
|
||||||
template: z.string(),
|
*
|
||||||
data: z.string(),
|
* @param bytes Number of bytes.
|
||||||
});
|
* @param si True to use metric (SI) units, aka powers of 1000. False to use
|
||||||
|
* binary (IEC), aka powers of 1024.
|
||||||
|
* @param dp Number of decimal places to display.
|
||||||
|
*
|
||||||
|
* @return Formatted string.
|
||||||
|
*/
|
||||||
|
function textualFileSize(bytes: number, si: boolean = false, dp: number = 2) {
|
||||||
|
const thresh = si ? 1000 : 1024;
|
||||||
|
|
||||||
const schemaList: Record<string, z.ZodObject<any> | null> = {
|
if (Math.abs(bytes) < thresh) {
|
||||||
'pull': null,
|
return bytes + ' B';
|
||||||
'push': null,
|
}
|
||||||
'mail': mailSchema,
|
|
||||||
|
const units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
let u = -1;
|
||||||
|
const r = 10**dp;
|
||||||
|
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||||
|
|
||||||
|
|
||||||
|
return bytes.toFixed(dp) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { z } from 'zod';
|
import { format, iconByType } from '~/shared/general.utils';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
interface File
|
||||||
|
{
|
||||||
|
path: string;
|
||||||
|
owner: number;
|
||||||
|
title: string;
|
||||||
|
type: "file" | "canvas" | "markdown" | 'folder';
|
||||||
|
size: number;
|
||||||
|
navigable: boolean;
|
||||||
|
private: boolean;
|
||||||
|
order: number;
|
||||||
|
visit: number;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
interface User
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
state: number;
|
||||||
|
session: {
|
||||||
|
id: number;
|
||||||
|
}[];
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
signin: string;
|
||||||
|
lastTimestamp: string;
|
||||||
|
logCount: number;
|
||||||
|
};
|
||||||
|
permission: string[];
|
||||||
|
}
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
rights: ['admin'],
|
rights: ['admin'],
|
||||||
})
|
});
|
||||||
const job = ref<string>('');
|
|
||||||
|
|
||||||
const toaster = useToast();
|
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;
|
|
||||||
|
|
||||||
|
const { data: users } = useFetch('/api/admin/users', {
|
||||||
|
transform: (users) => {
|
||||||
|
//@ts-ignore
|
||||||
|
users.forEach(e => e.permission = e.permission.map(p => p.permission));
|
||||||
|
//@ts-ignore
|
||||||
|
return users as User[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { data: pages } = useFetch('/api/admin/pages');
|
||||||
|
|
||||||
|
const sorter = ref<((a: File, b: File) => number) | null>(null);
|
||||||
|
const sortField = ref<keyof File | null>(null), sortOrder = ref<null | 'asc' | 'desc'>('asc');
|
||||||
|
const sortedPage = ref([...pages.value ?? []]);
|
||||||
|
|
||||||
|
const permissionCopy = ref<string[]>([]);
|
||||||
|
|
||||||
|
watch([sortField, sortOrder, sorter], () => {
|
||||||
|
sortedPage.value = (sorter.value === null ? ([...pages.value ?? []]) : sortedPage.value.sort(sorter.value))
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function sort(field: keyof File, type: 'string' | 'number')
|
||||||
|
{
|
||||||
|
if(sortField.value === field)
|
||||||
|
{
|
||||||
|
if(sortOrder.value === 'asc')
|
||||||
|
{
|
||||||
|
sortOrder.value = 'desc';
|
||||||
|
sorter.value = type === 'string' ? (a: File, b: File) => (b[field] as string).localeCompare(a[field] as string) : (a: File, b: File) => (b[field] as number) - (a[field] as number);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sortOrder.value = null;
|
||||||
|
sortField.value = null;
|
||||||
|
sorter.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sortField.value = field;
|
||||||
|
sortOrder.value = 'asc';
|
||||||
|
sorter.value = type === 'string' ? (a: File, b: File) => (a[field] as string).localeCompare(b[field] as string) : (a: File, b: File) => (a[field] as number) - (b[field] as number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function editPermissions(user: User)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const schema = schemaList[job.value];
|
await $fetch(`/api/admin/user/${user.id}/permissions`, {
|
||||||
|
|
||||||
if(schema)
|
|
||||||
{
|
|
||||||
console.log(payload);
|
|
||||||
const parsedPayload = schema.parse(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.value = await $fetch(`/api/admin/jobs/${job.value}`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: payload,
|
body: permissionCopy.value,
|
||||||
|
});
|
||||||
|
user.permission = permissionCopy.value;
|
||||||
|
toaster.add({
|
||||||
|
duration: 10000, type: 'success', content: 'Permissions mises à jour.', timer: true,
|
||||||
});
|
});
|
||||||
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)
|
catch(e)
|
||||||
{
|
{
|
||||||
status.value = 'error';
|
toaster.add({
|
||||||
error.value = e as Error;
|
duration: 10000, type: 'error', content: (e as any).message, timer: true,
|
||||||
success.value = false;
|
});
|
||||||
|
|
||||||
toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -68,22 +141,115 @@ async function fetch()
|
||||||
<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">
|
||||||
<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 class="flex flex-1 w-full justify-center items-stretch flex-row gap-4">
|
||||||
<TextInput label="Destinataire" class="w-full" v-model="payload.to" />
|
<div class="flex-1">
|
||||||
<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>
|
<Collapsible v-if=users :label="`Utilisateurs (${users.length})`">
|
||||||
|
<div class="flex flex-1 mt-2">
|
||||||
|
<table class="border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Utilisateur</th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Inscription</th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Dernière connexion</th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Mail</th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Sessions</th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1">Permissions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="font-normal">
|
||||||
|
<tr v-for="user in users">
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 max-w-32 truncate"><NuxtLink :to="{ name: 'user-id', params: { id: user.id } }" class="hover:text-accent-purple font-bold" :title="user.username">{{ user.username }}</NuxtLink></td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-sm text-light-70 dark:text-dark-70 text-center">{{ format(new Date(user.data.signin), 'dd/MM/yyyy') }}</td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-sm text-light-70 dark:text-dark-70 text-center">{{ format(new Date(user.data.lastTimestamp), 'dd/MM/yyyy HH:mm:ss') }}</td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center"><Icon :class="{ 'text-light-red dark:text-dark-red': user.state === 0, 'text-light-green dark:text-dark-green': user.state !== 0 }" :icon="user.state === 0 ? `radix-icons:cross-2` : `radix-icons:check`" /></td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1">
|
||||||
|
<DialogRoot>
|
||||||
|
<DialogTrigger asChild><span class="text-accent-blue hover:text-accent-purple font-bold cursor-pointer">{{ user.session.length }}</span></DialogTrigger>
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||||
|
<DialogContent
|
||||||
|
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||||
|
<DialogTitle class="text-3xl font-light relative -top-2">Deconnecter l'utilisateur ?
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="flex flex-1 justify-end gap-4">
|
||||||
|
<DialogClose asChild><Button>Non</Button></DialogClose>
|
||||||
|
<DialogClose asChild><Button class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Oui</Button></DialogClose>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</DialogRoot>
|
||||||
|
</td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1">
|
||||||
|
<AlertDialogRoot>
|
||||||
|
<AlertDialogTrigger asChild><span class="text-accent-blue hover:text-accent-purple font-bold" @click="permissionCopy = [...user.permission]">{{ user.permission.length }}</span></AlertDialogTrigger>
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||||
|
<AlertDialogContent
|
||||||
|
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||||
|
<AlertDialogTitle class="text-3xl font-light relative -top-2">Permissions de {{ user.username }}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription><TagsInput v-model="permissionCopy" /></AlertDialogDescription>
|
||||||
|
<div class="flex flex-1 justify-end gap-4">
|
||||||
|
<AlertDialogCancel asChild><Button>Annuler</Button></AlertDialogCancel>
|
||||||
|
<AlertDialogAction asChild><Button @click="() => editPermissions(user)" class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Modifier</Button></AlertDialogAction>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</AlertDialogRoot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<Collapsible v-if=pages :label="`Pages (${pages.length})`">
|
||||||
|
<div class="flex flex-1 mt-2">
|
||||||
|
<table class="border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Page</span><span @click="() => sort('title', 'string')"><Icon :icon="sortField === 'title' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Type</span></div></th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Propriétaire</span><span @click="() => sort('owner', 'number')"><Icon :icon="sortField === 'owner' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Status</span></div></th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Taille</span><span @click="() => sort('size', 'number')"><Icon :icon="sortField === 'size' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Visites</span><span @click="() => sort('visit', 'number')"><Icon :icon="sortField === 'visit' ? sortOrder === 'asc' ? 'radix-icons:chevron-down' : 'radix-icons:chevron-up' : 'radix-icons:caret-sort'" /></span></div></th>
|
||||||
|
<th class="border border-light-35 dark:border-dark-35 px-2 py-1"><div class="flex justify-center items-center gap-2"><span>Actions</span></div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="font-normal">
|
||||||
|
<DialogRoot>
|
||||||
|
<tr v-for="page in sortedPage" :id="page.path">
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 max-w-48 truncate"><NuxtLink :to="{ name: 'explore-path', params: { path: page.path } }" class="hover:text-accent-purple font-bold" :title="page.title">{{ page.title }}</NuxtLink></td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1"><Icon :icon="iconByType[page.type]" /></td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-sm text-light-70 dark:text-dark-70 text-center max-w-32 truncate"><span :title=" users?.find(e => e.id === page.owner)?.username ?? 'Inconnu'">{{ users?.find(e => e.id === page.owner)?.username ?? "Inconnu" }}</span></td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 ">
|
||||||
|
<div class="flex gap-2 justify-center">
|
||||||
|
<span>
|
||||||
|
<Icon v-if="page.private" icon="radix-icons:lock-closed" />
|
||||||
|
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:lock-open-2" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Icon v-if="page.navigable" icon="radix-icons:eye-open" />
|
||||||
|
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:eye-none" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center">{{ textualFileSize(page.size) }}</td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center">{{ page.visit }}</td>
|
||||||
|
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center"><div class="flex justify-center items-center"><NuxtLink :to="{ name: 'explore-edit-path', params: { path: page.path } }"><Icon icon="radix-icons:pencil-1" /></NuxtLink></div></td>
|
||||||
|
</tr>
|
||||||
|
</DialogRoot>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
|
||||||
<span>Executer</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -5,12 +5,10 @@
|
||||||
<div class="flex flex-col justify-start p-6">
|
<div class="flex flex-col justify-start p-6">
|
||||||
<ProseH2>Roadmap</ProseH2>
|
<ProseH2>Roadmap</ProseH2>
|
||||||
<div class="grid grid-cols-4 gap-x-2 gap-y-4">
|
<div class="grid grid-cols-4 gap-x-2 gap-y-4">
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
<div v-if="loggedIn && user && hasPermissions(user.permissions, ['admin'])" class="flex flex-col gap-2 justify-start">
|
||||||
<ProseH3>Administration</ProseH3>
|
<ProseH3>Administration</ProseH3>
|
||||||
<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=" ">Statistiques de consultation</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" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">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=" ">Statistiques de connexion</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" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Editeur de permissions</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=" ">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=" ">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>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
<div class="flex flex-col gap-2 justify-start">
|
||||||
|
|
@ -45,4 +43,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
|
const { loggedIn, user } = useUserSession();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -13,6 +13,6 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'login'
|
layout: 'login',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -79,19 +79,5 @@ async function deleteUser()
|
||||||
</AlertDialogRoot>
|
</AlertDialogRoot>
|
||||||
<NuxtLink v-if="hasPermissions(user.permissions, ['admin'])" :href="{ name: 'admin' }" class="flex" no-prefetch><Button class="flex-1">Administration</Button></NuxtLink>
|
<NuxtLink v-if="hasPermissions(user.permissions, ['admin'])" :href="{ name: 'admin' }" class="flex" no-prefetch><Button class="flex-1">Administration</Button></NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex" v-if="user.permissions">
|
|
||||||
<ProseTable class="!m-0">
|
|
||||||
<ProseThead>
|
|
||||||
<ProseTr>
|
|
||||||
<ProseTh>Permission</ProseTh>
|
|
||||||
</ProseTr>
|
|
||||||
</ProseThead>
|
|
||||||
<ProseTbody>
|
|
||||||
<ProseTr v-for="permission in user.permissions">
|
|
||||||
<ProseTd>{{ permission }}</ProseTd>
|
|
||||||
</ProseTr>
|
|
||||||
</ProseTbody>
|
|
||||||
</ProseTable>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://obsidian.peaceultime.com/sitemap.xml
|
||||||
|
|
@ -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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { ne, sql } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session || !session.user || !hasPermissions(session.user.permissions, ['admin']))
|
||||||
|
{
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const content = db.select({
|
||||||
|
path: explorerContentTable.path,
|
||||||
|
owner: explorerContentTable.owner,
|
||||||
|
title: explorerContentTable.title,
|
||||||
|
type: explorerContentTable.type,
|
||||||
|
size: sql<number>`CASE WHEN ${explorerContentTable.content} IS NULL THEN 0 ELSE length(${explorerContentTable.content}) END`.as('size'),
|
||||||
|
navigable: explorerContentTable.navigable,
|
||||||
|
private: explorerContentTable.private,
|
||||||
|
order: explorerContentTable.order,
|
||||||
|
visit: explorerContentTable.visit,
|
||||||
|
timestamp: explorerContentTable.timestamp,
|
||||||
|
}).from(explorerContentTable).all();
|
||||||
|
|
||||||
|
content.sort((a, b) => {
|
||||||
|
return a.path.split('/').length - b.path.split('/').length;
|
||||||
|
});
|
||||||
|
|
||||||
|
for(let i = 0; i < content.length; i++)
|
||||||
|
{
|
||||||
|
const path = content[i].path.substring(0, content[i].path.lastIndexOf('/'));
|
||||||
|
if(path !== '')
|
||||||
|
{
|
||||||
|
const parent = content.find(e => e.path === path);
|
||||||
|
|
||||||
|
if(parent)
|
||||||
|
{
|
||||||
|
content[i].private = content[i].private || parent.private;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.filter(e => e.type !== 'folder');
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { hasPermissions } from "~/shared/auth.util";
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { and, eq, notInArray } from "drizzle-orm";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { userPermissionsTable } from "~/db/schema";
|
||||||
|
|
||||||
|
const schema = z.array(z.string());
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session || !session.user || !hasPermissions(session.user.permissions, ['admin']))
|
||||||
|
{
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const param = getRouterParam(e, 'id');
|
||||||
|
|
||||||
|
if(!param)
|
||||||
|
{
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Forbidden',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Forbidden',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const id = parseInt(param, 10);
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const permissions = body.data.map(e => ({ id: id, permission: e }));
|
||||||
|
|
||||||
|
db.transaction((tx) => {
|
||||||
|
tx.delete(userPermissionsTable).where(eq(userPermissionsTable.id, id)).run();
|
||||||
|
tx.insert(userPermissionsTable).values(permissions).run();
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { userSessionsTable } from '~/db/schema';
|
||||||
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session || !session.user || !hasPermissions(session.user.permissions, ['admin']))
|
||||||
|
{
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
return db.query.usersTable.findMany({
|
||||||
|
columns: {
|
||||||
|
email: false,
|
||||||
|
hash: false,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
data: true,
|
||||||
|
permission: true,
|
||||||
|
session: {
|
||||||
|
columns: {
|
||||||
|
timestamp: false,
|
||||||
|
user_id: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).sync();
|
||||||
|
})
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ export function format(date: Date, template: string): string
|
||||||
"yyyy": (date: Date) => date.getUTCFullYear().toString(),
|
"yyyy": (date: Date) => date.getUTCFullYear().toString(),
|
||||||
"MM": (date: Date) => padRight((date.getUTCMonth() + 1).toString(), '0', 2),
|
"MM": (date: Date) => padRight((date.getUTCMonth() + 1).toString(), '0', 2),
|
||||||
"dd": (date: Date) => padRight(date.getUTCDate().toString(), '0', 2),
|
"dd": (date: Date) => padRight(date.getUTCDate().toString(), '0', 2),
|
||||||
"mm": (date: Date) => padRight(date.getFullYear().toString(), '0', 2),
|
"mm": (date: Date) => padRight(date.getUTCMinutes().toString(), '0', 2),
|
||||||
"HH": (date: Date) => padRight(date.getFullYear().toString(), '0', 2),
|
"HH": (date: Date) => padRight(date.getUTCHours().toString(), '0', 2),
|
||||||
"ss": (date: Date) => padRight(date.getFullYear().toString(), '0', 2),
|
"ss": (date: Date) => padRight(date.getUTCSeconds().toString(), '0', 2),
|
||||||
};
|
};
|
||||||
const keys = Object.keys(transforms);
|
const keys = Object.keys(transforms);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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[] };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue