From 83ddaf19d4d54cbcdb534668ae4a6c8badd262fa Mon Sep 17 00:00:00 2001 From: Peaceultime Date: Tue, 5 Nov 2024 18:09:42 +0100 Subject: [PATCH] Starting to put back the server part. Currently the registration and login are almost ready. --- bun.lockb | Bin 354899 -> 355274 bytes components/NavBar.vue | 36 --- components/base/TextInput.vue | 5 +- composables/useDatabase.ts | 5 +- composables/useToast.ts | 4 + composables/useUserSession.ts | 40 +++ db.sqlite | Bin 53248 -> 12288 bytes db/schema.ts | 25 +- ...nuci.sql => 0000_lonely_the_renegades.sql} | 12 +- drizzle/0001_lush_selene.sql | 18 ++ drizzle/meta/0000_snapshot.json | 120 +++++---- drizzle/meta/0001_snapshot.json | 252 ++++++++++++++++++ drizzle/meta/_journal.json | 11 +- drizzle/relations.ts | 29 ++ drizzle/schema.ts | 46 ++++ layouts/default.vue | 61 ++++- layouts/login.vue | 8 + migrate.ts | 7 + nuxt.config.ts | 6 + package.json | 6 +- pages/user/login.vue | 87 +++++- pages/user/register.vue | 156 ++++++++++- schemas/login.ts | 8 + schemas/registration.ts | 56 ++++ server/api/auth/login.post.ts | 105 ++++++++ server/api/auth/register.post.ts | 87 ++++++ server/api/auth/session.delete.ts | 8 + server/api/auth/session.get.ts | 13 + server/api/project.get.ts | 31 +++ server/api/project/[projectId].get.ts | 20 ++ server/api/project/[projectId].patch.ts | 20 ++ server/api/project/[projectId].post.ts | 20 ++ server/api/project/[projectId]/access.post.ts | 0 .../api/project/[projectId]/comment.post.ts | 0 server/api/project/[projectId]/file.get.ts | 54 ++++ server/api/project/[projectId]/file.post.ts | 0 .../project/[projectId]/file/[path].get.ts | 41 +++ .../api/project/[projectId]/navigation.get.ts | 72 +++++ .../api/project/[projectId]/tags/[tag].get.ts | 42 +++ server/api/search.get.ts | 21 ++ server/api/users/[id].get.ts | 16 ++ server/api/users/[id]/comments.get.ts | 16 ++ server/api/users/[id]/projects.get.ts | 16 ++ server/plugins/session.ts | 45 ++++ server/tasks/sync.ts | 164 ++++++++++++ server/tsconfig.json | 8 +- server/utils/session.ts | 110 ++++++++ server/utils/user.ts | 34 +++ tsconfig.json | 7 +- types/api.d.ts | 87 ++++++ types/auth.d.ts | 71 +++++ types/canvas.d.ts | 34 +++ 52 files changed, 2022 insertions(+), 118 deletions(-) delete mode 100644 components/NavBar.vue create mode 100644 composables/useToast.ts create mode 100644 composables/useUserSession.ts rename drizzle/{0000_youthful_ma_gnuci.sql => 0000_lonely_the_renegades.sql} (79%) create mode 100644 drizzle/0001_lush_selene.sql create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/relations.ts create mode 100644 drizzle/schema.ts create mode 100644 layouts/login.vue create mode 100644 migrate.ts create mode 100644 schemas/login.ts create mode 100644 schemas/registration.ts create mode 100644 server/api/auth/login.post.ts create mode 100644 server/api/auth/register.post.ts create mode 100644 server/api/auth/session.delete.ts create mode 100644 server/api/auth/session.get.ts create mode 100644 server/api/project.get.ts create mode 100644 server/api/project/[projectId].get.ts create mode 100644 server/api/project/[projectId].patch.ts create mode 100644 server/api/project/[projectId].post.ts create mode 100644 server/api/project/[projectId]/access.post.ts create mode 100644 server/api/project/[projectId]/comment.post.ts create mode 100644 server/api/project/[projectId]/file.get.ts create mode 100644 server/api/project/[projectId]/file.post.ts create mode 100644 server/api/project/[projectId]/file/[path].get.ts create mode 100644 server/api/project/[projectId]/navigation.get.ts create mode 100644 server/api/project/[projectId]/tags/[tag].get.ts create mode 100644 server/api/search.get.ts create mode 100644 server/api/users/[id].get.ts create mode 100644 server/api/users/[id]/comments.get.ts create mode 100644 server/api/users/[id]/projects.get.ts create mode 100644 server/plugins/session.ts create mode 100644 server/tasks/sync.ts create mode 100644 server/utils/session.ts create mode 100644 server/utils/user.ts create mode 100644 types/api.d.ts create mode 100644 types/auth.d.ts create mode 100644 types/canvas.d.ts diff --git a/bun.lockb b/bun.lockb index d0ca915eb2265a8420051d8d253872144fac0a18..e4ae815a7cdbe1261d46a70da7d198d871ccf11d 100644 GIT binary patch delta 65119 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�dFksJ~58~zkCls$+|JAKk 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 delta 64284 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%0DoKQl`4bUFUw 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 fEw2Z8v{4WL -

- - - \ No newline at end of file diff --git a/components/base/TextInput.vue b/components/base/TextInput.vue index 0efc357..6e87134 100644 --- a/components/base/TextInput.vue +++ b/components/base/TextInput.vue @@ -1,10 +1,11 @@ diff --git a/composables/useDatabase.ts b/composables/useDatabase.ts index e5486a3..342cfd1 100644 --- a/composables/useDatabase.ts +++ b/composables/useDatabase.ts @@ -1,11 +1,12 @@ import ".dotenv/config"; import { Database } from "bun:sqlite"; import { drizzle } from "drizzle-orm/bun-sqlite"; +import * as schema from '../db/schema'; export default function useDatabase() { - const sqlite = new Database(process.env.DB_FILE); - const db = drizzle({ client: sqlite }); + const sqlite = new Database(useRuntimeConfig().database); + const db = drizzle({ client: sqlite, schema }); db.run("PRAGMA journal_mode = WAL;"); diff --git a/composables/useToast.ts b/composables/useToast.ts new file mode 100644 index 0000000..2367c7b --- /dev/null +++ b/composables/useToast.ts @@ -0,0 +1,4 @@ +export default function useToast() +{ + +} \ No newline at end of file diff --git a/composables/useUserSession.ts b/composables/useUserSession.ts new file mode 100644 index 0000000..d225abb --- /dev/null +++ b/composables/useUserSession.ts @@ -0,0 +1,40 @@ +import type { UserSession, UserSessionComposable } from '~/types/auth' + +const useSessionState = () => useState('nuxt-session', () => ({})) +const useAuthReadyState = () => useState('nuxt-auth-ready', () => false) + +/** + * Composable to get back the user session and utils around it. + * @see https://github.com/atinux/nuxt-auth-utils + */ +export function useUserSession(): UserSessionComposable { + const sessionState = useSessionState() + const authReadyState = useAuthReadyState() + return { + ready: computed(() => authReadyState.value), + loggedIn: computed(() => Boolean(sessionState.value.user)), + user: computed(() => sessionState.value.user || null), + session: sessionState, + fetch, + clear, + } +} + +async function fetch() { + const authReadyState = useAuthReadyState() + useSessionState().value = await useRequestFetch()('/api/auth/session', { + headers: { + Accept: 'text/json', + }, + retry: false, + }).catch(() => ({})) + if (!authReadyState.value) { + authReadyState.value = true + } +} + +async function clear() { + await $fetch('/api/auth/session', { method: 'DELETE' }) + useSessionState().value = {} + useRouter().go(0); +} \ No newline at end of file diff --git a/db.sqlite b/db.sqlite index a0daa297273801d8bdf881f60ecd58a5844fd500..1f9d0b7130f0889bd1d7b3cfbfa64a77aa774226 100644 GIT binary patch delta 61 zcmZozz}%29L0XWJfq{V;h+%+nqK+|8P_L|mm;VO?6F)NpKQsUO&4L1x`8G52Gcs;w IG58}705S0kDF6Tf literal 53248 zcmeI(O;6)S7{GCxKtcj3NIm#e$-5Vb7Ft5c22O3dm{qhPupyB^m1;Z50N&<>O`I*= zL#0sl)LXwu5B&zc_tY=bL#1}?K(Ld*rIogd{#Ft^FZRsyo3UYJa`L+9xI#bgbX&Ho zXOvY%Rh2JvT~U-d`A*9B-L@!q=I;{nryAIQXm?Ke^!wA~%Ad+|JfW;aR(?sIe)QXN zBKgTuCGk^YFaCS{jVxe8009ILKmY**K8V2A%Q5XUO&vs>c0*kEd!pMjFYVr?*>5{< z`@+viz9<{{ilJ9ah1XR>FO>F;Q{9@VYw1U&+iXi;x4g4hDSIiVrRDi!f%A)&?KFc8 zEI-mfu<}?7evy@WlZa_K*~G_zCcMtI?Uo3(vh+wRWA(=xx+}F($MLB4?3sGz+O?+W zy=}_BBF&zVQ0@A7GI85nCI4m7(8ubod!5^mN2Ue`i!n{p)SGYobNZ_OM?7@=kXyPY z;+E5}bf@i#3(?h&%Z0;y`AmOhoay;$<)~1ShaDQFie5UB?`pBQA&(dZpu6JQ4HXR$ zC-eQ`o6PRHwkz(N-!~5O)nY~8N~8vvSWJ8JM7>Gh2VypC*Y>0F_ZFvwC99`Ab-w{G|9c0+jcbKfW$_c@8w&FVr- zTU%3qFh{+X1KR63op#Ub_$U#b?E9!}vipH9`}k&X%z}lk(-P9Ob#-qVC=QISAJoP$ zMDJ`VIj&(1zq|hd$zC+79j>X0e>vUGTf6Ue{InTDG1H-B?B*g8(>6ENA5~vdTwgUi zU5Qxl$muFyW9j*@>3H#w^~!cHt>HuraD?V?=i9dEP6f1F$8E~_Ix+3t@2;k0-*(i} zYt2s0yK>t0JLkf?*zZq&H|O1MUwBony3V_)7sa{V?0Gf9!!+#dzn!r5*axSWUm3=7KX?-x$({dy7SFE?5 z_ZRr;%CCxiu_1r}0tg_000IagfB*srAb`OCN8pvZpbU}=Sv$Mk$cmcS+1hICWwz_0 zp59L9>LQof+1;**-1DrxbDrB3*;+cC$<@=1+-|zDl}*>4XRm}qJ$zSJ z{!-+N4FLoYKmY**5I_I{1Q0*~0R(1HAQDmH{y+Gy&R}qaga85vAbX_&qe@H&AbO69NbzfB*srAb usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }); export const userSessionsTable = sqliteTable("user_sessions", { @@ -17,8 +19,8 @@ export const userSessionsTable = sqliteTable("user_sessions", { user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }, (table): SQLiteTableExtraConfig => { - return { - pk: primaryKey({ columns: [ table.id, table.user_id ] }), + return { + pk: primaryKey({ columns: [table.id, table.user_id] }), } }); @@ -30,4 +32,19 @@ export const explorerContentTable = sqliteTable("explorer_content", { content: blob({ mode: 'buffer' }), navigable: int({ mode: 'boolean' }).default(true), private: int({ mode: 'boolean' }).default(false), -}); \ No newline at end of file +}); + +export const usersRelation = relations(usersTable, ({one, many}) => ({ + data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }), + session: many(userSessionsTable), + content: many(explorerContentTable), +})); +export const usersDataRelation = relations(usersDataTable, ({one}) => ({ + users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }), +})); +export const userSessionsRelation = relations(userSessionsTable, ({one}) => ({ + users: one(usersTable, { fields: [userSessionsTable.user_id], references: [usersTable.id], }), +})); +export const explorerContentRelation = relations(explorerContentTable, ({one}) => ({ + users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }), +})); \ No newline at end of file diff --git a/drizzle/0000_youthful_ma_gnuci.sql b/drizzle/0000_lonely_the_renegades.sql similarity index 79% rename from drizzle/0000_youthful_ma_gnuci.sql rename to drizzle/0000_lonely_the_renegades.sql index 86df767..d442507 100644 --- a/drizzle/0000_youthful_ma_gnuci.sql +++ b/drizzle/0000_lonely_the_renegades.sql @@ -1,3 +1,6 @@ +-- Current sql file was generated after introspecting the database +-- If you want to run this migration please uncomment this code before executing migrations +/* CREATE TABLE `explorer_content` ( `path` text PRIMARY KEY NOT NULL, `owner` integer NOT NULL, @@ -30,6 +33,11 @@ CREATE TABLE `users` ( `state` integer DEFAULT 0 ); --> statement-breakpoint -CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint +CREATE UNIQUE INDEX `users_hash_unique` ON `users` (`hash`);--> statement-breakpoint CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint -CREATE UNIQUE INDEX `users_hash_unique` ON `users` (`hash`); \ No newline at end of file +CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint +CREATE TABLE `__drizzle_migrations` ( + +); + +*/ \ No newline at end of file diff --git a/drizzle/0001_lush_selene.sql b/drizzle/0001_lush_selene.sql new file mode 100644 index 0000000..eab6ade --- /dev/null +++ b/drizzle/0001_lush_selene.sql @@ -0,0 +1,18 @@ +DROP TABLE `__drizzle_migrations`;--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_users` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `username` text NOT NULL, + `email` text NOT NULL, + `hash` text NOT NULL, + `state` integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_users`("id", "username", "email", "hash", "state") SELECT "id", "username", "email", "hash", "state" FROM `users`;--> statement-breakpoint +DROP TABLE `users`;--> statement-breakpoint +ALTER TABLE `__new_users` RENAME TO `users`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint +CREATE UNIQUE INDEX `users_hash_unique` ON `users` (`hash`);--> statement-breakpoint +ALTER TABLE `users_data` ADD `signin` integer NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 351290c..38c7c47 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,64 +1,65 @@ { + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", "version": "6", "dialect": "sqlite", - "id": "ddf5d5b3-bf1e-4d8d-89cb-230f8e90137a", - "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "explorer_content": { "name": "explorer_content", "columns": { "path": { + "autoincrement": false, "name": "path", "type": "text", "primaryKey": true, - "notNull": true, - "autoincrement": false + "notNull": true }, "owner": { + "autoincrement": false, "name": "owner", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "title": { + "autoincrement": false, "name": "title", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "type": { + "autoincrement": false, "name": "type", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "content": { + "autoincrement": false, "name": "content", "type": "blob", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "navigable": { + "default": true, + "autoincrement": false, "name": "navigable", "type": "integer", "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": true + "notNull": false }, "private": { + "default": false, + "autoincrement": false, "name": "private", "type": "integer", "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": false + "notNull": false } }, + "compositePrimaryKeys": {}, "indexes": {}, "foreignKeys": { "explorer_content_owner_users_id_fk": { @@ -75,7 +76,6 @@ "onUpdate": "cascade" } }, - "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} }, @@ -83,25 +83,34 @@ "name": "user_sessions", "columns": { "id": { + "autoincrement": false, "name": "id", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "user_id": { + "autoincrement": false, "name": "user_id", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "timestamp": { + "autoincrement": false, "name": "timestamp", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true + } + }, + "compositePrimaryKeys": { + "user_sessions_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "user_sessions_id_user_id_pk" } }, "indexes": {}, @@ -120,15 +129,6 @@ "onUpdate": "cascade" } }, - "compositePrimaryKeys": { - "user_sessions_id_user_id_pk": { - "columns": [ - "id", - "user_id" - ], - "name": "user_sessions_id_user_id_pk" - } - }, "uniqueConstraints": {}, "checkConstraints": {} }, @@ -136,13 +136,14 @@ "name": "users_data", "columns": { "id": { + "autoincrement": false, "name": "id", "type": "integer", "primaryKey": true, - "notNull": true, - "autoincrement": false + "notNull": true } }, + "compositePrimaryKeys": {}, "indexes": {}, "foreignKeys": { "users_data_id_users_id_fk": { @@ -159,7 +160,6 @@ "onUpdate": "cascade" } }, - "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} }, @@ -167,47 +167,48 @@ "name": "users", "columns": { "id": { + "autoincrement": true, "name": "id", "type": "integer", "primaryKey": true, - "notNull": true, - "autoincrement": true + "notNull": true }, "username": { + "autoincrement": false, "name": "username", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "email": { + "autoincrement": false, "name": "email", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "hash": { + "autoincrement": false, "name": "hash", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "state": { + "default": 0, + "autoincrement": false, "name": "state", "type": "integer", "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": 0 + "notNull": false } }, + "compositePrimaryKeys": {}, "indexes": { - "users_username_unique": { - "name": "users_username_unique", + "users_hash_unique": { + "name": "users_hash_unique", "columns": [ - "username" + "hash" ], "isUnique": true }, @@ -218,16 +219,24 @@ ], "isUnique": true }, - "users_hash_unique": { - "name": "users_hash_unique", + "users_username_unique": { + "name": "users_username_unique", "columns": [ - "hash" + "username" ], "isUnique": true } }, "foreignKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "__drizzle_migrations": { + "name": "__drizzle_migrations", + "columns": {}, "compositePrimaryKeys": {}, + "indexes": {}, + "foreignKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} } @@ -238,8 +247,5 @@ "schemas": {}, "tables": {}, "columns": {} - }, - "internal": { - "indexes": {} } } \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..60b9c30 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,252 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0cca5664-1a0e-48ef-b70a-966b7a8142c7", + "prevId": "00000000-0000-0000-0000-000000000000", + "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": false, + "autoincrement": false, + "default": true + }, + "private": { + "name": "private", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 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_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 + } + }, + "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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 1da4d4d..b6090be 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,15 @@ { "idx": 0, "version": "6", - "when": 1730124775172, - "tag": "0000_youthful_ma_gnuci", + "when": 1730822816801, + "tag": "0000_lonely_the_renegades", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1730826510693, + "tag": "0001_lush_selene", "breakpoints": true } ] diff --git a/drizzle/relations.ts b/drizzle/relations.ts new file mode 100644 index 0000000..1574ff6 --- /dev/null +++ b/drizzle/relations.ts @@ -0,0 +1,29 @@ +import { relations } from "drizzle-orm/relations"; +import { users, explorerContent, userSessions, usersData } from "./schema"; + +export const explorerContentRelations = relations(explorerContent, ({one}) => ({ + user: one(users, { + fields: [explorerContent.owner], + references: [users.id] + }), +})); + +export const usersRelations = relations(users, ({many}) => ({ + explorerContents: many(explorerContent), + userSessions: many(userSessions), + usersData: many(usersData), +})); + +export const userSessionsRelations = relations(userSessions, ({one}) => ({ + user: one(users, { + fields: [userSessions.userId], + references: [users.id] + }), +})); + +export const usersDataRelations = relations(usersData, ({one}) => ({ + user: one(users, { + fields: [usersData.id], + references: [users.id] + }), +})); \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts new file mode 100644 index 0000000..2fa3065 --- /dev/null +++ b/drizzle/schema.ts @@ -0,0 +1,46 @@ +import { sqliteTable, AnySQLiteColumn, foreignKey, text, integer, blob, primaryKey, uniqueIndex } from "drizzle-orm/sqlite-core" + import { sql } from "drizzle-orm" + +export const explorerContent = sqliteTable("explorer_content", { + path: text().primaryKey().notNull(), + owner: integer().notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ), + title: text().notNull(), + type: text().notNull(), + content: blob(), + navigable: integer().default(true), + private: integer().default(false), +}); + +export const userSessions = sqliteTable("user_sessions", { + id: integer().notNull(), + userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ), + timestamp: integer().notNull(), +}, +(table) => { + return { + pk0: primaryKey({ columns: [table.id, table.userId], name: "user_sessions_id_user_id_pk"}) + } +}); + +export const usersData = sqliteTable("users_data", { + id: integer().primaryKey().notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ), +}); + +export const users = sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }).notNull(), + username: text().notNull(), + email: text().notNull(), + hash: text().notNull(), + state: integer().default(0), +}, +(table) => { + return { + hashUnique: uniqueIndex("users_hash_unique").on(table.hash), + emailUnique: uniqueIndex("users_email_unique").on(table.email), + usernameUnique: uniqueIndex("users_username_unique").on(table.username), + } +}); + +export const drizzleMigrations = sqliteTable("__drizzle_migrations", { +}); + diff --git a/layouts/default.vue b/layouts/default.vue index 0eba164..dfc2c8d 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -1,4 +1,59 @@ \ No newline at end of file + +
+
+ + + + Accueil +
+
+ + + +
+ +
+
+
+
+
+
+ +
+
+
+ + + +
+ + + +
+ +
+
+
+
+
+
+
+ Mentions légales +

Copyright Peaceultime - 2024

+
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/layouts/login.vue b/layouts/login.vue new file mode 100644 index 0000000..87676c6 --- /dev/null +++ b/layouts/login.vue @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/migrate.ts b/migrate.ts new file mode 100644 index 0000000..a0ee6e4 --- /dev/null +++ b/migrate.ts @@ -0,0 +1,7 @@ +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; +import { Database } from "bun:sqlite"; +import { drizzle } from "drizzle-orm/bun-sqlite"; + +const sqlite = new Database("db.sqlite"); +const db = drizzle(sqlite); +await migrate(db, { migrationsFolder: "./drizzle" }); \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index bd94dd9..42774c7 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -116,6 +116,12 @@ export default defineNuxtConfig({ tasks: true, }, }, + runtimeConfig: { + session: { + password: '699c46bd-9aaa-4364-ad01-510ee4fe7013' + }, + database: 'db.sqlite' + }, security: { rateLimiter: false, headers: { diff --git a/package.json b/package.json index 002d981..651235b 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,19 @@ "@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/tailwindcss": "^6.12.2", "@vueuse/nuxt": "^11.1.0", + "dotenv": "^16.4.5", "drizzle-orm": "^0.35.3", "nuxt": "^3.13.2", "nuxt-security": "^2.0.0", "radix-vue": "^1.9.8", "vue": "latest", - "vue-router": "latest" + "vue-router": "latest", + "zod": "^3.23.8" }, "devDependencies": { "@types/bun": "^1.1.12", "better-sqlite3": "^11.5.0", - "bun-types": "^1.1.33", + "bun-types": "^1.1.34", "drizzle-kit": "^0.26.2" } } diff --git a/pages/user/login.vue b/pages/user/login.vue index ccdba11..a797649 100644 --- a/pages/user/login.vue +++ b/pages/user/login.vue @@ -1,3 +1,86 @@ \ No newline at end of file + + Connexion + +
+ Connexion +
+ + + + Pas de compte ? + + +
+ + + \ No newline at end of file diff --git a/pages/user/register.vue b/pages/user/register.vue index ee39853..48d2bfb 100644 --- a/pages/user/register.vue +++ b/pages/user/register.vue @@ -1,3 +1,155 @@ \ No newline at end of file + + Inscription + +
+ Inscription +
+ + + +
+ Votre mot de passe doit respecter les critères de sécurité suivants + : + Entre 8 et 128 + caractères + Au moins + une minuscule et une majuscule + Au moins un + chiffre + Au moins un + caractère spécial parmis la liste suivante: +
! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~
+
+
+ + + Pas de compte ? + +
+ + + + + \ No newline at end of file diff --git a/schemas/login.ts b/schemas/login.ts new file mode 100644 index 0000000..c1db54c --- /dev/null +++ b/schemas/login.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const schema = z.object({ + usernameOrEmail: z.string({ required_error: "Nom d'utilisateur ou email obligatoire" }), + password: z.string({ required_error: "Mot de passe obligatoire" }), +}); + +export type Login = z.infer; \ No newline at end of file diff --git a/schemas/registration.ts b/schemas/registration.ts new file mode 100644 index 0000000..b9bba67 --- /dev/null +++ b/schemas/registration.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +function securePassword(password: string, ctx: z.RefinementCtx): void { + const lowercase = password.toLowerCase(); + const uppercase = password.toUpperCase(); + + if(lowercase === password) + { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Votre mot de passe doit contenir au moins une majuscule", + }); + } + if(uppercase === password) + { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Votre mot de passe doit contenir au moins une minuscule", + }); + } + if(!/[0-9]/.test(password)) + { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Votre mot de passe doit contenir au moins un chiffre", + }); + } + if(!" !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => password.includes(e))) + { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Votre mot de passe doit contenir au moins un symbole", + }); + } +} + +export const schema = z.object({ + username: z.string({ required_error: "Nom d'utilisateur obligatoire" }).min(3, "Votre nom d'utilisateur doit contenir au moins 3 caractères").max(32, "Votre nom d'utilisateur doit contenir au plus 32 caractères").superRefine((user, ctx) => { + const test = z.string().email().safeParse(user); + if(test.success) + { + ctx.addIssue({ + code: z.ZodIssueCode.invalid_string, + validation: 'email', + message: "Votre nom d'utilisateur ne peut pas être une addresse mail", + }); + } + }), + email: z.string({ required_error: "Email obligatoire" }).email("Adresse mail invalide"), + password: z.string({ required_error: "Mot de passe obligatoire" }).min(8, "Votre mot de passe doit contenir au moins 8 caractères").max(128, "Votre mot de passe doit contenir au moins 8 caractères").superRefine(securePassword), + data: z.object({ + + }).partial().nullish(), +}); + +export type Registration = z.infer; \ No newline at end of file diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts new file mode 100644 index 0000000..f85bbf1 --- /dev/null +++ b/server/api/auth/login.post.ts @@ -0,0 +1,105 @@ +import useDatabase from '~/composables/useDatabase'; +import { schema } from '~/schemas/login'; +import type { User, UserExtendedData, UserRawData, UserSession, UserSessionRequired } from '~/types/auth'; +import { ZodError } from 'zod'; +import { checkSession, logSession } from '~/server/utils/user'; +import { usersTable } from '~/db/schema'; +import { eq, or, sql } from 'drizzle-orm'; +import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite'; + +interface SuccessHandler +{ + success: true; + session: UserSession; +} +interface ErrorHandler +{ + success: false; + error: Error | ZodError<{ + usernameOrEmail: string; + password: string; + }>; +} +type Return = SuccessHandler | ErrorHandler; + +export default defineEventHandler(async (e): Promise => { + try + { + const session = await getUserSession(e); + const db = useDatabase(); + + const checkedSession = await checkSession(e, session); + + if(checkedSession !== undefined) + return checkedSession; + + const body = await readValidatedBody(e, schema.safeParse); + + if (!body.success) + { + await clearUserSession(e); + + setResponseStatus(e, 406); + return { success: false, error: body.error }; + } + + const hash = await Bun.password.hash(body.data.password); + const id = db.select({ id: usersTable.id, hash: usersTable.hash }).from(usersTable).where(or(eq(usersTable.username, sql.placeholder('username')), eq(usersTable.email, sql.placeholder('username')))).prepare().get({ username: body.data.usernameOrEmail }); + + if(!id || !id.id || !id.hash) + { + await clearUserSession(e); + + setResponseStatus(e, 401); + return { success: false, error: new ZodError([{ code: 'custom', path: ['username'], message: 'Identifiant inconnu' }]) }; + } + + const valid = await Bun.password.verify(body.data.password, id.hash); + + if(!valid) + { + await clearUserSession(e); + + setResponseStatus(e, 401); + return { success: false, error: new ZodError([{ code: 'custom', path: ['password'], message: 'Mot de passe incorrect' }]) }; + } + + const user = db.query.usersTable.findFirst({ + columns: { + id: true, + email: true, + username: true, + state: true, + }, + with: { + data: true, + }, + where: (table) => eq(table.id, sql.placeholder('id')) + }).prepare().get({ id: id.id }); + + if(!user) + { + setResponseStatus(e, 401); + return { success: false, error: new Error('Données utilisateur introuvable') }; + } + + const data = await logSession(e, await setUserSession(e, { + user: { + ...user.data, + email: user.email, + username: user.username, + state: user.state, + } + }) as UserSessionRequired); + + setResponseStatus(e, 201); + return { success: true, session: data }; + } + catch(err: any) + { + console.error(err); + + await clearUserSession(e); + return { success: false, error: err as Error }; + } +}); \ No newline at end of file diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts new file mode 100644 index 0000000..f082710 --- /dev/null +++ b/server/api/auth/register.post.ts @@ -0,0 +1,87 @@ +import { count, eq, sql } from 'drizzle-orm'; +import { ZodError, type ZodIssue } from 'zod'; +import useDatabase from '~/composables/useDatabase'; +import { usersDataTable, usersTable } from '~/db/schema'; +import { schema } from '~/schemas/registration'; +import { checkSession, logSession } from '~/server/utils/user'; +import type { UserSession, UserSessionRequired } from '~/types/auth'; + +interface SuccessHandler +{ + success: true; + session: UserSession; +} +interface ErrorHandler +{ + success: false; + error: Error | ZodError<{ + username: string; + email: string; + password: string; + }>; +} +type Return = SuccessHandler | ErrorHandler; + +export default defineEventHandler(async (e): Promise => { + try + { + const session = await getUserSession(e); + const db = useDatabase(); + + const checkedSession = await checkSession(e, session); + + if(checkedSession !== undefined) + return checkedSession; + + const body = await readValidatedBody(e, schema.safeParse); + + if (!body.success) + { + await clearUserSession(e); + + setResponseStatus(e, 406); + return { success: false, error: body.error }; + } + + const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username }); + const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email }); + + const errors: ZodIssue[] = []; + if(!checkUsername || checkUsername.count !== 0) + errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" }); + if(!checkEmail || checkEmail.count !== 0) + errors.push({ code: 'custom', path: ['email'], message: "Cette adresse mail est déjà utilisée" }); + + if(errors.length > 0) + { + setResponseStatus(e, 406); + return { success: false, error: new ZodError(errors) }; + } + else + { + const hash = await Bun.password.hash(body.data.password); + db.insert(usersTable).values({ username: sql.placeholder('username'), email: sql.placeholder('email'), hash: sql.placeholder('hash'), state: sql.placeholder('state') }).prepare().run({ username: body.data.username, email: body.data.email, hash, state: 0 }); + const id = db.select({ id: usersTable.id }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username }); + + if(!id || !id.id) + { + setResponseStatus(e, 406); + return { success: false, error: new Error('Erreur de création de compte') }; + } + + db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id }); + + logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date() } }) as UserSessionRequired); + + setResponseStatus(e, 201); + return { success: true, session }; + } + } + catch(err: any) + { + console.error(err); + + await clearUserSession(e); + return { success: false, error: err as Error }; + } +}); \ No newline at end of file diff --git a/server/api/auth/session.delete.ts b/server/api/auth/session.delete.ts new file mode 100644 index 0000000..a21b435 --- /dev/null +++ b/server/api/auth/session.delete.ts @@ -0,0 +1,8 @@ +import { eventHandler } from 'h3'; +import { clearUserSession } from '~/server/utils/session'; + +export default eventHandler(async (event) => { + await clearUserSession(event); + + return { loggedOut: true }; +}) \ No newline at end of file diff --git a/server/api/auth/session.get.ts b/server/api/auth/session.get.ts new file mode 100644 index 0000000..a7245a9 --- /dev/null +++ b/server/api/auth/session.get.ts @@ -0,0 +1,13 @@ +import { eventHandler } from 'h3' +import { getUserSession, sessionHooks } from '~/server/utils/session' +import type { UserSessionRequired } from '~/types/auth' + +export default eventHandler(async (event) => { + const session = await getUserSession(event) + + if (session.user) { + await sessionHooks.callHookParallel('fetch', session as UserSessionRequired, event) + } + + return session +}) \ No newline at end of file diff --git a/server/api/project.get.ts b/server/api/project.get.ts new file mode 100644 index 0000000..1d63086 --- /dev/null +++ b/server/api/project.get.ts @@ -0,0 +1,31 @@ +import useDatabase from '~/composables/useDatabase'; +import { ProjectSearch } from '~/types/api'; + +export default defineEventHandler(async (e) => { + const query = getQuery(e); + + const where = ["f.type != $type"]; + const criteria: Record = { $type: "Folder" }; + + if(query && query.owner !== undefined) + { + where.push("owner = $owner"); + criteria["$owner"] = query.owner; + } + if(query && query.name !== undefined) + { + where.push("name = $name"); + criteria["$name"] = query.name; + } + + const db = useDatabase(); + + const content = db.query(`SELECT p.*, u.username, COUNT(f.path) as pages FROM explorer_projects p LEFT JOIN users u ON p.owner = u.id LEFT JOIN explorer_files f ON f.project = p.id WHERE ${where.join(" AND ")} GROUP BY p.id`).all(criteria) as ProjectSearch[]; + + if(content.length > 0) + { + return content; + } + + setResponseStatus(e, 404); +}); \ No newline at end of file diff --git a/server/api/project/[projectId].get.ts b/server/api/project/[projectId].get.ts new file mode 100644 index 0000000..35af39f --- /dev/null +++ b/server/api/project/[projectId].get.ts @@ -0,0 +1,20 @@ +import useDatabase from '~/composables/useDatabase'; + +export default defineEventHandler(async (e) => { + const project = getRouterParam(e, "projectId"); + + const where = ["id = $id"]; + const criteria: Record = { $id: project }; + + if (!!project) { + const db = useDatabase(); + + const content = db.query(`SELECT * FROM explorer_projects WHERE ${where.join(" and ")}`).get(criteria) as Project; + + if (content) { + return content; + } + } + + setResponseStatus(e, 404); +}); \ No newline at end of file diff --git a/server/api/project/[projectId].patch.ts b/server/api/project/[projectId].patch.ts new file mode 100644 index 0000000..82694f5 --- /dev/null +++ b/server/api/project/[projectId].patch.ts @@ -0,0 +1,20 @@ +import useDatabase from '~/composables/useDatabase'; + +export default defineEventHandler(async (e) => { + const project = getRouterParam(e, "projectId"); + + const where = ["project = $project"]; + const criteria: Record = { $project: project }; + + if (!!project) { + const db = useDatabase(); + + const content = db.query(`SELECT * FROM explorer_projects WHERE ${where.join(" and ")}`).all(criteria) as Project[]; + + if (content.length > 0) { + return content; + } + } + + setResponseStatus(e, 404); +}); \ No newline at end of file diff --git a/server/api/project/[projectId].post.ts b/server/api/project/[projectId].post.ts new file mode 100644 index 0000000..82694f5 --- /dev/null +++ b/server/api/project/[projectId].post.ts @@ -0,0 +1,20 @@ +import useDatabase from '~/composables/useDatabase'; + +export default defineEventHandler(async (e) => { + const project = getRouterParam(e, "projectId"); + + const where = ["project = $project"]; + const criteria: Record = { $project: project }; + + if (!!project) { + const db = useDatabase(); + + const content = db.query(`SELECT * FROM explorer_projects WHERE ${where.join(" and ")}`).all(criteria) as Project[]; + + if (content.length > 0) { + return content; + } + } + + setResponseStatus(e, 404); +}); \ No newline at end of file diff --git a/server/api/project/[projectId]/access.post.ts b/server/api/project/[projectId]/access.post.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/api/project/[projectId]/comment.post.ts b/server/api/project/[projectId]/comment.post.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/api/project/[projectId]/file.get.ts b/server/api/project/[projectId]/file.get.ts new file mode 100644 index 0000000..26db9fb --- /dev/null +++ b/server/api/project/[projectId]/file.get.ts @@ -0,0 +1,54 @@ +import useDatabase from '~/composables/useDatabase'; +import type { File } from '~/types/api'; + +export default defineCachedEventHandler(async (e) => { + const project = getRouterParam(e, "projectId"); + const query = getQuery(e); + + if(!project) + { + setResponseStatus(e, 404); + return; + } + + const where = ["project = $project"]; + const criteria: Record = { $project: project }; + + if(query && query.path !== undefined) + { + where.push("path = $path"); + criteria["$path"] = query.path; + } + if(query && query.title !== undefined) + { + where.push("title = $title"); + criteria["$title"] = query.title; + } + if(query && query.type !== undefined) + { + where.push("type = $type"); + criteria["$type"] = query.type; + } + if (query && query.search !== undefined) + { + where.push("path LIKE $search"); + criteria["$search"] = query.search; + } + + if(where.length > 1) + { + const db = useDatabase(); + + const content = db.query(`SELECT * FROM explorer_files WHERE ${where.join(" and ")}`).all(criteria) as File[]; + + if(content.length > 0) + { + return content; + } + } + + setResponseStatus(e, 404); +}, { + maxAge: 60*60*24, + getKey: (e) => `${getRouterParam(e, "projectId")}-${JSON.stringify(getQuery(e))}` + }); \ No newline at end of file diff --git a/server/api/project/[projectId]/file.post.ts b/server/api/project/[projectId]/file.post.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/api/project/[projectId]/file/[path].get.ts b/server/api/project/[projectId]/file/[path].get.ts new file mode 100644 index 0000000..a03e65f --- /dev/null +++ b/server/api/project/[projectId]/file/[path].get.ts @@ -0,0 +1,41 @@ +import useDatabase from '~/composables/useDatabase'; +import type { CommentedFile, CommentSearch, File } from '~/types/api'; + +export default defineCachedEventHandler(async (e) => { + const project = getRouterParam(e, "projectId"); + const path = decodeURIComponent(getRouterParam(e, "path") ?? ''); + + if(!project) + { + setResponseStatus(e, 404); + return; + } + if(!path) + { + setResponseStatus(e, 404); + return; + } + + const where = ["project = $project", "path = $path"]; + const criteria: Record = { $project: project, $path: path }; + + if(where.length > 1) + { + const db = useDatabase(); + + const content = db.query(`SELECT * FROM explorer_files WHERE ${where.join(" and ")}`).get(criteria) as File; + + if(content !== undefined) + { + const comments = db.query(`SELECT comment.*, user.username FROM explorer_comments comment LEFT JOIN users user ON comment.user_id = user.id WHERE ${where.join(" and ")}`).all(criteria) as CommentSearch[]; + + return { ...content, comments } as CommentedFile; + } + } + + setResponseStatus(e, 404); + return; +}, { + maxAge: 60*60*24, + getKey: (e) => `file-${getRouterParam(e, "projectId")}-${getRouterParam(e, "path")}` + }); \ No newline at end of file diff --git a/server/api/project/[projectId]/navigation.get.ts b/server/api/project/[projectId]/navigation.get.ts new file mode 100644 index 0000000..0de8f58 --- /dev/null +++ b/server/api/project/[projectId]/navigation.get.ts @@ -0,0 +1,72 @@ +import useDatabase from '~/composables/useDatabase'; +import { Navigation } from '~/types/api'; + +type NavigatioNExtension = Navigation & { owner: number, navigable: boolean }; + +export default defineEventHandler(async (e) => { + const project = getRouterParam(e, "projectId"); + const { user } = await getUserSession(e); + + if(!project) + { + setResponseStatus(e, 404); + return; + } + + const db = useDatabase(); + + const content = db.query(`SELECT "path", "title", "type", "order", "private", "navigable", "owner" FROM explorer_files WHERE project = ?1`).all(project!).sort((a: any, b: any) => a.path.length - b.path.length) as NavigatioNExtension[]; + + if(content.length > 0) + { + const navigation: Navigation[] = []; + + for(const idx in content) + { + const item = content[idx]; + if(!!item.private && (user?.id ?? -1) !== item.owner || !item.navigable) + { + delete content[idx]; + continue; + } + + const parent = item.path.includes('/') ? item.path.substring(0, item.path.lastIndexOf('/')) : undefined; + + if(parent && !content.find(e => e && e.path === parent)) + { + delete content[idx]; + continue; + } + } + for(const item of content.filter(e => !!e)) + { + addChild(navigation, item); + } + + return navigation; + } + + setResponseStatus(e, 404); +}); + +function addChild(arr: Navigation[], e: Navigation): void +{ + const parent = arr.find(f => e.path.startsWith(f.path)); + + if(parent) + { + if(!parent.children) + parent.children = []; + + addChild(parent.children, e); + } + else + { + arr.push({ title: e.title, path: e.path, type: e.type, order: e.order, private: e.private }); + arr.sort((a, b) => { + if(a.order && b.order) + return a.order - b.order; + return a.title.localeCompare(b.title); + }); + } +} \ No newline at end of file diff --git a/server/api/project/[projectId]/tags/[tag].get.ts b/server/api/project/[projectId]/tags/[tag].get.ts new file mode 100644 index 0000000..4b9f359 --- /dev/null +++ b/server/api/project/[projectId]/tags/[tag].get.ts @@ -0,0 +1,42 @@ +import useDatabase from '~/composables/useDatabase'; +import type { Tag } from '~/types/api'; + +export default defineCachedEventHandler(async (e) => { + try + { + const project = getRouterParam(e, "projectId"); + const tag = decodeURIComponent(getRouterParam(e, "tag") ?? ''); + + if(!project) + { + setResponseStatus(e, 404); + return; + } + if(!tag) + { + setResponseStatus(e, 404); + return; + } + + const where = ["project = $project", "tag = $tag"]; + const criteria: Record = { $project: project, $tag: tag }; + + const db = useDatabase(); + const content = db.query(`SELECT * FROM explorer_tags WHERE ${where.join(" and ")}`).get(criteria) as Tag; + + if(content !== undefined) + { + return content; + } + + setResponseStatus(e, 404); + } + catch(err) + { + console.error(err); + setResponseStatus(e, 500); + } +}, { + maxAge: 60*60*24, + getKey: (e) => `tag-${getRouterParam(e, "projectId")}-${getRouterParam(e, "tag")}` + }); \ No newline at end of file diff --git a/server/api/search.get.ts b/server/api/search.get.ts new file mode 100644 index 0000000..265b3c1 --- /dev/null +++ b/server/api/search.get.ts @@ -0,0 +1,21 @@ +import useDatabase from '~/composables/useDatabase'; + +export default defineEventHandler(async (e) => { + const query = getQuery(e); + + if (query.search) { + const db = useDatabase(); + + const projects = db.query(`SELECT p.*, u.username, COUNT(f.path) as pages FROM explorer_projects p LEFT JOIN users u ON p.owner = u.id LEFT JOIN explorer_files f ON f.project = p.id WHERE name LIKE ?1 AND f.type != "Folder" GROUP BY p.id`).all(query.search) as ProjectSearch[]; + const files = db.query(`SELECT f.*, u.username, count(c.path) as comments FROM explorer_files f LEFT JOIN users u ON f.owner = u.id LEFT JOIN explorer_comments c ON c.project = f.project AND c.path = f.path WHERE title LIKE ?1 AND private = 0 AND type != "Folder" GROUP BY f.project, f.path`).all(query.search) as FileSearch[]; + const users = db.query(`SELECT id, username FROM users WHERE username LIKE ?1`).all(query.search) as UserSearch[]; + + return { + projects, + files, + users + } as Search; + } + + setResponseStatus(e, 404); +}); \ No newline at end of file diff --git a/server/api/users/[id].get.ts b/server/api/users/[id].get.ts new file mode 100644 index 0000000..1e65580 --- /dev/null +++ b/server/api/users/[id].get.ts @@ -0,0 +1,16 @@ +import useDatabase from "~/composables/useDatabase"; +import type { User } from "~/types/auth"; + +export default defineEventHandler((e) => { + const id = getRouterParam(e, 'id'); + + if(!id) + { + setResponseStatus(e, 400); + return; + } + + const db = useDatabase(); + + return db.query(`SELECT id, username, email, state, d.* FROM users u LEFT JOIN users_data d ON u.id = d.user_id WHERE u.id = ?1`).get(id) as User; +}); \ No newline at end of file diff --git a/server/api/users/[id]/comments.get.ts b/server/api/users/[id]/comments.get.ts new file mode 100644 index 0000000..dd2956b --- /dev/null +++ b/server/api/users/[id]/comments.get.ts @@ -0,0 +1,16 @@ +import useDatabase from "~/composables/useDatabase"; +import type { CommentSearch } from "~/types/api"; + +export default defineEventHandler((e) => { + const id = getRouterParam(e, 'id'); + + if(!id) + { + setResponseStatus(e, 400); + return; + } + + const db = useDatabase(); + + return db.query(`SELECT * FROM explorer_comments WHERE user_id = ?1`).all(id) as CommentSearch[]; +}); \ No newline at end of file diff --git a/server/api/users/[id]/projects.get.ts b/server/api/users/[id]/projects.get.ts new file mode 100644 index 0000000..74b3665 --- /dev/null +++ b/server/api/users/[id]/projects.get.ts @@ -0,0 +1,16 @@ +import useDatabase from "~/composables/useDatabase"; +import type { ProjectSearch } from "~/types/api"; + +export default defineEventHandler((e) => { + const id = getRouterParam(e, 'id'); + + if(!id) + { + setResponseStatus(e, 400); + return; + } + + const db = useDatabase(); + + return db.query(`SELECT p.*, count(f.path) as pages FROM explorer_projects p LEFT JOIN explorer_files f ON p.id = f.project WHERE p.owner = ?1`).all(id) as Omit[]; +}); \ No newline at end of file diff --git a/server/plugins/session.ts b/server/plugins/session.ts new file mode 100644 index 0000000..600ce9a --- /dev/null +++ b/server/plugins/session.ts @@ -0,0 +1,45 @@ +import useDatabase from "~/composables/useDatabase"; +import { userSessionsTable as sessions } from "~/db/schema"; +import { eq, and } from "drizzle-orm"; + +const monthAsMs = 60 * 60 * 24 * 30 * 1000; + +export default defineNitroPlugin(() => { + const db = useDatabase(); + + sessionHooks.hook('fetch', async (session, event) => { + const result = await db.query.userSessionsTable.findFirst({ + columns: { + timestamp: true, + }, + where: and(eq(sessions.id, session.id as unknown as number), eq(sessions.user_id, session.user.id)) + }); + + if(!result) + { + await clearUserSession(event); + throw createError({ statusCode: 401, message: 'Unauthorized' }); + } + else if(result && result.timestamp && result.timestamp.getTime() < Date.now() - monthAsMs) + { + await clearUserSession(event); + throw createError({ statusCode: 401, message: 'Session has expired' }); + } + else + { + await db.update(sessions).set({ + timestamp: new Date(), + }).where(and(eq(sessions.id, session.id as unknown as number), eq(sessions.user_id, session.user.id))); + } + }); + sessionHooks.hook('clear', async (session, event) => { + if(session.id && session.user) + { + try + { + await db.delete(sessions).where(and(eq(sessions.id, session.id as unknown as number), eq(sessions.user_id, session.user.id))); + } + catch(e) { } + } + }); +}); \ No newline at end of file diff --git a/server/tasks/sync.ts b/server/tasks/sync.ts new file mode 100644 index 0000000..3354a2f --- /dev/null +++ b/server/tasks/sync.ts @@ -0,0 +1,164 @@ +import useDatabase from "~/composables/useDatabase"; +import { extname, basename } from 'node:path'; +import type { File, FileType, Tag } from '~/types/api'; +import type { CanvasColor, CanvasContent } from "~/types/canvas"; + +const typeMapping: Record = { + ".md": "Markdown", + ".canvas": "Canvas" +}; + +export default defineTask({ + meta: { + name: 'sync', + description: 'Synchronise the project 1 with Obsidian', + }, + async run(event) { + /* try { + const tree = await $fetch('https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/git/trees/master', { + method: 'get', + headers: { + accept: 'application/json', + }, + params: { + recursive: true, + per_page: 1000, + } + }) as any; + + const files: File[] = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => { + if(e.type === 'tree') + { + const title = basename(e.path); + const order = /(\d+)\. ?(.+)/gsmi.exec(title); + const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/'); + return { + path: path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""), + order: order && order[1] ? order[1] : 50, + title: order && order[2] ? order[2] : title, + type: 'Folder', + content: null + } + } + + const extension = extname(e.path); + const title = basename(e.path, extension); + const order = /(\d+)\. ?(.+)/gsmi.exec(title); + const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/'); + const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`)); + + return { + path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""), + order: order && order[1] ? order[1] : 50, + title: order && order[2] ? order[2] : title, + type: (typeMapping[extension] ?? 'File'), + content: reshapeContent(content as string, typeMapping[extension] ?? 'File') + } + })); + + let tags: Tag[] = []; + const tagFile = files.find(e => e.path === "tags"); + + if(tagFile) + { + const parsed = useMarkdown()(tagFile.content); + const titles = parsed.children.filter(e => e.type === 'element' && e.tagName.match(/h\d/)); + for(let i = 0; i < titles.length; i++) + { + const start = titles[i].position?.start.offset ?? 0; + const end = titles.length === i + 1 ? tagFile.content.length : titles[i + 1].position.start.offset - 1; + tags.push({ tag: titles[i].properties.id, description: tagFile.content.substring(titles[i].position?.end.offset + 1, end) }); + } + } + + const db = useDatabase(); + + const oldFiles = db.prepare(`SELECT * FROM explorer_files WHERE project = ?1`).all('1') as File[]; + const removeFiles = db.prepare(`DELETE FROM explorer_files WHERE project = ?1 AND path = ?2`); + db.transaction((data: File[]) => data.forEach(e => removeFiles.run('1', e.path)))(oldFiles.filter(e => !files.find(f => f.path = e.path))); + removeFiles.finalize(); + + const oldTags = db.prepare(`SELECT * FROM explorer_tags WHERE project = ?1`).all('1') as Tag[]; + const removeTags = db.prepare(`DELETE FROM explorer_tags WHERE project = ?1 AND tag = ?2`); + db.transaction((data: Tag[]) => data.forEach(e => removeTags.run('1', e.tag)))(oldTags.filter(e => !tags.find(f => f.tag = e.tag))); + removeTags.finalize(); + + const insertFiles = db.prepare(`INSERT INTO explorer_files("project", "path", "owner", "title", "order", "type", "content") VALUES (1, $path, 1, $title, $order, $type, $content)`); + const updateFiles = db.prepare(`UPDATE explorer_files SET content = $content WHERE project = 1 AND path = $path`); + db.transaction((content) => { + for (const item of content) { + let order = item.order; + + if (typeof order === 'string') + order = parseInt(item.order, 10); + + if (isNaN(order)) + order = 999; + + if(oldFiles.find(e => item.path === e.path)) + updateFiles.run({ $path: item.path, $content: item.content }); + else + insertFiles.run({ $path: item.path, $title: item.title, $type: item.type, $content: item.content, $order: order }); + } + })(files); + + insertFiles.finalize(); + updateFiles.finalize(); + + const insertTags = db.prepare(`INSERT INTO explorer_tags("project", "tag", "description") VALUES (1, $tag, $description)`); + const updateTags = db.prepare(`UPDATE explorer_tags SET description = $description WHERE project = 1 AND tag = $tag`); + db.transaction((content) => { + for (const item of content) { + if (oldTags.find(e => item.tag === e.tag)) + updateTags.run({ $tag: item.tag, $description: item.description }); + else + insertTags.run({ $tag: item.tag, $description: item.description }); + } + })(tags); + + insertTags.finalize(); + updateTags.finalize(); + + useStorage('cache').clear(); + + return { result: true }; + } + catch(e) + { + return { result: false }; + } */ + }, +}) + +function reshapeContent(content: string, type: FileType): string | null +{ + switch(type) + { + case "Markdown": + case "File": + return content; + case "Canvas": + const data = JSON.parse(content) as CanvasContent; + data.edges.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined); + data.nodes.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined); + return JSON.stringify(data); + default: + case 'Folder': + return null; + } +} +function getColor(color: string): CanvasColor +{ + const colors: Record = { + '1': 'red', + '2': 'orange', + '3': 'yellow', + '4': 'green', + '5': 'cyan', + '6': 'purple', + }; + if(colors.hasOwnProperty(color)) + return { class: colors[color] }; + else + return { hex: color }; +} \ No newline at end of file diff --git a/server/tsconfig.json b/server/tsconfig.json index b9ed69c..1b107a3 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,3 +1,7 @@ { - "extends": "../.nuxt/tsconfig.server.json" -} + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "../.nuxt/tsconfig.json", + "compilerOptions": { + "types": ["bun-types"], + } +} \ No newline at end of file diff --git a/server/utils/session.ts b/server/utils/session.ts new file mode 100644 index 0000000..6136a72 --- /dev/null +++ b/server/utils/session.ts @@ -0,0 +1,110 @@ +import type { H3Event, SessionConfig } from 'h3' +import { useSession, createError } from 'h3' +import { defu } from 'defu' +import { createHooks } from 'hookable' +import { useRuntimeConfig } from '#imports' +import type { UserSession, UserSessionRequired } from '~/types/auth' + +export interface SessionHooks { + /** + * Called when fetching the session from the API + * - Add extra properties to the session + * - Throw an error if the session could not be verified (with a database for example) + */ + fetch: (session: UserSessionRequired, event: H3Event) => void | Promise + /** + * Called before clearing the session + */ + clear: (session: UserSession, event: H3Event) => void | Promise +} + +export const sessionHooks = createHooks() + +/** + * Get the user session from the current request + * @param event The Request (h3) event + * @returns The user session + */ +export async function getUserSession(event: H3Event) { + const session = await _useSession(event); + + if(!session.data || !session.data.id) + { + await session.update(defu({ id: session.id }, session.data)); + } + + return session.data; +} +/** + * Set a user session + * @param event The Request (h3) event + * @param data User session data, please only store public information since it can be decoded with API calls + * @see https://github.com/atinux/nuxt-auth-utils + */ +export async function setUserSession(event: H3Event, data: UserSession) { + const session = await _useSession(event) + + await session.update(defu(data, session.data)) + + return session.data +} + +/** + * Replace a user session + * @param event The Request (h3) event + * @param data User session data, please only store public information since it can be decoded with API calls + */ +export async function replaceUserSession(event: H3Event, data: UserSession) { + const session = await _useSession(event) + + await session.clear() + await session.update(data) + + return session.data +} + +/** + * Clear the user session and removing the session cookie + * @param event The Request (h3) event + * @returns true if the session was cleared + */ +export async function clearUserSession(event: H3Event) { + const session = await _useSession(event) + + await sessionHooks.callHookParallel('clear', session.data, event) + await session.clear() + + return true +} + +/** + * Require a user session, throw a 401 error if the user is not logged in + * @param event + * @param opts Options to customize the error message and status code + * @param opts.statusCode The status code to use for the error (defaults to 401) + * @param opts.message The message to use for the error (defaults to Unauthorized) + * @see https://github.com/atinux/nuxt-auth-utils + */ +export async function requireUserSession(event: H3Event, opts: { statusCode?: number, message?: string } = {}): Promise { + const userSession = await getUserSession(event) + + if (!userSession.user) { + throw createError({ + statusCode: opts.statusCode || 401, + message: opts.message || 'Unauthorized', + }) + } + + return userSession as UserSessionRequired +} + +let sessionConfig: SessionConfig + +function _useSession(event: H3Event) { + if (!sessionConfig) { + const runtimeConfig = useRuntimeConfig(event) + + sessionConfig = runtimeConfig.session; + } + return useSession(event, sessionConfig) +} \ No newline at end of file diff --git a/server/utils/user.ts b/server/utils/user.ts new file mode 100644 index 0000000..4dd9a4d --- /dev/null +++ b/server/utils/user.ts @@ -0,0 +1,34 @@ +import { eq, sql, and } from "drizzle-orm"; +import useDatabase from "~/composables/useDatabase"; +import { userSessionsTable } from "~/db/schema"; +import type { Return } from "~/types/api"; +import type { UserSession, UserSessionRequired } from "~/types/auth"; + +export function checkSession(e: H3Event, session: UserSession): Return | undefined +{ + const db = useDatabase(); + + if(session.id && session.user?.id) + { + const sessionId = db.select({ user_id: userSessionsTable.user_id }).from(userSessionsTable).where(and(eq(userSessionsTable.id, sql.placeholder('id')), eq(userSessionsTable.user_id, sql.placeholder('user_id')))).prepare().get({ id: session.id, user_id: session.user.id }) + + if(sessionId && sessionId.user_id === session.user?.id) + { + return { success: true, session }; + } + else + { + clearUserSession(e); + + setResponseStatus(e, 406); + return { success: false, error: new Error('Vous êtes déjà connecté') }; + } + } +} +export function logSession(e: H3Event, session: UserSessionRequired): UserSessionRequired +{ + const db = useDatabase(); + + db.insert(userSessionsTable).values({ id: sql.placeholder('id'), user_id: sql.placeholder('user_id'), timestamp: sql.placeholder('timestamp') }).prepare().execute({ id: session.id, user_id: session.user.id, timestamp: new Date()}); + return session; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a746f2a..a2787e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,7 @@ { // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json" -} + "extends": "./.nuxt/tsconfig.json", + "compilerOptions": { + "types": ["bun-types"], + } +} \ No newline at end of file diff --git a/types/api.d.ts b/types/api.d.ts new file mode 100644 index 0000000..e79d033 --- /dev/null +++ b/types/api.d.ts @@ -0,0 +1,87 @@ +export interface SuccessHandler +{ + success: true; + session: UserSession; +} +export interface ErrorHandler +{ + success: false; + error: Error | ZodError; +} +export type Return = SuccessHandler | ErrorHandler; + +export interface Project { + id: number; + name: string; + owner: number; + home: string; + summary: string; +} +export interface Navigation { + title: string; + path: string; + type: string; + order: number; + private: boolean; + children?: Navigation[]; +} +export type FileMetadata = Record; +export type FileType = 'Markdown' | 'Canvas' | 'File' | 'Folder'; +export interface File { + project: number; + path: string; + owner: number; + title: string; + order: number; + type: FileType; + content: string; + navigable: boolean; + private: boolean; + metadata: FileMetadata; +} +export interface Comment { + project: number; + path: number; + user_id: number; + sequence: number; + position: number; + length: number; + content: string; +} +export interface User { + id: number; + username: string; +} +export interface Tag { + tag: string; + project: number; + description: string; +} + + +export type ProjectSearch = Project & +{ + pages: number; + username: string; +} +export type FileSearch = Omit & +{ + comments: number; + username: string; +} +export type CommentSearch = Comment & +{ + username: string; +} +export type UserSearch = User & +{ +} +export type CommentedFile = File & +{ + comments: CommentSearch[]; +} +export interface Search { + projects: ProjectSearch[]; + files: FileSearch[]; + users: UserSearch[]; +} \ No newline at end of file diff --git a/types/auth.d.ts b/types/auth.d.ts new file mode 100644 index 0000000..8361487 --- /dev/null +++ b/types/auth.d.ts @@ -0,0 +1,71 @@ +import type { ComputedRef, Ref } from 'vue' + +import 'vue-router'; +declare module 'vue-router' +{ + interface RouteMeta + { + requiresAuth?: boolean; + guestsGoesTo?: string; + usersGoesTo?: string; + } +} + +import 'nuxt'; +declare module 'nuxt' +{ + interface RuntimeConfig + { + session: SessionConfig; + } +} + +export interface UserRawData { + id: number; + username: string; + email: string; + state: number; +} + +export interface UserExtendedData { + signin: Date; +} + +export type User = UserRawData & UserExtendedData; + +export interface UserSession { + user?: User; + id?: string; +} + +export interface UserSessionRequired extends UserSession { + user: User; + id: string; +} + +export interface UserSessionComposable { + /** + * Computed indicating if the auth session is ready + */ + ready: ComputedRef + /** + * Computed indicating if the user is logged in. + */ + loggedIn: ComputedRef + /** + * The user object if logged in, null otherwise. + */ + user: ComputedRef + /** + * The session object. + */ + session: Ref + /** + * Fetch the user session from the server. + */ + fetch: () => Promise + /** + * Clear the user session and remove the session cookie. + */ + clear: () => Promise +} \ No newline at end of file diff --git a/types/canvas.d.ts b/types/canvas.d.ts new file mode 100644 index 0000000..a126928 --- /dev/null +++ b/types/canvas.d.ts @@ -0,0 +1,34 @@ +export interface CanvasContent { + nodes: CanvasNode[]; + edges: CanvasEdge[]; + groups: CanvasGroup[]; +} +export type CanvasColor = { + class?: string; +} & { + hex?: string; +} +export interface CanvasNode { + type: 'group' | 'text'; + id: string; + x: number; + y: number; + width: number; + height: number; + color?: CanvasColor; + label?: string; + text?: any; +}; +export interface CanvasEdge { + id: string; + fromNode: string; + fromSide: 'bottom' | 'top' | 'left' | 'right'; + toNode: string; + toSide: 'bottom' | 'top' | 'left' | 'right'; + color?: CanvasColor; + label?: string; +}; +export interface CanvasGroup { + name: string; + nodes: string[]; +} \ No newline at end of file