Add ISAC user operations and access roles
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { user, children }: {
|
||||
let { user, isacAccess, children }: {
|
||||
user: { username: string; role: string } | null;
|
||||
isacAccess: boolean;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
@@ -26,8 +27,8 @@
|
||||
{#if user}
|
||||
<a class:active={page.url.pathname.startsWith('/account')} href="/account">My Profile</a>
|
||||
{/if}
|
||||
{#if user?.role === 'admin'}
|
||||
<a class:active={page.url.pathname.startsWith('/admin')} href="/admin/stats">Admin</a>
|
||||
{#if isacAccess}
|
||||
<a class:active={page.url.pathname.startsWith('/admin')} href="/admin">ISAC</a>
|
||||
{/if}
|
||||
</nav>
|
||||
{#if user}
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import type { AuthUser } from './session';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
/**
|
||||
* Every admin page, action, and endpoint must call this server-side.
|
||||
* The capability-based permission checks will replace the broad admin role
|
||||
* as the permission editor is introduced.
|
||||
*/
|
||||
export async function getPermissions(user: AuthUser | null): Promise<string[]> {
|
||||
if (!user) return [];
|
||||
if (user.role === 'admin') return ['*'];
|
||||
const database = await getDatabase();
|
||||
const rows = await (await database.prepare(`
|
||||
SELECT permissions.name FROM permissions
|
||||
JOIN role_permissions ON role_permissions.permission_id=permissions.id
|
||||
JOIN user_roles ON user_roles.role_id=role_permissions.role_id
|
||||
WHERE user_roles.user_id=?
|
||||
UNION
|
||||
SELECT permissions.name FROM permissions
|
||||
JOIN user_permissions ON user_permissions.permission_id=permissions.id
|
||||
WHERE user_permissions.user_id=?
|
||||
`)).all(user.id, user.id);
|
||||
const permissions = rows.map((row) => String(row.name));
|
||||
return permissions.includes('system.admin') ? ['*'] : permissions;
|
||||
}
|
||||
|
||||
export async function hasPermission(user: AuthUser | null, permission: string): Promise<boolean> {
|
||||
const permissions = await getPermissions(user);
|
||||
return permissions.includes('*') || permissions.includes(permission);
|
||||
}
|
||||
|
||||
export async function requirePermission(user: AuthUser | null, permission: string): Promise<AuthUser> {
|
||||
if (!user) error(401, 'Sign in required');
|
||||
if (!(await hasPermission(user, permission))) error(403, 'ISAC permission required');
|
||||
return user;
|
||||
}
|
||||
|
||||
export function requireAdmin(user: AuthUser | null): asserts user is AuthUser {
|
||||
if (!user) error(401, 'Sign in required');
|
||||
if (user.role !== 'admin') error(403, 'Administrator permission required');
|
||||
|
||||
@@ -43,15 +43,23 @@ export async function createSession(cookies: Cookies, userId: string): Promise<v
|
||||
});
|
||||
}
|
||||
|
||||
export async function recordLogin(userId: string | null, provider: 'password' | 'discord', succeeded: boolean): Promise<void> {
|
||||
const database = await getDatabase();
|
||||
await (await database.prepare('INSERT INTO login_events(id,user_id,provider,succeeded,created_at) VALUES(?,?,?,?,?)'))
|
||||
.run(randomUUID(), userId, provider, succeeded ? 1 : 0, new Date().toISOString());
|
||||
}
|
||||
|
||||
export async function getSessionUser(cookies: Cookies): Promise<AuthUser | null> {
|
||||
const token = cookies.get(COOKIE_NAME);
|
||||
if (!token) return null;
|
||||
|
||||
const database = await getDatabase();
|
||||
const statement = await database.prepare(`
|
||||
SELECT users.id, users.username, users.role, users.discord_id
|
||||
SELECT users.id, users.username, users.role, users.discord_id,
|
||||
user_status.last_active_at,user_status.blocked_at,user_status.banned_until
|
||||
FROM sessions
|
||||
JOIN users ON users.id = sessions.user_id
|
||||
LEFT JOIN user_status ON user_status.user_id=users.id
|
||||
WHERE sessions.token_hash = ? AND sessions.expires_at > ?
|
||||
`);
|
||||
const user = await statement.get(hashToken(token), new Date().toISOString());
|
||||
@@ -60,6 +68,18 @@ export async function getSessionUser(cookies: Cookies): Promise<AuthUser | null>
|
||||
cookies.delete(COOKIE_NAME, { path: '/' });
|
||||
return null;
|
||||
}
|
||||
const now = new Date();
|
||||
const banned = typeof user.banned_until === 'string' && new Date(user.banned_until) > now;
|
||||
if (user.blocked_at || banned) {
|
||||
await (await database.prepare('DELETE FROM sessions WHERE token_hash=?')).run(hashToken(token));
|
||||
cookies.delete(COOKIE_NAME, { path: '/' });
|
||||
return null;
|
||||
}
|
||||
if (!user.last_active_at || now.getTime() - new Date(String(user.last_active_at)).getTime() > 5 * 60 * 1000) {
|
||||
await (await database.prepare(`INSERT INTO user_status(user_id,last_active_at,updated_at) VALUES(?,?,?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET last_active_at=excluded.last_active_at,updated_at=excluded.updated_at`))
|
||||
.run(String(user.id), now.toISOString(), now.toISOString());
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(user.id),
|
||||
|
||||
Reference in New Issue
Block a user