Audit logs
This commit is contained in:
parent
af9c70f169
commit
63f54c3338
@ -8,12 +8,18 @@
|
|||||||
$: firstPage = pageNum === 1;
|
$: firstPage = pageNum === 1;
|
||||||
$: lastPage = pageNum === meta.pageCount;
|
$: lastPage = pageNum === meta.pageCount;
|
||||||
$: pageButtons = Array.from({ length: meta.pageCount }, (_, i) => i + 1);
|
$: 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>
|
</script>
|
||||||
|
|
||||||
<nav class="pager">
|
<nav class="pager">
|
||||||
<a
|
<a
|
||||||
class="page-button page-prev {firstPage ? 'disabled' : ''}"
|
class="page-button page-prev {firstPage ? 'disabled' : ''}"
|
||||||
href={`?page=${pageNum - 1}`}
|
href={`?${makePageUrl($page.url.searchParams, pageNum - 1)}`}
|
||||||
tabindex={firstPage ? -1 : 0}
|
tabindex={firstPage ? -1 : 0}
|
||||||
aria-label={$t('common.previous')}
|
aria-label={$t('common.previous')}
|
||||||
aria-disabled={firstPage}><</a
|
aria-disabled={firstPage}><</a
|
||||||
@ -23,7 +29,7 @@
|
|||||||
{@const active = buttonNumber === pageNum}
|
{@const active = buttonNumber === pageNum}
|
||||||
<a
|
<a
|
||||||
class="page-button page-link {active ? 'disabled' : ''}"
|
class="page-button page-link {active ? 'disabled' : ''}"
|
||||||
href={`?page=${buttonNumber}`}
|
href={`?${makePageUrl($page.url.searchParams, buttonNumber)}`}
|
||||||
tabindex={active ? -1 : 0}
|
tabindex={active ? -1 : 0}
|
||||||
aria-label={`${$t('common.page')} ${buttonNumber}`}
|
aria-label={`${$t('common.page')} ${buttonNumber}`}
|
||||||
aria-disabled={active}>{buttonNumber}</a
|
aria-disabled={active}>{buttonNumber}</a
|
||||||
@ -33,7 +39,7 @@
|
|||||||
<a
|
<a
|
||||||
class="page-button page-prev {lastPage ? 'disabled' : ''}"
|
class="page-button page-prev {lastPage ? 'disabled' : ''}"
|
||||||
tabindex={lastPage ? -1 : 0}
|
tabindex={lastPage ? -1 : 0}
|
||||||
href={`?page=${pageNum + 1}`}
|
href={`?${makePageUrl($page.url.searchParams, pageNum + 1)}`}
|
||||||
aria-label={$t('common.next')}
|
aria-label={$t('common.next')}
|
||||||
aria-disabled={lastPage}>></a
|
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">
|
<script lang="ts">
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import type { PageData } from '../../../routes/ssoadmin/oauth2/$types';
|
import type { PageData } from '../../../routes/ssoadmin/oauth2/$types';
|
||||||
|
import AdminDateTime from './AdminDateTime.svelte';
|
||||||
|
|
||||||
export let client: PageData['list'][0];
|
export let client: PageData['list'][0];
|
||||||
|
|
||||||
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
|
|
||||||
const formatDate = dateFormat.format.bind(null);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="client">
|
<div class="client">
|
||||||
@ -37,7 +35,7 @@
|
|||||||
<dd>{$t(`common.bool.${Boolean(client.confidential)}`)}</dd>
|
<dd>{$t(`common.bool.${Boolean(client.confidential)}`)}</dd>
|
||||||
|
|
||||||
<dt>{$t('admin.oauth2.created')}</dt>
|
<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>
|
<dt>{$t('admin.oauth2.owner')}</dt>
|
||||||
<dd>
|
<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">
|
<script lang="ts">
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
|
import AdminDateTime from './AdminDateTime.svelte';
|
||||||
import type { PageData } from '../../../routes/ssoadmin/users/$types';
|
import type { PageData } from '../../../routes/ssoadmin/users/$types';
|
||||||
|
|
||||||
export let user: PageData['list'][0];
|
export let user: PageData['list'][0];
|
||||||
|
|
||||||
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
|
|
||||||
const formatDate = dateFormat.format.bind(null);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="user">
|
<div class="user">
|
||||||
@ -33,7 +31,7 @@
|
|||||||
<dd>{$t(`common.bool.${Boolean(user.activated)}`)}</dd>
|
<dd>{$t(`common.bool.${Boolean(user.activated)}`)}</dd>
|
||||||
|
|
||||||
<dt>{$t('admin.users.registered')}</dt>
|
<dt>{$t('admin.users.registered')}</dt>
|
||||||
<dd>{formatDate(user.created_at)}</dd>
|
<dd><AdminDateTime date={user.created_at} /></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</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.",
|
"noFile": "Please upload a file first.",
|
||||||
"tooManyTimes": "You are doing that too much, please, slow down!"
|
"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' })
|
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', {
|
export const document = mysqlTable('document', {
|
||||||
id: int('id').autoincrement().notNull(),
|
id: int('id').autoincrement().notNull(),
|
||||||
title: text('title').notNull(),
|
title: text('title').notNull(),
|
||||||
|
@ -107,6 +107,7 @@ export class Users {
|
|||||||
*/
|
*/
|
||||||
static async toSession(user: User): Promise<UserSession> {
|
static async toSession(user: User): Promise<UserSession> {
|
||||||
return {
|
return {
|
||||||
|
sid: CryptoUtils.generateString(32),
|
||||||
uid: user.id,
|
uid: user.id,
|
||||||
uuid: user.uuid,
|
uuid: user.uuid,
|
||||||
name: user.display_name,
|
name: user.display_name,
|
||||||
@ -217,8 +218,6 @@ export class Users {
|
|||||||
await Users.sendRegistrationEmail(newUser);
|
await Users.sendRegistrationEmail(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: audit log
|
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export interface UserSession {
|
export interface UserSession {
|
||||||
|
sid: string;
|
||||||
uid: number;
|
uid: number;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: 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 { Challenge } from '$lib/server/challenge.js';
|
||||||
import type { User } from '$lib/server/drizzle';
|
import type { User } from '$lib/server/drizzle';
|
||||||
import { Uploads } from '$lib/server/upload.js';
|
import { Uploads } from '$lib/server/upload.js';
|
||||||
@ -21,7 +23,7 @@ export const actions = {
|
|||||||
await locals.session.destroy();
|
await locals.session.destroy();
|
||||||
return redirect(303, '/');
|
return redirect(303, '/');
|
||||||
},
|
},
|
||||||
update: async ({ request, locals }) => {
|
update: async ({ request, locals, getClientAddress }) => {
|
||||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
await locals.session.destroy();
|
await locals.session.destroy();
|
||||||
@ -123,10 +125,20 @@ export const actions = {
|
|||||||
|
|
||||||
if (data.newEmail) {
|
if (data.newEmail) {
|
||||||
updates.email = data.newEmail;
|
updates.email = data.newEmail;
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.EMAIL_CHANGE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.newPassword) {
|
if (data.newPassword) {
|
||||||
updates.password = await Users.hashPassword(data.newPassword);
|
updates.password = await Users.hashPassword(data.newPassword);
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.PASSWORD_CHANGE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update(currentUser, updates);
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -147,7 +159,7 @@ export const actions = {
|
|||||||
displayName: data.displayName || currentUser.display_name
|
displayName: data.displayName || currentUser.display_name
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
avatar: async ({ request, locals }) => {
|
avatar: async ({ request, locals, getClientAddress }) => {
|
||||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
await locals.session.destroy();
|
await locals.session.destroy();
|
||||||
@ -165,10 +177,16 @@ export const actions = {
|
|||||||
const { file } = formData as { file: File };
|
const { file } = formData as { file: File };
|
||||||
|
|
||||||
await Uploads.saveAvatar(currentUser, file);
|
await Uploads.saveAvatar(currentUser, file);
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.USER_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
'upload avatar'
|
||||||
|
);
|
||||||
|
|
||||||
return { avatarChanged: true };
|
return { avatarChanged: true };
|
||||||
},
|
},
|
||||||
removeAvatar: async ({ locals }) => {
|
removeAvatar: async ({ locals, ...event }) => {
|
||||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
await locals.session.destroy();
|
await locals.session.destroy();
|
||||||
@ -176,6 +194,7 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Uploads.removeAvatar(currentUser);
|
await Uploads.removeAvatar(currentUser);
|
||||||
|
await Audit.insertRequest(AuditAction.USER_UPDATE, event, currentUser, 'remove avatar');
|
||||||
|
|
||||||
return { avatarChanged: true };
|
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 { Challenge, type ChallengeBody } from '$lib/server/challenge.js';
|
||||||
import { Changesets } from '$lib/server/changesets.js';
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
import { CryptoUtils } from '$lib/server/crypto-utils';
|
import { CryptoUtils } from '$lib/server/crypto-utils';
|
||||||
@ -26,7 +28,7 @@ const issueActivateChallenge = async (subject: User) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
activate: async ({ locals, request }) => {
|
activate: async ({ locals, request, getClientAddress }) => {
|
||||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
await locals.session.destroy();
|
await locals.session.destroy();
|
||||||
@ -54,11 +56,15 @@ export const actions = {
|
|||||||
|
|
||||||
await TimeOTP.saveUserOtp(currentUser, decoded.data.secret);
|
await TimeOTP.saveUserOtp(currentUser, decoded.data.secret);
|
||||||
|
|
||||||
// TODO: audit log
|
await Audit.insertRequest(
|
||||||
|
AuditAction.TOTP_ACTIVATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser
|
||||||
|
);
|
||||||
|
|
||||||
return { success: true, action: 'activate' };
|
return { success: true, action: 'activate' };
|
||||||
},
|
},
|
||||||
deactivate: async ({ request, locals }) => {
|
deactivate: async ({ request, locals, getClientAddress }) => {
|
||||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
await locals.session.destroy();
|
await locals.session.destroy();
|
||||||
@ -98,7 +104,11 @@ export const actions = {
|
|||||||
|
|
||||||
await UserTokens.remove(userOtp);
|
await UserTokens.remove(userOtp);
|
||||||
|
|
||||||
// TODO: audit log
|
await Audit.insertRequest(
|
||||||
|
AuditAction.TOTP_DEACTIVATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser
|
||||||
|
);
|
||||||
|
|
||||||
return { success: true, action: 'deactivate' };
|
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 { OAuth2Clients, OAuth2DeviceCodes, OAuth2Users } from '$lib/server/oauth2/index.js';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { error, fail, redirect } from '@sveltejs/kit';
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
@ -19,13 +21,21 @@ export const actions = {
|
|||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const code = body.get('code') as string;
|
const code = body.get('code') as string;
|
||||||
if (!body.has('code') || !code) {
|
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'] });
|
return fail(400, { errors: ['noCode'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await OAuth2DeviceCodes.getByUserCode(code);
|
const token = await OAuth2DeviceCodes.getByUserCode(code);
|
||||||
if (!token?.clientId) {
|
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'] });
|
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 { Challenge } from '$lib/server/challenge.js';
|
||||||
import { Changesets } from '$lib/server/changesets.js';
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
import { Users } from '$lib/server/users/index.js';
|
import { Users } from '$lib/server/users/index.js';
|
||||||
@ -27,7 +29,10 @@ const limiter = new RateLimiter({
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
const { request, locals, url } = 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
|
// Redirect
|
||||||
const redirectUrl = url.searchParams.has('redirectTo')
|
const redirectUrl = url.searchParams.has('redirectTo')
|
||||||
@ -39,8 +44,6 @@ export const actions = {
|
|||||||
return redirect(303, redirectUrl);
|
return redirect(303, redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Audit log failed attempts
|
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { email, password, challenge, otpCode } = Changesets.take<LoginParams>(
|
const { email, password, challenge, otpCode } = Changesets.take<LoginParams>(
|
||||||
['email', 'password', 'challenge', 'otpCode'],
|
['email', 'password', 'challenge', 'otpCode'],
|
||||||
@ -58,7 +61,16 @@ export const actions = {
|
|||||||
// Find existing active user
|
// Find existing active user
|
||||||
const loginUser = await Users.getByLogin(email);
|
const loginUser = await Users.getByLogin(email);
|
||||||
if (!loginUser) {
|
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 });
|
return fail(400, { email, incorrect: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +98,16 @@ export const actions = {
|
|||||||
} else {
|
} else {
|
||||||
// Compare user password
|
// Compare user password
|
||||||
if (!password || !(await Users.validatePassword(loginUser, 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 });
|
return fail(400, { email, incorrect: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,11 +125,13 @@ export const actions = {
|
|||||||
const sessionUser = await Users.toSession(loginUser);
|
const sessionUser = await Users.toSession(loginUser);
|
||||||
await locals.session.set({ user: sessionUser });
|
await locals.session.set({ user: sessionUser });
|
||||||
|
|
||||||
|
await Audit.insertRequest(AuditAction.LOGIN, event, loginUser, sessionUser.sid);
|
||||||
|
|
||||||
return redirect(303, redirectUrl);
|
return redirect(303, redirectUrl);
|
||||||
}
|
}
|
||||||
} as Actions;
|
} as Actions;
|
||||||
|
|
||||||
export const load = async ({ locals, url }) => {
|
export const load = async ({ locals, url, ...event }) => {
|
||||||
if (url.searchParams.has('redirectTo')) {
|
if (url.searchParams.has('redirectTo')) {
|
||||||
// Check that the redirect URL is a local path
|
// Check that the redirect URL is a local path
|
||||||
if (!url.searchParams.get('redirectTo')?.startsWith('/')) {
|
if (!url.searchParams.get('redirectTo')?.startsWith('/')) {
|
||||||
@ -134,6 +157,7 @@ export const load = async ({ locals, url }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Users.activateUserBy(activationInfo.token, activationInfo.user);
|
await Users.activateUserBy(activationInfo.token, activationInfo.user);
|
||||||
|
await Audit.insertRequest(AuditAction.USER_UPDATE, event, activationInfo?.user, 'activate');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activated: true
|
activated: true
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Audit, AuditAction } from '$lib/server/audit';
|
||||||
import { Changesets } from '$lib/server/changesets.js';
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
import { Users } from '$lib/server/users/index.js';
|
import { Users } from '$lib/server/users/index.js';
|
||||||
import { UserTokens } from '$lib/server/users/tokens.js';
|
import { UserTokens } from '$lib/server/users/tokens.js';
|
||||||
@ -22,7 +23,11 @@ const limiter = new RateLimiter({
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
sendEmail: async (event) => {
|
sendEmail: async (event) => {
|
||||||
const { locals, request } = 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) {
|
if (locals.session.data?.user) {
|
||||||
return redirect(303, '/');
|
return redirect(303, '/');
|
||||||
}
|
}
|
||||||
@ -49,7 +54,7 @@ export const actions = {
|
|||||||
|
|
||||||
return { success: 'sent' };
|
return { success: 'sent' };
|
||||||
},
|
},
|
||||||
setPassword: async ({ locals, request, url }) => {
|
setPassword: async ({ locals, request, url, getClientAddress }) => {
|
||||||
if (locals.session.data?.user) {
|
if (locals.session.data?.user) {
|
||||||
return redirect(303, '/');
|
return redirect(303, '/');
|
||||||
}
|
}
|
||||||
@ -96,6 +101,12 @@ export const actions = {
|
|||||||
const hashed = await Users.hashPassword(newPassword);
|
const hashed = await Users.hashPassword(newPassword);
|
||||||
await Users.update(user, { password: hashed });
|
await Users.update(user, { password: hashed });
|
||||||
await UserTokens.remove(exists);
|
await UserTokens.remove(exists);
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.PASSWORD_CHANGE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
user,
|
||||||
|
'reset token'
|
||||||
|
);
|
||||||
|
|
||||||
return { success: 'set' };
|
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 { OAuth2DeviceAuthorizationController } from '$lib/server/oauth2/controller/device-authorization.js';
|
||||||
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { Audit, AuditAction } from '$lib/server/audit';
|
||||||
|
|
||||||
const limiter = new RateLimiter({
|
const limiter = new RateLimiter({
|
||||||
IP: [6, 'm']
|
IP: [6, 'm']
|
||||||
@ -10,7 +11,15 @@ const limiter = new RateLimiter({
|
|||||||
|
|
||||||
export const POST = async (event) => {
|
export const POST = async (event) => {
|
||||||
const { request, url } = 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 {
|
try {
|
||||||
return await OAuth2DeviceAuthorizationController.postRequest({ request });
|
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 { OAuth2Response } from '$lib/server/oauth2/response.js';
|
||||||
import { OAuth2TokenController } from '$lib/server/oauth2/controller/token.js';
|
import { OAuth2TokenController } from '$lib/server/oauth2/controller/token.js';
|
||||||
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||||
|
import { Audit, AuditAction } from '$lib/server/audit';
|
||||||
|
|
||||||
const limiter = new RateLimiter({
|
const limiter = new RateLimiter({
|
||||||
IP: [15, 'm']
|
IP: [15, 'm']
|
||||||
@ -12,6 +13,7 @@ export const POST = async (event) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (await limiter.isLimited(event)) {
|
if (await limiter.isLimited(event)) {
|
||||||
|
await Audit.insertRequest(AuditAction.THROTTLE, event, undefined, `oauth2 token attempt`);
|
||||||
throw new SlowDown('Please, slow down!');
|
throw new SlowDown('Please, slow down!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
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 { Changesets } from '$lib/server/changesets.js';
|
||||||
import { Users } from '$lib/server/users/index.js';
|
import { Users } from '$lib/server/users/index.js';
|
||||||
import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js';
|
import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js';
|
||||||
@ -98,6 +100,8 @@ export const actions = {
|
|||||||
// TODO: check for registration token
|
// TODO: check for registration token
|
||||||
const newUser = await Users.register({ username, displayName, password, email });
|
const newUser = await Users.register({ username, displayName, password, email });
|
||||||
|
|
||||||
|
await Audit.insertRequest(AuditAction.REGISTRATION, event, newUser);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: newUser.activated ? 'userCreated' : 'emailSent'
|
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 { Users } from '$lib/server/users/index.js';
|
||||||
import { hasPrivileges } from '$lib/utils.js';
|
import { hasPrivileges } from '$lib/utils.js';
|
||||||
import { error, redirect } from '@sveltejs/kit';
|
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 userInfo = locals.session.data?.user;
|
||||||
const currentUser = await Users.getBySession(userInfo);
|
const currentUser = await Users.getBySession(userInfo);
|
||||||
if (!userInfo || !currentUser) {
|
if (!userInfo || !currentUser) {
|
||||||
@ -13,6 +14,13 @@ export const load = async ({ url, locals }) => {
|
|||||||
// Only users with 'admin' privilege can access
|
// Only users with 'admin' privilege can access
|
||||||
const privileges = await Users.getUserPrivileges(currentUser);
|
const privileges = await Users.getUserPrivileges(currentUser);
|
||||||
if (!hasPrivileges(privileges, ['admin', 'self:oauth2'])) {
|
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');
|
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 { OAUTH2_MAX_REDIRECTS, OAUTH2_MAX_URLS } from '$lib/constants.js';
|
||||||
import { AdminUtils } from '$lib/server/admin-utils';
|
import { AdminUtils } from '$lib/server/admin-utils';
|
||||||
|
import { Audit, AuditAction } from '$lib/server/audit';
|
||||||
import { Changesets } from '$lib/server/changesets.js';
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
import { CryptoUtils } from '$lib/server/crypto-utils.js';
|
import { CryptoUtils } from '$lib/server/crypto-utils.js';
|
||||||
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
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 getActionData = async (locals: App.Locals, uuid: string) => {
|
||||||
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
'admin:oauth2',
|
||||||
|
'self:oauth2'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
@ -77,8 +79,8 @@ export const actions = {
|
|||||||
/**
|
/**
|
||||||
* Update the OAuth2 Client general information.
|
* Update the OAuth2 Client general information.
|
||||||
*/
|
*/
|
||||||
update: async ({ locals, request, params: { uuid } }) => {
|
update: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||||
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
const { details, fullPrivileges, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { title, description, activated, verified, confidential } =
|
const { title, description, activated, verified, confidential } =
|
||||||
@ -111,6 +113,13 @@ export const actions = {
|
|||||||
confidential: actuallyConfidential
|
confidential: actuallyConfidential
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`client_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -118,7 +127,7 @@ export const actions = {
|
|||||||
*
|
*
|
||||||
* Only works for disabled clients, and only the owner or admin can do it.
|
* 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);
|
const { currentUser, fullPrivileges, details } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
if (details.activated === 1) {
|
if (details.activated === 1) {
|
||||||
@ -131,6 +140,13 @@ export const actions = {
|
|||||||
|
|
||||||
await OAuth2Clients.deleteClient(details);
|
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');
|
return redirect(303, '/ssoadmin/oauth2');
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -151,6 +167,12 @@ export const actions = {
|
|||||||
|
|
||||||
// Allow secret regeneration only once per minute.
|
// Allow secret regeneration only once per minute.
|
||||||
if (!fullPrivileges && (await oneOffLimiter.isLimited(event))) {
|
if (!fullPrivileges && (await oneOffLimiter.isLimited(event))) {
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.THROTTLE,
|
||||||
|
event,
|
||||||
|
currentUser,
|
||||||
|
`secret regeneration attempt`
|
||||||
|
);
|
||||||
return fail(429, { errors: ['tooManyTimes'] });
|
return fail(429, { errors: ['tooManyTimes'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,13 +180,20 @@ export const actions = {
|
|||||||
client_secret: CryptoUtils.generateSecret()
|
client_secret: CryptoUtils.generateSecret()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_REGENERATE,
|
||||||
|
event,
|
||||||
|
currentUser,
|
||||||
|
`client_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Remove an URL reference.
|
* Remove an URL reference.
|
||||||
*/
|
*/
|
||||||
removeUrl: async ({ locals, url, params: { uuid } }) => {
|
removeUrl: async ({ locals, url, params: { uuid }, ...event }) => {
|
||||||
const { details } = await getActionData(locals, uuid);
|
const { details, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const id = Number(url.searchParams.get('id'));
|
const id = Number(url.searchParams.get('id'));
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
@ -173,13 +202,20 @@ export const actions = {
|
|||||||
|
|
||||||
await OAuth2Clients.deleteUrl(details, id);
|
await OAuth2Clients.deleteUrl(details, id);
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
event,
|
||||||
|
currentUser,
|
||||||
|
`delete url\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Add an URL reference.
|
* Add an URL reference.
|
||||||
*/
|
*/
|
||||||
addUrl: async ({ locals, request, params: { uuid } }) => {
|
addUrl: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||||
const { details } = await getActionData(locals, uuid);
|
const { details, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { type, url } = Changesets.take<AddUrlRequest>(['type', 'url'], body);
|
const { type, url } = Changesets.take<AddUrlRequest>(['type', 'url'], body);
|
||||||
@ -202,13 +238,20 @@ export const actions = {
|
|||||||
|
|
||||||
await OAuth2Clients.addUrl(details, type, url);
|
await OAuth2Clients.addUrl(details, type, url);
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`add url\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Remove a privilege reference.
|
* Remove a privilege reference.
|
||||||
*/
|
*/
|
||||||
removePrivilege: async ({ locals, url, params: { uuid } }) => {
|
removePrivilege: async ({ locals, url, params: { uuid }, ...event }) => {
|
||||||
const { details } = await getActionData(locals, uuid);
|
const { details, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const id = Number(url.searchParams.get('id'));
|
const id = Number(url.searchParams.get('id'));
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
@ -217,13 +260,20 @@ export const actions = {
|
|||||||
|
|
||||||
await OAuth2Clients.deletePrivilege(details, id);
|
await OAuth2Clients.deletePrivilege(details, id);
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
event,
|
||||||
|
currentUser,
|
||||||
|
`remove privilege\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Add a privilege reference.
|
* Add a privilege reference.
|
||||||
*/
|
*/
|
||||||
addPrivilege: async ({ locals, request, params: { uuid } }) => {
|
addPrivilege: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||||
const { details } = await getActionData(locals, uuid);
|
const { details, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { name } = Changesets.take<AddPrivilegeRequest>(['name'], body);
|
const { name } = Changesets.take<AddPrivilegeRequest>(['name'], body);
|
||||||
@ -234,13 +284,20 @@ export const actions = {
|
|||||||
|
|
||||||
await OAuth2Clients.addPrivilege(details, name);
|
await OAuth2Clients.addPrivilege(details, name);
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`add privilege\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Update the OAuth2 Client allowed grants list.
|
* Update the OAuth2 Client allowed grants list.
|
||||||
*/
|
*/
|
||||||
grants: async ({ locals, request, params: { uuid } }) => {
|
grants: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||||
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
const { details, fullPrivileges, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const allowedGrants = fullPrivileges
|
const allowedGrants = fullPrivileges
|
||||||
? OAuth2Clients.availableGrantTypes
|
? OAuth2Clients.availableGrantTypes
|
||||||
@ -257,13 +314,20 @@ export const actions = {
|
|||||||
grants: deduplicatedAllowedGrants.join(' ')
|
grants: deduplicatedAllowedGrants.join(' ')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`grants\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Update the OAuth2 Client allowed scopes list.
|
* Update the OAuth2 Client allowed scopes list.
|
||||||
*/
|
*/
|
||||||
scopes: async ({ locals, request, params: { uuid } }) => {
|
scopes: async ({ locals, request, params: { uuid }, getClientAddress }) => {
|
||||||
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
const { details, fullPrivileges, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const allowedScopes = fullPrivileges
|
const allowedScopes = fullPrivileges
|
||||||
? OAuth2Clients.availableScopes
|
? OAuth2Clients.availableScopes
|
||||||
@ -280,12 +344,19 @@ export const actions = {
|
|||||||
scope: OAuth2Clients.joinScope(deduplicatedAllowedScopes)
|
scope: OAuth2Clients.joinScope(deduplicatedAllowedScopes)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`scopes\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Update the OAuth2 Client icon picture.
|
* 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 { currentUser, details } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const formData = Object.fromEntries(await request.formData());
|
const formData = Object.fromEntries(await request.formData());
|
||||||
@ -299,16 +370,30 @@ export const actions = {
|
|||||||
|
|
||||||
await Uploads.saveClientAvatar(details, currentUser, file);
|
await Uploads.saveClientAvatar(details, currentUser, file);
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`upload avatar\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Delete the OAuth2 Client icon picture.
|
* Delete the OAuth2 Client icon picture.
|
||||||
*/
|
*/
|
||||||
removeAvatar: async ({ locals, params: { uuid } }) => {
|
removeAvatar: async ({ locals, params: { uuid }, ...event }) => {
|
||||||
const { details } = await getActionData(locals, uuid);
|
const { details, currentUser } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
await Uploads.removeClientAvatar(details);
|
await Uploads.removeClientAvatar(details);
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
event,
|
||||||
|
currentUser,
|
||||||
|
`remove avatar\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -347,6 +432,13 @@ export const actions = {
|
|||||||
|
|
||||||
await OAuth2Clients.sendManagerInvitationEmail(details as OAuth2Client, currentUser, email);
|
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: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -354,7 +446,7 @@ export const actions = {
|
|||||||
*
|
*
|
||||||
* Only the owner or admin can do it.
|
* 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 { details, currentUser, fullPrivileges } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const id = Number(url.searchParams.get('id'));
|
const id = Number(url.searchParams.get('id'));
|
||||||
@ -367,6 +459,12 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.removeManager(details, id);
|
await OAuth2Clients.removeManager(details, id);
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_UPDATE,
|
||||||
|
event,
|
||||||
|
currentUser,
|
||||||
|
`remove manager\nclient_id=${details.client_id}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AdminUtils } from '$lib/server/admin-utils';
|
import { AdminUtils } from '$lib/server/admin-utils';
|
||||||
|
import { Audit, AuditAction } from '$lib/server/audit';
|
||||||
import { Changesets } from '$lib/server/changesets.js';
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||||
import { OAuth2Clients } from '$lib/server/oauth2';
|
import { OAuth2Clients } from '$lib/server/oauth2';
|
||||||
@ -11,7 +12,7 @@ interface PrivilegesRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
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, [
|
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
@ -60,6 +61,13 @@ export const actions = {
|
|||||||
|
|
||||||
await Users.setUserPrivileges(targetUser, splitFilter, details.id);
|
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, '..');
|
return redirect(303, '..');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AdminUtils } from '$lib/server/admin-utils';
|
import { AdminUtils } from '$lib/server/admin-utils';
|
||||||
|
import { Audit, AuditAction } from '$lib/server/audit';
|
||||||
import { Changesets } from '$lib/server/changesets.js';
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
import { OAuth2Clients } from '$lib/server/oauth2/index.js';
|
import { OAuth2Clients } from '$lib/server/oauth2/index.js';
|
||||||
import { fail, redirect } from '@sveltejs/kit';
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
@ -11,7 +12,7 @@ interface CreateClientRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ locals, request }) => {
|
default: async ({ locals, request, getClientAddress }) => {
|
||||||
const { currentUser } = await AdminUtils.getActionUser(locals, [
|
const { currentUser } = await AdminUtils.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2:create']
|
['admin:oauth2', 'self:oauth2:create']
|
||||||
]);
|
]);
|
||||||
@ -42,6 +43,13 @@ export const actions = {
|
|||||||
!!confidential
|
!!confidential
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.OAUTH2_CREATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`client_id=${uuid}`
|
||||||
|
);
|
||||||
|
|
||||||
return redirect(303, `/ssoadmin/oauth2/${uuid}`);
|
return redirect(303, `/ssoadmin/oauth2/${uuid}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { AdminUtils } from '$lib/server/admin-utils';
|
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 { Changesets } from '$lib/server/changesets.js';
|
||||||
import { OAuth2Tokens } from '$lib/server/oauth2/index.js';
|
import { OAuth2Tokens } from '$lib/server/oauth2/index.js';
|
||||||
import { Uploads } from '$lib/server/upload.js';
|
import { Uploads } from '$lib/server/upload.js';
|
||||||
@ -30,16 +32,14 @@ export const actions = {
|
|||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
deleteInfo: async ({ locals, params: { uuid } }) => {
|
deleteInfo: async ({ locals, params: { uuid }, request, getClientAddress }) => {
|
||||||
await AdminUtils.getActionUser(locals, ['admin', 'admin:user']);
|
const { currentUser } = await AdminUtils.getActionUser(locals, ['admin', 'admin:user']);
|
||||||
|
|
||||||
const targetUser = await Users.getByUuid(uuid, false);
|
const targetUser = await Users.getByUuid(uuid, false);
|
||||||
if (!targetUser || !!targetUser.activated) {
|
if (!targetUser || !!targetUser.activated) {
|
||||||
return fail(404, { errors: ['invalidUuid'] });
|
return fail(404, { errors: ['invalidUuid'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: audit log
|
|
||||||
|
|
||||||
const [stubName] = uuid.split('-');
|
const [stubName] = uuid.split('-');
|
||||||
|
|
||||||
// Nuke EVERYTHING
|
// Nuke EVERYTHING
|
||||||
@ -54,6 +54,13 @@ export const actions = {
|
|||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Audit.insertRequest(
|
||||||
|
AuditAction.USER_DELETE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`uuid=${targetUser.uuid}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
email: async ({ locals, params: { uuid }, url }) => {
|
email: async ({ locals, params: { uuid }, url }) => {
|
||||||
@ -80,11 +87,9 @@ export const actions = {
|
|||||||
return fail(403, { errors: ['invalidEmailType'] });
|
return fail(403, { errors: ['invalidEmailType'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: audit log
|
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
update: async ({ locals, params: { uuid }, request }) => {
|
update: async ({ locals, params: { uuid }, request, getClientAddress }) => {
|
||||||
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||||
'admin',
|
'admin',
|
||||||
'admin:user'
|
'admin:user'
|
||||||
@ -139,7 +144,12 @@ export const actions = {
|
|||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: audit log
|
await Audit.insertRequest(
|
||||||
|
AuditAction.USER_UPDATE,
|
||||||
|
{ request, getClientAddress },
|
||||||
|
currentUser,
|
||||||
|
`uuid=${targetUser.uuid}`
|
||||||
|
);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user