diff --git a/bun.lockb b/bun.lockb index 969a46f..b030e64 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/composables/useDatabase.ts b/composables/useDatabase.ts index af7fd46..124b78c 100644 --- a/composables/useDatabase.ts +++ b/composables/useDatabase.ts @@ -7,7 +7,8 @@ export default function useDatabase() { if(!instance) { - const sqlite = new Database(useRuntimeConfig().database); + const database = useRuntimeConfig().database; + const sqlite = new Database(database); instance = drizzle({ client: sqlite, schema }); instance.run("PRAGMA journal_mode = WAL;"); diff --git a/db.sqlite b/db.sqlite index de49dc7..658ba1b 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-shm b/db.sqlite-shm index df4d197..e58aa6b 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index 8502f26..e90d54e 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/db/schema.ts b/db/schema.ts index 0f72ba1..897541c 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -26,10 +26,10 @@ export const userSessionsTable = sqliteTable("user_sessions", { export const userPermissionsTable = sqliteTable("user_permissions", { id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), - permissions: text().notNull(), + permission: text().notNull(), }, (table): SQLiteTableExtraConfig => { return { - pk: primaryKey({ columns: [table.id, table.permissions] }), + pk: primaryKey({ columns: [table.id, table.permission] }), } }); @@ -43,16 +43,16 @@ export const explorerContentTable = sqliteTable("explorer_content", { private: int({ mode: 'boolean' }).default(false), }); -export const usersRelation = relations(usersTable, ({one, many}) => ({ +export const usersRelation = relations(usersTable, ({ one, many }) => ({ data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }), session: many(userSessionsTable), permission: many(userPermissionsTable), content: many(explorerContentTable), })); -export const usersDataRelation = relations(usersDataTable, ({one}) => ({ +export const usersDataRelation = relations(usersDataTable, ({ one }) => ({ users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }), })); -export const userSessionsRelation = relations(userSessionsTable, ({one}) => ({ +export const userSessionsRelation = relations(userSessionsTable, ({ one }) => ({ users: one(usersTable, { fields: [userSessionsTable.user_id], references: [usersTable.id], }), })); export const userPermissionsRelation = relations(userPermissionsTable, ({ one }) => ({ diff --git a/drizzle/0002_messy_solo.sql b/drizzle/0002_messy_solo.sql new file mode 100644 index 0000000..c6502ea --- /dev/null +++ b/drizzle/0002_messy_solo.sql @@ -0,0 +1,12 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_user_permissions` ( + `id` integer NOT NULL, + `permission` text NOT NULL, + PRIMARY KEY(`id`, `permission`), + FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_user_permissions`("id", "permission") SELECT "id", "permission" FROM `user_permissions`;--> statement-breakpoint +DROP TABLE `user_permissions`;--> statement-breakpoint +ALTER TABLE `__new_user_permissions` RENAME TO `user_permissions`;--> statement-breakpoint +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..dfe0c7c --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,300 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6da7ff20-0db8-4055-a353-bb0ea2fa5e0b", + "prevId": "854cab71-b937-4f4f-80b0-cbb09c7b5944", + "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_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 + } + }, + "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": { + "\"user_permissions\".\"permissions\"": "\"user_permissions\".\"permission\"" + } + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 9d07fdf..614b26a 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1730832678255, "tag": "0001_sticky_jack_flag", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1730985155814, + "tag": "0002_messy_solo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/relations.ts b/drizzle/relations.ts new file mode 100644 index 0000000..08e8ed3 --- /dev/null +++ b/drizzle/relations.ts @@ -0,0 +1,37 @@ +import { relations } from "drizzle-orm/relations"; +import { users, explorerContent, userSessions, usersData, userPermissions } 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), + userPermissions: many(userPermissions), +})); + +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] + }), +})); + +export const userPermissionsRelations = relations(userPermissions, ({one}) => ({ + user: one(users, { + fields: [userPermissions.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..a9b4539 --- /dev/null +++ b/drizzle/schema.ts @@ -0,0 +1,57 @@ +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" } ), + signin: integer().notNull(), +}); + +export const users = sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }).notNull(), + username: text().notNull(), + email: text().notNull(), + hash: text().notNull(), + state: integer().default(0).notNull(), +}, +(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 userPermissions = sqliteTable("user_permissions", { + id: integer().notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ), + permissions: text().notNull(), +}, +(table) => { + return { + pk0: primaryKey({ columns: [table.id, table.permissions], name: "user_permissions_id_permissions_pk"}) + } +}); + +export const drizzleMigrations = sqliteTable("__drizzle_migrations", { +}); + diff --git a/middleware/auth.global.ts b/middleware/auth.global.ts index b302d00..6dab773 100644 --- a/middleware/auth.global.ts +++ b/middleware/auth.global.ts @@ -1,9 +1,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => { - const { loggedIn, ready, fetch } = useUserSession(); + const { loggedIn, fetch, user } = useUserSession(); const meta = to.meta; - if(!ready) - await fetch(); + await fetch(); if(!!meta.guestsGoesTo && !loggedIn.value) { @@ -11,12 +10,40 @@ export default defineNuxtRouteMiddleware(async (to, from) => { } else if(meta.requireAuth && !loggedIn.value) { - return abortNavigation(); + return abortNavigation({ statusCode: 401, message: 'Unauthorized', }); } else if(!!meta.usersGoesTo && loggedIn.value) { return navigateTo(meta.usersGoesTo); } + else if(!!meta.validState && (!loggedIn.value || (user.value?.state ?? 0) === 0)) + { + return abortNavigation({ statusCode: 401, message: 'Unauthorized', }); + } + else if(!!meta.rights) + { + if(!user.value) + { + return abortNavigation({ statusCode: 401, message: 'Unauthorized', }); + } + else + { + let valid = false; + for(let i = 0; i < meta.rights.length; i++) + { + const list = meta.rights[i].split(' '); + + if(list.every(e => user.value.permissions.includes(e))) + { + valid = true; + break; + } + } + + if(!valid) + return abortNavigation({ statusCode: 401, message: 'Unauthorized', }); + } + } return; }); \ No newline at end of file diff --git a/package.json b/package.json index 837dd7b..aefe438 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@nuxtjs/tailwindcss": "^6.12.2", "@vueuse/nuxt": "^11.1.0", "drizzle-orm": "^0.35.3", - "nuxt": "^3.13.2", + "nuxt": "^3.14.159", "nuxt-security": "^2.0.0", "radix-vue": "^1.9.8", "vue": "latest", diff --git a/pages/admin/index.vue b/pages/admin/index.vue index e1f6e9f..9cf47b5 100644 --- a/pages/admin/index.vue +++ b/pages/admin/index.vue @@ -1,6 +1,10 @@ diff --git a/pages/user/profile.vue b/pages/user/profile.vue index ad58f34..03acf1d 100644 --- a/pages/user/profile.vue +++ b/pages/user/profile.vue @@ -9,8 +9,8 @@ let { user, clear } = useUserSession(); Mon profil -
-
+
+
{{ user.username }} @@ -24,5 +24,19 @@ let { user, clear } = useUserSession();
+
+ + + + Permission + + + + + {{ permission }} + + + +
\ No newline at end of file diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index 4961a18..29a63d2 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -73,6 +73,7 @@ export default defineEventHandler(async (e): Promise => { }, with: { data: true, + permission: true, }, where: (table) => eq(table.id, sql.placeholder('id')) }).prepare().get({ id: id.id }); @@ -89,6 +90,7 @@ export default defineEventHandler(async (e): Promise => { email: user.email, username: user.username, state: user.state, + permissions: user.permission.map(e => e.permission), } }) as UserSessionRequired); diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts index f082710..e029c21 100644 --- a/server/api/auth/register.post.ts +++ b/server/api/auth/register.post.ts @@ -71,7 +71,7 @@ export default defineEventHandler(async (e): Promise => { 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); + logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [] } }) as UserSessionRequired); setResponseStatus(e, 201); return { success: true, session }; diff --git a/server/plugins/session.ts b/server/plugins/session.ts index 7ca88f2..4fc12a8 100644 --- a/server/plugins/session.ts +++ b/server/plugins/session.ts @@ -1,6 +1,7 @@ import useDatabase from "~/composables/useDatabase"; import { userSessionsTable } from "~/db/schema"; import { eq, and, sql } from "drizzle-orm"; +import { refreshSessionFromDB } from "../utils/user"; const monthAsMs = 60 * 60 * 24 * 30 * 1000; @@ -25,6 +26,7 @@ export default defineNitroPlugin(() => { await db.update(userSessionsTable).set({ 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 }); + await refreshSessionFromDB(event, session.id); } }); sessionHooks.hook('clear', async (session, event) => { diff --git a/server/utils/user.ts b/server/utils/user.ts index 8d4ff01..156550d 100644 --- a/server/utils/user.ts +++ b/server/utils/user.ts @@ -29,7 +29,43 @@ export function logSession(e: H3Event, session: UserSession { const db = useDatabase(); - console.log("Logging session %s", session.id) db.insert(userSessionsTable).values({ id: sql.placeholder('id'), user_id: sql.placeholder('user_id'), timestamp: sql.placeholder('timestamp') }).prepare().run({ id: session.id, user_id: session.user.id, timestamp: new Date() }); return session; +} +export async function refreshSessionFromDB(e: H3Event, sessionId: string): Promise +{ + const db = useDatabase(); + + const user = db.query.userSessionsTable.findFirst({ + columns: { + id: false, + }, + with: { + users: { + with: { + permission: true, + data: true, + } + } + }, + where: (table) => eq(table.id, sql.placeholder('id')) + }).prepare().get({ id: sessionId }); + + if(user) + { + await replaceUserSession(e, { + id: sessionId, + user: { + ...user.users.data, + email: user.users.email, + username: user.users.username, + state: user.users.state, + permissions: user.users.permission.map(e => e.permission), + } + }); + } + else + { + throw createError({ statusCode: 401, message: 'Invalid session' }); + } } \ No newline at end of file diff --git a/types/auth.d.ts b/types/auth.d.ts index 8361487..8fd86d0 100644 --- a/types/auth.d.ts +++ b/types/auth.d.ts @@ -8,6 +8,8 @@ declare module 'vue-router' requiresAuth?: boolean; guestsGoesTo?: string; usersGoesTo?: string; + rights?: string[]; + validState?: boolean; } } @@ -31,7 +33,9 @@ export interface UserExtendedData { signin: Date; } -export type User = UserRawData & UserExtendedData; +export type Permissions = { permissions: string[] }; + +export type User = UserRawData & UserExtendedData & Permissions; export interface UserSession { user?: User;