oauth2 client adminning
This commit is contained in:
parent
d258880ac4
commit
d11403a073
101
src/lib/components/admin/AdminClientCard.svelte
Normal file
101
src/lib/components/admin/AdminClientCard.svelte
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import type { PageData } from '../../../routes/ssoadmin/oauth2/$types';
|
||||||
|
|
||||||
|
export let client: PageData['list'][0];
|
||||||
|
|
||||||
|
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
|
||||||
|
const formatDate = dateFormat.format.bind(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="client">
|
||||||
|
<div class="client-avatar-wrapper">
|
||||||
|
<img class="client-avatar" src={`/api/avatar/client/${client.client_id}`} alt={client.title} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="client-info">
|
||||||
|
<h2 class="client-name">{client.title}</h2>
|
||||||
|
<span class="client-description">{client.description}</span>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>{$t('admin.oauth2.clientId')}</dt>
|
||||||
|
<dd>{client.client_id}</dd>
|
||||||
|
|
||||||
|
<dt>{$t('admin.oauth2.scopes')}</dt>
|
||||||
|
<dd>{client.scope}</dd>
|
||||||
|
|
||||||
|
<dt>{$t('admin.oauth2.grants')}</dt>
|
||||||
|
<dd>{client.grants}</dd>
|
||||||
|
|
||||||
|
<dt>{$t('admin.oauth2.activated')}</dt>
|
||||||
|
<dd>{$t(`common.bool.${Boolean(client.activated)}`)}</dd>
|
||||||
|
|
||||||
|
<dt>{$t('admin.oauth2.verified')}</dt>
|
||||||
|
<dd>{$t(`common.bool.${Boolean(client.verified)}`)}</dd>
|
||||||
|
|
||||||
|
<dt>{$t('admin.oauth2.created')}</dt>
|
||||||
|
<dd>{formatDate(client.created_at)}</dd>
|
||||||
|
|
||||||
|
<dt>{$t('admin.oauth2.owner')}</dt>
|
||||||
|
<dd>
|
||||||
|
{client.ownerInfo?.uuid} ({client.ownerInfo?.name})
|
||||||
|
{#if client.isOwner}<i> - {$t('admin.oauth2.ownerMe')}</i>{/if}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>{$t('admin.oauth2.urls.title')}</dt>
|
||||||
|
<dd>
|
||||||
|
{#each client.urls as url}
|
||||||
|
<a href={url.url} target="_blank" rel="nofollow noreferrer" class="client-url"
|
||||||
|
>{$t(`admin.oauth2.urls.types.${url.type}`)} <{url.url}></a
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.client {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
transition: transform 100ms linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .client-avatar {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .client-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > dl {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& > dt {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .client-name {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .client-url {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -10,6 +10,7 @@
|
|||||||
import { allowedImages } from '$lib/constants';
|
import { allowedImages } from '$lib/constants';
|
||||||
|
|
||||||
export let show: Writable<boolean>;
|
export let show: Writable<boolean>;
|
||||||
|
export let url: string = '/account';
|
||||||
|
|
||||||
let cropper: Cropper;
|
let cropper: Cropper;
|
||||||
let image: HTMLImageElement;
|
let image: HTMLImageElement;
|
||||||
@ -75,7 +76,7 @@
|
|||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', resultBlob);
|
data.append('file', resultBlob);
|
||||||
|
|
||||||
await fetch(`/account?/avatar`, {
|
await fetch(`${url}?/avatar`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:global(textarea),
|
||||||
|
:global(select),
|
||||||
: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);
|
||||||
@ -11,6 +13,7 @@
|
|||||||
&:not([type]),
|
&:not([type]),
|
||||||
&[type='text'],
|
&[type='text'],
|
||||||
&[type='password'],
|
&[type='password'],
|
||||||
|
&[type='url'],
|
||||||
&[type='email'] {
|
&[type='email'] {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -47,6 +50,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control > :global(label):has(+ input[required])::after {
|
.form-control > :global(label):has(+ input[required])::after {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"title": "Admin",
|
"title": "Admin",
|
||||||
"menu": {
|
"menu": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"oauth2": "OAuth2 clients",
|
"oauth2": "OAuth2 applications",
|
||||||
"audit": "Audit logs"
|
"audit": "Audit logs"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@ -27,5 +27,86 @@
|
|||||||
"invalidDisplayName": "Invalid display name",
|
"invalidDisplayName": "Invalid display name",
|
||||||
"invalidEmail": "Invalid email address"
|
"invalidEmail": "Invalid email address"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"oauth2": {
|
||||||
|
"title": "OAuth2 applications",
|
||||||
|
"clientTitle": "Application name",
|
||||||
|
"clientId": "Client ID",
|
||||||
|
"clientSecret": "Client secret",
|
||||||
|
"description": "Application description",
|
||||||
|
"reveal": "Reveal secret",
|
||||||
|
"regenerate": "Regenerate secret",
|
||||||
|
"activated": "Activated",
|
||||||
|
"verified": "Verified",
|
||||||
|
"scopes": "Available scopes",
|
||||||
|
"scopesHint": "The level of access to information you will be needing for this application.",
|
||||||
|
"grants": "Available grant types",
|
||||||
|
"grantsHint": "The OAuth2 authorization flows you will be using with this application.",
|
||||||
|
"created": "Created at",
|
||||||
|
"owner": "Created by",
|
||||||
|
"ownerMe": "that's you!",
|
||||||
|
"actions": "Actions",
|
||||||
|
"delete": "Delete application permanently",
|
||||||
|
"avatar": {
|
||||||
|
"title": "Application icon",
|
||||||
|
"remove": "Delete application icon",
|
||||||
|
"change": "Change application icon"
|
||||||
|
},
|
||||||
|
"authorizations": "Authorized users",
|
||||||
|
"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",
|
||||||
|
"noAuthorizations": "There are no authorizations on record for this application.",
|
||||||
|
"privileges": {
|
||||||
|
"title": "Application privileges",
|
||||||
|
"name": "Privilege name",
|
||||||
|
"nameHint": "An unique prefix \"{{prefix}}\" will be prepended automatically. English alphabet and ._-: characters only.",
|
||||||
|
"add": "Add a new privilege",
|
||||||
|
"addHint": "You may assign application-specific privileges to your authorized users. You may use this system for permissions or for tagging your users, all up to you!",
|
||||||
|
"remove": "Remove",
|
||||||
|
"manage": "Manage user privileges",
|
||||||
|
"new": "Add a new privilege"
|
||||||
|
},
|
||||||
|
"urls": {
|
||||||
|
"title": "Application URLs",
|
||||||
|
"new": "Add a new URL",
|
||||||
|
"type": "URL type",
|
||||||
|
"url": "URL",
|
||||||
|
"types": {
|
||||||
|
"website": "Website",
|
||||||
|
"privacy": "Privacy Policy",
|
||||||
|
"terms": "Terms of Service",
|
||||||
|
"redirect_uri": "Redirect URI"
|
||||||
|
},
|
||||||
|
"add": "Add URL"
|
||||||
|
},
|
||||||
|
"grantTexts": {
|
||||||
|
"authorization_code": "Authorization code",
|
||||||
|
"client_credentials": "Client credentials",
|
||||||
|
"refresh_token": "Refresh token",
|
||||||
|
"implicit": "Implicit token",
|
||||||
|
"id_token": "ID token (OpenID Connect)"
|
||||||
|
},
|
||||||
|
"scopeTexts": {
|
||||||
|
"picture": "Access profile picture URL",
|
||||||
|
"profile": "Basic profile information",
|
||||||
|
"email": "Access user email address",
|
||||||
|
"privileges": "Access user privilege list",
|
||||||
|
"management": "Manage your application",
|
||||||
|
"account": "Change user account settings",
|
||||||
|
"openid": "Get an ID token JWT (OpenID Connect)"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"noRedirect": "At least one Redirect URI is required for you to be able to use this application!",
|
||||||
|
"forbidden": "This action is forbidden for this user.",
|
||||||
|
"invalidTitle": "Name must be between 3 and 32 characters long.",
|
||||||
|
"invalidDescription": "Description must be at most 1000 characters long.",
|
||||||
|
"deleteActivated": "Cannot delete an active application. Please deactivate it first.",
|
||||||
|
"invalidUrlId": "Invalid URL ID for deletion.",
|
||||||
|
"invalidUrlType": "Invalid URL type provided.",
|
||||||
|
"invalidUrl": "Invalid URL provided.",
|
||||||
|
"invalidPrivilegeId": "Invalid privilege ID for deletion.",
|
||||||
|
"invalidPrivilege": "Invalid privilege provided.",
|
||||||
|
"noFile": "Please upload a file first."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,5 +15,6 @@
|
|||||||
"false": "No"
|
"false": "No"
|
||||||
},
|
},
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
"current": "Current"
|
"current": "Current",
|
||||||
|
"remove": "Remove"
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import i18n, { type Config } from 'sveltekit-i18n';
|
import i18n, { type Config } from 'sveltekit-i18n';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
siteName: string;
|
siteName?: string;
|
||||||
|
prefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: Config<Params> = {
|
const config: Config<Params> = {
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
import { db, oauth2Client, oauth2ClientUrl, type OAuth2Client } from '$lib/server/drizzle';
|
import {
|
||||||
import { and, eq } from 'drizzle-orm';
|
db,
|
||||||
|
oauth2Client,
|
||||||
|
oauth2ClientManager,
|
||||||
|
oauth2ClientUrl,
|
||||||
|
privilege,
|
||||||
|
user,
|
||||||
|
type OAuth2Client,
|
||||||
|
type OAuth2ClientUrl,
|
||||||
|
type User
|
||||||
|
} from '$lib/server/drizzle';
|
||||||
|
import { Uploads } from '$lib/server/upload';
|
||||||
|
import type { PaginationMeta } from '$lib/types';
|
||||||
|
import { and, count, eq, ilike, like, or } from 'drizzle-orm';
|
||||||
|
|
||||||
export enum OAuth2ClientURLType {
|
export enum OAuth2ClientURLType {
|
||||||
REDIRECT_URI = 'redirect_uri',
|
REDIRECT_URI = 'redirect_uri',
|
||||||
@ -8,17 +20,25 @@ export enum OAuth2ClientURLType {
|
|||||||
WEBSITE = 'website'
|
WEBSITE = 'website'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OAuth2ClientAdminListItem
|
||||||
|
extends PartialK<OAuth2Client, 'client_secret' | 'ownerId'> {
|
||||||
|
isOwner?: boolean;
|
||||||
|
ownerInfo?: { uuid: string; name: string };
|
||||||
|
urls: Omit<OAuth2ClientUrl, 'created_at' | 'updated_at' | 'clientId'>[];
|
||||||
|
}
|
||||||
|
|
||||||
export class OAuth2Clients {
|
export class OAuth2Clients {
|
||||||
public static availableGrantTypes = [
|
public static availableGrantTypes = [
|
||||||
'authorization_code',
|
'authorization_code',
|
||||||
|
'client_credentials',
|
||||||
'refresh_token',
|
'refresh_token',
|
||||||
'id_token',
|
'id_token',
|
||||||
'implicit'
|
'implicit'
|
||||||
];
|
];
|
||||||
|
|
||||||
public static availableScopes = [
|
public static availableScopes = [
|
||||||
'picture',
|
|
||||||
'profile',
|
'profile',
|
||||||
|
'picture',
|
||||||
'email',
|
'email',
|
||||||
'privileges',
|
'privileges',
|
||||||
'management',
|
'management',
|
||||||
@ -26,9 +46,26 @@ export class OAuth2Clients {
|
|||||||
'openid'
|
'openid'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static userSetScopes = [
|
||||||
|
'profile',
|
||||||
|
'picture',
|
||||||
|
'email',
|
||||||
|
'privileges',
|
||||||
|
'management',
|
||||||
|
'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'];
|
||||||
|
|
||||||
|
public static availableUrlTypes: OAuth2ClientURLType[] = Object.values(OAuth2ClientURLType);
|
||||||
|
|
||||||
static async fetchById(id: string | number) {
|
static async fetchById(id: string | number) {
|
||||||
const [client] = await db
|
const [client] = await db
|
||||||
.select()
|
.select()
|
||||||
@ -104,6 +141,145 @@ export class OAuth2Clients {
|
|||||||
return scope.join(' ');
|
return scope.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getClientByAdminUser(
|
||||||
|
subject: User,
|
||||||
|
filters?: {
|
||||||
|
clientId?: string;
|
||||||
|
filter?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
omitSecret?: boolean;
|
||||||
|
listAll?: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const filterText = `%${filters?.filter}%`;
|
||||||
|
const limit = filters?.limit || 20;
|
||||||
|
const allowedClients = db
|
||||||
|
.select({ id: oauth2Client.id })
|
||||||
|
.from(oauth2Client)
|
||||||
|
.leftJoin(oauth2ClientManager, eq(oauth2ClientManager.clientId, oauth2Client.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
!filters?.listAll
|
||||||
|
? or(eq(oauth2Client.ownerId, subject.id), eq(oauth2ClientManager.userId, subject.id))
|
||||||
|
: undefined,
|
||||||
|
filters?.clientId ? eq(oauth2Client.client_id, filters.clientId) : undefined,
|
||||||
|
filters?.filter
|
||||||
|
? or(ilike(oauth2Client.title, filterText), like(oauth2Client.client_id, filterText))
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.groupBy(oauth2Client.id)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(filters?.offset || 0)
|
||||||
|
.as('allowedClients');
|
||||||
|
|
||||||
|
const [{ rowCount }] = await db
|
||||||
|
.select({
|
||||||
|
rowCount: count(oauth2Client.id).mapWith(Number)
|
||||||
|
})
|
||||||
|
.from(allowedClients)
|
||||||
|
.innerJoin(oauth2Client, eq(allowedClients.id, oauth2Client.id));
|
||||||
|
|
||||||
|
const junkList = await db
|
||||||
|
.select({
|
||||||
|
o_auth2_client: oauth2Client,
|
||||||
|
o_auth2_client_url: oauth2ClientUrl,
|
||||||
|
user: user
|
||||||
|
})
|
||||||
|
.from(allowedClients)
|
||||||
|
.innerJoin(oauth2Client, eq(allowedClients.id, oauth2Client.id))
|
||||||
|
.leftJoin(oauth2ClientUrl, eq(oauth2ClientUrl.clientId, oauth2Client.id))
|
||||||
|
.leftJoin(user, eq(oauth2Client.ownerId, user.id));
|
||||||
|
|
||||||
|
const list = junkList.reduce<OAuth2ClientAdminListItem[]>((accum, dbo) => {
|
||||||
|
let client = accum.find(({ id }) => id === dbo.o_auth2_client.id);
|
||||||
|
if (!client) {
|
||||||
|
client = {
|
||||||
|
...dbo.o_auth2_client,
|
||||||
|
isOwner: dbo.o_auth2_client.ownerId === subject.id,
|
||||||
|
urls: []
|
||||||
|
};
|
||||||
|
accum.push(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbo.o_auth2_client_url) {
|
||||||
|
if (!client.urls.some(({ id }) => dbo.o_auth2_client_url?.id === id)) {
|
||||||
|
client.urls.push({
|
||||||
|
...dbo.o_auth2_client_url,
|
||||||
|
clientId: undefined,
|
||||||
|
created_at: undefined,
|
||||||
|
updated_at: undefined
|
||||||
|
} as OAuth2ClientAdminListItem['urls'][0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbo.user) {
|
||||||
|
client.ownerInfo = {
|
||||||
|
uuid: dbo.user.uuid,
|
||||||
|
name: dbo.user.display_name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.omitSecret) {
|
||||||
|
delete client.client_secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accum;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const meta: PaginationMeta = {
|
||||||
|
rowCount,
|
||||||
|
pageSize: limit,
|
||||||
|
pageCount: Math.ceil(rowCount / limit)
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
meta
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteClient(client: OAuth2Client) {
|
||||||
|
if (client.pictureId) {
|
||||||
|
await Uploads.removeClientAvatar(client);
|
||||||
|
}
|
||||||
|
await db.delete(privilege).where(eq(privilege.clientId, client.id));
|
||||||
|
await db.delete(oauth2Client).where(eq(oauth2Client.id, client.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteUrl(client: OAuth2Client, urlId: number) {
|
||||||
|
await db
|
||||||
|
.delete(oauth2ClientUrl)
|
||||||
|
.where(and(eq(oauth2ClientUrl.clientId, client.id), eq(oauth2ClientUrl.id, urlId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addUrl(client: OAuth2Client, type: OAuth2ClientURLType, url: string) {
|
||||||
|
await db.insert(oauth2ClientUrl).values({
|
||||||
|
type,
|
||||||
|
url,
|
||||||
|
clientId: client.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deletePrivilege(client: OAuth2Client, privilegeId: number) {
|
||||||
|
await db
|
||||||
|
.delete(privilege)
|
||||||
|
.where(and(eq(privilege.clientId, client.id), eq(privilege.id, privilegeId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addPrivilege(client: OAuth2Client, name: string) {
|
||||||
|
const realName = `${client.client_id.split('-')[0]}:${name}`;
|
||||||
|
await db.insert(privilege).values({
|
||||||
|
name: realName,
|
||||||
|
clientId: client.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(client: OAuth2Client, body: Partial<OAuth2Client>) {
|
||||||
|
await db.update(oauth2Client).set(body).where(eq(oauth2Client.id, client.id));
|
||||||
|
}
|
||||||
|
|
||||||
static async authorizeClientInfo(client: OAuth2Client, scope: string[]) {
|
static async authorizeClientInfo(client: OAuth2Client, scope: string[]) {
|
||||||
const links = await OAuth2Clients.getClientUrls(client);
|
const links = await OAuth2Clients.getClientUrls(client);
|
||||||
const filteredLinks = links
|
const filteredLinks = links
|
||||||
@ -127,8 +303,6 @@ export class OAuth2Clients {
|
|||||||
allowedScopes.push('management');
|
allowedScopes.push('management');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: client picture
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
links: filteredLinks,
|
links: filteredLinks,
|
||||||
client_id: client.client_id,
|
client_id: client.client_id,
|
||||||
|
@ -1,16 +1,37 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db, upload, user, type Upload, type User } from './drizzle';
|
import {
|
||||||
|
db,
|
||||||
|
oauth2Client,
|
||||||
|
upload,
|
||||||
|
user,
|
||||||
|
type OAuth2Client,
|
||||||
|
type Upload,
|
||||||
|
type User
|
||||||
|
} from './drizzle';
|
||||||
import { Users } from './users';
|
import { Users } from './users';
|
||||||
import { readFile, unlink, writeFile } from 'fs/promises';
|
import { readFile, unlink, writeFile } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import * as mime from 'mime-types';
|
import * as mime from 'mime-types';
|
||||||
|
import { OAuth2Clients } from './oauth2';
|
||||||
|
|
||||||
const fallbackImage = await readFile(join('static', 'avatar.png'));
|
const userFallbackImage = await readFile(join('static', 'avatar.png'));
|
||||||
|
const clientFallbackImage = await readFile(join('static', 'application.png'));
|
||||||
|
|
||||||
export class Uploads {
|
export class Uploads {
|
||||||
static fallbackImage = fallbackImage;
|
static userFallbackImage = userFallbackImage;
|
||||||
|
static clientFallbackImage = clientFallbackImage;
|
||||||
static uploads = join('uploads');
|
static uploads = join('uploads');
|
||||||
|
|
||||||
|
static async removeUpload(subject: Upload) {
|
||||||
|
try {
|
||||||
|
unlink(join(Uploads.uploads, subject.file));
|
||||||
|
} catch {
|
||||||
|
// ignore unlink error
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(upload).where(eq(upload.id, subject.id));
|
||||||
|
}
|
||||||
|
|
||||||
static async getAvatarByUuid(
|
static async getAvatarByUuid(
|
||||||
uuid: string
|
uuid: string
|
||||||
): Promise<{ file: string; mimetype: string } | undefined> {
|
): Promise<{ file: string; mimetype: string } | undefined> {
|
||||||
@ -26,14 +47,19 @@ export class Uploads {
|
|||||||
return picture;
|
return picture;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeUpload(subject: Upload) {
|
static async getClientAvatarById(
|
||||||
try {
|
id: string
|
||||||
unlink(join(Uploads.uploads, subject.file));
|
): Promise<{ file: string; mimetype: string } | undefined> {
|
||||||
} catch {
|
const client = await OAuth2Clients.fetchById(id);
|
||||||
// ignore unlink error
|
if (!client?.pictureId) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.delete(upload).where(eq(upload.id, subject.id));
|
const [picture] = await db
|
||||||
|
.select({ mimetype: upload.mimetype, file: upload.file })
|
||||||
|
.from(upload)
|
||||||
|
.where(eq(upload.id, client.pictureId));
|
||||||
|
return picture;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeAvatar(subject: User) {
|
static async removeAvatar(subject: User) {
|
||||||
@ -47,6 +73,17 @@ export class Uploads {
|
|||||||
await db.update(user).set({ pictureId: null }).where(eq(user.id, subject.id));
|
await db.update(user).set({ pictureId: null }).where(eq(user.id, subject.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async removeClientAvatar(client: OAuth2Client) {
|
||||||
|
if (!client.pictureId) return;
|
||||||
|
|
||||||
|
const [fileinfo] = await db.select().from(upload).where(eq(upload.id, client.pictureId));
|
||||||
|
if (fileinfo) {
|
||||||
|
await Uploads.removeUpload(fileinfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.update(oauth2Client).set({ pictureId: null }).where(eq(oauth2Client.id, client.id));
|
||||||
|
}
|
||||||
|
|
||||||
static async saveAvatar(subject: User, file: File) {
|
static async saveAvatar(subject: User, file: File) {
|
||||||
const ext = mime.extension(file.type);
|
const ext = mime.extension(file.type);
|
||||||
const newName = `user-${subject.uuid.split('-')[0]}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
const newName = `user-${subject.uuid.split('-')[0]}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
||||||
@ -64,4 +101,25 @@ export class Uploads {
|
|||||||
});
|
});
|
||||||
await db.update(user).set({ pictureId: retval.insertId }).where(eq(user.id, subject.id));
|
await db.update(user).set({ pictureId: retval.insertId }).where(eq(user.id, subject.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async saveClientAvatar(client: OAuth2Client, uploader: User, file: File) {
|
||||||
|
const ext = mime.extension(file.type);
|
||||||
|
const newName = `client-${client.client_id.substring(0, 8)}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
// Write to filesystem
|
||||||
|
await writeFile(join(Uploads.uploads, newName), Buffer.from(arrayBuffer));
|
||||||
|
// Remove old
|
||||||
|
await Uploads.removeClientAvatar(client);
|
||||||
|
// Update DB
|
||||||
|
const [retval] = await db.insert(upload).values({
|
||||||
|
original_name: file.name,
|
||||||
|
mimetype: file.type,
|
||||||
|
file: newName,
|
||||||
|
uploaderId: uploader.id
|
||||||
|
});
|
||||||
|
await db
|
||||||
|
.update(oauth2Client)
|
||||||
|
.set({ pictureId: retval.insertId })
|
||||||
|
.where(eq(oauth2Client.id, client.id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,14 +221,19 @@ export class Users {
|
|||||||
.where(clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId));
|
.where(clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getUserPrivileges(subject: User) {
|
static async getUserPrivileges(subject: User, clientId?: number) {
|
||||||
const list = await db
|
const list = await db
|
||||||
.select({
|
.select({
|
||||||
privilege: privilege.name
|
privilege: privilege.name
|
||||||
})
|
})
|
||||||
.from(privilege)
|
.from(privilege)
|
||||||
.innerJoin(userPrivilegesPrivilege, eq(privilege.id, userPrivilegesPrivilege.privilegeId))
|
.innerJoin(userPrivilegesPrivilege, eq(privilege.id, userPrivilegesPrivilege.privilegeId))
|
||||||
.where(eq(userPrivilegesPrivilege.userId, subject.id));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userPrivilegesPrivilege.userId, subject.id),
|
||||||
|
clientId ? eq(privilege.clientId, clientId) : isNull(privilege.clientId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return list.reduce<string[]>(
|
return list.reduce<string[]>(
|
||||||
(accum, { privilege }) => (!accum.includes(privilege) ? [...accum, privilege] : accum),
|
(accum, { privilege }) => (!accum.includes(privilege) ? [...accum, privilege] : accum),
|
||||||
|
@ -2,3 +2,4 @@ export const emailRegex =
|
|||||||
/^[-!#$%&'*+\\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
|
/^[-!#$%&'*+\\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
|
||||||
export const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d\w\W]{8,}$/;
|
export const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d\w\W]{8,}$/;
|
||||||
export const usernameRegex = /^[a-zA-Z0-9_\-.]{3,26}$/;
|
export const usernameRegex = /^[a-zA-Z0-9_\-.]{3,26}$/;
|
||||||
|
export const privilegeRegex = /^[a-zA-Z0-9_\-.:]{3,26}$/;
|
||||||
|
@ -2,11 +2,10 @@ import { Uploads } from '$lib/server/upload.js';
|
|||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
export async function GET({ params }) {
|
export async function GET({ params: { uuid } }) {
|
||||||
const uuid = params.slug;
|
|
||||||
const uploadFile = await Uploads.getAvatarByUuid(uuid);
|
const uploadFile = await Uploads.getAvatarByUuid(uuid);
|
||||||
if (!uploadFile) {
|
if (!uploadFile) {
|
||||||
return new Response(Uploads.fallbackImage, {
|
return new Response(Uploads.userFallbackImage, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/png'
|
'Content-Type': 'image/png'
|
23
src/routes/api/avatar/client/[uuid]/+server.ts
Normal file
23
src/routes/api/avatar/client/[uuid]/+server.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Uploads } from '$lib/server/upload.js';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export async function GET({ params: { uuid } }) {
|
||||||
|
const uploadFile = await Uploads.getClientAvatarById(uuid);
|
||||||
|
if (!uploadFile) {
|
||||||
|
return new Response(Uploads.clientFallbackImage, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/png'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const readUpload = await readFile(join(Uploads.uploads, uploadFile.file));
|
||||||
|
return new Response(readUpload, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': uploadFile.mimetype
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -9,6 +9,7 @@ import { Users } from '$lib/server/users/index.js';
|
|||||||
|
|
||||||
export const GET = async ({ request, url, locals }) => {
|
export const GET = async ({ request, url, locals }) => {
|
||||||
let user: User | undefined = undefined;
|
let user: User | undefined = undefined;
|
||||||
|
let clientId: number | undefined = undefined;
|
||||||
let tokenScopes: string[] | undefined = undefined;
|
let tokenScopes: string[] | undefined = undefined;
|
||||||
|
|
||||||
if (locals.session?.data?.user) {
|
if (locals.session?.data?.user) {
|
||||||
@ -21,6 +22,7 @@ export const GET = async ({ request, url, locals }) => {
|
|||||||
if (token?.userId) {
|
if (token?.userId) {
|
||||||
tokenScopes = OAuth2Clients.splitScope(token.scope || '');
|
tokenScopes = OAuth2Clients.splitScope(token.scope || '');
|
||||||
user = await Users.getById(token.userId);
|
user = await Users.getById(token.userId);
|
||||||
|
clientId = token.clientId || undefined;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OAuth2Error) {
|
if (error instanceof OAuth2Error) {
|
||||||
@ -58,7 +60,9 @@ export const GET = async ({ request, url, locals }) => {
|
|||||||
userData.picture = `${PUBLIC_URL}/api/avatar/${user.uuid}`;
|
userData.picture = `${PUBLIC_URL}/api/avatar/${user.uuid}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: privileges
|
if (scopelessAccess || tokenScopes?.includes('privileges')) {
|
||||||
|
userData.privileges = await Users.getUserPrivileges(user, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
return ApiUtils.json(userData);
|
return ApiUtils.json(userData);
|
||||||
};
|
};
|
||||||
|
@ -46,8 +46,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="graphic" aria-hidden="true"></div>
|
<div class="graphic" aria-hidden="true"></div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<!-- TODO: client pictures -->
|
<AvatarCard src={`/api/avatar/client/${data.client.client_id}`} alt={data.client.title}>
|
||||||
<AvatarCard src={`${assets}/application.png`} alt={data.client.title}>
|
|
||||||
<div class="card-inner">
|
<div class="card-inner">
|
||||||
<span class="card-display-name">{data.client.title}</span>
|
<span class="card-display-name">{data.client.title}</span>
|
||||||
<span class="card-user-name">{data.client.description}</span>
|
<span class="card-user-name">{data.client.description}</span>
|
||||||
|
44
src/routes/ssoadmin/oauth2/+page.server.ts
Normal file
44
src/routes/ssoadmin/oauth2/+page.server.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { AdminUtils } from '$lib/server/admin-utils';
|
||||||
|
import type { User } from '$lib/server/drizzle/schema.js';
|
||||||
|
import { OAuth2Clients } from '$lib/server/oauth2/index.js';
|
||||||
|
import { Users } from '$lib/server/users';
|
||||||
|
import { hasPrivileges } from '$lib/utils.js';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
export const load = async ({ parent, url }) => {
|
||||||
|
const { user } = await parent();
|
||||||
|
const currentUser = await Users.getBySession(user);
|
||||||
|
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]);
|
||||||
|
|
||||||
|
let limit = PAGE_SIZE;
|
||||||
|
let page = 1;
|
||||||
|
let filter: string | undefined = undefined;
|
||||||
|
if (url.searchParams.has('page')) {
|
||||||
|
page = Number(url.searchParams.get('page')) || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.searchParams.has('pageSize')) {
|
||||||
|
limit = Number(url.searchParams.get('pageSize')) || PAGE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.searchParams.has('filter')) {
|
||||||
|
filter = url.searchParams.get('filter') as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const fullPrivileges = hasPrivileges(user.privileges, ['admin:oauth2']);
|
||||||
|
|
||||||
|
const data = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
filter,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
omitSecret: true,
|
||||||
|
listAll: fullPrivileges
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullPrivileges,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
};
|
33
src/routes/ssoadmin/oauth2/+page.svelte
Normal file
33
src/routes/ssoadmin/oauth2/+page.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Paginator from '$lib/components/Paginator.svelte';
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import ClientCard from '$lib/components/admin/AdminClientCard.svelte';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{$t('admin.oauth2.title')}</h1>
|
||||||
|
|
||||||
|
<div class="client-list">
|
||||||
|
<Paginator meta={data.meta} />
|
||||||
|
{#each data.list as client}
|
||||||
|
<a href={`oauth2/${client.client_id}`} class="client-link">
|
||||||
|
<ClientCard {client} />
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
<Paginator meta={data.meta} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.client-link {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
402
src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts
Normal file
402
src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
import { AdminUtils } from '$lib/server/admin-utils';
|
||||||
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
|
import { CryptoUtils } from '$lib/server/crypto-utils.js';
|
||||||
|
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||||
|
import { OAuth2ClientURLType, OAuth2Clients } from '$lib/server/oauth2';
|
||||||
|
import { Uploads } from '$lib/server/upload.js';
|
||||||
|
import { Users } from '$lib/server/users';
|
||||||
|
import { UsersAdmin } from '$lib/server/users/admin';
|
||||||
|
import { hasPrivileges } from '$lib/utils';
|
||||||
|
import { privilegeRegex } from '$lib/validators.js';
|
||||||
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
interface AddUrlRequest {
|
||||||
|
type: OAuth2ClientURLType;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateRequest {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
activated?: string;
|
||||||
|
verified?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddPrivilegeRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
update: async ({ locals, request, params: { uuid } }) => {
|
||||||
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
|
const body = await request.formData();
|
||||||
|
const { title, description, activated, verified } = Changesets.take<UpdateRequest>(
|
||||||
|
['title', 'description', 'activated', 'verified'],
|
||||||
|
body
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!!verified && !fullPrivileges) {
|
||||||
|
return fail(403, { errors: ['forbidden'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const actuallyVerified = fullPrivileges ? Number(!!verified) : undefined;
|
||||||
|
const actuallyActivated = Number(!!activated);
|
||||||
|
|
||||||
|
if (title && (title.length < 3 || title.length > 32)) {
|
||||||
|
return fail(403, { errors: ['invalidTitle'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description && description.length > 1000) {
|
||||||
|
return fail(403, { errors: ['invalidDescription'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.update(details as OAuth2Client, {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
verified: actuallyVerified,
|
||||||
|
activated: actuallyActivated
|
||||||
|
});
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
delete: async ({ locals, params: { uuid } }) => {
|
||||||
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.activated === 1) {
|
||||||
|
return fail(400, { errors: ['deleteActivated'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
if (details.ownerId !== currentUser.id && !fullPrivileges) {
|
||||||
|
return fail(403, { errors: ['forbidden'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.deleteClient(details as OAuth2Client);
|
||||||
|
|
||||||
|
return redirect(303, '/ssoadmin/oauth2');
|
||||||
|
},
|
||||||
|
regenerate: async ({ locals, params: { uuid } }) => {
|
||||||
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
if (!fullPrivileges && !details.isOwner) {
|
||||||
|
return fail(403, { errors: ['forbidden'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.update(details as OAuth2Client, {
|
||||||
|
client_secret: CryptoUtils.generateSecret()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
removeUrl: async ({ locals, url, params: { uuid } }) => {
|
||||||
|
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(url.searchParams.get('id'));
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return fail(400, { errors: ['invalidUrlId'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.deleteUrl(details as OAuth2Client, id);
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
addUrl: async ({ locals, request, params: { uuid } }) => {
|
||||||
|
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.formData();
|
||||||
|
const { type, url } = Changesets.take<AddUrlRequest>(['type', 'url'], body);
|
||||||
|
if (!type || !OAuth2Clients.availableUrlTypes.includes(type)) {
|
||||||
|
return fail(400, { errors: ['invalidUrlType'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return fail(400, { errors: ['invalidUrl'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.addUrl(details as OAuth2Client, type, url);
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
removePrivilege: async ({ locals, url, params: { uuid } }) => {
|
||||||
|
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(url.searchParams.get('id'));
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return fail(400, { errors: ['invalidPrivilegeId'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.deletePrivilege(details as OAuth2Client, id);
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
addPrivilege: async ({ locals, request, params: { uuid } }) => {
|
||||||
|
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.formData();
|
||||||
|
const { name } = Changesets.take<AddPrivilegeRequest>(['name'], body);
|
||||||
|
|
||||||
|
if (!name || !privilegeRegex.test(name)) {
|
||||||
|
return fail(400, { errors: ['invalidPrivilege'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await OAuth2Clients.addPrivilege(details as OAuth2Client, name);
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
grants: async ({ locals, request, params: { uuid } }) => {
|
||||||
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
const allowedGrants = fullPrivileges
|
||||||
|
? OAuth2Clients.availableGrantTypes
|
||||||
|
: OAuth2Clients.userSetGrants;
|
||||||
|
|
||||||
|
const body = await request.formData();
|
||||||
|
const values = Array.from(body.keys());
|
||||||
|
values.unshift('authorization_code');
|
||||||
|
const deduplicatedAllowedGrants = values.filter(
|
||||||
|
(value, index, array) => allowedGrants.includes(value) && array.indexOf(value) === index
|
||||||
|
);
|
||||||
|
|
||||||
|
await OAuth2Clients.update(details as OAuth2Client, {
|
||||||
|
grants: deduplicatedAllowedGrants.join(' ')
|
||||||
|
});
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
scopes: async ({ locals, request, params: { uuid } }) => {
|
||||||
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
const allowedScopes = fullPrivileges
|
||||||
|
? OAuth2Clients.availableScopes
|
||||||
|
: OAuth2Clients.userSetScopes;
|
||||||
|
|
||||||
|
const body = await request.formData();
|
||||||
|
const values = Array.from(body.keys());
|
||||||
|
values.unshift('profile');
|
||||||
|
const deduplicatedAllowedScopes = values.filter(
|
||||||
|
(value, index, array) => allowedScopes.includes(value) && array.indexOf(value) === index
|
||||||
|
);
|
||||||
|
|
||||||
|
await OAuth2Clients.update(details as OAuth2Client, {
|
||||||
|
scope: OAuth2Clients.joinScope(deduplicatedAllowedScopes)
|
||||||
|
});
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
avatar: async ({ request, locals, params: { uuid } }) => {
|
||||||
|
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = Object.fromEntries(await request.formData());
|
||||||
|
if (!(formData.file as File)?.name || (formData.file as File).name === 'undefined') {
|
||||||
|
return fail(400, {
|
||||||
|
errors: ['noFile']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { file } = formData as { file: File };
|
||||||
|
|
||||||
|
await Uploads.saveClientAvatar(details as OAuth2Client, currentUser, file);
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
removeAvatar: async ({ locals, params: { uuid } }) => {
|
||||||
|
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
||||||
|
['admin:oauth2', 'self:oauth2']
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Uploads.removeClientAvatar(details as OAuth2Client);
|
||||||
|
|
||||||
|
return { errors: [] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const load = async ({ params: { uuid }, parent }) => {
|
||||||
|
const { user } = await parent();
|
||||||
|
const currentUser = await Users.getBySession(user);
|
||||||
|
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(user.privileges, ['admin:oauth2']);
|
||||||
|
|
||||||
|
const {
|
||||||
|
list: [details]
|
||||||
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
|
clientId: uuid,
|
||||||
|
listAll: false,
|
||||||
|
omitSecret: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return error(404, 'Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const privileges = await Users.getAvailablePrivileges(details.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableUrls: OAuth2Clients.availableUrlTypes,
|
||||||
|
availablePrivileges: privileges,
|
||||||
|
availableGrants: fullPrivileges
|
||||||
|
? OAuth2Clients.availableGrantTypes
|
||||||
|
: OAuth2Clients.userSetGrants,
|
||||||
|
availableScopes: fullPrivileges ? OAuth2Clients.availableScopes : OAuth2Clients.userSetScopes,
|
||||||
|
fullPrivileges,
|
||||||
|
details
|
||||||
|
};
|
||||||
|
};
|
333
src/routes/ssoadmin/oauth2/[uuid]/+page.svelte
Normal file
333
src/routes/ssoadmin/oauth2/[uuid]/+page.svelte
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
||||||
|
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
|
||||||
|
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||||
|
import SplitView from '$lib/components/container/SplitView.svelte';
|
||||||
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import type { ActionData, PageData } from './$types';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
export let form: ActionData;
|
||||||
|
|
||||||
|
let showAvatarModal = writable(false);
|
||||||
|
let secret = false;
|
||||||
|
let addingUrl = false;
|
||||||
|
let addingPrivilege = false;
|
||||||
|
|
||||||
|
$: noRedirects = !data.details.urls.some(({ type }) => type === 'redirect_uri');
|
||||||
|
$: availableUrls = data.availableUrls.filter((type) => {
|
||||||
|
// Can have up to three redirect URIs, only one of other types
|
||||||
|
const countOfType = data.details.urls.filter(({ type: subType }) => type === subType).length;
|
||||||
|
if (type === 'redirect_uri') {
|
||||||
|
return countOfType < 3;
|
||||||
|
}
|
||||||
|
return !countOfType;
|
||||||
|
});
|
||||||
|
$: splitScopes = data.details.scope?.split(' ') || [];
|
||||||
|
$: splitGrants = data.details.grants?.split(' ') || [];
|
||||||
|
$: uuidPrefix = data.details.client_id.split('-')[0] + ':';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{$t('admin.oauth2.title')} / {data.details.title}</h1>
|
||||||
|
|
||||||
|
<ColumnView>
|
||||||
|
<SplitView>
|
||||||
|
<ColumnView>
|
||||||
|
<AvatarCard src={`/api/avatar/client/${data.details.client_id}?t=${data.renderrt}`}>
|
||||||
|
<ColumnView>
|
||||||
|
<div>
|
||||||
|
<Button variant="primary" on:click={() => ($showAvatarModal = true)}
|
||||||
|
>{$t('admin.oauth2.avatar.change')}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{#if data.details.pictureId}
|
||||||
|
<form action="?/removeAvatar" method="POST">
|
||||||
|
<Button type="submit" variant="link">{$t('admin.oauth2.avatar.remove')}</Button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</ColumnView>
|
||||||
|
</AvatarCard>
|
||||||
|
|
||||||
|
<form action="?/update" method="POST">
|
||||||
|
<FormWrapper>
|
||||||
|
<FormErrors errors={form?.errors || []} prefix="admin.oauth2.errors" />
|
||||||
|
<FormSection>
|
||||||
|
<FormControl>
|
||||||
|
<label for="client-title">{$t('admin.oauth2.clientTitle')}</label>
|
||||||
|
<input name="title" id="client-title" value={data.details.title} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="client-id">{$t('admin.oauth2.clientId')}</label>
|
||||||
|
<input readonly id="client-id" value={data.details.client_id} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="client-secret">{$t('admin.oauth2.clientSecret')}</label>
|
||||||
|
{#if secret}
|
||||||
|
<input readonly id="client-secret" value={data.details.client_secret} />
|
||||||
|
{:else}
|
||||||
|
<Button on:click={() => (secret = true)}>{$t('admin.oauth2.reveal')}</Button>
|
||||||
|
{/if}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="client-description">{$t('admin.oauth2.description')}</label>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
id="client-description"
|
||||||
|
value={data.details.description}
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="client-activated">{$t('admin.oauth2.activated')}</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="activated"
|
||||||
|
id="client-activated"
|
||||||
|
checked={Boolean(data.details.activated)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{#if data.fullPrivileges}
|
||||||
|
<FormControl>
|
||||||
|
<label for="client-verified">{$t('admin.oauth2.verified')}</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="verified"
|
||||||
|
id="client-verified"
|
||||||
|
checked={Boolean(data.details.verified)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{/if}
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
|
||||||
|
</FormWrapper>
|
||||||
|
</form>
|
||||||
|
</ColumnView>
|
||||||
|
|
||||||
|
<ColumnView>
|
||||||
|
<h2>{$t('admin.oauth2.actions')}</h2>
|
||||||
|
{#if data.fullPrivileges || data.details.isOwner}
|
||||||
|
{#if !data.details.activated}
|
||||||
|
<form action="?/delete" method="POST">
|
||||||
|
<Button type="submit" variant="link">{$t('admin.oauth2.delete')}</Button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<form action="?/regenerate" method="POST">
|
||||||
|
<Button type="submit" variant="link">{$t('admin.oauth2.regenerate')}</Button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h2>{$t('admin.oauth2.urls.title')}</h2>
|
||||||
|
{#if noRedirects}
|
||||||
|
<Alert type="error">
|
||||||
|
{$t('admin.oauth2.errors.noRedirect')}
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
<div class="urls addremove">
|
||||||
|
{#each data.details.urls as url}
|
||||||
|
<div class="url addremove-item">
|
||||||
|
<div>
|
||||||
|
<div class="url-type">{$t(`admin.oauth2.urls.types.${url.type}`)}</div>
|
||||||
|
<a class="url-url" href={url.url} target="_blank" rel="nofollow noreferrer"
|
||||||
|
>{url.url}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="?/removeUrl&id={url.id}" method="POST">
|
||||||
|
<Button type="submit" variant="link">{$t('common.remove')}</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !addingUrl && availableUrls.length}
|
||||||
|
<div>
|
||||||
|
<Button variant="link" on:click={() => (addingUrl = true)}
|
||||||
|
>+ {$t('admin.oauth2.urls.add')}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else if addingUrl}
|
||||||
|
<form action="?/addUrl" method="POST">
|
||||||
|
<FormWrapper>
|
||||||
|
<FormSection title={$t('admin.oauth2.urls.new')}>
|
||||||
|
<FormControl>
|
||||||
|
<label for="newurl-type">{$t('admin.oauth2.urls.type')}</label>
|
||||||
|
<select name="type" id="newurl-type">
|
||||||
|
{#each availableUrls as urlType}
|
||||||
|
<option value={urlType}>{$t(`admin.oauth2.urls.types.${urlType}`)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="newurl-url">{$t('admin.oauth2.urls.url')}</label>
|
||||||
|
<input name="url" type="url" id="newurl-url" />
|
||||||
|
</FormControl>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
|
||||||
|
<Button variant="link" on:click={() => (addingUrl = false)}
|
||||||
|
>{$t('common.cancel')}</Button
|
||||||
|
>
|
||||||
|
</ButtonRow>
|
||||||
|
</FormWrapper>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h2>{$t('admin.oauth2.privileges.title')}</h2>
|
||||||
|
<p>{$t('admin.oauth2.privileges.addHint')}</p>
|
||||||
|
<div class="privileges addremove">
|
||||||
|
{#each data.availablePrivileges as privilege}
|
||||||
|
{@const [idPart, ...rest] = privilege.name.split(':')}
|
||||||
|
<div class="privilege addremove-item">
|
||||||
|
<span class="privilege-name"
|
||||||
|
><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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !addingPrivilege}
|
||||||
|
<div>
|
||||||
|
<Button variant="link" on:click={() => (addingPrivilege = true)}
|
||||||
|
>+ {$t('admin.oauth2.privileges.add')}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form action="?/addPrivilege" method="POST">
|
||||||
|
<FormWrapper>
|
||||||
|
<FormSection title={$t('admin.oauth2.privileges.new')}>
|
||||||
|
<FormControl>
|
||||||
|
<label for="newpriv-name">{$t('admin.oauth2.privileges.name')}</label>
|
||||||
|
|
||||||
|
<input name="name" id="newpriv-name" />
|
||||||
|
<span>{$t('admin.oauth2.privileges.nameHint', { prefix: uuidPrefix })}</span>
|
||||||
|
</FormControl>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
|
||||||
|
<Button variant="link" on:click={() => (addingPrivilege = false)}
|
||||||
|
>{$t('common.cancel')}</Button
|
||||||
|
>
|
||||||
|
</ButtonRow>
|
||||||
|
</FormWrapper>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</ColumnView>
|
||||||
|
</SplitView>
|
||||||
|
|
||||||
|
<h2>{$t('admin.oauth2.grants')}</h2>
|
||||||
|
<p>{$t('admin.oauth2.grantsHint')}</p>
|
||||||
|
|
||||||
|
<form action="?/grants" method="POST">
|
||||||
|
<div class="scope-cloud">
|
||||||
|
{#each data.availableGrants as grant}
|
||||||
|
<FormControl>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`grant-${grant}`}
|
||||||
|
name={grant}
|
||||||
|
disabled={grant === 'authorization_code'}
|
||||||
|
checked={grant === 'authorization_code' ? true : splitGrants.includes(grant)}
|
||||||
|
/>
|
||||||
|
<label for={`grant-${grant}`}
|
||||||
|
><code>{grant}</code> - {$t(`admin.oauth2.grantTexts.${grant}`)}</label
|
||||||
|
>
|
||||||
|
</FormControl>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>{$t('admin.oauth2.scopes')}</h2>
|
||||||
|
<p>{$t('admin.oauth2.scopesHint')}</p>
|
||||||
|
|
||||||
|
<form action="?/scopes" method="POST">
|
||||||
|
<div class="scope-cloud">
|
||||||
|
{#each data.availableScopes as scope}
|
||||||
|
<FormControl>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`scope-${scope}`}
|
||||||
|
name={scope}
|
||||||
|
disabled={scope === 'profile'}
|
||||||
|
checked={scope === 'profile' ? true : splitScopes.includes(scope)}
|
||||||
|
/>
|
||||||
|
<label for={`scope-${scope}`}
|
||||||
|
><code>{scope}</code> - {$t(`admin.oauth2.scopeTexts.${scope}`)}</label
|
||||||
|
>
|
||||||
|
</FormControl>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>{$t('admin.oauth2.authorizations')}</h2>
|
||||||
|
<p>{$t('admin.oauth2.authorizationsHint')}</p>
|
||||||
|
|
||||||
|
<b>{$t('admin.oauth2.noAuthorizations')}</b>
|
||||||
|
</ColumnView>
|
||||||
|
|
||||||
|
<AvatarModal show={showAvatarModal} url={$page.url.pathname} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addremove {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
& .addremove-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .privilege-id {
|
||||||
|
color: #646464;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .url-type {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .url-url {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-cloud {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
column-gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -18,7 +18,7 @@ interface UpdateRequest {
|
|||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
removeOtp: async () => {},
|
removeOtp: async () => {},
|
||||||
removeAvatar: async ({ locals, params: { slug: uuid } }) => {
|
removeAvatar: async ({ locals, params: { uuid } }) => {
|
||||||
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
||||||
|
|
||||||
const targetUser = await Users.getByUuid(uuid, false);
|
const targetUser = await Users.getByUuid(uuid, false);
|
||||||
@ -30,7 +30,7 @@ export const actions = {
|
|||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
deleteInfo: async ({ locals, params: { slug: uuid } }) => {
|
deleteInfo: async ({ locals, params: { uuid } }) => {
|
||||||
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
||||||
|
|
||||||
const targetUser = await Users.getByUuid(uuid, false);
|
const targetUser = await Users.getByUuid(uuid, false);
|
||||||
@ -56,7 +56,7 @@ export const actions = {
|
|||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
email: async ({ locals, params: { slug: uuid }, url }) => {
|
email: async ({ locals, params: { uuid }, url }) => {
|
||||||
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
await UsersAdmin.getActionUser(locals, ['admin', 'admin:user']);
|
||||||
|
|
||||||
const type = url.searchParams.get('type') as 'password' | 'activate';
|
const type = url.searchParams.get('type') as 'password' | 'activate';
|
||||||
@ -84,7 +84,7 @@ export const actions = {
|
|||||||
|
|
||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
update: async ({ locals, params: { slug: uuid }, request }) => {
|
update: async ({ locals, params: { uuid }, request }) => {
|
||||||
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
'admin',
|
'admin',
|
||||||
'admin:user'
|
'admin:user'
|
||||||
@ -136,11 +136,10 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const load = async ({ parent, params }) => {
|
export const load = async ({ parent, params: { uuid } }) => {
|
||||||
const { user } = await parent();
|
const { user } = await parent();
|
||||||
AdminUtils.checkPrivileges(user, ['admin:user']);
|
AdminUtils.checkPrivileges(user, ['admin:user']);
|
||||||
|
|
||||||
const uuid = params.slug;
|
|
||||||
const userInfo = await UsersAdmin.getUserDetails(uuid);
|
const userInfo = await UsersAdmin.getUserDetails(uuid);
|
||||||
if (!userInfo) {
|
if (!userInfo) {
|
||||||
error(404, 'User not found');
|
error(404, 'User not found');
|
Loading…
Reference in New Issue
Block a user