diff --git a/src/app.css b/src/app.css index 738a483..8841df8 100644 --- a/src/app.css +++ b/src/app.css @@ -1,34 +1,11 @@ +@import url('./colors.css'); + *, *::before, *::after { 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 { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; @@ -40,11 +17,12 @@ body { margin: 0; width: 100%; height: 100%; + overscroll-behavior: none; } body { background-color: var(--in-normalized-background); - background-image: url('/background.jpg'); + background-image: var(--in-background-image); background-attachment: fixed; background-repeat: no-repeat; background-size: cover; @@ -72,7 +50,7 @@ a { a[target='_blank']::after { 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; height: 0.95rem; display: inline-block; diff --git a/src/colors.css b/src/colors.css new file mode 100644 index 0000000..e81de2c --- /dev/null +++ b/src/colors.css @@ -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); +} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 57e84ef..64d0068 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,6 @@ import { AUTO_MIGRATE, SESSION_SECRET } from '$env/static/private'; import { db } from '$lib/server/drizzle'; +import { runSeeds } from '$lib/server/drizzle/seeds'; import { migrate } from 'drizzle-orm/mysql2/migrator'; import { handleSession } from 'svelte-kit-cookie-session'; @@ -7,6 +8,8 @@ if (AUTO_MIGRATE === 'true') { await migrate(db, { migrationsFolder: './migrations' }); } +await runSeeds(); + export const handle = handleSession({ secret: SESSION_SECRET }); diff --git a/src/lib/components/ActionButton.svelte b/src/lib/components/ActionButton.svelte new file mode 100644 index 0000000..00d8a73 --- /dev/null +++ b/src/lib/components/ActionButton.svelte @@ -0,0 +1,18 @@ + + +
+ + + diff --git a/src/lib/components/Alert.svelte b/src/lib/components/Alert.svelte index a35f7ca..fb42600 100644 --- a/src/lib/components/Alert.svelte +++ b/src/lib/components/Alert.svelte @@ -18,7 +18,7 @@ background-color: var(--in-alert-color); width: 100%; border-radius: 8px; - padding: 24px 28px; + padding: 16px 18px; font-size: 1.15rem; & p { diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index fc5fe58..9eb66ae 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -17,7 +17,7 @@ border: 0; padding: 0; background: transparent; - color: var(--in-text-color); + color: var(--in-link-color); font-size: 1rem; cursor: pointer; @@ -32,11 +32,21 @@ .btn-default, .btn-primary { - background-color: #fff; - color: #000; - border: 2px solid #ddd; + background-color: var(--in-button-primary-background); + color: var(--in-button-primary-color); + border: var(--in-button-primary-border); padding: 6px 12px; border-radius: 4px; 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; + } } diff --git a/src/lib/components/LogoutButton.svelte b/src/lib/components/LogoutButton.svelte index e580ca7..fd6755f 100644 --- a/src/lib/components/LogoutButton.svelte +++ b/src/lib/components/LogoutButton.svelte @@ -1,8 +1,6 @@ -
- -
+{$t('account.logout')} diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 147bc2d..a09ea79 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -2,6 +2,7 @@ import { createEventDispatcher } from 'svelte'; export let showModal: boolean; + export let dismissable = true; let dialog: HTMLDialogElement; const dispatch = createEventDispatcher(); @@ -17,7 +18,7 @@ showModal = false; dispatch('close'); }} - on:click|self={() => dialog.close()} + on:click|self={() => dismissable && dialog.close()} >
@@ -43,6 +44,7 @@ border-radius: 0.2em; border: none; padding: 0; + color: var(--in-modal-text-color); background-color: var(--in-modal-background); } diff --git a/src/lib/components/Paginator.svelte b/src/lib/components/Paginator.svelte index f88b934..14c7a83 100644 --- a/src/lib/components/Paginator.svelte +++ b/src/lib/components/Paginator.svelte @@ -16,7 +16,7 @@ href={`?page=${pageNum - 1}`} tabindex={firstPage ? -1 : 0} aria-label={$t('common.previous')} - aria-disabled={firstPage}><<< {#each pageButtons as buttonNumber} @@ -35,7 +35,7 @@ tabindex={lastPage ? -1 : 0} href={`?page=${pageNum + 1}`} aria-label={$t('common.next')} - aria-disabled={lastPage}>>>> @@ -49,8 +49,8 @@ .page-button { --in-page-button-size: 28px; display: block; - background-color: #fff; - color: #000; + background-color: var(--in-button-primary-background); + color: var(--in-button-primary-color); min-width: var(--in-page-button-size); height: var(--in-page-button-size); line-height: var(--in-page-button-size); @@ -60,14 +60,14 @@ text-decoration: none; &:hover { - background-color: #ececec; + background-color: var(--in-button-primary-hover-background); } &.disabled { user-select: none; pointer-events: none; - background-color: #ececec; - color: #616161; + background-color: var(--in-button-primary-disabled-background); + color: var(--in-button-primary-disabled-color); } } diff --git a/src/lib/components/admin/AdminClientCard.svelte b/src/lib/components/admin/AdminClientCard.svelte index 01050f8..6706252 100644 --- a/src/lib/components/admin/AdminClientCard.svelte +++ b/src/lib/components/admin/AdminClientCard.svelte @@ -59,9 +59,9 @@ display: flex; gap: 1rem; padding: 8px; - background-color: #fff; + background-color: var(--ina-card-background); border-radius: 8px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.25); + box-shadow: var(--ina-card-shadow); flex-grow: 1; transition: transform 100ms linear; diff --git a/src/lib/components/admin/AdminHeader.svelte b/src/lib/components/admin/AdminHeader.svelte index 1870b79..69c9123 100644 --- a/src/lib/components/admin/AdminHeader.svelte +++ b/src/lib/components/admin/AdminHeader.svelte @@ -18,8 +18,7 @@ justify-content: space-between; align-items: center; padding: 8px 16px; - background: #00aaff; - background: linear-gradient(180deg, #00aaff 0%, #005bff 100%); + background: var(--ina-header-color); } .admin-user { @@ -27,9 +26,9 @@ align-items: center; gap: 8px; padding: 4px 8px 4px 4px; - background-color: #004edf; + background-color: var(--ina-header-tag-color); border-radius: 40px; - color: #fff; + color: var(--ina-header-tag-text-color); & .admin-user-avatar { width: 32px; @@ -41,10 +40,10 @@ a { text-decoration: none; - color: #fff; + color: var(--ina-header-link-color); &:visited { - color: #fff; + color: var(--ina-header-link-color); } } diff --git a/src/lib/components/admin/AdminSidebar.svelte b/src/lib/components/admin/AdminSidebar.svelte index b4a3287..1d794ee 100644 --- a/src/lib/components/admin/AdminSidebar.svelte +++ b/src/lib/components/admin/AdminSidebar.svelte @@ -47,11 +47,11 @@ .admin-sidebar { display: flex; flex-direction: column; - background-color: #dddddd; + background-color: var(--ina-sidebar-color); height: 100%; max-width: 240px; width: 100%; - border-right: 2px solid #b4b4b4; + border-right: 2px solid var(--ina-sidebar-border-color); & > nav { margin-top: 16px; @@ -69,11 +69,11 @@ text-decoration: none; &.active { - background-color: #c7c7c7; + background-color: var(--ina-sidebar-link-active-color); } &:hover { - background-color: #e4e4e4; + background-color: var(--ina-sidebar-link-hover-color); } } } @@ -82,6 +82,6 @@ .admin-sidebar, .admin-sidebar :global(a) { - color: #000; + color: var(--ina-sidebar-link-text-color); } diff --git a/src/lib/components/admin/AdminUserCard.svelte b/src/lib/components/admin/AdminUserCard.svelte index b3fe984..86e5cd6 100644 --- a/src/lib/components/admin/AdminUserCard.svelte +++ b/src/lib/components/admin/AdminUserCard.svelte @@ -43,9 +43,9 @@ display: flex; gap: 1rem; padding: 8px; - background-color: #fff; + background-color: var(--ina-card-background); border-radius: 8px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.25); + box-shadow: var(--ina-card-shadow); flex-grow: 1; transition: transform 100ms linear; diff --git a/src/lib/components/container/MainContainer.svelte b/src/lib/components/container/MainContainer.svelte index 71be5df..4ffe9d8 100644 --- a/src/lib/components/container/MainContainer.svelte +++ b/src/lib/components/container/MainContainer.svelte @@ -11,6 +11,7 @@ display: flex; justify-content: center; width: 100%; + flex-grow: 1; } .page-inner { @@ -18,6 +19,7 @@ flex-direction: column; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + background: var(--in-container-background); padding: 40px; max-width: 1080px; width: 100%; @@ -25,7 +27,6 @@ main { min-height: 100vh; - height: 100%; display: flex; flex-direction: column; } diff --git a/src/lib/components/container/SideContainer.svelte b/src/lib/components/container/SideContainer.svelte index e546568..4b8518f 100644 --- a/src/lib/components/container/SideContainer.svelte +++ b/src/lib/components/container/SideContainer.svelte @@ -20,6 +20,7 @@ flex-direction: column; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + background: var(--in-container-background); padding: 40px; max-width: 600px; width: 100%; diff --git a/src/lib/components/form/FormControl.svelte b/src/lib/components/form/FormControl.svelte index 5653294..87dff8b 100644 --- a/src/lib/components/form/FormControl.svelte +++ b/src/lib/components/form/FormControl.svelte @@ -8,7 +8,7 @@ :global(input) { background-color: var(--in-input-background); color: var(--in-input-color); - border: 2px solid var(--in-input-border-color); + border: var(--in-input-border); &:not([type]), &[type='text'], @@ -17,7 +17,7 @@ &[type='email'] { padding: 8px; font-size: 1rem; - border-radius: 6px; + border-radius: 4px; &:focus-visible { outline: var(--in-focus-outline); @@ -34,6 +34,11 @@ border-color: var(--in-input-border-color-invalid); } } + + &[type='file'] { + padding: 1rem; + border-radius: 4px; + } } .form-control > :global(label) { diff --git a/src/lib/i18n/en/admin.json b/src/lib/i18n/en/admin.json index d5908ae..135b036 100644 --- a/src/lib/i18n/en/admin.json +++ b/src/lib/i18n/en/admin.json @@ -1,130 +1,132 @@ { - "title": "Admin", - "menu": { - "users": "Users", - "oauth2": "OAuth2 applications", - "audit": "Audit logs" - }, - "users": { - "title": "Users", - "uuid": "UUID", - "email": "Email", - "privileges": "Privileges", - "actions": "Account actions", - "activated": "Activated", - "registered": "Registered", - "deactivate": "Deactivate account", - "deactivateOtp": "Remove two-factor authentication", - "deleteInfo": "Delete account information", - "deleteInfoHint": "All personalized information will be deleted. The account will remain in the database as an UUID stub. This action is irreversible.", - "passwordEmail": "Send password email", - "activationEmail": "Send activation email", - "errors": { - "invalidUuid": "Invalid user or impossible action", - "invalidEmailType": "Invalid email type", - "unauthorized": "Unauthorized changes", - "lockout": "You cannot lock yourself out!", - "invalidDisplayName": "Invalid display name", - "invalidEmail": "Invalid email address" - } - }, - "oauth2": { - "title": "OAuth2 applications", - "new": "Create a new application", - "clientTitle": "Application name", - "clientId": "Client ID", - "clientSecret": "Client secret", - "description": "Application description", - "reveal": "Reveal secret", - "regenerate": "Regenerate secret", - "activated": "Activated", - "verified": "Official", - "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", - "edit": "Edit privileges" - }, - "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" - }, - "apis": { - "title": "OAuth2 APIs", - "authorize": "OAuth2 Authorization endpoint", - "token": "OAuth2 Token endpoint", - "introspect": "OAuth2 Introspection endpoint", - "userinfo": "User information endpoint (Bearer)", - "openid": "OpenID Connect configuration" - }, - "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)" - }, - "managers": { - "title": "Application members", - "hint": "These users can edit this application just like you can, except that they cannot regenerate the secret or delete the application. Please note that they must have an active account on {{siteName}}, but not necessarily with the provided address.", - "add": "Invite a new member", - "invite": "Invite" - }, - "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.", - "invalidEmail": "Invalid email address.", - "emailExists": "This email address is already added.", - "noFile": "Please upload a file first." - } - } + "title": "Admin", + "menu": { + "users": "Users", + "oauth2": "OAuth2 applications", + "audit": "Audit logs" + }, + "users": { + "title": "Users", + "uuid": "UUID", + "email": "Email", + "privileges": "Privileges", + "actions": "Account actions", + "activated": "Activated", + "registered": "Registered", + "deactivate": "Deactivate account", + "deactivateOtp": "Remove two-factor authentication", + "deleteInfo": "Delete account information", + "deleteInfoHint": "All personalized information will be deleted. The account will remain in the database as an UUID stub. This action is irreversible.", + "passwordEmail": "Send password email", + "activationEmail": "Send activation email", + "errors": { + "invalidUuid": "Invalid user or impossible action", + "invalidEmailType": "Invalid email type", + "unauthorized": "Unauthorized changes", + "lockout": "You cannot lock yourself out!", + "invalidDisplayName": "Invalid display name", + "invalidEmail": "Invalid email address" + } + }, + "oauth2": { + "title": "OAuth2 applications", + "new": "Create a new application", + "clientTitle": "Application name", + "clientId": "Client ID", + "clientSecret": "Client secret", + "description": "Application description", + "reveal": "Reveal secret", + "regenerate": "Regenerate secret", + "activated": "Activated", + "verified": "Official", + "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.", + "success": "Success!", + "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", + "edit": "Edit privileges" + }, + "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" + }, + "apis": { + "title": "OAuth2 APIs", + "authorize": "OAuth2 Authorization endpoint", + "token": "OAuth2 Token endpoint", + "introspect": "OAuth2 Introspection endpoint", + "userinfo": "User information endpoint (Bearer)", + "openid": "OpenID Connect configuration" + }, + "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)" + }, + "managers": { + "title": "Application members", + "hint": "These users can edit this application just like you can, except that they cannot regenerate the secret or delete the application. Please note that they must have an active account on {{siteName}}, but not necessarily with the provided address.", + "add": "Invite a new member", + "invite": "Invite" + }, + "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.", + "invalidEmail": "Invalid email address.", + "emailExists": "This email address is already added.", + "noFile": "Please upload a file first.", + "tooManyTimes": "You are doing that too much, please, slow down!" + } + } } diff --git a/src/lib/i18n/en/common.json b/src/lib/i18n/en/common.json index b7d2eed..b7f88a3 100644 --- a/src/lib/i18n/en/common.json +++ b/src/lib/i18n/en/common.json @@ -1,6 +1,6 @@ { "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 completely open source 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 completely open source and can be audited by anyone.", "submit": "Submit", "cancel": "Cancel", "manage": "Manage", diff --git a/src/lib/server/admin-utils.ts b/src/lib/server/admin-utils.ts index d0fa06c..94bbc16 100644 --- a/src/lib/server/admin-utils.ts +++ b/src/lib/server/admin-utils.ts @@ -1,5 +1,5 @@ import { error } from '@sveltejs/kit'; -import type { UserSession } from './users'; +import { Users, type UserSession } from './users'; import { hasPrivileges, type RequiredPrivileges } from '$lib/utils'; export class AdminUtils { @@ -8,4 +8,23 @@ export class AdminUtils { 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 }; + } } diff --git a/src/lib/server/drizzle/seeds/index.ts b/src/lib/server/drizzle/seeds/index.ts new file mode 100644 index 0000000..9338eea --- /dev/null +++ b/src/lib/server/drizzle/seeds/index.ts @@ -0,0 +1,9 @@ +import privilegesSeed from './privileges'; + +const seeds = [privilegesSeed]; + +export const runSeeds = async () => { + for (const fn of seeds) { + await fn(); + } +}; diff --git a/src/lib/server/drizzle/seeds/privileges.ts b/src/lib/server/drizzle/seeds/privileges.ts new file mode 100644 index 0000000..c23a4eb --- /dev/null +++ b/src/lib/server/drizzle/seeds/privileges.ts @@ -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 }); + } +} diff --git a/src/lib/server/email/templates/oauth2-invitation.email.ts b/src/lib/server/email/templates/oauth2-invitation.email.ts index 76339c7..59f15e5 100644 --- a/src/lib/server/email/templates/oauth2-invitation.email.ts +++ b/src/lib/server/email/templates/oauth2-invitation.email.ts @@ -11,7 +11,7 @@ ${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} @@ -22,7 +22,7 @@ This email was sent to you because someone invited you to contribute to an appli

${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}. -

Please click on the following link to accept the invitation ${PUBLIC_SITE_NAME}.

+

Please use the following link to accept the invitation:

Accept invitation: ${url}

diff --git a/src/lib/server/oauth2/model/client.ts b/src/lib/server/oauth2/model/client.ts index 70c0776..bb02a91 100644 --- a/src/lib/server/oauth2/model/client.ts +++ b/src/lib/server/oauth2/model/client.ts @@ -15,7 +15,7 @@ import { } from '$lib/server/drizzle'; import { Emails, OAuth2InvitationEmail } from '$lib/server/email'; 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 { and, count, eq, like, or, sql } from 'drizzle-orm'; @@ -64,6 +64,9 @@ export class OAuth2Clients { 'openid' ]; + // Non-administrator capabilities + public static implicitGrantTypes = ['id_token', 'implicit']; + public static userSetGrants = ['authorization_code', 'client_credentials', 'refresh_token']; public static userSetScopes = [ 'profile', 'picture', @@ -72,12 +75,6 @@ export class OAuth2Clients { 'management', 'openid' ]; - public static userSetGrants = [ - 'authorization_code', - 'client_credentials', - 'refresh_token', - 'id_token' - ]; public static describedScopes = ['email', 'picture', 'account']; public static alwaysPresentScopes = ['profile']; @@ -317,7 +314,7 @@ export class OAuth2Clients { client_id: uid, client_secret: secret, grants: 'authorization_code', - scope: 'profile', + scope: OAuth2Clients.joinScope(OAuth2Clients.alwaysPresentScopes), ownerId: subject.id, created_at: new Date(), activated: 1, @@ -382,6 +379,13 @@ export class OAuth2Clients { } 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({ clientId: client.id, userId: subject.id, diff --git a/src/lib/server/users/admin.ts b/src/lib/server/users/admin.ts index c7f1297..7129aa8 100644 --- a/src/lib/server/users/admin.ts +++ b/src/lib/server/users/admin.ts @@ -1,4 +1,4 @@ -import { count, eq, like, or, sql } from 'drizzle-orm'; +import { asc, count, eq, like, or, sql } from 'drizzle-orm'; import { db, privilege, @@ -8,16 +8,17 @@ import { type User } from '../drizzle'; 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 { privileges: Privilege[]; } export class UsersAdmin { + /** + * Map a list of users with their privileges. + * @param junkList User-privilege amalgamation + * @returns List of users + */ static mergeUserResponse( junkList: { 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({ filter, offset = 0, @@ -69,14 +75,26 @@ export class UsersAdmin { .from(user) .where(searchExpression); - const junkList = await db - .select() + const baseQuery = db + .select({ id: user.id }) .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(privilege, eq(userPrivilegesPrivilege.privilegeId, privilege.id)) - .where(searchExpression) - .limit(limit) - .offset(offset); + .where(searchExpression); const meta: PaginationMeta = { 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) { const junkList = await db .select() @@ -102,17 +125,4 @@ export class UsersAdmin { const [userInfo] = UsersAdmin.mergeUserResponse(junkList); 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 }; - } } diff --git a/src/lib/server/users/index.ts b/src/lib/server/users/index.ts index 4d53d4b..93f35fa 100644 --- a/src/lib/server/users/index.ts +++ b/src/lib/server/users/index.ts @@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs'; import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'; import { db, privilege, user, userPrivilegesPrivilege, type User } from '../drizzle'; import type { UserSession } from './types'; -import { redirect } from '@sveltejs/kit'; +import { error, redirect } from '@sveltejs/kit'; import { CryptoUtils } from '../crypto-utils'; import { EMAIL_ENABLED } from '$env/static/private'; 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'; export class Users { + /** + * Get user by their primary key ID. + * @param id User ID + * @returns User + */ static async getById(id: number): Promise { const [result] = await db .select() @@ -19,6 +24,11 @@ export class Users { return result; } + /** + * Get user by their UUID (public-facing ID). + * @param id User ID + * @returns User + */ static async getByUuid(uuid: string, activatedCheck = true): Promise { const [result] = await db .select() @@ -28,15 +38,30 @@ export class Users { 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 { const [result] = await db .select() .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); return result; } + /** + * Get an user by their session cookie. + * @param session Session from cookie + * @returns User + */ static async getBySession(session?: UserSession): Promise { if (!session) return undefined; const [result] = await db @@ -47,18 +72,39 @@ export class Users { return result; } + /** + * Update an user. + * @param subject User to update + * @param fields Fields to set + */ static async update(subject: User, fields: Partial) { 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 { return bcrypt.compare(password, user.password as string); } + /** + * Hash a password. + * @param password Plaintext password + * @returns Hashed password + */ static async hashPassword(password: string): Promise { return bcrypt.hash(password, 10); } + /** + * Create a session cookie from user information. + * @param user User + * @returns Cookie + */ static async toSession(user: User): Promise { return { 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) { const currentUser = await Users.getBySession(locals.session.data?.user); if (!currentUser) { @@ -77,6 +129,12 @@ export class Users { 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) { return !( await db @@ -91,6 +149,11 @@ export class Users { )?.length; } + /** + * Get user information by activation token. + * @param token Activation token + * @returns User information + */ static async getActivationToken(token: string) { const returnedToken = await UserTokens.getByToken(token, 'activation'); 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) { await db .update(user) @@ -114,6 +182,11 @@ export class Users { await UserTokens.remove(token); } + /** + * Register a new user. + * @param details User details + * @returns New User + */ static async register({ username, displayName, @@ -149,6 +222,10 @@ export class Users { return newUser; } + /** + * Send an activation email to an user. + * @param user User + */ static async sendRegistrationEmail(user: User) { const token = await UserTokens.create( 'activation', @@ -172,6 +249,10 @@ export class Users { } } + /** + * Send a password reset email to an user. + * @param user User + */ static async sendPasswordEmail(user: User) { const token = await UserTokens.create('password', new Date(Date.now() + 3600 * 1000), user.id); 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) { const token = await UserTokens.create( '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) { return await db .select() @@ -222,6 +312,12 @@ export class Users { .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) { const list = await db .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) { + // 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 .select({ 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) { const [name, domain] = email.split('@'); const namePart = `${name.charAt(0)}${''.padStart(name.length - 2, '*')}${name.charAt(name.length - 1)}`; diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 72b9df3..7991445 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -17,6 +17,7 @@ import { writable } from 'svelte/store'; import { PUBLIC_SITE_NAME } from '$env/static/public'; import FormErrors from '$lib/components/form/FormErrors.svelte'; + import TitleRow from '$lib/components/container/TitleRow.svelte'; export let data: PageData; export let form: ActionData; @@ -58,7 +59,12 @@ -

{PUBLIC_SITE_NAME}

+ +

{PUBLIC_SITE_NAME}

+ + +
+

{$t('account.title')}

@@ -160,8 +166,6 @@ - -
diff --git a/src/routes/login/password/+page.svelte b/src/routes/login/password/+page.svelte index 2cfa838..279f57c 100644 --- a/src/routes/login/password/+page.svelte +++ b/src/routes/login/password/+page.svelte @@ -4,6 +4,7 @@ import { PUBLIC_SITE_NAME } from '$env/static/public'; import Alert from '$lib/components/Alert.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 SideContainer from '$lib/components/container/SideContainer.svelte'; import FormControl from '$lib/components/form/FormControl.svelte'; @@ -88,9 +89,12 @@ {/if} - + + + + {/if} diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte index c90292c..69d37ad 100644 --- a/src/routes/register/+page.svelte +++ b/src/routes/register/+page.svelte @@ -12,6 +12,7 @@ import FormSection from '$lib/components/form/FormSection.svelte'; import { enhance } from '$app/forms'; import FormErrors from '$lib/components/form/FormErrors.svelte'; + import ButtonRow from '$lib/components/container/ButtonRow.svelte'; export let data: PageData; export let form: ActionData; @@ -120,12 +121,14 @@ - + + + + {/if} - diff --git a/src/routes/ssoadmin/+layout.svelte b/src/routes/ssoadmin/+layout.svelte index 8b9c2ce..c4141ed 100644 --- a/src/routes/ssoadmin/+layout.svelte +++ b/src/routes/ssoadmin/+layout.svelte @@ -1,8 +1,19 @@
@@ -11,19 +22,13 @@
diff --git a/src/routes/ssoadmin/oauth2/[uuid]/user/[user]/+page.server.ts b/src/routes/ssoadmin/oauth2/[uuid]/user/[user]/+page.server.ts index 8b92a70..80897bb 100644 --- a/src/routes/ssoadmin/oauth2/[uuid]/user/[user]/+page.server.ts +++ b/src/routes/ssoadmin/oauth2/[uuid]/user/[user]/+page.server.ts @@ -3,7 +3,7 @@ import { Changesets } from '$lib/server/changesets.js'; import type { OAuth2Client, User } from '$lib/server/drizzle'; import { OAuth2Clients } from '$lib/server/oauth2'; import { Users } from '$lib/server/users'; -import { UsersAdmin } from '$lib/server/users/admin'; +import { hasPrivileges } from '$lib/utils'; import { error, redirect } from '@sveltejs/kit'; interface PrivilegesRequest { @@ -12,15 +12,17 @@ interface PrivilegesRequest { export const actions = { 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'] ]); + const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']); + const { list: [details] } = await OAuth2Clients.getClientByAdminUser(currentUser as User, { clientId: uuid, - listAll: false, + listAll: fullPrivileges, omitSecret: false }); @@ -67,11 +69,13 @@ export const load = async ({ params: { uuid, user: userId }, 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, + listAll: fullPrivileges, omitSecret: false }); diff --git a/src/routes/ssoadmin/oauth2/invite/+server.ts b/src/routes/ssoadmin/oauth2/invite/+server.ts index 90d8740..5508f95 100644 --- a/src/routes/ssoadmin/oauth2/invite/+server.ts +++ b/src/routes/ssoadmin/oauth2/invite/+server.ts @@ -34,6 +34,5 @@ export const GET = async ({ locals, url }) => { await OAuth2Clients.addManager(client, inviter, currentUser); await UserTokens.remove(fetch); - console.log('?'); return ApiUtils.redirect(`/ssoadmin/oauth2/${client.client_id}`); }; diff --git a/src/routes/ssoadmin/oauth2/new/+page.server.ts b/src/routes/ssoadmin/oauth2/new/+page.server.ts index 4ed0620..ada19cb 100644 --- a/src/routes/ssoadmin/oauth2/new/+page.server.ts +++ b/src/routes/ssoadmin/oauth2/new/+page.server.ts @@ -1,7 +1,6 @@ import { AdminUtils } from '$lib/server/admin-utils'; import { Changesets } from '$lib/server/changesets.js'; import { OAuth2Clients } from '$lib/server/oauth2/index.js'; -import { UsersAdmin } from '$lib/server/users/admin'; import { fail, redirect } from '@sveltejs/kit'; interface CreateClientRequest { @@ -12,8 +11,8 @@ interface CreateClientRequest { export const actions = { default: async ({ locals, request }) => { - const { currentUser } = await UsersAdmin.getActionUser(locals, [ - ['admin:oauth2', 'self:oauth2'] + const { currentUser } = await AdminUtils.getActionUser(locals, [ + ['admin:oauth2', 'self:oauth2:create'] ]); const body = await request.formData(); @@ -42,7 +41,7 @@ export const actions = { export const load = async ({ parent }) => { const { user } = await parent(); - AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]); + AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2:create']]); return {}; }; diff --git a/src/routes/ssoadmin/users/[uuid]/+page.server.ts b/src/routes/ssoadmin/users/[uuid]/+page.server.ts index 8eb2859..09097c3 100644 --- a/src/routes/ssoadmin/users/[uuid]/+page.server.ts +++ b/src/routes/ssoadmin/users/[uuid]/+page.server.ts @@ -19,7 +19,7 @@ interface UpdateRequest { export const actions = { removeOtp: async () => {}, 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); if (!targetUser) { @@ -31,7 +31,7 @@ export const actions = { return { errors: [] }; }, 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); if (!targetUser || !!targetUser.activated) { @@ -57,7 +57,7 @@ export const actions = { return { errors: [] }; }, 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'; if (!type) { @@ -85,7 +85,7 @@ export const actions = { return { errors: [] }; }, update: async ({ locals, params: { uuid }, request }) => { - const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [ + const { currentUser, userSession } = await AdminUtils.getActionUser(locals, [ 'admin', 'admin:user' ]); diff --git a/src/routes/ssoadmin/users/[uuid]/+page.svelte b/src/routes/ssoadmin/users/[uuid]/+page.svelte index 5983270..a8ef680 100644 --- a/src/routes/ssoadmin/users/[uuid]/+page.svelte +++ b/src/routes/ssoadmin/users/[uuid]/+page.svelte @@ -11,6 +11,8 @@ import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte'; import FormErrors from '$lib/components/form/FormErrors.svelte'; 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 form: ActionData; @@ -30,13 +32,14 @@ {#if data.details.pictureId} -
- -
+ {$t('account.avatar.remove')} {/if}
+ {#if form?.errors && !form.errors.length} + {$t('admin.oauth2.success')} + {/if}
@@ -81,20 +84,15 @@

{$t('account.otp.title')}

{$t(`account.otp.${data.details.otpEnabled ? 'enabled' : 'unavailable'}`)}

{#if data.details.otpEnabled} - - -
+ {$t('admin.users.deactivateOtp')} {/if}

{$t('admin.users.actions')}

{#if data.details.activated} -
- -
+ {$t('admin.users.passwordEmail')} {:else} -
- -
+ {$t('admin.users.activationEmail')}
- {$t('admin.users.deleteInfoHint')}