Audit logs
This commit is contained in:
parent
af9c70f169
commit
63f54c3338
@ -8,12 +8,18 @@
|
||||
$: firstPage = pageNum === 1;
|
||||
$: lastPage = pageNum === meta.pageCount;
|
||||
$: pageButtons = Array.from({ length: meta.pageCount }, (_, i) => i + 1);
|
||||
|
||||
const makePageUrl = (params: URLSearchParams, pageNumber: number) => {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
searchParams.set('page', String(pageNumber));
|
||||
return searchParams.toString();
|
||||
};
|
||||
</script>
|
||||
|
||||
<nav class="pager">
|
||||
<a
|
||||
class="page-button page-prev {firstPage ? 'disabled' : ''}"
|
||||
href={`?page=${pageNum - 1}`}
|
||||
href={`?${makePageUrl($page.url.searchParams, pageNum - 1)}`}
|
||||
tabindex={firstPage ? -1 : 0}
|
||||
aria-label={$t('common.previous')}
|
||||
aria-disabled={firstPage}><</a
|
||||
@ -23,7 +29,7 @@
|
||||
{@const active = buttonNumber === pageNum}
|
||||
<a
|
||||
class="page-button page-link {active ? 'disabled' : ''}"
|
||||
href={`?page=${buttonNumber}`}
|
||||
href={`?${makePageUrl($page.url.searchParams, buttonNumber)}`}
|
||||
tabindex={active ? -1 : 0}
|
||||
aria-label={`${$t('common.page')} ${buttonNumber}`}
|
||||
aria-disabled={active}>{buttonNumber}</a
|
||||
@ -33,7 +39,7 @@
|
||||
<a
|
||||
class="page-button page-prev {lastPage ? 'disabled' : ''}"
|
||||
tabindex={lastPage ? -1 : 0}
|
||||
href={`?page=${pageNum + 1}`}
|
||||
href={`?${makePageUrl($page.url.searchParams, pageNum + 1)}`}
|
||||
aria-label={$t('common.next')}
|
||||
aria-disabled={lastPage}>></a
|
||||
>
|
||||
|
84
src/lib/components/admin/AdminAuditCard.svelte
Normal file
84
src/lib/components/admin/AdminAuditCard.svelte
Normal file
@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n';
|
||||
import type { PageData } from '../../../routes/ssoadmin/audit/$types';
|
||||
import Button from '../Button.svelte';
|
||||
import Icon from '../icons/Icon.svelte';
|
||||
import AdminDateTime from './AdminDateTime.svelte';
|
||||
|
||||
export let audit: PageData['list'][0];
|
||||
let expanded = false;
|
||||
</script>
|
||||
|
||||
<div class="audit-log{expanded ? ' expanded' : ''}{audit.flagged ? ' flagged' : ''}">
|
||||
<div class="audit-log-title">
|
||||
<span class="audit-action">{audit.action}</span>
|
||||
<span class="audit-stamp"><AdminDateTime date={audit.created_at} /></span>
|
||||
<Button on:click={() => (expanded = !expanded)}><Icon icon="ChevronDown" /></Button>
|
||||
</div>
|
||||
|
||||
{#if expanded}
|
||||
<dl>
|
||||
<dt>{$t('admin.audit.action')}</dt>
|
||||
<dd>{audit.action}</dd>
|
||||
{#if audit.user}
|
||||
<dt>{$t('admin.audit.user')}</dt>
|
||||
<dd>{audit.user.uuid} ({audit.user.name})</dd>
|
||||
{/if}
|
||||
{#if audit.ip}
|
||||
<dt>{$t('admin.audit.ip')}</dt>
|
||||
<dd>{audit.ip}</dd>
|
||||
{/if}
|
||||
{#if audit.ua}
|
||||
<dt>{$t('admin.audit.ua')}</dt>
|
||||
<dd>{audit.ua}</dd>
|
||||
{/if}
|
||||
<dt>{$t('admin.audit.createdAt')}</dt>
|
||||
<dd><AdminDateTime date={audit.created_at} /></dd>
|
||||
{#if audit.content}
|
||||
<dt>{$t('admin.audit.comment')}</dt>
|
||||
<dd><pre>{audit.content}</pre></dd>
|
||||
{/if}
|
||||
</dl>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.audit-log {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 8px;
|
||||
background-color: var(--ina-card-background);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--ina-card-shadow);
|
||||
flex-grow: 1;
|
||||
|
||||
&.flagged {
|
||||
background-color: var(--in-error-color);
|
||||
}
|
||||
|
||||
& > dl {
|
||||
margin: 0;
|
||||
|
||||
& > dt {
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
& > dd > pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audit-log-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.audit-log.expanded :global(svg) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n';
|
||||
import type { PageData } from '../../../routes/ssoadmin/oauth2/$types';
|
||||
import AdminDateTime from './AdminDateTime.svelte';
|
||||
|
||||
export let client: PageData['list'][0];
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
|
||||
const formatDate = dateFormat.format.bind(null);
|
||||
</script>
|
||||
|
||||
<div class="client">
|
||||
@ -37,7 +35,7 @@
|
||||
<dd>{$t(`common.bool.${Boolean(client.confidential)}`)}</dd>
|
||||
|
||||
<dt>{$t('admin.oauth2.created')}</dt>
|
||||
<dd>{formatDate(client.created_at)}</dd>
|
||||
<dd><AdminDateTime date={client.created_at} /></dd>
|
||||
|
||||
<dt>{$t('admin.oauth2.owner')}</dt>
|
||||
<dd>
|
||||
|
10
src/lib/components/admin/AdminDateTime.svelte
Normal file
10
src/lib/components/admin/AdminDateTime.svelte
Normal file
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
|
||||
const formatDate = dateFormat.format.bind(null);
|
||||
|
||||
export let date: Date;
|
||||
|
||||
$: dateStr = formatDate(date);
|
||||
</script>
|
||||
|
||||
{dateStr}
|
@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n';
|
||||
import AdminDateTime from './AdminDateTime.svelte';
|
||||
import type { PageData } from '../../../routes/ssoadmin/users/$types';
|
||||
|
||||
export let user: PageData['list'][0];
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
|
||||
const formatDate = dateFormat.format.bind(null);
|
||||
</script>
|
||||
|
||||
<div class="user">
|
||||
@ -33,7 +31,7 @@
|
||||
<dd>{$t(`common.bool.${Boolean(user.activated)}`)}</dd>
|
||||
|
||||
<dt>{$t('admin.users.registered')}</dt>
|
||||
<dd>{formatDate(user.created_at)}</dd>
|
||||
<dd><AdminDateTime date={user.created_at} /></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
3
src/lib/components/icons/svg/ChevronDown.svelte
Normal file
3
src/lib/components/icons/svg/ChevronDown.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"
|
||||
><path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z" /></svg
|
||||
>
|
After Width: | Height: | Size: 141 B |
@ -133,5 +133,14 @@
|
||||
"noFile": "Please upload a file first.",
|
||||
"tooManyTimes": "You are doing that too much, please, slow down!"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"title": "Audit logs",
|
||||
"action": "Action",
|
||||
"comment": "Comment",
|
||||
"user": "Actor",
|
||||
"ip": "IP address",
|
||||
"ua": "User agent",
|
||||
"createdAt": "Created at"
|
||||
}
|
||||
}
|
||||
|
162
src/lib/server/audit/audit.ts
Normal file
162
src/lib/server/audit/audit.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { SQL, count, desc, eq, inArray, or, sql } from 'drizzle-orm';
|
||||
import { DB, auditLog, user, type AuditLog, type NewAuditLog, type User } from '../drizzle';
|
||||
import {
|
||||
AuditAction,
|
||||
type AuditListItem,
|
||||
type AuditSearchClause,
|
||||
type MinimalRequestEvent
|
||||
} from './types';
|
||||
import type { Paginated, PaginationMeta } from '$lib/types';
|
||||
|
||||
const AUTOFLAG = [
|
||||
AuditAction.MALICIOUS_REQUEST,
|
||||
AuditAction.THROTTLE,
|
||||
AuditAction.DEACTIVATION_REQUEST,
|
||||
AuditAction.DATA_DOWNLOAD_REQUEST
|
||||
];
|
||||
|
||||
export class Audit {
|
||||
public static async insert(
|
||||
action: AuditAction,
|
||||
comment?: string,
|
||||
user?: User,
|
||||
ip?: string,
|
||||
userAgent?: string
|
||||
) {
|
||||
const newAuditLog: NewAuditLog = {
|
||||
action,
|
||||
content: comment,
|
||||
actorId: user?.id,
|
||||
actor_ip: ip,
|
||||
actor_ua: userAgent,
|
||||
flagged: Number(AUTOFLAG.includes(action))
|
||||
};
|
||||
|
||||
// TODO: send flagged to administrator
|
||||
|
||||
await DB.drizzle.insert(auditLog).values(newAuditLog);
|
||||
}
|
||||
|
||||
public static async insertRequest(
|
||||
action: AuditAction,
|
||||
{ request, getClientAddress }: MinimalRequestEvent,
|
||||
user?: User,
|
||||
comment?: string
|
||||
) {
|
||||
const ip = getClientAddress();
|
||||
const userAgent = request.headers.get('user-agent') || undefined;
|
||||
|
||||
return Audit.insert(action, comment, user, ip, userAgent);
|
||||
}
|
||||
|
||||
public static async auditCount(search: AuditSearchClause) {
|
||||
const [{ rowCount }] = await DB.drizzle
|
||||
.select({ rowCount: count(auditLog.id).mapWith(Number) })
|
||||
.from(auditLog)
|
||||
.leftJoin(user, eq(user.id, auditLog.actorId))
|
||||
.where(Audit.getAuditWhere(search));
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
public static async searchAudit(
|
||||
search: AuditSearchClause & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
) {
|
||||
const limit = search.limit || 20;
|
||||
const rowCount = await Audit.auditCount(search);
|
||||
|
||||
const auditSubquery = DB.drizzle
|
||||
.select({ id: auditLog.id })
|
||||
.from(auditLog)
|
||||
.leftJoin(user, eq(user.id, auditLog.actorId))
|
||||
.limit(limit)
|
||||
.offset(search.offset || 0)
|
||||
.where(Audit.getAuditWhere(search))
|
||||
.orderBy(desc(auditLog.created_at))
|
||||
.as('auditSubquery');
|
||||
|
||||
const junkList = await DB.drizzle
|
||||
.select({
|
||||
audit_log: auditLog,
|
||||
user: user
|
||||
})
|
||||
.from(auditSubquery)
|
||||
.innerJoin(auditLog, eq(auditLog.id, auditSubquery.id))
|
||||
.leftJoin(user, eq(user.id, auditLog.actorId));
|
||||
|
||||
const list = Audit.mapAuditRows(junkList);
|
||||
const meta: PaginationMeta = {
|
||||
rowCount,
|
||||
pageSize: limit,
|
||||
pageCount: Math.ceil(rowCount / limit)
|
||||
};
|
||||
|
||||
return { list, meta } satisfies Paginated<AuditListItem>;
|
||||
}
|
||||
|
||||
private static getAuditWhere(search: AuditSearchClause) {
|
||||
const selectList: SQL<unknown>[] = [];
|
||||
if (search.actions) {
|
||||
const actions = Array.isArray(search.actions)
|
||||
? search.actions
|
||||
: (search.actions as string).split(',');
|
||||
selectList.push(inArray(auditLog.action, actions));
|
||||
}
|
||||
|
||||
if (search.content) {
|
||||
selectList.push(sql`lower(${auditLog.content}) LIKE ${`%${search.content.toLowerCase()}%`}`);
|
||||
}
|
||||
|
||||
if (search.ip) {
|
||||
selectList.push(sql`lower(${auditLog.actor_ip}) LIKE ${`%${search.ip.toLowerCase()}%`}`);
|
||||
}
|
||||
|
||||
if (search.ua) {
|
||||
selectList.push(sql`lower(${auditLog.actor_ua}) LIKE ${`%${search.ua.toLowerCase()}%`}`);
|
||||
}
|
||||
|
||||
if (search.user) {
|
||||
selectList.push(eq(user.uuid, search.user));
|
||||
}
|
||||
|
||||
if (search.flagged) {
|
||||
selectList.push(eq(auditLog.flagged, 1));
|
||||
}
|
||||
|
||||
return or(...selectList);
|
||||
}
|
||||
|
||||
private static mapAuditRows(
|
||||
rows: {
|
||||
audit_log: AuditLog;
|
||||
user?: User | null;
|
||||
}[]
|
||||
) {
|
||||
return rows.reduce<AuditListItem[]>((accum, entry) => {
|
||||
let existingEntry = accum.find((e) => e.id === entry.audit_log.id);
|
||||
if (!existingEntry) {
|
||||
existingEntry = {
|
||||
id: entry.audit_log.id,
|
||||
action: entry.audit_log.action as AuditAction,
|
||||
ip: entry.audit_log.actor_ip || undefined,
|
||||
ua: entry.audit_log.actor_ua || undefined,
|
||||
content: entry.audit_log.content || undefined,
|
||||
flagged: Boolean(entry.audit_log.flagged),
|
||||
created_at: entry.audit_log.created_at
|
||||
};
|
||||
accum.push(existingEntry);
|
||||
}
|
||||
|
||||
if (entry.user) {
|
||||
existingEntry.user = {
|
||||
uuid: entry.user.uuid,
|
||||
name: entry.user.display_name
|
||||
};
|
||||
}
|
||||
|
||||
return accum;
|
||||
}, []);
|
||||
}
|
||||
}
|
2
src/lib/server/audit/index.ts
Normal file
2
src/lib/server/audit/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './audit';
|
46
src/lib/server/audit/types.ts
Normal file
46
src/lib/server/audit/types.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export enum AuditAction {
|
||||
LOGIN = 'login',
|
||||
REGISTRATION = 'registration',
|
||||
USER_UPDATE = 'user_update',
|
||||
USER_DELETE = 'user_delete',
|
||||
TOTP_ACTIVATE = 'totp_activate',
|
||||
TOTP_DEACTIVATE = 'totp_deactivate',
|
||||
PASSWORD_CHANGE = 'password_change',
|
||||
EMAIL_CHANGE = 'email_change',
|
||||
MALICIOUS_REQUEST = 'malicious_request',
|
||||
THROTTLE = 'throttle',
|
||||
DEACTIVATION_REQUEST = 'deactivation_request',
|
||||
DATA_DOWNLOAD_REQUEST = 'data_download_request',
|
||||
OAUTH2_CREATE = 'oauth2_create',
|
||||
OAUTH2_DELETE = 'oauth2_delete',
|
||||
OAUTH2_UPDATE = 'oauth2_update',
|
||||
OAUTH2_REGENERATE = 'oauth2_regenerate',
|
||||
OAUTH2_INVITE = 'oauth2_invite'
|
||||
}
|
||||
|
||||
export type MinimalRequestEvent = Pick<RequestEvent, 'getClientAddress' | 'request'>;
|
||||
|
||||
export interface AuditSearchClause {
|
||||
actions?: AuditAction[] | string;
|
||||
user?: string;
|
||||
ip?: string;
|
||||
ua?: string;
|
||||
content?: string;
|
||||
flagged?: boolean;
|
||||
}
|
||||
|
||||
export interface AuditListItem {
|
||||
id: number;
|
||||
action: AuditAction;
|
||||
user?: {
|
||||
uuid: string;
|
||||
name: string;
|
||||
};
|
||||
ip?: string;
|
||||
ua?: string;
|
||||
content?: string;
|
||||
flagged: boolean;
|
||||
created_at: Date;
|
||||
}
|
@ -26,6 +26,9 @@ export const auditLog = mysqlTable('audit_log', {
|
||||
actorId: int('actorId').references(() => user.id, { onDelete: 'set null' })
|
||||
});
|
||||
|
||||
export type AuditLog = typeof auditLog.$inferSelect;
|
||||
export type NewAuditLog = typeof auditLog.$inferInsert;
|
||||
|
||||
export const document = mysqlTable('document', {
|
||||
id: int('id').autoincrement().notNull(),
|
||||
title: text('title').notNull(),
|
||||
|
@ -107,6 +107,7 @@ export class Users {
|
||||
*/
|
||||
static async toSession(user: User): Promise<UserSession> {
|
||||
return {
|
||||
sid: CryptoUtils.generateString(32),
|
||||
uid: user.id,
|
||||
uuid: user.uuid,
|
||||
name: user.display_name,
|
||||
@ -217,8 +218,6 @@ export class Users {
|
||||
await Users.sendRegistrationEmail(newUser);
|
||||
}
|
||||
|
||||
// TODO: audit log
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface UserSession {
|
||||
sid: string;
|
||||
uid: number;
|
||||
uuid: string;
|
||||
name: string;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Audit } from '$lib/server/audit/audit.js';
|
||||
import { AuditAction } from '$lib/server/audit/types.js';
|
||||
import { Challenge } from '$lib/server/challenge.js';
|
||||
import type { User } from '$lib/server/drizzle';
|
||||
import { Uploads } from '$lib/server/upload.js';
|
||||
@ -21,7 +23,7 @@ export const actions = {
|
||||
await locals.session.destroy();
|
||||
return redirect(303, '/');
|
||||
},
|
||||
update: async ({ request, locals }) => {
|
||||
update: async ({ request, locals, getClientAddress }) => {
|
||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||
if (!currentUser) {
|
||||
await locals.session.destroy();
|
||||
@ -123,10 +125,20 @@ export const actions = {
|
||||
|
||||
if (data.newEmail) {
|
||||
updates.email = data.newEmail;
|
||||
await Audit.insertRequest(
|
||||
AuditAction.EMAIL_CHANGE,
|
||||
{ request, getClientAddress },
|
||||
currentUser
|
||||
);
|
||||
}
|
||||
|
||||
if (data.newPassword) {
|
||||
updates.password = await Users.hashPassword(data.newPassword);
|
||||
await Audit.insertRequest(
|
||||
AuditAction.PASSWORD_CHANGE,
|
||||
{ request, getClientAddress },
|
||||
currentUser
|
||||
);
|
||||
}
|
||||
|
||||
await Users.update(currentUser, updates);
|
||||
@ -138,7 +150,7 @@ export const actions = {
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO: audit log
|
||||
await Audit.insertRequest(AuditAction.USER_UPDATE, { request, getClientAddress }, currentUser);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@ -147,7 +159,7 @@ export const actions = {
|
||||
displayName: data.displayName || currentUser.display_name
|
||||
};
|
||||
},
|
||||
avatar: async ({ request, locals }) => {
|
||||
avatar: async ({ request, locals, getClientAddress }) => {
|
||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||
if (!currentUser) {
|
||||
await locals.session.destroy();
|
||||
@ -165,10 +177,16 @@ export const actions = {
|
||||
const { file } = formData as { file: File };
|
||||
|
||||
await Uploads.saveAvatar(currentUser, file);
|
||||
await Audit.insertRequest(
|
||||
AuditAction.USER_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
'upload avatar'
|
||||
);
|
||||
|
||||
return { avatarChanged: true };
|
||||
},
|
||||
removeAvatar: async ({ locals }) => {
|
||||
removeAvatar: async ({ locals, ...event }) => {
|
||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||
if (!currentUser) {
|
||||
await locals.session.destroy();
|
||||
@ -176,6 +194,7 @@ export const actions = {
|
||||
}
|
||||
|
||||
await Uploads.removeAvatar(currentUser);
|
||||
await Audit.insertRequest(AuditAction.USER_UPDATE, event, currentUser, 'remove avatar');
|
||||
|
||||
return { avatarChanged: true };
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Audit } from '$lib/server/audit/audit.js';
|
||||
import { AuditAction } from '$lib/server/audit/types.js';
|
||||
import { Challenge, type ChallengeBody } from '$lib/server/challenge.js';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { CryptoUtils } from '$lib/server/crypto-utils';
|
||||
@ -26,7 +28,7 @@ const issueActivateChallenge = async (subject: User) => {
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
activate: async ({ locals, request }) => {
|
||||
activate: async ({ locals, request, getClientAddress }) => {
|
||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||
if (!currentUser) {
|
||||
await locals.session.destroy();
|
||||
@ -54,11 +56,15 @@ export const actions = {
|
||||
|
||||
await TimeOTP.saveUserOtp(currentUser, decoded.data.secret);
|
||||
|
||||
// TODO: audit log
|
||||
await Audit.insertRequest(
|
||||
AuditAction.TOTP_ACTIVATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser
|
||||
);
|
||||
|
||||
return { success: true, action: 'activate' };
|
||||
},
|
||||
deactivate: async ({ request, locals }) => {
|
||||
deactivate: async ({ request, locals, getClientAddress }) => {
|
||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||
if (!currentUser) {
|
||||
await locals.session.destroy();
|
||||
@ -98,7 +104,11 @@ export const actions = {
|
||||
|
||||
await UserTokens.remove(userOtp);
|
||||
|
||||
// TODO: audit log
|
||||
await Audit.insertRequest(
|
||||
AuditAction.TOTP_DEACTIVATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser
|
||||
);
|
||||
|
||||
return { success: true, action: 'deactivate' };
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Audit } from '$lib/server/audit/audit.js';
|
||||
import { AuditAction } from '$lib/server/audit/types.js';
|
||||
import { OAuth2Clients, OAuth2DeviceCodes, OAuth2Users } from '$lib/server/oauth2/index.js';
|
||||
import { Users } from '$lib/server/users';
|
||||
import { error, fail, redirect } from '@sveltejs/kit';
|
||||
@ -19,13 +21,21 @@ export const actions = {
|
||||
const body = await request.formData();
|
||||
const code = body.get('code') as string;
|
||||
if (!body.has('code') || !code) {
|
||||
if (await limiter.isLimited(event)) throw error(429, "You're doing that too much!");
|
||||
if (await limiter.isLimited(event)) {
|
||||
await Audit.insertRequest(AuditAction.THROTTLE, event, currentUser, `device code attempt`);
|
||||
throw error(429, "You're doing that too much!");
|
||||
}
|
||||
|
||||
return fail(400, { errors: ['noCode'] });
|
||||
}
|
||||
|
||||
const token = await OAuth2DeviceCodes.getByUserCode(code);
|
||||
if (!token?.clientId) {
|
||||
if (await limiter.isLimited(event)) throw error(429, "You're doing that too much!");
|
||||
if (await limiter.isLimited(event)) {
|
||||
await Audit.insertRequest(AuditAction.THROTTLE, event, currentUser, `device code attempt`);
|
||||
throw error(429, "You're doing that too much!");
|
||||
}
|
||||
|
||||
return fail(404, { errors: ['invalidCode'] });
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Audit } from '$lib/server/audit/audit.js';
|
||||
import { AuditAction } from '$lib/server/audit/types.js';
|
||||
import { Challenge } from '$lib/server/challenge.js';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { Users } from '$lib/server/users/index.js';
|
||||
@ -27,7 +29,10 @@ const limiter = new RateLimiter({
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const { request, locals, url } = event;
|
||||
if (await limiter.isLimited(event)) throw error(429);
|
||||
if (await limiter.isLimited(event)) {
|
||||
await Audit.insertRequest(AuditAction.THROTTLE, event, undefined, 'login');
|
||||
throw error(429);
|
||||
}
|
||||
|
||||
// Redirect
|
||||
const redirectUrl = url.searchParams.has('redirectTo')
|
||||
@ -39,8 +44,6 @@ export const actions = {
|
||||
return redirect(303, redirectUrl);
|
||||
}
|
||||
|
||||
// TODO: Audit log failed attempts
|
||||
|
||||
const body = await request.formData();
|
||||
const { email, password, challenge, otpCode } = Changesets.take<LoginParams>(
|
||||
['email', 'password', 'challenge', 'otpCode'],
|
||||
@ -58,7 +61,16 @@ export const actions = {
|
||||
// Find existing active user
|
||||
const loginUser = await Users.getByLogin(email);
|
||||
if (!loginUser) {
|
||||
if (await rainbowTableLimiter.isLimited(event)) throw error(429);
|
||||
if (await rainbowTableLimiter.isLimited(event)) {
|
||||
await Audit.insertRequest(
|
||||
AuditAction.THROTTLE,
|
||||
event,
|
||||
undefined,
|
||||
`rainbow table\nemail=${email}`
|
||||
);
|
||||
throw error(429);
|
||||
}
|
||||
|
||||
return fail(400, { email, incorrect: true });
|
||||
}
|
||||
|
||||
@ -86,7 +98,16 @@ export const actions = {
|
||||
} else {
|
||||
// Compare user password
|
||||
if (!password || !(await Users.validatePassword(loginUser, password))) {
|
||||
if (await rainbowTableLimiter.isLimited(event)) throw error(429);
|
||||
if (await rainbowTableLimiter.isLimited(event)) {
|
||||
await Audit.insertRequest(
|
||||
AuditAction.THROTTLE,
|
||||
event,
|
||||
loginUser,
|
||||
`password attempts\nemail=${email}`
|
||||
);
|
||||
throw error(429);
|
||||
}
|
||||
|
||||
return fail(400, { email, incorrect: true });
|
||||
}
|
||||
}
|
||||
@ -104,11 +125,13 @@ export const actions = {
|
||||
const sessionUser = await Users.toSession(loginUser);
|
||||
await locals.session.set({ user: sessionUser });
|
||||
|
||||
await Audit.insertRequest(AuditAction.LOGIN, event, loginUser, sessionUser.sid);
|
||||
|
||||
return redirect(303, redirectUrl);
|
||||
}
|
||||
} as Actions;
|
||||
|
||||
export const load = async ({ locals, url }) => {
|
||||
export const load = async ({ locals, url, ...event }) => {
|
||||
if (url.searchParams.has('redirectTo')) {
|
||||
// Check that the redirect URL is a local path
|
||||
if (!url.searchParams.get('redirectTo')?.startsWith('/')) {
|
||||
@ -134,6 +157,7 @@ export const load = async ({ locals, url }) => {
|
||||
}
|
||||
|
||||
await Users.activateUserBy(activationInfo.token, activationInfo.user);
|
||||
await Audit.insertRequest(AuditAction.USER_UPDATE, event, activationInfo?.user, 'activate');
|
||||
|
||||
return {
|
||||
activated: true
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Audit, AuditAction } from '$lib/server/audit';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { Users } from '$lib/server/users/index.js';
|
||||
import { UserTokens } from '$lib/server/users/tokens.js';
|
||||
@ -22,7 +23,11 @@ const limiter = new RateLimiter({
|
||||
export const actions = {
|
||||
sendEmail: async (event) => {
|
||||
const { locals, request } = event;
|
||||
if (await limiter.isLimited(event)) throw error(429);
|
||||
if (await limiter.isLimited(event)) {
|
||||
await Audit.insertRequest(AuditAction.THROTTLE, event, undefined, `password reset email`);
|
||||
throw error(429);
|
||||
}
|
||||
|
||||
if (locals.session.data?.user) {
|
||||
return redirect(303, '/');
|
||||
}
|
||||
@ -49,7 +54,7 @@ export const actions = {
|
||||
|
||||
return { success: 'sent' };
|
||||
},
|
||||
setPassword: async ({ locals, request, url }) => {
|
||||
setPassword: async ({ locals, request, url, getClientAddress }) => {
|
||||
if (locals.session.data?.user) {
|
||||
return redirect(303, '/');
|
||||
}
|
||||
@ -96,6 +101,12 @@ export const actions = {
|
||||
const hashed = await Users.hashPassword(newPassword);
|
||||
await Users.update(user, { password: hashed });
|
||||
await UserTokens.remove(exists);
|
||||
await Audit.insertRequest(
|
||||
AuditAction.PASSWORD_CHANGE,
|
||||
{ request, getClientAddress },
|
||||
user,
|
||||
'reset token'
|
||||
);
|
||||
|
||||
return { success: 'set' };
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { OAuth2Response } from '$lib/server/oauth2/response.js';
|
||||
import { OAuth2DeviceAuthorizationController } from '$lib/server/oauth2/controller/device-authorization.js';
|
||||
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { Audit, AuditAction } from '$lib/server/audit';
|
||||
|
||||
const limiter = new RateLimiter({
|
||||
IP: [6, 'm']
|
||||
@ -10,7 +11,15 @@ const limiter = new RateLimiter({
|
||||
|
||||
export const POST = async (event) => {
|
||||
const { request, url } = event;
|
||||
if (await limiter.isLimited(event)) error(429, "You're doing that too much!");
|
||||
if (await limiter.isLimited(event)) {
|
||||
await Audit.insertRequest(
|
||||
AuditAction.THROTTLE,
|
||||
event,
|
||||
undefined,
|
||||
`device authorization attempt`
|
||||
);
|
||||
error(429, "You're doing that too much!");
|
||||
}
|
||||
|
||||
try {
|
||||
return await OAuth2DeviceAuthorizationController.postRequest({ request });
|
||||
|
@ -2,6 +2,7 @@ import { OAuth2Error, SlowDown } from '$lib/server/oauth2/error.js';
|
||||
import { OAuth2Response } from '$lib/server/oauth2/response.js';
|
||||
import { OAuth2TokenController } from '$lib/server/oauth2/controller/token.js';
|
||||
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||
import { Audit, AuditAction } from '$lib/server/audit';
|
||||
|
||||
const limiter = new RateLimiter({
|
||||
IP: [15, 'm']
|
||||
@ -12,6 +13,7 @@ export const POST = async (event) => {
|
||||
|
||||
try {
|
||||
if (await limiter.isLimited(event)) {
|
||||
await Audit.insertRequest(AuditAction.THROTTLE, event, undefined, `oauth2 token attempt`);
|
||||
throw new SlowDown('Please, slow down!');
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { AuditAction } from '$lib/server/audit';
|
||||
import { Audit } from '$lib/server/audit/audit.js';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { Users } from '$lib/server/users/index.js';
|
||||
import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js';
|
||||
@ -98,6 +100,8 @@ export const actions = {
|
||||
// TODO: check for registration token
|
||||
const newUser = await Users.register({ username, displayName, password, email });
|
||||
|
||||
await Audit.insertRequest(AuditAction.REGISTRATION, event, newUser);
|
||||
|
||||
return {
|
||||
success: newUser.activated ? 'userCreated' : 'emailSent'
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Audit, AuditAction } from '$lib/server/audit';
|
||||
import { Users } from '$lib/server/users/index.js';
|
||||
import { hasPrivileges } from '$lib/utils.js';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async ({ url, locals }) => {
|
||||
export const load = async ({ url, locals, ...event }) => {
|
||||
const userInfo = locals.session.data?.user;
|
||||
const currentUser = await Users.getBySession(userInfo);
|
||||
if (!userInfo || !currentUser) {
|
||||
@ -13,6 +14,13 @@ export const load = async ({ url, locals }) => {
|
||||
// Only users with 'admin' privilege can access
|
||||
const privileges = await Users.getUserPrivileges(currentUser);
|
||||
if (!hasPrivileges(privileges, ['admin', 'self:oauth2'])) {
|
||||
await Audit.insertRequest(
|
||||
AuditAction.MALICIOUS_REQUEST,
|
||||
event,
|
||||
currentUser,
|
||||
`unauthorized direct admin access\nurl=${url.toString()}`
|
||||
);
|
||||
|
||||
return error(404, 'Not Found');
|
||||
}
|
||||
|
||||
|
43
src/routes/ssoadmin/audit/+page.server.ts
Normal file
43
src/routes/ssoadmin/audit/+page.server.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { AdminUtils } from '$lib/server/admin-utils';
|
||||
import { Audit } from '$lib/server/audit/audit.js';
|
||||
import { AuditAction } from '$lib/server/audit/types.js';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
export const load = async ({ url, parent }) => {
|
||||
const { user: userInfo } = await parent();
|
||||
AdminUtils.checkPrivileges(userInfo, ['admin:audit']);
|
||||
|
||||
let limit = PAGE_SIZE;
|
||||
let page = 1;
|
||||
let actions: AuditAction[] | undefined = undefined;
|
||||
let user: string | undefined = undefined;
|
||||
let content: string | undefined = undefined;
|
||||
if (url.searchParams.has('page')) {
|
||||
page = Number(url.searchParams.get('page')) || 1;
|
||||
}
|
||||
|
||||
if (url.searchParams.has('pageSize')) {
|
||||
limit = Number(url.searchParams.get('pageSize')) || PAGE_SIZE;
|
||||
}
|
||||
|
||||
if (url.searchParams.has('actions')) {
|
||||
actions = url.searchParams.getAll('actions') as AuditAction[];
|
||||
}
|
||||
|
||||
if (url.searchParams.has('user')) {
|
||||
user = url.searchParams.get('user') as string;
|
||||
}
|
||||
|
||||
if (url.searchParams.has('content')) {
|
||||
content = url.searchParams.get('content') as string;
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const data = await Audit.searchAudit({ limit, offset, actions, user, content });
|
||||
return {
|
||||
...data,
|
||||
actions: Object.values(AuditAction)
|
||||
};
|
||||
};
|
60
src/routes/ssoadmin/audit/+page.svelte
Normal file
60
src/routes/ssoadmin/audit/+page.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import Paginator from '$lib/components/Paginator.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import type { PageData } from './$types';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SplitView from '$lib/components/container/SplitView.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import AdminAuditCard from '../../../lib/components/admin/AdminAuditCard.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('admin.audit.title')} - {env.PUBLIC_SITE_NAME} {$t('admin.title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{$t('admin.audit.title')} ({data.meta.rowCount})</h1>
|
||||
|
||||
<ColumnView>
|
||||
<form action="" method="get">
|
||||
<ColumnView>
|
||||
<SplitView>
|
||||
<FormControl>
|
||||
<label for="actions">{$t('admin.audit.action')}</label>
|
||||
<select name="actions" value={$page.url.searchParams.getAll('actions')} multiple>
|
||||
{#each data.actions as action}
|
||||
<option value={action}>{action}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<label for="content">{$t('admin.audit.comment')}</label>
|
||||
<input name="content" value={$page.url.searchParams.get('content')} />
|
||||
</FormControl>
|
||||
</SplitView>
|
||||
<div>
|
||||
<Button type="submit">{$t('common.filter')}</Button>
|
||||
</div>
|
||||
</ColumnView>
|
||||
</form>
|
||||
|
||||
<div class="audit-list">
|
||||
<Paginator meta={data.meta} />
|
||||
{#each data.list as audit}
|
||||
<AdminAuditCard {audit} />
|
||||
{/each}
|
||||
<Paginator meta={data.meta} />
|
||||
</div>
|
||||
</ColumnView>
|
||||
|
||||
<style>
|
||||
.audit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
@ -1,5 +1,6 @@
|
||||
import { OAUTH2_MAX_REDIRECTS, OAUTH2_MAX_URLS } from '$lib/constants.js';
|
||||
import { AdminUtils } from '$lib/server/admin-utils';
|
||||
import { Audit, AuditAction } from '$lib/server/audit';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { CryptoUtils } from '$lib/server/crypto-utils.js';
|
||||
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||
@ -53,7 +54,8 @@ const inviteLimiter = new RateLimiter({
|
||||
*/
|
||||
const getActionData = async (locals: App.Locals, uuid: string) => {
|
||||
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||
['admin:oauth2', 'self:oauth2']
|
||||
'admin:oauth2',
|
||||
'self:oauth2'
|
||||
]);
|
||||
|
||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||
@ -77,8 +79,8 @@ export const actions = {
|
||||
/**
|
||||
* Update the OAuth2 Client general information.
|
||||
*/
|
||||
update: async ({ locals, request, params: { uuid } }) => {
|
||||
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
||||
update: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||
const { details, fullPrivileges, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
const body = await request.formData();
|
||||
const { title, description, activated, verified, confidential } =
|
||||
@ -111,6 +113,13 @@ export const actions = {
|
||||
confidential: actuallyConfidential
|
||||
});
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`client_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
@ -118,7 +127,7 @@ export const actions = {
|
||||
*
|
||||
* Only works for disabled clients, and only the owner or admin can do it.
|
||||
*/
|
||||
delete: async ({ locals, params: { uuid } }) => {
|
||||
delete: async ({ locals, params: { uuid }, ...event }) => {
|
||||
const { currentUser, fullPrivileges, details } = await getActionData(locals, uuid);
|
||||
|
||||
if (details.activated === 1) {
|
||||
@ -131,6 +140,13 @@ export const actions = {
|
||||
|
||||
await OAuth2Clients.deleteClient(details);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_DELETE,
|
||||
event,
|
||||
currentUser,
|
||||
`client_id=${details.client_id}\ntitle=${details.title}`
|
||||
);
|
||||
|
||||
return redirect(303, '/ssoadmin/oauth2');
|
||||
},
|
||||
/**
|
||||
@ -151,6 +167,12 @@ export const actions = {
|
||||
|
||||
// Allow secret regeneration only once per minute.
|
||||
if (!fullPrivileges && (await oneOffLimiter.isLimited(event))) {
|
||||
await Audit.insertRequest(
|
||||
AuditAction.THROTTLE,
|
||||
event,
|
||||
currentUser,
|
||||
`secret regeneration attempt`
|
||||
);
|
||||
return fail(429, { errors: ['tooManyTimes'] });
|
||||
}
|
||||
|
||||
@ -158,13 +180,20 @@ export const actions = {
|
||||
client_secret: CryptoUtils.generateSecret()
|
||||
});
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_REGENERATE,
|
||||
event,
|
||||
currentUser,
|
||||
`client_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Remove an URL reference.
|
||||
*/
|
||||
removeUrl: async ({ locals, url, params: { uuid } }) => {
|
||||
const { details } = await getActionData(locals, uuid);
|
||||
removeUrl: async ({ locals, url, params: { uuid }, ...event }) => {
|
||||
const { details, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
if (isNaN(id)) {
|
||||
@ -173,13 +202,20 @@ export const actions = {
|
||||
|
||||
await OAuth2Clients.deleteUrl(details, id);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
event,
|
||||
currentUser,
|
||||
`delete url\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Add an URL reference.
|
||||
*/
|
||||
addUrl: async ({ locals, request, params: { uuid } }) => {
|
||||
const { details } = await getActionData(locals, uuid);
|
||||
addUrl: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||
const { details, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
const body = await request.formData();
|
||||
const { type, url } = Changesets.take<AddUrlRequest>(['type', 'url'], body);
|
||||
@ -202,13 +238,20 @@ export const actions = {
|
||||
|
||||
await OAuth2Clients.addUrl(details, type, url);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`add url\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Remove a privilege reference.
|
||||
*/
|
||||
removePrivilege: async ({ locals, url, params: { uuid } }) => {
|
||||
const { details } = await getActionData(locals, uuid);
|
||||
removePrivilege: async ({ locals, url, params: { uuid }, ...event }) => {
|
||||
const { details, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
if (isNaN(id)) {
|
||||
@ -217,13 +260,20 @@ export const actions = {
|
||||
|
||||
await OAuth2Clients.deletePrivilege(details, id);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
event,
|
||||
currentUser,
|
||||
`remove privilege\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Add a privilege reference.
|
||||
*/
|
||||
addPrivilege: async ({ locals, request, params: { uuid } }) => {
|
||||
const { details } = await getActionData(locals, uuid);
|
||||
addPrivilege: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||
const { details, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
const body = await request.formData();
|
||||
const { name } = Changesets.take<AddPrivilegeRequest>(['name'], body);
|
||||
@ -234,13 +284,20 @@ export const actions = {
|
||||
|
||||
await OAuth2Clients.addPrivilege(details, name);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`add privilege\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Update the OAuth2 Client allowed grants list.
|
||||
*/
|
||||
grants: async ({ locals, request, params: { uuid } }) => {
|
||||
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
||||
grants: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||
const { details, fullPrivileges, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
const allowedGrants = fullPrivileges
|
||||
? OAuth2Clients.availableGrantTypes
|
||||
@ -257,13 +314,20 @@ export const actions = {
|
||||
grants: deduplicatedAllowedGrants.join(' ')
|
||||
});
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`grants\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Update the OAuth2 Client allowed scopes list.
|
||||
*/
|
||||
scopes: async ({ locals, request, params: { uuid } }) => {
|
||||
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
||||
scopes: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||
const { details, fullPrivileges, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
const allowedScopes = fullPrivileges
|
||||
? OAuth2Clients.availableScopes
|
||||
@ -280,12 +344,19 @@ export const actions = {
|
||||
scope: OAuth2Clients.joinScope(deduplicatedAllowedScopes)
|
||||
});
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`scopes\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Update the OAuth2 Client icon picture.
|
||||
*/
|
||||
avatar: async ({ request, locals, params: { uuid } }) => {
|
||||
avatar: async ({ request, locals, params: { uuid }, getClientAddress }) => {
|
||||
const { currentUser, details } = await getActionData(locals, uuid);
|
||||
|
||||
const formData = Object.fromEntries(await request.formData());
|
||||
@ -299,16 +370,30 @@ export const actions = {
|
||||
|
||||
await Uploads.saveClientAvatar(details, currentUser, file);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`upload avatar\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
* Delete the OAuth2 Client icon picture.
|
||||
*/
|
||||
removeAvatar: async ({ locals, params: { uuid } }) => {
|
||||
const { details } = await getActionData(locals, uuid);
|
||||
removeAvatar: async ({ locals, params: { uuid }, ...event }) => {
|
||||
const { details, currentUser } = await getActionData(locals, uuid);
|
||||
|
||||
await Uploads.removeClientAvatar(details);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
event,
|
||||
currentUser,
|
||||
`remove avatar\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
@ -347,6 +432,13 @@ export const actions = {
|
||||
|
||||
await OAuth2Clients.sendManagerInvitationEmail(details as OAuth2Client, currentUser, email);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_INVITE,
|
||||
event,
|
||||
currentUser,
|
||||
`invite\nclient_id=${details.client_id}\nemail=${email}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
/**
|
||||
@ -354,7 +446,7 @@ export const actions = {
|
||||
*
|
||||
* Only the owner or admin can do it.
|
||||
*/
|
||||
removeManager: async ({ locals, url, params: { uuid } }) => {
|
||||
removeManager: async ({ locals, url, params: { uuid }, ...event }) => {
|
||||
const { details, currentUser, fullPrivileges } = await getActionData(locals, uuid);
|
||||
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
@ -367,6 +459,12 @@ export const actions = {
|
||||
}
|
||||
|
||||
await OAuth2Clients.removeManager(details, id);
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
event,
|
||||
currentUser,
|
||||
`remove manager\nclient_id=${details.client_id}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AdminUtils } from '$lib/server/admin-utils';
|
||||
import { Audit, AuditAction } from '$lib/server/audit';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||
import { OAuth2Clients } from '$lib/server/oauth2';
|
||||
@ -11,7 +12,7 @@ interface PrivilegesRequest {
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
privileges: async ({ locals, params: { uuid, user: userId }, request }) => {
|
||||
privileges: async ({ locals, params: { uuid, user: userId }, request, getClientAddress }) => {
|
||||
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||
['admin:oauth2', 'self:oauth2']
|
||||
]);
|
||||
@ -60,6 +61,13 @@ export const actions = {
|
||||
|
||||
await Users.setUserPrivileges(targetUser, splitFilter, details.id);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`update user privileges\nclient_id=${details.client_id}\nuser=${targetUser.uuid}`
|
||||
);
|
||||
|
||||
return redirect(303, '..');
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AdminUtils } from '$lib/server/admin-utils';
|
||||
import { Audit, AuditAction } from '$lib/server/audit';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { OAuth2Clients } from '$lib/server/oauth2/index.js';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
@ -11,7 +12,7 @@ interface CreateClientRequest {
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
default: async ({ locals, request }) => {
|
||||
default: async ({ locals, request, getClientAddress }) => {
|
||||
const { currentUser } = await AdminUtils.getActionUser(locals, [
|
||||
['admin:oauth2', 'self:oauth2:create']
|
||||
]);
|
||||
@ -42,6 +43,13 @@ export const actions = {
|
||||
!!confidential
|
||||
);
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.OAUTH2_CREATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`client_id=${uuid}`
|
||||
);
|
||||
|
||||
return redirect(303, `/ssoadmin/oauth2/${uuid}`);
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { AdminUtils } from '$lib/server/admin-utils';
|
||||
import { Audit } from '$lib/server/audit/audit.js';
|
||||
import { AuditAction } from '$lib/server/audit/types.js';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { OAuth2Tokens } from '$lib/server/oauth2/index.js';
|
||||
import { Uploads } from '$lib/server/upload.js';
|
||||
@ -30,16 +32,14 @@ export const actions = {
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
deleteInfo: async ({ locals, params: { uuid } }) => {
|
||||
await AdminUtils.getActionUser(locals, ['admin', 'admin:user']);
|
||||
deleteInfo: async ({ locals, params: { uuid }, request, getClientAddress }) => {
|
||||
const { currentUser } = await AdminUtils.getActionUser(locals, ['admin', 'admin:user']);
|
||||
|
||||
const targetUser = await Users.getByUuid(uuid, false);
|
||||
if (!targetUser || !!targetUser.activated) {
|
||||
return fail(404, { errors: ['invalidUuid'] });
|
||||
}
|
||||
|
||||
// TODO: audit log
|
||||
|
||||
const [stubName] = uuid.split('-');
|
||||
|
||||
// Nuke EVERYTHING
|
||||
@ -54,6 +54,13 @@ export const actions = {
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
await Audit.insertRequest(
|
||||
AuditAction.USER_DELETE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`uuid=${targetUser.uuid}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
email: async ({ locals, params: { uuid }, url }) => {
|
||||
@ -80,11 +87,9 @@ export const actions = {
|
||||
return fail(403, { errors: ['invalidEmailType'] });
|
||||
}
|
||||
|
||||
// TODO: audit log
|
||||
|
||||
return { errors: [] };
|
||||
},
|
||||
update: async ({ locals, params: { uuid }, request }) => {
|
||||
update: async ({ locals, params: { uuid }, request, getClientAddress }) => {
|
||||
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||
'admin',
|
||||
'admin:user'
|
||||
@ -139,7 +144,12 @@ export const actions = {
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
// TODO: audit log
|
||||
await Audit.insertRequest(
|
||||
AuditAction.USER_UPDATE,
|
||||
{ request, getClientAddress },
|
||||
currentUser,
|
||||
`uuid=${targetUser.uuid}`
|
||||
);
|
||||
|
||||
return { errors: [] };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user