Add permissions

This commit is contained in:
Peaceultime 2024-11-07 14:26:57 +01:00
parent a392841012
commit 41951d7603
20 changed files with 523 additions and 16 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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;");

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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] }),
}
});

View File

@ -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;

View File

@ -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": {}
}
}

View File

@ -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
}
]
}

37
drizzle/relations.ts Normal file
View File

@ -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]
}),
}));

57
drizzle/schema.ts Normal file
View File

@ -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", {
});

View File

@ -1,8 +1,7 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const { loggedIn, ready, fetch } = useUserSession();
const { loggedIn, fetch, user } = useUserSession();
const meta = to.meta;
if(!ready)
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;
});

View File

@ -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",

View File

@ -1,6 +1,10 @@
<script setup lang="ts">
definePageMeta({
rights: ['admin'],
})
const job = ref<string>('');
const toaster = useToast();
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), err = ref(false), error = ref();
async function fetch()
{
@ -19,6 +23,8 @@ async function fetch()
error.value = null;
err.value = false;
success.value = true;
toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
}
catch(e)
{
@ -26,6 +32,8 @@ async function fetch()
error.value = e;
err.value = true;
success.value = false;
toaster.add({ duration: 10000, content: error.value, type: 'error', timer: true, });
}
}
</script>

View File

@ -9,8 +9,8 @@ let { user, clear } = useUserSession();
<Head>
<Title>Mon profil</Title>
</Head>
<div class="flex w-full items-start py-8 gap-6" v-if="user">
<div class="flex gap-4 min-w-1/3 max-w-2/3 border border-light-35 dark:border-dark-35 p-4">
<div class="grid grid-cols-4 w-full items-start py-8 gap-6 content-start" v-if="user">
<div class="flex gap-4 col-span-3 border border-light-35 dark:border-dark-35 p-4">
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
<div class="flex flex-col items-start">
<ProseH5>{{ user.username }}</ProseH5>
@ -24,5 +24,19 @@ let { user, clear } = useUserSession();
<div class="flex-1">
<Button @click="async () => await clear()">Se deconnecter</Button>
</div>
<div class="flex">
<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>
</template>

View File

@ -73,6 +73,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
},
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<Return> => {
email: user.email,
username: user.username,
state: user.state,
permissions: user.permission.map(e => e.permission),
}
}) as UserSessionRequired);

View File

@ -71,7 +71,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
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 };

View File

@ -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) => {

View File

@ -29,7 +29,43 @@ export function logSession(e: H3Event<EventRequestHandler>, 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<EventRequestHandler>, sessionId: string): Promise<void>
{
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' });
}
}

6
types/auth.d.ts vendored
View File

@ -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;