add membership and permissions

This commit is contained in:
Fran Jurmanović
2025-06-26 00:52:10 +02:00
parent 47a72c82f4
commit 53c023ca4d
11 changed files with 156 additions and 17 deletions

View File

@@ -44,7 +44,7 @@ export async function fetchAPIEvent(
data: { token }
} = await redisSessionManager.getSession(event.cookies);
return fetchAPI(endpoint, method, body, { Authorization: `Basic ${token}` });
return fetchAPI(endpoint, method, body, { Authorization: `Bearer ${token}` });
}
export default fetchAPI;

View File

@@ -1,19 +1,35 @@
import fetchAPI, { fetchAPIEvent } from '$api/apiService';
import { fetchAPIEvent } from '$api/apiService';
import { env } from '$env/dynamic/private';
import { authStore } from '$stores/authStore';
import { redisSessionManager } from '$stores/redisSessionManager';
import type { RequestEvent } from '@sveltejs/kit';
import { v4 as uuidv4 } from 'uuid';
export const login = async (event: RequestEvent, username: string, password: string) => {
const token = btoa(`${username}:${password}`);
await redisSessionManager.createSession(event.cookies, { token }, uuidv4());
if (!(await checkAuth(event))) {
{
authStore.set({ token: undefined, error: 'Invalid username or password.' });
try {
const response = await fetch(`${env.API_BASE_URL}/auth/login`, {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Invalid username or password.' }));
authStore.set({ token: undefined, error: errorData.error || 'Invalid username or password.' });
return false;
}
const { token } = await response.json();
await redisSessionManager.createSession(event.cookies, { token }, uuidv4());
return true;
} catch (err) {
authStore.set({ token: undefined, error: 'Login failed. Please try again.' });
return false;
}
return true;
};
export const logout = (event: RequestEvent) => {

4
src/app.d.ts vendored
View File

@@ -3,7 +3,9 @@
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
user: import('$models/user').User | null;
}
// interface PageData {}
// interface PageState {}
// interface Platform {}

17
src/models/user.ts Normal file
View File

@@ -0,0 +1,17 @@
export interface Permission {
id: string;
name: string;
}
export interface Role {
id: string;
name: string;
permissions: Permission[];
}
export interface User {
id: string;
username: string;
role_id: string;
role: Role;
}

View File

@@ -0,0 +1,7 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return {
user: locals.user
};
};

View File

@@ -1,6 +1,12 @@
<script>
<script lang="ts">
import '../app.css';
let { children } = $props();
import { user } from '$stores/user';
import type { LayoutData } from './$types';
let { data } = $props<LayoutData>();
// Set the user store with data from the server
$: $user = data.user;
</script>
<div class="layout">

View File

@@ -2,6 +2,7 @@
import ServerCard from '$components/ServerCard.svelte';
import Toast from '$components/Toast.svelte';
import type { Server } from '$models/server';
import { user, hasPermission } from '$stores/user';
const { data, form } = $props();
let servers: Server[] = data.servers;
@@ -16,6 +17,14 @@
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<h1 class="text-2xl font-bold">ACC Server Manager</h1>
<div class="flex items-center space-x-4">
{#if hasPermission($user, 'membership.view')}
<a href="/dashboard/membership" class="flex items-center text-gray-300 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M15 21a6 6 0 00-9-5.197m0 0A5.975 5.975 0 0112 13a5.975 5.975 0 01-3-1.197" />
</svg>
<span class="ml-1 hidden sm:inline">Users</span>
</a>
{/if}
<a href="/logout">
<button class="flex items-center text-gray-300 hover:text-white">
<svg
@@ -67,9 +76,4 @@
</svelte:head>
<style>
.server-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
</style>

View File

@@ -0,0 +1,18 @@
import { redirect } from '@sveltejs/kit';
export const load = async ({ locals, fetch }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
const response = await fetch('/api/users');
if (!response.ok) {
return { users: [] };
}
const users = await response.json();
return {
users
};
};

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { user, hasPermission } from '$stores/user';
import type { PageData } from './$types';
export let data: PageData;
$: users = data.users || [];
</script>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">User Management</h1>
{#if hasPermission($user, 'membership.create')}
<div class="flex justify-end mb-4">
<button class="btn btn-primary">Create User</button>
</div>
{/if}
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
{#if hasPermission($user, 'membership.edit')}
<th>Actions</th>
{/if}
</tr>
</thead>
<tbody>
{#each users as user (user.id)}
<tr>
<td>{user.username}</td>
<td>{user.role.name}</td>
{#if hasPermission($user, 'membership.edit')}
<td>
<button class="btn btn-sm">Edit</button>
<button class="btn btn-sm btn-error">Delete</button>
</td>
{/if}
</tr>
{/each}
</tbody>
</table>
</div>
</div>

15
src/stores/user.ts Normal file
View File

@@ -0,0 +1,15 @@
import { writable, type Writable } from 'svelte/store';
import type { User } from '$models/user';
export const user: Writable<User | null> = writable(null);
export function hasPermission(user: User | null, permission: string): boolean {
if (!user || !user.role || !user.role.permissions) {
return false;
}
// Super Admins have all permissions
if (user.role.name === 'Super Admin') {
return true;
}
return user.role.permissions.some((p) => p.name === permission);
}

View File

@@ -3,5 +3,13 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit(), tailwindcss()]
plugins: [sveltekit(), tailwindcss()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
});