Retheme, organize, comment
This commit is contained in:
parent
dc9be2016b
commit
77e2b49aa6
32
src/app.css
32
src/app.css
@ -1,34 +1,11 @@
|
|||||||
|
@import url('./colors.css');
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
|
||||||
--in-text-color: #fff;
|
|
||||||
--in-link-color: #fff;
|
|
||||||
--in-outline-color: #00aaff;
|
|
||||||
--in-normalized-background: #000;
|
|
||||||
--in-input-background: #fff;
|
|
||||||
--in-input-background-disabled: #c2c2c2;
|
|
||||||
--in-input-required-color: #ff0000;
|
|
||||||
--in-input-color: #000;
|
|
||||||
--in-input-color-disabled: #414141;
|
|
||||||
--in-input-border-color: #ddd;
|
|
||||||
--in-input-border-color-disabled: #a0a0a0;
|
|
||||||
--in-input-border-color-invalid: #ff0000;
|
|
||||||
|
|
||||||
--in-alert-color: #006597;
|
|
||||||
--in-error-color: #b52e2e;
|
|
||||||
--in-success-color: #1e7f27;
|
|
||||||
|
|
||||||
--in-modal-background: #fff;
|
|
||||||
--in-modal-backdrop: rgba(0, 0, 0, 0.3);
|
|
||||||
--in-modal-divider-color: #ddd;
|
|
||||||
|
|
||||||
--in-focus-outline: 3px solid var(--in-outline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||||
'Open Sans', 'Helvetica Neue', sans-serif;
|
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
@ -40,11 +17,12 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--in-normalized-background);
|
background-color: var(--in-normalized-background);
|
||||||
background-image: url('/background.jpg');
|
background-image: var(--in-background-image);
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@ -72,7 +50,7 @@ a {
|
|||||||
|
|
||||||
a[target='_blank']::after {
|
a[target='_blank']::after {
|
||||||
content: '';
|
content: '';
|
||||||
background-image: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 style=%27width:24px;height:24px%27 viewBox=%270 0 24 24%27%3E%3Cpath fill=%27%23ffffff%27 d=%27M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z%27 /%3E%3C/svg%3E');
|
background-image: var(--in-external-link-icon);
|
||||||
width: 0.95rem;
|
width: 0.95rem;
|
||||||
height: 0.95rem;
|
height: 0.95rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
122
src/colors.css
Normal file
122
src/colors.css
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/**************/
|
||||||
|
/* Light Mode */
|
||||||
|
/**************/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--in-background-image: none;
|
||||||
|
--in-normalized-background: #cceaff;
|
||||||
|
--in-container-background: #f1faff;
|
||||||
|
|
||||||
|
--in-text-color: #000000;
|
||||||
|
--in-link-color: #000000;
|
||||||
|
--in-external-link-icon: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 style=%27width:24px;height:24px%27 viewBox=%270 0 24 24%27%3E%3Cpath fill=%27000000%27 d=%27M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z%27 /%3E%3C/svg%3E');
|
||||||
|
--in-outline-color: #00aaff;
|
||||||
|
|
||||||
|
--in-input-background: #fff;
|
||||||
|
--in-input-required-color: #ff0000;
|
||||||
|
--in-input-color: #000;
|
||||||
|
--in-input-border: 1px solid #cfcfcf;
|
||||||
|
--in-input-border-color-invalid: #ff0000;
|
||||||
|
|
||||||
|
--in-input-color-disabled: #535353;
|
||||||
|
--in-input-background-disabled: #e9e9e9;
|
||||||
|
--in-input-border-color-disabled: #b9b9b9;
|
||||||
|
|
||||||
|
--in-alert-color: #86d7ff;
|
||||||
|
--in-error-color: #ff8888;
|
||||||
|
--in-success-color: #6ade6d;
|
||||||
|
|
||||||
|
--in-modal-background: #fff;
|
||||||
|
--in-modal-backdrop: rgba(0, 0, 0, 0.3);
|
||||||
|
--in-modal-divider-color: #cfcfcf;
|
||||||
|
--in-modal-text-color: #000;
|
||||||
|
|
||||||
|
--in-button-primary-background: #fff;
|
||||||
|
--in-button-primary-hover-background: #ececec;
|
||||||
|
--in-button-primary-border: 1px solid #cfcfcf;
|
||||||
|
--in-button-primary-color: #000;
|
||||||
|
|
||||||
|
--in-button-primary-disabled-background: #d5d5d5;
|
||||||
|
--in-button-primary-disabled-border: 1px solid #a5a5a5;
|
||||||
|
--in-button-primary-disabled-color: #626262;
|
||||||
|
|
||||||
|
--in-focus-outline: 3px solid var(--in-outline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ina-background-color: #ececec;
|
||||||
|
|
||||||
|
--ina-header-color: linear-gradient(180deg, #00aaff 0%, #0092db 100%);
|
||||||
|
--ina-header-link-color: #ffffff;
|
||||||
|
--ina-header-tag-color: #006eb8;
|
||||||
|
--ina-header-tag-text-color: #ffffff;
|
||||||
|
|
||||||
|
--ina-sidebar-color: #dddddd;
|
||||||
|
--ina-sidebar-border-color: #b4b4b4;
|
||||||
|
--ina-sidebar-link-text-color: #000;
|
||||||
|
--ina-sidebar-link-active-color: #c7c7c7;
|
||||||
|
--ina-sidebar-link-hover-color: #e4e4e4;
|
||||||
|
|
||||||
|
--ina-card-background: #fff;
|
||||||
|
--ina-card-text-muted: #646464;
|
||||||
|
--ina-card-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************/
|
||||||
|
/* Dark Mode */
|
||||||
|
/*************/
|
||||||
|
|
||||||
|
html[theme-base='dark'] {
|
||||||
|
--in-background-image: url('/background.jpg');
|
||||||
|
--in-normalized-background: #000;
|
||||||
|
--in-container-background: transparent;
|
||||||
|
|
||||||
|
--in-text-color: #fff;
|
||||||
|
--in-link-color: #fff;
|
||||||
|
--in-external-link-icon: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 style=%27width:24px;height:24px%27 viewBox=%270 0 24 24%27%3E%3Cpath fill=%27%23ffffff%27 d=%27M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z%27 /%3E%3C/svg%3E');
|
||||||
|
|
||||||
|
--in-input-background: #2e2e2e;
|
||||||
|
--in-input-background-disabled: #101010;
|
||||||
|
--in-input-required-color: #ff0000;
|
||||||
|
--in-input-color: #fff;
|
||||||
|
--in-input-color-disabled: #8f8f8f;
|
||||||
|
--in-input-border-color: #242424;
|
||||||
|
--in-input-border-color-disabled: #242424;
|
||||||
|
--in-input-border-color-invalid: #ff0000;
|
||||||
|
|
||||||
|
--in-alert-color: #006597;
|
||||||
|
--in-error-color: #b52e2e;
|
||||||
|
--in-success-color: #1e7f27;
|
||||||
|
|
||||||
|
--in-modal-background: #232323;
|
||||||
|
--in-modal-divider-color: #181818;
|
||||||
|
--in-modal-text-color: #fff;
|
||||||
|
|
||||||
|
--in-button-primary-background: #2e2e2e;
|
||||||
|
--in-button-primary-hover-background: #4b4b4b;
|
||||||
|
--in-button-primary-border: 2px solid #595959;
|
||||||
|
--in-button-primary-color: #fff;
|
||||||
|
|
||||||
|
--in-button-primary-disabled-background: #101010;
|
||||||
|
--in-button-primary-disabled-border: 2px solid #242424;
|
||||||
|
--in-button-primary-disabled-color: #8f8f8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[theme-base='dark'] {
|
||||||
|
--ina-background-color: #232323;
|
||||||
|
|
||||||
|
--ina-header-color: linear-gradient(180deg, #00aaff 0%, #0092db 100%);
|
||||||
|
--ina-header-link-color: #ffffff;
|
||||||
|
--ina-header-tag-color: #006eb8;
|
||||||
|
--ina-header-tag-text-color: #ffffff;
|
||||||
|
|
||||||
|
--ina-sidebar-color: #191919;
|
||||||
|
--ina-sidebar-border-color: #0b0b0b;
|
||||||
|
--ina-sidebar-link-text-color: #ffffff;
|
||||||
|
--ina-sidebar-link-active-color: #2c2c2c;
|
||||||
|
--ina-sidebar-link-hover-color: #242424;
|
||||||
|
|
||||||
|
--ina-card-background: #1c1c1c;
|
||||||
|
--ina-card-text-muted: #a1a1a1;
|
||||||
|
--ina-card-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { AUTO_MIGRATE, SESSION_SECRET } from '$env/static/private';
|
import { AUTO_MIGRATE, SESSION_SECRET } from '$env/static/private';
|
||||||
import { db } from '$lib/server/drizzle';
|
import { db } from '$lib/server/drizzle';
|
||||||
|
import { runSeeds } from '$lib/server/drizzle/seeds';
|
||||||
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||||
import { handleSession } from 'svelte-kit-cookie-session';
|
import { handleSession } from 'svelte-kit-cookie-session';
|
||||||
|
|
||||||
@ -7,6 +8,8 @@ if (AUTO_MIGRATE === 'true') {
|
|||||||
await migrate(db, { migrationsFolder: './migrations' });
|
await migrate(db, { migrationsFolder: './migrations' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runSeeds();
|
||||||
|
|
||||||
export const handle = handleSession({
|
export const handle = handleSession({
|
||||||
secret: SESSION_SECRET
|
secret: SESSION_SECRET
|
||||||
});
|
});
|
||||||
|
18
src/lib/components/ActionButton.svelte
Normal file
18
src/lib/components/ActionButton.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import type { SubmitFunction } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export let action: string;
|
||||||
|
export let variant: ComponentProps<Button>['variant'] = 'link';
|
||||||
|
|
||||||
|
export let enhanced = false;
|
||||||
|
export let enhanceFn: SubmitFunction | undefined = undefined;
|
||||||
|
$: enhancer = enhanced ? enhance : () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form {action} method="POST" use:enhancer={enhanceFn}>
|
||||||
|
<slot name="form" />
|
||||||
|
<Button {variant} type="submit"><slot /></Button>
|
||||||
|
</form>
|
@ -18,7 +18,7 @@
|
|||||||
background-color: var(--in-alert-color);
|
background-color: var(--in-alert-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 24px 28px;
|
padding: 16px 18px;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
|
|
||||||
& p {
|
& p {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--in-text-color);
|
color: var(--in-link-color);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@ -32,11 +32,21 @@
|
|||||||
|
|
||||||
.btn-default,
|
.btn-default,
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #fff;
|
background-color: var(--in-button-primary-background);
|
||||||
color: #000;
|
color: var(--in-button-primary-color);
|
||||||
border: 2px solid #ddd;
|
border: var(--in-button-primary-border);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--in-button-primary-hover-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
background-color: var(--in-button-primary-disabled-background);
|
||||||
|
color: var(--in-button-primary-disabled-color);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import Button from './Button.svelte';
|
import ActionButton from './ActionButton.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form action="/account?/logout" method="POST">
|
<ActionButton action="/account?/logout">{$t('account.logout')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('account.logout')}</Button>
|
|
||||||
</form>
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
export let showModal: boolean;
|
export let showModal: boolean;
|
||||||
|
export let dismissable = true;
|
||||||
let dialog: HTMLDialogElement;
|
let dialog: HTMLDialogElement;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@ -17,7 +18,7 @@
|
|||||||
showModal = false;
|
showModal = false;
|
||||||
dispatch('close');
|
dispatch('close');
|
||||||
}}
|
}}
|
||||||
on:click|self={() => dialog.close()}
|
on:click|self={() => dismissable && dialog.close()}
|
||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div on:click|stopPropagation>
|
<div on:click|stopPropagation>
|
||||||
@ -43,6 +44,7 @@
|
|||||||
border-radius: 0.2em;
|
border-radius: 0.2em;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
color: var(--in-modal-text-color);
|
||||||
background-color: var(--in-modal-background);
|
background-color: var(--in-modal-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
href={`?page=${pageNum - 1}`}
|
href={`?page=${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
|
||||||
>
|
>
|
||||||
|
|
||||||
{#each pageButtons as buttonNumber}
|
{#each pageButtons as buttonNumber}
|
||||||
@ -35,7 +35,7 @@
|
|||||||
tabindex={lastPage ? -1 : 0}
|
tabindex={lastPage ? -1 : 0}
|
||||||
href={`?page=${pageNum + 1}`}
|
href={`?page=${pageNum + 1}`}
|
||||||
aria-label={$t('common.next')}
|
aria-label={$t('common.next')}
|
||||||
aria-disabled={lastPage}>>></a
|
aria-disabled={lastPage}>></a
|
||||||
>
|
>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -49,8 +49,8 @@
|
|||||||
.page-button {
|
.page-button {
|
||||||
--in-page-button-size: 28px;
|
--in-page-button-size: 28px;
|
||||||
display: block;
|
display: block;
|
||||||
background-color: #fff;
|
background-color: var(--in-button-primary-background);
|
||||||
color: #000;
|
color: var(--in-button-primary-color);
|
||||||
min-width: var(--in-page-button-size);
|
min-width: var(--in-page-button-size);
|
||||||
height: var(--in-page-button-size);
|
height: var(--in-page-button-size);
|
||||||
line-height: var(--in-page-button-size);
|
line-height: var(--in-page-button-size);
|
||||||
@ -60,14 +60,14 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #ececec;
|
background-color: var(--in-button-primary-hover-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-color: #ececec;
|
background-color: var(--in-button-primary-disabled-background);
|
||||||
color: #616161;
|
color: var(--in-button-primary-disabled-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -59,9 +59,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: #fff;
|
background-color: var(--ina-card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
box-shadow: var(--ina-card-shadow);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
transition: transform 100ms linear;
|
transition: transform 100ms linear;
|
||||||
|
@ -18,8 +18,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: #00aaff;
|
background: var(--ina-header-color);
|
||||||
background: linear-gradient(180deg, #00aaff 0%, #005bff 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-user {
|
.admin-user {
|
||||||
@ -27,9 +26,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 4px 8px 4px 4px;
|
padding: 4px 8px 4px 4px;
|
||||||
background-color: #004edf;
|
background-color: var(--ina-header-tag-color);
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
color: #fff;
|
color: var(--ina-header-tag-text-color);
|
||||||
|
|
||||||
& .admin-user-avatar {
|
& .admin-user-avatar {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
@ -41,10 +40,10 @@
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #fff;
|
color: var(--ina-header-link-color);
|
||||||
|
|
||||||
&:visited {
|
&:visited {
|
||||||
color: #fff;
|
color: var(--ina-header-link-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -47,11 +47,11 @@
|
|||||||
.admin-sidebar {
|
.admin-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #dddddd;
|
background-color: var(--ina-sidebar-color);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-right: 2px solid #b4b4b4;
|
border-right: 2px solid var(--ina-sidebar-border-color);
|
||||||
|
|
||||||
& > nav {
|
& > nav {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@ -69,11 +69,11 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #c7c7c7;
|
background-color: var(--ina-sidebar-link-active-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #e4e4e4;
|
background-color: var(--ina-sidebar-link-hover-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,6 +82,6 @@
|
|||||||
|
|
||||||
.admin-sidebar,
|
.admin-sidebar,
|
||||||
.admin-sidebar :global(a) {
|
.admin-sidebar :global(a) {
|
||||||
color: #000;
|
color: var(--ina-sidebar-link-text-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -43,9 +43,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: #fff;
|
background-color: var(--ina-card-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
box-shadow: var(--ina-card-shadow);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
transition: transform 100ms linear;
|
transition: transform 100ms linear;
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-inner {
|
.page-inner {
|
||||||
@ -18,6 +19,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
background: var(--in-container-background);
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
max-width: 1080px;
|
max-width: 1080px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -25,7 +27,6 @@
|
|||||||
|
|
||||||
main {
|
main {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
background: var(--in-container-background);
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
:global(input) {
|
:global(input) {
|
||||||
background-color: var(--in-input-background);
|
background-color: var(--in-input-background);
|
||||||
color: var(--in-input-color);
|
color: var(--in-input-color);
|
||||||
border: 2px solid var(--in-input-border-color);
|
border: var(--in-input-border);
|
||||||
|
|
||||||
&:not([type]),
|
&:not([type]),
|
||||||
&[type='text'],
|
&[type='text'],
|
||||||
@ -17,7 +17,7 @@
|
|||||||
&[type='email'] {
|
&[type='email'] {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: var(--in-focus-outline);
|
outline: var(--in-focus-outline);
|
||||||
@ -34,6 +34,11 @@
|
|||||||
border-color: var(--in-input-border-color-invalid);
|
border-color: var(--in-input-border-color-invalid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[type='file'] {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control > :global(label) {
|
.form-control > :global(label) {
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
"authorizationsHint": "These users have authorized this application at least once. You may assign application privileges to each user individually.",
|
"authorizationsHint": "These users have authorized this application at least once. You may assign application privileges to each user individually.",
|
||||||
"revoked": "This authorization has been revoked by the user",
|
"revoked": "This authorization has been revoked by the user",
|
||||||
"noAuthorizations": "There are no authorizations on record for this application.",
|
"noAuthorizations": "There are no authorizations on record for this application.",
|
||||||
|
"success": "Success!",
|
||||||
"privileges": {
|
"privileges": {
|
||||||
"title": "Application privileges",
|
"title": "Application privileges",
|
||||||
"name": "Privilege name",
|
"name": "Privilege name",
|
||||||
@ -124,7 +125,8 @@
|
|||||||
"invalidPrivilege": "Invalid privilege provided.",
|
"invalidPrivilege": "Invalid privilege provided.",
|
||||||
"invalidEmail": "Invalid email address.",
|
"invalidEmail": "Invalid email address.",
|
||||||
"emailExists": "This email address is already added.",
|
"emailExists": "This email address is already added.",
|
||||||
"noFile": "Please upload a file first."
|
"noFile": "Please upload a file first.",
|
||||||
|
"tooManyTimes": "You are doing that too much, please, slow down!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
||||||
"cookieDisclaimer": "The website may use temporary cookies for storing your login session and ensuring your security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/icynet-auth-server\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
"cookieDisclaimer": "The website may use temporary cookies for storing your login session and ensuring your security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/sso-core\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { UserSession } from './users';
|
import { Users, type UserSession } from './users';
|
||||||
import { hasPrivileges, type RequiredPrivileges } from '$lib/utils';
|
import { hasPrivileges, type RequiredPrivileges } from '$lib/utils';
|
||||||
|
|
||||||
export class AdminUtils {
|
export class AdminUtils {
|
||||||
@ -8,4 +8,23 @@ export class AdminUtils {
|
|||||||
error(403, 'Forbidden resource');
|
error(403, 'Forbidden resource');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user who is committing an action.
|
||||||
|
* @param locals App locals
|
||||||
|
* @param privileges Required privileges for action
|
||||||
|
* @returns Current user and their session cookie
|
||||||
|
*/
|
||||||
|
static async getActionUser(locals: App.Locals, privileges: RequiredPrivileges) {
|
||||||
|
const userSession = locals.session.data?.user;
|
||||||
|
const currentUser = await Users.getBySession(userSession);
|
||||||
|
if (!userSession || !currentUser) {
|
||||||
|
return error(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
userSession.privileges = await Users.getUserPrivileges(currentUser);
|
||||||
|
AdminUtils.checkPrivileges(userSession, privileges);
|
||||||
|
|
||||||
|
return { currentUser, userSession };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
9
src/lib/server/drizzle/seeds/index.ts
Normal file
9
src/lib/server/drizzle/seeds/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import privilegesSeed from './privileges';
|
||||||
|
|
||||||
|
const seeds = [privilegesSeed];
|
||||||
|
|
||||||
|
export const runSeeds = async () => {
|
||||||
|
for (const fn of seeds) {
|
||||||
|
await fn();
|
||||||
|
}
|
||||||
|
};
|
28
src/lib/server/drizzle/seeds/privileges.ts
Normal file
28
src/lib/server/drizzle/seeds/privileges.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db, privilege } from '..';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System privileges which must always exist in the database.
|
||||||
|
*/
|
||||||
|
const privileges = [
|
||||||
|
'admin',
|
||||||
|
'admin:user',
|
||||||
|
'admin:user:privilege',
|
||||||
|
'admin:oauth2',
|
||||||
|
'admin:audit',
|
||||||
|
'admin:document',
|
||||||
|
'self:oauth2',
|
||||||
|
'self:oauth2:implicit',
|
||||||
|
'self:oauth2:create'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async function privilegesSeed() {
|
||||||
|
for (const priv of privileges) {
|
||||||
|
const [exists] = await db
|
||||||
|
.select({ id: privilege.id })
|
||||||
|
.from(privilege)
|
||||||
|
.where(eq(privilege.name, priv));
|
||||||
|
if (exists) continue;
|
||||||
|
await db.insert(privilege).values({ name: priv });
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ ${PUBLIC_SITE_NAME}
|
|||||||
|
|
||||||
${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}.
|
${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}.
|
||||||
|
|
||||||
Please click on the following link to accept the invitation.
|
Please use the following link to accept the invitation.
|
||||||
|
|
||||||
Accept invitation: ${url}
|
Accept invitation: ${url}
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ This email was sent to you because someone invited you to contribute to an appli
|
|||||||
|
|
||||||
<p>${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}.
|
<p>${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}.
|
||||||
|
|
||||||
<p><b>Please click on the following link to accept the invitation ${PUBLIC_SITE_NAME}.</b></p>
|
<p><b>Please use the following link to accept the invitation:</b></p>
|
||||||
|
|
||||||
<p>Accept invitation: <a href="${url}" target="_blank">${url}</a></p>
|
<p>Accept invitation: <a href="${url}" target="_blank">${url}</a></p>
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
} from '$lib/server/drizzle';
|
} from '$lib/server/drizzle';
|
||||||
import { Emails, OAuth2InvitationEmail } from '$lib/server/email';
|
import { Emails, OAuth2InvitationEmail } from '$lib/server/email';
|
||||||
import { Uploads } from '$lib/server/upload';
|
import { Uploads } from '$lib/server/upload';
|
||||||
import { UserTokens } from '$lib/server/users';
|
import { UserTokens, Users } from '$lib/server/users';
|
||||||
import type { PaginationMeta } from '$lib/types';
|
import type { PaginationMeta } from '$lib/types';
|
||||||
import { and, count, eq, like, or, sql } from 'drizzle-orm';
|
import { and, count, eq, like, or, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
@ -64,6 +64,9 @@ export class OAuth2Clients {
|
|||||||
'openid'
|
'openid'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Non-administrator capabilities
|
||||||
|
public static implicitGrantTypes = ['id_token', 'implicit'];
|
||||||
|
public static userSetGrants = ['authorization_code', 'client_credentials', 'refresh_token'];
|
||||||
public static userSetScopes = [
|
public static userSetScopes = [
|
||||||
'profile',
|
'profile',
|
||||||
'picture',
|
'picture',
|
||||||
@ -72,12 +75,6 @@ export class OAuth2Clients {
|
|||||||
'management',
|
'management',
|
||||||
'openid'
|
'openid'
|
||||||
];
|
];
|
||||||
public static userSetGrants = [
|
|
||||||
'authorization_code',
|
|
||||||
'client_credentials',
|
|
||||||
'refresh_token',
|
|
||||||
'id_token'
|
|
||||||
];
|
|
||||||
|
|
||||||
public static describedScopes = ['email', 'picture', 'account'];
|
public static describedScopes = ['email', 'picture', 'account'];
|
||||||
public static alwaysPresentScopes = ['profile'];
|
public static alwaysPresentScopes = ['profile'];
|
||||||
@ -317,7 +314,7 @@ export class OAuth2Clients {
|
|||||||
client_id: uid,
|
client_id: uid,
|
||||||
client_secret: secret,
|
client_secret: secret,
|
||||||
grants: 'authorization_code',
|
grants: 'authorization_code',
|
||||||
scope: 'profile',
|
scope: OAuth2Clients.joinScope(OAuth2Clients.alwaysPresentScopes),
|
||||||
ownerId: subject.id,
|
ownerId: subject.id,
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
activated: 1,
|
activated: 1,
|
||||||
@ -382,6 +379,13 @@ export class OAuth2Clients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async addManager(client: OAuth2Client, actor: User, subject: User) {
|
static async addManager(client: OAuth2Client, actor: User, subject: User) {
|
||||||
|
// Check if the subject has access to OAuth2 management already,
|
||||||
|
// if they do not, give them the regular user's OAuth2 management privilege.
|
||||||
|
const privList = await Users.getUserPrivileges(subject);
|
||||||
|
if (!privList.some((name) => ['self:oauth2', 'admin:oauth2'].includes(name))) {
|
||||||
|
await Users.grantPrivilege(subject, 'self:oauth2');
|
||||||
|
}
|
||||||
|
|
||||||
await db.insert(oauth2ClientManager).values({
|
await db.insert(oauth2ClientManager).values({
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
userId: subject.id,
|
userId: subject.id,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { count, eq, like, or, sql } from 'drizzle-orm';
|
import { asc, count, eq, like, or, sql } from 'drizzle-orm';
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
privilege,
|
privilege,
|
||||||
@ -8,16 +8,17 @@ import {
|
|||||||
type User
|
type User
|
||||||
} from '../drizzle';
|
} from '../drizzle';
|
||||||
import type { Paginated, PaginationMeta } from '$lib/types';
|
import type { Paginated, PaginationMeta } from '$lib/types';
|
||||||
import type { RequiredPrivileges } from '$lib/utils';
|
|
||||||
import { Users } from '.';
|
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import { AdminUtils } from '../admin-utils';
|
|
||||||
|
|
||||||
export interface AdminUserListItem extends Omit<User, 'password'> {
|
export interface AdminUserListItem extends Omit<User, 'password'> {
|
||||||
privileges: Privilege[];
|
privileges: Privilege[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UsersAdmin {
|
export class UsersAdmin {
|
||||||
|
/**
|
||||||
|
* Map a list of users with their privileges.
|
||||||
|
* @param junkList User-privilege amalgamation
|
||||||
|
* @returns List of users
|
||||||
|
*/
|
||||||
static mergeUserResponse(
|
static mergeUserResponse(
|
||||||
junkList: {
|
junkList: {
|
||||||
user: User;
|
user: User;
|
||||||
@ -45,6 +46,11 @@ export class UsersAdmin {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all users and their privileges.
|
||||||
|
* @param params Search params
|
||||||
|
* @returns List of users
|
||||||
|
*/
|
||||||
static async getAllUsers({
|
static async getAllUsers({
|
||||||
filter,
|
filter,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
@ -69,14 +75,26 @@ export class UsersAdmin {
|
|||||||
.from(user)
|
.from(user)
|
||||||
.where(searchExpression);
|
.where(searchExpression);
|
||||||
|
|
||||||
const junkList = await db
|
const baseQuery = db
|
||||||
.select()
|
.select({ id: user.id })
|
||||||
.from(user)
|
.from(user)
|
||||||
|
.where(searchExpression)
|
||||||
|
.orderBy(asc(user.id))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.as('searchBase');
|
||||||
|
|
||||||
|
const junkList = await db
|
||||||
|
.select({
|
||||||
|
user: user,
|
||||||
|
user_privileges_privilege: userPrivilegesPrivilege,
|
||||||
|
privilege: privilege
|
||||||
|
})
|
||||||
|
.from(baseQuery)
|
||||||
|
.innerJoin(user, eq(baseQuery.id, user.id))
|
||||||
.leftJoin(userPrivilegesPrivilege, eq(userPrivilegesPrivilege.userId, user.id))
|
.leftJoin(userPrivilegesPrivilege, eq(userPrivilegesPrivilege.userId, user.id))
|
||||||
.leftJoin(privilege, eq(userPrivilegesPrivilege.privilegeId, privilege.id))
|
.leftJoin(privilege, eq(userPrivilegesPrivilege.privilegeId, privilege.id))
|
||||||
.where(searchExpression)
|
.where(searchExpression);
|
||||||
.limit(limit)
|
|
||||||
.offset(offset);
|
|
||||||
|
|
||||||
const meta: PaginationMeta = {
|
const meta: PaginationMeta = {
|
||||||
rowCount,
|
rowCount,
|
||||||
@ -92,6 +110,11 @@ export class UsersAdmin {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an user's information by their UUID.
|
||||||
|
* @param uuid User UUID
|
||||||
|
* @returns User infor
|
||||||
|
*/
|
||||||
static async getUserDetails(uuid: string) {
|
static async getUserDetails(uuid: string) {
|
||||||
const junkList = await db
|
const junkList = await db
|
||||||
.select()
|
.select()
|
||||||
@ -102,17 +125,4 @@ export class UsersAdmin {
|
|||||||
const [userInfo] = UsersAdmin.mergeUserResponse(junkList);
|
const [userInfo] = UsersAdmin.mergeUserResponse(junkList);
|
||||||
return userInfo;
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getActionUser(locals: App.Locals, privileges: RequiredPrivileges) {
|
|
||||||
const userSession = locals.session.data?.user;
|
|
||||||
const currentUser = await Users.getBySession(userSession);
|
|
||||||
if (!userSession || !currentUser) {
|
|
||||||
return error(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
userSession.privileges = await Users.getUserPrivileges(currentUser);
|
|
||||||
AdminUtils.checkPrivileges(userSession, privileges);
|
|
||||||
|
|
||||||
return { currentUser, userSession };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs';
|
|||||||
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm';
|
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm';
|
||||||
import { db, privilege, user, userPrivilegesPrivilege, type User } from '../drizzle';
|
import { db, privilege, user, userPrivilegesPrivilege, type User } from '../drizzle';
|
||||||
import type { UserSession } from './types';
|
import type { UserSession } from './types';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
import { CryptoUtils } from '../crypto-utils';
|
import { CryptoUtils } from '../crypto-utils';
|
||||||
import { EMAIL_ENABLED } from '$env/static/private';
|
import { EMAIL_ENABLED } from '$env/static/private';
|
||||||
import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email';
|
import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email';
|
||||||
@ -10,6 +10,11 @@ import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public';
|
|||||||
import { UserTokens } from './tokens';
|
import { UserTokens } from './tokens';
|
||||||
|
|
||||||
export class Users {
|
export class Users {
|
||||||
|
/**
|
||||||
|
* Get user by their primary key ID.
|
||||||
|
* @param id User ID
|
||||||
|
* @returns User
|
||||||
|
*/
|
||||||
static async getById(id: number): Promise<User | undefined> {
|
static async getById(id: number): Promise<User | undefined> {
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
@ -19,6 +24,11 @@ export class Users {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user by their UUID (public-facing ID).
|
||||||
|
* @param id User ID
|
||||||
|
* @returns User
|
||||||
|
*/
|
||||||
static async getByUuid(uuid: string, activatedCheck = true): Promise<User | undefined> {
|
static async getByUuid(uuid: string, activatedCheck = true): Promise<User | undefined> {
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
@ -28,15 +38,30 @@ export class Users {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an user by their login (username or email)
|
||||||
|
* @param login Username or email address
|
||||||
|
* @returns User
|
||||||
|
*/
|
||||||
static async getByLogin(login: string): Promise<User | undefined> {
|
static async getByLogin(login: string): Promise<User | undefined> {
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(user)
|
.from(user)
|
||||||
.where(and(or(eq(user.email, login), eq(user.username, login)), eq(user.activated, 1)))
|
.where(
|
||||||
|
and(
|
||||||
|
or(eq(user.email, login), sql`lower(${user.username}) = ${login.toLowerCase()}`),
|
||||||
|
eq(user.activated, 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an user by their session cookie.
|
||||||
|
* @param session Session from cookie
|
||||||
|
* @returns User
|
||||||
|
*/
|
||||||
static async getBySession(session?: UserSession): Promise<User | undefined> {
|
static async getBySession(session?: UserSession): Promise<User | undefined> {
|
||||||
if (!session) return undefined;
|
if (!session) return undefined;
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
@ -47,18 +72,39 @@ export class Users {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an user.
|
||||||
|
* @param subject User to update
|
||||||
|
* @param fields Fields to set
|
||||||
|
*/
|
||||||
static async update(subject: User, fields: Partial<User>) {
|
static async update(subject: User, fields: Partial<User>) {
|
||||||
return db.update(user).set(fields).where(eq(user.id, subject.id));
|
return db.update(user).set(fields).where(eq(user.id, subject.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check an user's password.
|
||||||
|
* @param user User
|
||||||
|
* @param password Plaintext password
|
||||||
|
* @returns Boolean
|
||||||
|
*/
|
||||||
static async validatePassword(user: User, password: string): Promise<boolean> {
|
static async validatePassword(user: User, password: string): Promise<boolean> {
|
||||||
return bcrypt.compare(password, user.password as string);
|
return bcrypt.compare(password, user.password as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a password.
|
||||||
|
* @param password Plaintext password
|
||||||
|
* @returns Hashed password
|
||||||
|
*/
|
||||||
static async hashPassword(password: string): Promise<string> {
|
static async hashPassword(password: string): Promise<string> {
|
||||||
return bcrypt.hash(password, 10);
|
return bcrypt.hash(password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a session cookie from user information.
|
||||||
|
* @param user User
|
||||||
|
* @returns Cookie
|
||||||
|
*/
|
||||||
static async toSession(user: User): Promise<UserSession> {
|
static async toSession(user: User): Promise<UserSession> {
|
||||||
return {
|
return {
|
||||||
uid: user.id,
|
uid: user.id,
|
||||||
@ -68,6 +114,12 @@ export class Users {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an user from the request locals or redirect to the login page.
|
||||||
|
* @param locals App locals
|
||||||
|
* @param url URL
|
||||||
|
* @returns User
|
||||||
|
*/
|
||||||
static async readSessionOrRedirect(locals: App.Locals, url: URL) {
|
static async readSessionOrRedirect(locals: App.Locals, url: URL) {
|
||||||
const currentUser = await Users.getBySession(locals.session.data?.user);
|
const currentUser = await Users.getBySession(locals.session.data?.user);
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
@ -77,6 +129,12 @@ export class Users {
|
|||||||
return currentUser;
|
return currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an user exists with the provided username and/or email address.
|
||||||
|
* @param username Username
|
||||||
|
* @param email Email address
|
||||||
|
* @returns true if existing user does not exist
|
||||||
|
*/
|
||||||
static async checkRegistration(username: string, email: string) {
|
static async checkRegistration(username: string, email: string) {
|
||||||
return !(
|
return !(
|
||||||
await db
|
await db
|
||||||
@ -91,6 +149,11 @@ export class Users {
|
|||||||
)?.length;
|
)?.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user information by activation token.
|
||||||
|
* @param token Activation token
|
||||||
|
* @returns User information
|
||||||
|
*/
|
||||||
static async getActivationToken(token: string) {
|
static async getActivationToken(token: string) {
|
||||||
const returnedToken = await UserTokens.getByToken(token, 'activation');
|
const returnedToken = await UserTokens.getByToken(token, 'activation');
|
||||||
if (!returnedToken?.userId) return undefined;
|
if (!returnedToken?.userId) return undefined;
|
||||||
@ -106,6 +169,11 @@ export class Users {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate an user by an activation token.
|
||||||
|
* @param token Token
|
||||||
|
* @param subject User
|
||||||
|
*/
|
||||||
static async activateUserBy(token: string, subject: User) {
|
static async activateUserBy(token: string, subject: User) {
|
||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
@ -114,6 +182,11 @@ export class Users {
|
|||||||
await UserTokens.remove(token);
|
await UserTokens.remove(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new user.
|
||||||
|
* @param details User details
|
||||||
|
* @returns New User
|
||||||
|
*/
|
||||||
static async register({
|
static async register({
|
||||||
username,
|
username,
|
||||||
displayName,
|
displayName,
|
||||||
@ -149,6 +222,10 @@ export class Users {
|
|||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an activation email to an user.
|
||||||
|
* @param user User
|
||||||
|
*/
|
||||||
static async sendRegistrationEmail(user: User) {
|
static async sendRegistrationEmail(user: User) {
|
||||||
const token = await UserTokens.create(
|
const token = await UserTokens.create(
|
||||||
'activation',
|
'activation',
|
||||||
@ -172,6 +249,10 @@ export class Users {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a password reset email to an user.
|
||||||
|
* @param user User
|
||||||
|
*/
|
||||||
static async sendPasswordEmail(user: User) {
|
static async sendPasswordEmail(user: User) {
|
||||||
const token = await UserTokens.create('password', new Date(Date.now() + 3600 * 1000), user.id);
|
const token = await UserTokens.create('password', new Date(Date.now() + 3600 * 1000), user.id);
|
||||||
const params = new URLSearchParams({ token: token.token });
|
const params = new URLSearchParams({ token: token.token });
|
||||||
@ -192,6 +273,10 @@ export class Users {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a registration invite to an email address.
|
||||||
|
* @param email Email to invite
|
||||||
|
*/
|
||||||
static async sendInvitationEmail(email: string) {
|
static async sendInvitationEmail(email: string) {
|
||||||
const token = await UserTokens.create(
|
const token = await UserTokens.create(
|
||||||
'invite',
|
'invite',
|
||||||
@ -215,6 +300,11 @@ export class Users {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available privileges.
|
||||||
|
* @param clientId (optional) Client privileges
|
||||||
|
* @returns Available privileges
|
||||||
|
*/
|
||||||
static async getAvailablePrivileges(clientId?: number) {
|
static async getAvailablePrivileges(clientId?: number) {
|
||||||
return await db
|
return await db
|
||||||
.select()
|
.select()
|
||||||
@ -222,6 +312,12 @@ export class Users {
|
|||||||
.where(clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId));
|
.where(clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an user's privileges.
|
||||||
|
* @param subject User
|
||||||
|
* @param clientId (optional) OAuth2 client ID, returns privileges for a specific client only
|
||||||
|
* @returns User privileges (string list)
|
||||||
|
*/
|
||||||
static async getUserPrivileges(subject: User, clientId?: number) {
|
static async getUserPrivileges(subject: User, clientId?: number) {
|
||||||
const list = await db
|
const list = await db
|
||||||
.select({
|
.select({
|
||||||
@ -242,7 +338,64 @@ export class Users {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grant a new privilege to a user.
|
||||||
|
*
|
||||||
|
* **This can only be used to grant system privileges.**
|
||||||
|
* @see setUserPrivileges for general privilege changes.
|
||||||
|
* @param subject Recipient
|
||||||
|
* @param name Privilege name
|
||||||
|
* @returns Boolean, whether the privilege was granted or not.
|
||||||
|
*/
|
||||||
|
static async grantPrivilege(subject: User, name: string) {
|
||||||
|
const [existingPrivilege] = await db
|
||||||
|
.select({ id: privilege.id })
|
||||||
|
.from(privilege)
|
||||||
|
.where(and(eq(privilege.name, name), isNull(privilege.clientId)));
|
||||||
|
if (!existingPrivilege) return false;
|
||||||
|
|
||||||
|
const [alreadyHas] = await db
|
||||||
|
.select({ privilegeId: userPrivilegesPrivilege.privilegeId })
|
||||||
|
.from(userPrivilegesPrivilege)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userPrivilegesPrivilege.userId, subject.id),
|
||||||
|
eq(userPrivilegesPrivilege.privilegeId, existingPrivilege.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (alreadyHas) return true;
|
||||||
|
|
||||||
|
await db.insert(userPrivilegesPrivilege).values({
|
||||||
|
privilegeId: existingPrivilege.id,
|
||||||
|
userId: subject.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an user's privileges to an array of IDs.
|
||||||
|
* @param subject User
|
||||||
|
* @param privilegeIds Privileges
|
||||||
|
* @param clientId (optional) OAuth2 client ID, updates privileges related to a specific client only.
|
||||||
|
*/
|
||||||
static async setUserPrivileges(subject: User, privilegeIds: number[], clientId?: number) {
|
static async setUserPrivileges(subject: User, privilegeIds: number[], clientId?: number) {
|
||||||
|
// Prevent tricksters from trying to give themselves system privileges
|
||||||
|
// through the OAuth2 client privilege system. (or remove them from others)
|
||||||
|
// The privileges in question must actually be related to the specified client.
|
||||||
|
if (clientId) {
|
||||||
|
for (const id of privilegeIds) {
|
||||||
|
const [exists] = await db
|
||||||
|
.select({ id: privilege.id })
|
||||||
|
.from(privilege)
|
||||||
|
.where(and(eq(privilege.id, id), eq(privilege.clientId, clientId)));
|
||||||
|
if (!exists) {
|
||||||
|
// TODO: logging
|
||||||
|
return error(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const current = await db
|
const current = await db
|
||||||
.select({
|
.select({
|
||||||
privilegeId: userPrivilegesPrivilege.privilegeId
|
privilegeId: userPrivilegesPrivilege.privilegeId
|
||||||
@ -285,6 +438,11 @@ export class Users {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide an email addresses username part partially as a hint.
|
||||||
|
* @param email Email address
|
||||||
|
* @returns Slightly redacted email address
|
||||||
|
*/
|
||||||
static anonymizeEmail(email: string) {
|
static anonymizeEmail(email: string) {
|
||||||
const [name, domain] = email.split('@');
|
const [name, domain] = email.split('@');
|
||||||
const namePart = `${name.charAt(0)}${''.padStart(name.length - 2, '*')}${name.charAt(name.length - 1)}`;
|
const namePart = `${name.charAt(0)}${''.padStart(name.length - 2, '*')}${name.charAt(name.length - 1)}`;
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
|
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
@ -58,7 +59,12 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
|
<TitleRow>
|
||||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||||
|
|
||||||
|
<LogoutButton />
|
||||||
|
</TitleRow>
|
||||||
|
|
||||||
<SplitView>
|
<SplitView>
|
||||||
<div>
|
<div>
|
||||||
<h2>{$t('account.title')}</h2>
|
<h2>{$t('account.title')}</h2>
|
||||||
@ -160,8 +166,6 @@
|
|||||||
<Button variant="primary" type="submit">{$t('account.submit')}</Button>
|
<Button variant="primary" type="submit">{$t('account.submit')}</Button>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<LogoutButton />
|
|
||||||
</ViewColumn>
|
</ViewColumn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
import Alert from '$lib/components/Alert.svelte';
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||||
import SideContainer from '$lib/components/container/SideContainer.svelte';
|
import SideContainer from '$lib/components/container/SideContainer.svelte';
|
||||||
import FormControl from '$lib/components/form/FormControl.svelte';
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
@ -88,9 +89,12 @@
|
|||||||
<input type="email" name="email" id="password-email" autocomplete="email" />
|
<input type="email" name="email" id="password-email" autocomplete="email" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{/if}
|
{/if}
|
||||||
|
<ButtonRow>
|
||||||
<Button type="submit" variant="primary" disabled={submitted}
|
<Button type="submit" variant="primary" disabled={submitted}
|
||||||
>{$t('account.submit')}</Button
|
>{$t('account.submit')}</Button
|
||||||
>
|
>
|
||||||
|
<div><a href="/login">{$t('account.login.title')}</a></div>
|
||||||
|
</ButtonRow>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import FormSection from '$lib/components/form/FormSection.svelte';
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
@ -120,12 +121,14 @@
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
<Button type="submit" variant="primary" disabled={submitted}
|
<Button type="submit" variant="primary" disabled={submitted}
|
||||||
>{$t('account.register.submit')}</Button
|
>{$t('account.register.submit')}</Button
|
||||||
>
|
>
|
||||||
|
<div><a href="/login">{$t('account.login.title')}</a></div>
|
||||||
|
</ButtonRow>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
<div><a href="/login">{$t('account.login.title')}</a></div>
|
|
||||||
</ColumnView>
|
</ColumnView>
|
||||||
</SideContainer>
|
</SideContainer>
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { afterNavigate } from '$app/navigation';
|
||||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte';
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte';
|
||||||
import AdminSidebar from '$lib/components/admin/AdminSidebar.svelte';
|
import AdminSidebar from '$lib/components/admin/AdminSidebar.svelte';
|
||||||
|
import { tick } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
let container: HTMLElement;
|
||||||
|
|
||||||
|
// Reset container scroll.
|
||||||
|
afterNavigate(async ({ type }) => {
|
||||||
|
if (type !== 'link') return;
|
||||||
|
await tick();
|
||||||
|
container?.scrollTo(0, 0);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="admin-wrapper">
|
<div class="admin-wrapper">
|
||||||
@ -11,19 +22,13 @@
|
|||||||
<div class="sidebar-wrapper">
|
<div class="sidebar-wrapper">
|
||||||
<AdminSidebar user={data.user} />
|
<AdminSidebar user={data.user} />
|
||||||
|
|
||||||
<main>
|
<main bind:this={container}>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.admin-wrapper {
|
|
||||||
--in-text-color: #000;
|
|
||||||
--in-link-color: #000;
|
|
||||||
--in-error-color: #ff8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-wrapper {
|
.admin-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -40,8 +45,8 @@
|
|||||||
& > main {
|
& > main {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background-color: #ececec;
|
background-color: var(--ina-background-color);
|
||||||
color: #000;
|
color: var(--in-text-color);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ export const load = async ({ parent, url }) => {
|
|||||||
|
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
const fullPrivileges = hasPrivileges(user.privileges, ['admin:oauth2']);
|
const fullPrivileges = hasPrivileges(user.privileges, ['admin:oauth2']);
|
||||||
|
const createPrivileges = hasPrivileges(user.privileges, [['admin:oauth2', 'self:oauth2:create']]);
|
||||||
|
|
||||||
const data = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
const data = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
filter,
|
filter,
|
||||||
@ -39,6 +40,7 @@ export const load = async ({ parent, url }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
fullPrivileges,
|
fullPrivileges,
|
||||||
|
createPrivileges,
|
||||||
...data
|
...data
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
<TitleRow>
|
<TitleRow>
|
||||||
<h1>{$t('admin.oauth2.title')}</h1>
|
<h1>{$t('admin.oauth2.title')}</h1>
|
||||||
|
{#if data.createPrivileges}
|
||||||
<a href="oauth2/new">{$t('admin.oauth2.new')}</a>
|
<a href="oauth2/new">{$t('admin.oauth2.new')}</a>
|
||||||
|
{/if}
|
||||||
</TitleRow>
|
</TitleRow>
|
||||||
|
|
||||||
<ColumnView>
|
<ColumnView>
|
||||||
|
@ -5,10 +5,10 @@ import type { OAuth2Client, User } from '$lib/server/drizzle';
|
|||||||
import { OAuth2ClientURLType, OAuth2Clients } from '$lib/server/oauth2';
|
import { OAuth2ClientURLType, OAuth2Clients } from '$lib/server/oauth2';
|
||||||
import { Uploads } from '$lib/server/upload.js';
|
import { Uploads } from '$lib/server/upload.js';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { UsersAdmin } from '$lib/server/users/admin';
|
|
||||||
import { hasPrivileges } from '$lib/utils';
|
import { hasPrivileges } from '$lib/utils';
|
||||||
import { emailRegex, privilegeRegex } from '$lib/validators.js';
|
import { emailRegex, privilegeRegex } from '$lib/validators.js';
|
||||||
import { error, fail, redirect } from '@sveltejs/kit';
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { RateLimiter } from 'sveltekit-rate-limiter/server';
|
||||||
|
|
||||||
interface AddUrlRequest {
|
interface AddUrlRequest {
|
||||||
type: OAuth2ClientURLType;
|
type: OAuth2ClientURLType;
|
||||||
@ -30,9 +30,23 @@ interface InviteRequest {
|
|||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
const oneOffLimiter = new RateLimiter({
|
||||||
update: async ({ locals, request, params: { uuid } }) => {
|
IP: [1, 'm']
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
});
|
||||||
|
|
||||||
|
const inviteLimiter = new RateLimiter({
|
||||||
|
IP: [3, 'm']
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current user's information and the client the action is performed on.
|
||||||
|
* Errors if client doesn't exist or user does not have permission to change it.
|
||||||
|
* @param locals App locals
|
||||||
|
* @param uuid Client ID
|
||||||
|
* @returns Current user, their session, full OAuth2 privileges flag and the actionable client
|
||||||
|
*/
|
||||||
|
const getActionData = async (locals: App.Locals, uuid: string) => {
|
||||||
|
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -50,6 +64,16 @@ export const actions = {
|
|||||||
return error(404, 'Client not found');
|
return error(404, 'Client not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { currentUser, userSession, fullPrivileges, details: details as OAuth2Client };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
/**
|
||||||
|
* Update the OAuth2 Client general information.
|
||||||
|
*/
|
||||||
|
update: async ({ locals, request, params: { uuid } }) => {
|
||||||
|
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { title, description, activated, verified } = Changesets.take<UpdateRequest>(
|
const { title, description, activated, verified } = Changesets.take<UpdateRequest>(
|
||||||
['title', 'description', 'activated', 'verified'],
|
['title', 'description', 'activated', 'verified'],
|
||||||
@ -71,7 +95,7 @@ export const actions = {
|
|||||||
return fail(403, { errors: ['invalidDescription'] });
|
return fail(403, { errors: ['invalidDescription'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.update(details as OAuth2Client, {
|
await OAuth2Clients.update(details, {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
verified: actuallyVerified,
|
verified: actuallyVerified,
|
||||||
@ -80,24 +104,13 @@ export const actions = {
|
|||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Permanently delete the OAuth2 Client.
|
||||||
|
*
|
||||||
|
* Only works for disabled clients, and only the owner or admin can do it.
|
||||||
|
*/
|
||||||
delete: async ({ locals, params: { uuid } }) => {
|
delete: async ({ locals, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, fullPrivileges, details } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (details.activated === 1) {
|
if (details.activated === 1) {
|
||||||
return fail(400, { errors: ['deleteActivated'] });
|
return fail(400, { errors: ['deleteActivated'] });
|
||||||
@ -107,85 +120,57 @@ export const actions = {
|
|||||||
return fail(403, { errors: ['forbidden'] });
|
return fail(403, { errors: ['forbidden'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.deleteClient(details as OAuth2Client);
|
await OAuth2Clients.deleteClient(details);
|
||||||
|
|
||||||
return redirect(303, '/ssoadmin/oauth2');
|
return redirect(303, '/ssoadmin/oauth2');
|
||||||
},
|
},
|
||||||
regenerate: async ({ locals, params: { uuid } }) => {
|
/**
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
* Regenerate OAuth2 Client secret key.
|
||||||
['admin:oauth2', 'self:oauth2']
|
*
|
||||||
]);
|
* Only the owner or admin can do it.
|
||||||
|
*/
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
regenerate: async (event) => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
locals,
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
params: { uuid }
|
||||||
clientId: uuid,
|
} = event;
|
||||||
listAll: fullPrivileges,
|
const { currentUser, fullPrivileges, details } = await getActionData(locals, uuid);
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
if (!fullPrivileges && details.ownerId !== currentUser.id) {
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fullPrivileges && !details.isOwner) {
|
|
||||||
return fail(403, { errors: ['forbidden'] });
|
return fail(403, { errors: ['forbidden'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.update(details as OAuth2Client, {
|
// Allow secret regeneration only once per minute.
|
||||||
|
if (!fullPrivileges && (await oneOffLimiter.isLimited(event))) {
|
||||||
|
return fail(429, { errors: ['tooManyTimes'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.update(details, {
|
||||||
client_secret: CryptoUtils.generateSecret()
|
client_secret: CryptoUtils.generateSecret()
|
||||||
});
|
});
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Remove an URL reference.
|
||||||
|
*/
|
||||||
removeUrl: async ({ locals, url, params: { uuid } }) => {
|
removeUrl: async ({ locals, url, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = Number(url.searchParams.get('id'));
|
const id = Number(url.searchParams.get('id'));
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return fail(400, { errors: ['invalidUrlId'] });
|
return fail(400, { errors: ['invalidUrlId'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.deleteUrl(details as OAuth2Client, id);
|
await OAuth2Clients.deleteUrl(details, id);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Add an URL reference.
|
||||||
|
*/
|
||||||
addUrl: async ({ locals, request, params: { uuid } }) => {
|
addUrl: async ({ locals, request, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
@ -197,56 +182,30 @@ export const actions = {
|
|||||||
return fail(400, { errors: ['invalidUrl'] });
|
return fail(400, { errors: ['invalidUrl'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.addUrl(details as OAuth2Client, type, url);
|
await OAuth2Clients.addUrl(details, type, url);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Remove a privilege reference.
|
||||||
|
*/
|
||||||
removePrivilege: async ({ locals, url, params: { uuid } }) => {
|
removePrivilege: async ({ locals, url, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = Number(url.searchParams.get('id'));
|
const id = Number(url.searchParams.get('id'));
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return fail(400, { errors: ['invalidPrivilegeId'] });
|
return fail(400, { errors: ['invalidPrivilegeId'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.deletePrivilege(details as OAuth2Client, id);
|
await OAuth2Clients.deletePrivilege(details, id);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Add a privilege reference.
|
||||||
|
*/
|
||||||
addPrivilege: async ({ locals, request, params: { uuid } }) => {
|
addPrivilege: async ({ locals, request, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { name } = Changesets.take<AddPrivilegeRequest>(['name'], body);
|
const { name } = Changesets.take<AddPrivilegeRequest>(['name'], body);
|
||||||
@ -255,28 +214,15 @@ export const actions = {
|
|||||||
return fail(400, { errors: ['invalidPrivilege'] });
|
return fail(400, { errors: ['invalidPrivilege'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.addPrivilege(details as OAuth2Client, name);
|
await OAuth2Clients.addPrivilege(details, name);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Update the OAuth2 Client allowed grants list.
|
||||||
|
*/
|
||||||
grants: async ({ locals, request, params: { uuid } }) => {
|
grants: async ({ locals, request, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedGrants = fullPrivileges
|
const allowedGrants = fullPrivileges
|
||||||
? OAuth2Clients.availableGrantTypes
|
? OAuth2Clients.availableGrantTypes
|
||||||
@ -289,30 +235,17 @@ export const actions = {
|
|||||||
(value, index, array) => allowedGrants.includes(value) && array.indexOf(value) === index
|
(value, index, array) => allowedGrants.includes(value) && array.indexOf(value) === index
|
||||||
);
|
);
|
||||||
|
|
||||||
await OAuth2Clients.update(details as OAuth2Client, {
|
await OAuth2Clients.update(details, {
|
||||||
grants: deduplicatedAllowedGrants.join(' ')
|
grants: deduplicatedAllowedGrants.join(' ')
|
||||||
});
|
});
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Update the OAuth2 Client allowed scopes list.
|
||||||
|
*/
|
||||||
scopes: async ({ locals, request, params: { uuid } }) => {
|
scopes: async ({ locals, request, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details, fullPrivileges } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedScopes = fullPrivileges
|
const allowedScopes = fullPrivileges
|
||||||
? OAuth2Clients.availableScopes
|
? OAuth2Clients.availableScopes
|
||||||
@ -325,30 +258,17 @@ export const actions = {
|
|||||||
(value, index, array) => allowedScopes.includes(value) && array.indexOf(value) === index
|
(value, index, array) => allowedScopes.includes(value) && array.indexOf(value) === index
|
||||||
);
|
);
|
||||||
|
|
||||||
await OAuth2Clients.update(details as OAuth2Client, {
|
await OAuth2Clients.update(details, {
|
||||||
scope: OAuth2Clients.joinScope(deduplicatedAllowedScopes)
|
scope: OAuth2Clients.joinScope(deduplicatedAllowedScopes)
|
||||||
});
|
});
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Update the OAuth2 Client icon picture.
|
||||||
|
*/
|
||||||
avatar: async ({ request, locals, params: { uuid } }) => {
|
avatar: async ({ request, locals, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, details } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = Object.fromEntries(await request.formData());
|
const formData = Object.fromEntries(await request.formData());
|
||||||
if (!(formData.file as File)?.name || (formData.file as File).name === 'undefined') {
|
if (!(formData.file as File)?.name || (formData.file as File).name === 'undefined') {
|
||||||
@ -359,51 +279,33 @@ export const actions = {
|
|||||||
|
|
||||||
const { file } = formData as { file: File };
|
const { file } = formData as { file: File };
|
||||||
|
|
||||||
await Uploads.saveClientAvatar(details as OAuth2Client, currentUser, file);
|
await Uploads.saveClientAvatar(details, currentUser, file);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Delete the OAuth2 Client icon picture.
|
||||||
|
*/
|
||||||
removeAvatar: async ({ locals, params: { uuid } }) => {
|
removeAvatar: async ({ locals, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
await Uploads.removeClientAvatar(details);
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
await Uploads.removeClientAvatar(details as OAuth2Client);
|
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
invite: async ({ locals, request, params: { uuid } }) => {
|
/**
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
* Invite a new manager to the OAuth2 Client.
|
||||||
['admin:oauth2', 'self:oauth2']
|
* Managers can do most changes the client's owner can.
|
||||||
]);
|
*
|
||||||
|
* Only the owner or admin can do it.
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
*/
|
||||||
|
invite: async (event) => {
|
||||||
const {
|
const {
|
||||||
list: [details]
|
locals,
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
request,
|
||||||
clientId: uuid,
|
params: { uuid }
|
||||||
listAll: fullPrivileges,
|
} = event;
|
||||||
omitSecret: false
|
const { currentUser, details, fullPrivileges } = await getActionData(locals, uuid);
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { email } = Changesets.take<InviteRequest>(['email'], body);
|
const { email } = Changesets.take<InviteRequest>(['email'], body);
|
||||||
@ -412,40 +314,41 @@ export const actions = {
|
|||||||
return fail(400, { errors: ['invalidEmail'] });
|
return fail(400, { errors: ['invalidEmail'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const managers = await OAuth2Clients.getManagers(details as OAuth2Client);
|
if (details.ownerId !== currentUser.id && !fullPrivileges) {
|
||||||
|
return fail(403, { errors: ['forbidden'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const managers = await OAuth2Clients.getManagers(details);
|
||||||
if (managers.some((entry) => entry.email.toLowerCase() === email.toLowerCase())) {
|
if (managers.some((entry) => entry.email.toLowerCase() === email.toLowerCase())) {
|
||||||
return fail(400, { errors: ['emailExists'] });
|
return fail(400, { errors: ['emailExists'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fullPrivileges && (await inviteLimiter.isLimited(event))) {
|
||||||
|
return fail(429, { errors: ['tooManyTimes'] });
|
||||||
|
}
|
||||||
|
|
||||||
await OAuth2Clients.sendManagerInvitationEmail(details as OAuth2Client, currentUser, email);
|
await OAuth2Clients.sendManagerInvitationEmail(details as OAuth2Client, currentUser, email);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Remove a manager form the OAuth2 Client.
|
||||||
|
*
|
||||||
|
* Only the owner or admin can do it.
|
||||||
|
*/
|
||||||
removeManager: async ({ locals, url, params: { uuid } }) => {
|
removeManager: async ({ locals, url, params: { uuid } }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { details, currentUser, fullPrivileges } = await getActionData(locals, uuid);
|
||||||
['admin:oauth2', 'self:oauth2']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const {
|
|
||||||
list: [details]
|
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
|
||||||
clientId: uuid,
|
|
||||||
listAll: fullPrivileges,
|
|
||||||
omitSecret: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!details) {
|
|
||||||
return error(404, 'Client not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = Number(url.searchParams.get('id'));
|
const id = Number(url.searchParams.get('id'));
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return fail(400, { errors: ['invalidManagerId'] });
|
return fail(400, { errors: ['invalidManagerId'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Clients.removeManager(details as OAuth2Client, id);
|
if (details.ownerId !== currentUser.id && !fullPrivileges) {
|
||||||
|
return fail(403, { errors: ['forbidden'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.removeManager(details, id);
|
||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
}
|
}
|
||||||
@ -474,15 +377,29 @@ export const load = async ({ params: { uuid }, parent }) => {
|
|||||||
const users = await OAuth2Clients.getAuthorizedUsers(details as OAuth2Client);
|
const users = await OAuth2Clients.getAuthorizedUsers(details as OAuth2Client);
|
||||||
const managers = await OAuth2Clients.getManagers(details as OAuth2Client);
|
const managers = await OAuth2Clients.getManagers(details as OAuth2Client);
|
||||||
|
|
||||||
|
const availableGrants: string[] = [];
|
||||||
|
if (fullPrivileges) {
|
||||||
|
availableGrants.push(...OAuth2Clients.availableGrantTypes);
|
||||||
|
} else {
|
||||||
|
availableGrants.push(...OAuth2Clients.userSetGrants);
|
||||||
|
|
||||||
|
// Implicit grants can be allowed additionally.
|
||||||
|
if (hasPrivileges(user.privileges, ['self:oauth2:implicit'])) {
|
||||||
|
availableGrants.push(...OAuth2Clients.implicitGrantTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableScopes = fullPrivileges
|
||||||
|
? OAuth2Clients.availableScopes
|
||||||
|
: OAuth2Clients.userSetScopes;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users,
|
users,
|
||||||
managers,
|
managers,
|
||||||
availableUrls: OAuth2Clients.availableUrlTypes,
|
availableUrls: OAuth2Clients.availableUrlTypes,
|
||||||
availablePrivileges: privileges,
|
availablePrivileges: privileges,
|
||||||
availableGrants: fullPrivileges
|
availableGrants,
|
||||||
? OAuth2Clients.availableGrantTypes
|
availableScopes,
|
||||||
: OAuth2Clients.userSetGrants,
|
|
||||||
availableScopes: fullPrivileges ? OAuth2Clients.availableScopes : OAuth2Clients.userSetScopes,
|
|
||||||
fullPrivileges,
|
fullPrivileges,
|
||||||
details
|
details
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public';
|
import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public';
|
||||||
|
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
@ -27,10 +28,10 @@
|
|||||||
|
|
||||||
$: noRedirects = !data.details.urls.some(({ type }) => type === 'redirect_uri');
|
$: noRedirects = !data.details.urls.some(({ type }) => type === 'redirect_uri');
|
||||||
$: availableUrls = data.availableUrls.filter((type) => {
|
$: availableUrls = data.availableUrls.filter((type) => {
|
||||||
// Can have up to three redirect URIs, only one of other types
|
// Can have up to five redirect URIs, only one of other types
|
||||||
const countOfType = data.details.urls.filter(({ type: subType }) => type === subType).length;
|
const countOfType = data.details.urls.filter(({ type: subType }) => type === subType).length;
|
||||||
if (type === 'redirect_uri') {
|
if (type === 'redirect_uri') {
|
||||||
return countOfType < 3;
|
return countOfType < 5;
|
||||||
}
|
}
|
||||||
return !countOfType;
|
return !countOfType;
|
||||||
});
|
});
|
||||||
@ -59,15 +60,16 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if data.details.pictureId}
|
{#if data.details.pictureId}
|
||||||
<form action="?/removeAvatar" method="POST">
|
<ActionButton action="?/removeAvatar">{$t('admin.oauth2.avatar.remove')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('admin.oauth2.avatar.remove')}</Button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ColumnView>
|
</ColumnView>
|
||||||
</AvatarCard>
|
</AvatarCard>
|
||||||
|
|
||||||
<form action="?/update" method="POST">
|
<form action="?/update" method="POST">
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
|
{#if form?.errors && !form.errors.length}
|
||||||
|
<Alert type="success">{$t('admin.oauth2.success')}</Alert>
|
||||||
|
{/if}
|
||||||
<FormErrors errors={form?.errors || []} prefix="admin.oauth2.errors" />
|
<FormErrors errors={form?.errors || []} prefix="admin.oauth2.errors" />
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -131,13 +133,9 @@
|
|||||||
<h2>{$t('admin.oauth2.actions')}</h2>
|
<h2>{$t('admin.oauth2.actions')}</h2>
|
||||||
{#if data.fullPrivileges || data.details.isOwner}
|
{#if data.fullPrivileges || data.details.isOwner}
|
||||||
{#if !data.details.activated}
|
{#if !data.details.activated}
|
||||||
<form action="?/delete" method="POST">
|
<ActionButton action="?/delete">{$t('admin.oauth2.delete')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('admin.oauth2.delete')}</Button>
|
|
||||||
</form>
|
|
||||||
{:else}
|
{:else}
|
||||||
<form action="?/regenerate" method="POST">
|
<ActionButton action="?/regenerate">{$t('admin.oauth2.regenerate')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('admin.oauth2.regenerate')}</Button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -157,9 +155,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="?/removeUrl&id={url.id}" method="POST">
|
<ActionButton action="?/removeUrl&id={url.id}">{$t('common.remove')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('common.remove')}</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@ -208,9 +204,10 @@
|
|||||||
<span class="privilege-name"
|
<span class="privilege-name"
|
||||||
><span class="privilege-id">{idPart}:</span>{rest.join(':')}</span
|
><span class="privilege-id">{idPart}:</span>{rest.join(':')}</span
|
||||||
>
|
>
|
||||||
<form action="?/removePrivilege&id={privilege.id}" method="POST">
|
|
||||||
<Button type="submit" variant="link">{$t('common.remove')}</Button>
|
<ActionButton action="?/removePrivilege&id={privilege.id}"
|
||||||
</form>
|
>{$t('common.remove')}</ActionButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@ -249,7 +246,7 @@
|
|||||||
<p>{$t('admin.oauth2.grantsHint')}</p>
|
<p>{$t('admin.oauth2.grantsHint')}</p>
|
||||||
|
|
||||||
<form action="?/grants" method="POST">
|
<form action="?/grants" method="POST">
|
||||||
<div class="scope-cloud">
|
<div class="checkbox-grid">
|
||||||
{#each data.availableGrants as grant}
|
{#each data.availableGrants as grant}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<input
|
<input
|
||||||
@ -272,7 +269,7 @@
|
|||||||
<p>{$t('admin.oauth2.scopesHint')}</p>
|
<p>{$t('admin.oauth2.scopesHint')}</p>
|
||||||
|
|
||||||
<form action="?/scopes" method="POST">
|
<form action="?/scopes" method="POST">
|
||||||
<div class="scope-cloud">
|
<div class="checkbox-grid">
|
||||||
{#each data.availableScopes as scope}
|
{#each data.availableScopes as scope}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<input
|
<input
|
||||||
@ -301,9 +298,10 @@
|
|||||||
{#each data.managers as user}
|
{#each data.managers as user}
|
||||||
<div class="addremove-item">
|
<div class="addremove-item">
|
||||||
<b>{user.email}</b>
|
<b>{user.email}</b>
|
||||||
<form action="?/removeManager&id={user.id}" method="POST">
|
|
||||||
<Button type="submit" variant="link">{$t('common.remove')}</Button>
|
<ActionButton action="?/removeManager&id={user.id}"
|
||||||
</form>
|
>{$t('common.remove')}</ActionButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@ -392,9 +390,11 @@
|
|||||||
<div class="auth-user-name">
|
<div class="auth-user-name">
|
||||||
<b>{user.uuid}</b> ({user.name})
|
<b>{user.uuid}</b> ({user.name})
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auth-user-privileges">
|
<div class="auth-user-privileges">
|
||||||
{user.privileges.map(({ name }) => name).join(', ')}
|
{user.privileges.map(({ name }) => name).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !user.current}
|
{#if !user.current}
|
||||||
<b>{$t('admin.oauth2.revoked')}</b>
|
<b>{$t('admin.oauth2.revoked')}</b>
|
||||||
{/if}
|
{/if}
|
||||||
@ -425,13 +425,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: #fff;
|
background-color: var(--ina-card-background);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .privilege-id {
|
& .privilege-id {
|
||||||
color: #646464;
|
color: var(--ina-card-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .url-type {
|
& .url-type {
|
||||||
@ -444,10 +444,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope-cloud {
|
.checkbox-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -3,7 +3,7 @@ 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';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { UsersAdmin } from '$lib/server/users/admin';
|
import { hasPrivileges } from '$lib/utils';
|
||||||
import { error, redirect } from '@sveltejs/kit';
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
interface PrivilegesRequest {
|
interface PrivilegesRequest {
|
||||||
@ -12,15 +12,17 @@ interface PrivilegesRequest {
|
|||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
privileges: async ({ locals, params: { uuid, user: userId }, request }) => {
|
privileges: async ({ locals, params: { uuid, user: userId }, request }) => {
|
||||||
const { currentUser } = await UsersAdmin.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 {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,11 +69,13 @@ export const load = async ({ params: { uuid, user: userId }, parent }) => {
|
|||||||
const currentUser = await Users.getBySession(user);
|
const currentUser = await Users.getBySession(user);
|
||||||
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]);
|
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(user.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,6 +34,5 @@ export const GET = async ({ locals, url }) => {
|
|||||||
await OAuth2Clients.addManager(client, inviter, currentUser);
|
await OAuth2Clients.addManager(client, inviter, currentUser);
|
||||||
await UserTokens.remove(fetch);
|
await UserTokens.remove(fetch);
|
||||||
|
|
||||||
console.log('?');
|
|
||||||
return ApiUtils.redirect(`/ssoadmin/oauth2/${client.client_id}`);
|
return ApiUtils.redirect(`/ssoadmin/oauth2/${client.client_id}`);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { AdminUtils } from '$lib/server/admin-utils';
|
import { AdminUtils } from '$lib/server/admin-utils';
|
||||||
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 { UsersAdmin } from '$lib/server/users/admin';
|
|
||||||
import { fail, redirect } from '@sveltejs/kit';
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
interface CreateClientRequest {
|
interface CreateClientRequest {
|
||||||
@ -12,8 +11,8 @@ interface CreateClientRequest {
|
|||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ locals, request }) => {
|
default: async ({ locals, request }) => {
|
||||||
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser } = await AdminUtils.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2:create']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
@ -42,7 +41,7 @@ export const actions = {
|
|||||||
|
|
||||||
export const load = async ({ parent }) => {
|
export const load = async ({ parent }) => {
|
||||||
const { user } = await parent();
|
const { user } = await parent();
|
||||||
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]);
|
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2:create']]);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@ interface UpdateRequest {
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
removeOtp: async () => {},
|
removeOtp: async () => {},
|
||||||
removeAvatar: async ({ locals, params: { uuid } }) => {
|
removeAvatar: async ({ locals, params: { uuid } }) => {
|
||||||
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
await AdminUtils.getActionUser(locals, ['admin', 'admin:user']);
|
||||||
|
|
||||||
const targetUser = await Users.getByUuid(uuid, false);
|
const targetUser = await Users.getByUuid(uuid, false);
|
||||||
if (!targetUser) {
|
if (!targetUser) {
|
||||||
@ -31,7 +31,7 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
deleteInfo: async ({ locals, params: { uuid } }) => {
|
deleteInfo: async ({ locals, params: { uuid } }) => {
|
||||||
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
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) {
|
||||||
@ -57,7 +57,7 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
email: async ({ locals, params: { uuid }, url }) => {
|
email: async ({ locals, params: { uuid }, url }) => {
|
||||||
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
await AdminUtils.getActionUser(locals, ['admin', 'admin:user']);
|
||||||
|
|
||||||
const type = url.searchParams.get('type') as 'password' | 'activate';
|
const type = url.searchParams.get('type') as 'password' | 'activate';
|
||||||
if (!type) {
|
if (!type) {
|
||||||
@ -85,7 +85,7 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
update: async ({ locals, params: { uuid }, request }) => {
|
update: async ({ locals, params: { uuid }, request }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [
|
||||||
'admin',
|
'admin',
|
||||||
'admin:user'
|
'admin:user'
|
||||||
]);
|
]);
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
|
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||||
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
@ -30,13 +32,14 @@
|
|||||||
<AvatarCard src={`/api/avatar/${data.details.uuid}?t=${data.renderrt}`}>
|
<AvatarCard src={`/api/avatar/${data.details.uuid}?t=${data.renderrt}`}>
|
||||||
<ColumnView>
|
<ColumnView>
|
||||||
{#if data.details.pictureId}
|
{#if data.details.pictureId}
|
||||||
<form action="?/removeAvatar" method="POST">
|
<ActionButton action="?/removeAvatar">{$t('account.avatar.remove')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('account.avatar.remove')}</Button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ColumnView>
|
</ColumnView>
|
||||||
</AvatarCard>
|
</AvatarCard>
|
||||||
|
|
||||||
|
{#if form?.errors && !form.errors.length}
|
||||||
|
<Alert type="success">{$t('admin.oauth2.success')}</Alert>
|
||||||
|
{/if}
|
||||||
<FormErrors errors={form?.errors || []} prefix="admin.users.errors" />
|
<FormErrors errors={form?.errors || []} prefix="admin.users.errors" />
|
||||||
|
|
||||||
<form action="?/update" method="POST">
|
<form action="?/update" method="POST">
|
||||||
@ -81,20 +84,15 @@
|
|||||||
<h3>{$t('account.otp.title')}</h3>
|
<h3>{$t('account.otp.title')}</h3>
|
||||||
<p>{$t(`account.otp.${data.details.otpEnabled ? 'enabled' : 'unavailable'}`)}</p>
|
<p>{$t(`account.otp.${data.details.otpEnabled ? 'enabled' : 'unavailable'}`)}</p>
|
||||||
{#if data.details.otpEnabled}
|
{#if data.details.otpEnabled}
|
||||||
<form action="?/removeOtp" method="POST">
|
<ActionButton action="?/removeOtp">{$t('admin.users.deactivateOtp')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('admin.users.deactivateOtp')}</Button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3>{$t('admin.users.actions')}</h3>
|
<h3>{$t('admin.users.actions')}</h3>
|
||||||
{#if data.details.activated}
|
{#if data.details.activated}
|
||||||
<form action="?/email&type=password" method="POST">
|
<ActionButton action="?/email&type=password">{$t('admin.users.passwordEmail')}</ActionButton>
|
||||||
<Button type="submit" variant="link">{$t('admin.users.passwordEmail')}</Button>
|
|
||||||
</form>
|
|
||||||
{:else}
|
{:else}
|
||||||
<form action="?/email&type=activate" method="POST">
|
<ActionButton action="?/email&type=activate">{$t('admin.users.activationEmail')}</ActionButton
|
||||||
<Button type="submit" variant="link">{$t('admin.users.activationEmail')}</Button>
|
>
|
||||||
</form>
|
|
||||||
<form action="?/deleteInfo" method="POST">
|
<form action="?/deleteInfo" method="POST">
|
||||||
<Button type="submit" variant="link">{$t('admin.users.deleteInfo')}</Button>
|
<Button type="submit" variant="link">{$t('admin.users.deleteInfo')}</Button>
|
||||||
- <span>{$t('admin.users.deleteInfoHint')}</span>
|
- <span>{$t('admin.users.deleteInfoHint')}</span>
|
||||||
|
Loading…
Reference in New Issue
Block a user