register and activate
This commit is contained in:
parent
d47b68736d
commit
1cdd511ac9
19
package-lock.json
generated
19
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"jose": "^5.3.0",
|
"jose": "^5.3.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mysql2": "^3.9.7",
|
"mysql2": "^3.9.7",
|
||||||
|
"nodemailer": "^6.9.13",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"svelte-kit-cookie-session": "^4.0.0",
|
"svelte-kit-cookie-session": "^4.0.0",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
"@types/eslint": "^8.56.0",
|
"@types/eslint": "^8.56.0",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^20.12.12",
|
||||||
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
@ -1559,6 +1561,15 @@
|
|||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "6.4.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz",
|
||||||
|
"integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/pug": {
|
"node_modules/@types/pug": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
||||||
@ -4108,6 +4119,14 @@
|
|||||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.9.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz",
|
||||||
|
"integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@types/eslint": "^8.56.0",
|
"@types/eslint": "^8.56.0",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^20.12.12",
|
||||||
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
@ -44,6 +45,7 @@
|
|||||||
"jose": "^5.3.0",
|
"jose": "^5.3.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mysql2": "^3.9.7",
|
"mysql2": "^3.9.7",
|
||||||
|
"nodemailer": "^6.9.13",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"svelte-kit-cookie-session": "^4.0.0",
|
"svelte-kit-cookie-session": "^4.0.0",
|
||||||
|
@ -11,10 +11,12 @@
|
|||||||
--in-normalized-background: #000;
|
--in-normalized-background: #000;
|
||||||
--in-input-background: #fff;
|
--in-input-background: #fff;
|
||||||
--in-input-background-disabled: #c2c2c2;
|
--in-input-background-disabled: #c2c2c2;
|
||||||
|
--in-input-required-color: #ff0000;
|
||||||
--in-input-color: #000;
|
--in-input-color: #000;
|
||||||
--in-input-color-disabled: #414141;
|
--in-input-color-disabled: #414141;
|
||||||
--in-input-border-color: #ddd;
|
--in-input-border-color: #ddd;
|
||||||
--in-input-border-color-disabled: #a0a0a0;
|
--in-input-border-color-disabled: #a0a0a0;
|
||||||
|
--in-input-border-color-invalid: #ff0000;
|
||||||
|
|
||||||
--in-alert-color: #006597;
|
--in-alert-color: #006597;
|
||||||
--in-error-color: #b52e2e;
|
--in-error-color: #b52e2e;
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
|
|
||||||
export let type: 'button' | 'submit' = 'button';
|
export let type: 'button' | 'submit' = 'button';
|
||||||
export let variant: 'default' | 'primary' | 'link' = 'default';
|
export let variant: 'default' | 'primary' | 'link' = 'default';
|
||||||
|
export let disabled = false;
|
||||||
const dispath = createEventDispatcher();
|
const dispath = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button {type} class="btn btn-{variant}" on:click={(e) => dispath('click', e)}><slot /></button>
|
<button {type} class="btn btn-{variant}" on:click={(e) => dispath('click', e)} {disabled}
|
||||||
|
><slot /></button
|
||||||
|
>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.btn {
|
.btn {
|
||||||
|
@ -26,6 +26,10 @@
|
|||||||
color: var(--in-input-color-disabled);
|
color: var(--in-input-color-disabled);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:user-invalid {
|
||||||
|
border-color: var(--in-input-border-color-invalid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,4 +48,11 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-control > :global(label):has(+ input[required])::after {
|
||||||
|
content: '*';
|
||||||
|
color: var(--in-input-required-color);
|
||||||
|
margin-left: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
|
||||||
export let title: string = '';
|
export let title: string = '';
|
||||||
|
export let required = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
{#if title}<div class="form-subtitle">{title}</div>{/if}
|
{#if title}<div class="form-subtitle">{title}</div>{/if}
|
||||||
|
{#if required}<span class="form-required">{$t('common.required')}</span>{/if}
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -20,4 +24,15 @@
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-required {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '*';
|
||||||
|
color: var(--in-input-required-color);
|
||||||
|
margin-right: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
{
|
{
|
||||||
"title": "Manage your account",
|
"title": "Manage your account",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
"usernameHint": "Only the English alphabet, numbers and _-. are allowed.",
|
||||||
"displayName": "Display Name",
|
"displayName": "Display Name",
|
||||||
|
"displayNameHint": "Must be between 3 and 32 characters.",
|
||||||
"changeEmail": "Change email address",
|
"changeEmail": "Change email address",
|
||||||
"currentEmail": "Current email address",
|
"currentEmail": "Current email address",
|
||||||
|
"email": "Email address",
|
||||||
"newEmail": "New email address",
|
"newEmail": "New email address",
|
||||||
|
"password": "Password",
|
||||||
|
"repeatPassword": "Repeat password",
|
||||||
"changePassword": "Change password",
|
"changePassword": "Change password",
|
||||||
"currentPassword": "Current password",
|
"currentPassword": "Current password",
|
||||||
"newPassword": "New password",
|
"newPassword": "New password",
|
||||||
"repeatPassword": "Repeat new password",
|
"repeatNewPassword": "Repeat new password",
|
||||||
"passwordHint": "At least 8 characters, a capital letter and a number required.",
|
"passwordHint": "At least 8 characters, a capital letter and a number required.",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"changeSuccess": "Account settings changed successfully!",
|
"changeSuccess": "Account settings changed successfully!",
|
||||||
|
"activateSuccess": "Your account has been activated successfully! You may now log in.",
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"title": "Profile avatar",
|
"title": "Profile avatar",
|
||||||
"change": "Change avatar",
|
"change": "Change avatar",
|
||||||
@ -30,16 +36,27 @@
|
|||||||
"otp": "Two-factor authentication is enabled",
|
"otp": "Two-factor authentication is enabled",
|
||||||
"otpCode": "Enter the code displayed on the authenticator app"
|
"otpCode": "Enter the code displayed on the authenticator app"
|
||||||
},
|
},
|
||||||
|
"register": {
|
||||||
|
"title": "Create a new account",
|
||||||
|
"disabled": "Registrations are currently disabled by the administrator! Sorry.",
|
||||||
|
"userCreated": "User account has been created successfully! You may now log in.",
|
||||||
|
"emailSent": "User account has been created successfully, and an activation email has been sent to your email address.",
|
||||||
|
"submit": "Create account"
|
||||||
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidLogin": "Invalid email or password!",
|
"invalidLogin": "Invalid email or password!",
|
||||||
"invalidRequest": "Invalid request! Please try again.",
|
"invalidRequest": "Invalid request! Please try again.",
|
||||||
"emailRequired": "Email address is required.",
|
"emailRequired": "Email address is required.",
|
||||||
|
"invalidUsername": "The username entered is invalid or not allowed",
|
||||||
"invalidEmail": "The email address is invalid.",
|
"invalidEmail": "The email address is invalid.",
|
||||||
"passwordRequired": "The password is required.",
|
"passwordRequired": "The password is required.",
|
||||||
"passwordMismatch": "The passwords do not match!",
|
"passwordMismatch": "The passwords do not match!",
|
||||||
"invalidPassword": "The provided password is invalid.",
|
"invalidPassword": "The provided password is invalid.",
|
||||||
"invalidDisplayName": "The provided display name is invalid.",
|
"invalidDisplayName": "The provided display name is invalid.",
|
||||||
"otpFailed": "The code you entered was invalid. Please note that you will be given a new QR code for subsequent retries."
|
"otpFailed": "The code you entered was invalid. Please note that you will be given a new QR code for subsequent retries.",
|
||||||
|
"required": "Please fill in the required fields",
|
||||||
|
"existingRegistration": "The username or email address is already in use.",
|
||||||
|
"activationFailed": "Your account could not be activated - the URL might have been used or expired already."
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
"title": "Two-factor authentication",
|
"title": "Two-factor authentication",
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"siteName": "Icy Network",
|
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
||||||
"description": "Icy Network is a Single-Sign-On service used by other applications.",
|
|
||||||
"cookieDisclaimer": "The website may use temporary cookies for storing your login session and ensuring your security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/icynet-auth-server\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
"cookieDisclaimer": "The website may use temporary cookies for storing your login session and ensuring your security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/icynet-auth-server\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
"back": "Go back",
|
"back": "Go back",
|
||||||
"home": "Home page"
|
"home": "Home page",
|
||||||
|
"required": "Required fields"
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,7 @@
|
|||||||
"email": "Your email address",
|
"email": "Your email address",
|
||||||
"picture": "Your profile picture",
|
"picture": "Your profile picture",
|
||||||
"account": "Changing your password or other account settings",
|
"account": "Changing your password or other account settings",
|
||||||
"management": "Manage Icy Network on your behalf",
|
"management": "Commit administrative actions to the extent of your user privileges"
|
||||||
"admin": "Commit administrative actions to the extent of your user privileges"
|
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import i18n from 'sveltekit-i18n';
|
import i18n, { type Config } from 'sveltekit-i18n';
|
||||||
|
|
||||||
const config = {
|
interface Params {
|
||||||
|
siteName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Config<Params> = {
|
||||||
loaders: [
|
loaders: [
|
||||||
{
|
{
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
@ -1,129 +0,0 @@
|
|||||||
import { relations } from 'drizzle-orm/relations';
|
|
||||||
import {
|
|
||||||
user,
|
|
||||||
auditLog,
|
|
||||||
document,
|
|
||||||
oauth2Client,
|
|
||||||
upload,
|
|
||||||
oauth2ClientAuthorization,
|
|
||||||
oauth2ClientUrl,
|
|
||||||
oauth2Token,
|
|
||||||
userPrivilegesPrivilege,
|
|
||||||
privilege,
|
|
||||||
userToken
|
|
||||||
} from '../schema';
|
|
||||||
|
|
||||||
export const auditLogRelations = relations(auditLog, ({ one }) => ({
|
|
||||||
user: one(user, {
|
|
||||||
fields: [auditLog.actorId],
|
|
||||||
references: [user.id]
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const userRelations = relations(user, ({ one, many }) => ({
|
|
||||||
audit_logs: many(auditLog),
|
|
||||||
documents: many(document),
|
|
||||||
o_auth2_clients: many(oauth2Client),
|
|
||||||
o_auth2_client_authorizations: many(oauth2ClientAuthorization),
|
|
||||||
o_auth2_tokens: many(oauth2Token),
|
|
||||||
uploads: many(upload, {
|
|
||||||
relationName: 'upload_uploaderId_user_id'
|
|
||||||
}),
|
|
||||||
upload: one(upload, {
|
|
||||||
fields: [user.pictureId],
|
|
||||||
references: [upload.id],
|
|
||||||
relationName: 'user_pictureId_upload_id'
|
|
||||||
}),
|
|
||||||
user_privileges_privileges: many(userPrivilegesPrivilege),
|
|
||||||
user_tokens: many(userToken)
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const documentRelations = relations(document, ({ one }) => ({
|
|
||||||
user: one(user, {
|
|
||||||
fields: [document.authorId],
|
|
||||||
references: [user.id]
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const oauth2ClientRelations = relations(oauth2Client, ({ one, many }) => ({
|
|
||||||
user: one(user, {
|
|
||||||
fields: [oauth2Client.ownerId],
|
|
||||||
references: [user.id]
|
|
||||||
}),
|
|
||||||
upload: one(upload, {
|
|
||||||
fields: [oauth2Client.pictureId],
|
|
||||||
references: [upload.id]
|
|
||||||
}),
|
|
||||||
o_auth2_client_authorizations: many(oauth2ClientAuthorization),
|
|
||||||
o_auth2_client_urls: many(oauth2ClientUrl),
|
|
||||||
o_auth2_tokens: many(oauth2Token)
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const uploadRelations = relations(upload, ({ one, many }) => ({
|
|
||||||
o_auth2_clients: many(oauth2Client),
|
|
||||||
user: one(user, {
|
|
||||||
fields: [upload.uploaderId],
|
|
||||||
references: [user.id],
|
|
||||||
relationName: 'upload_uploaderId_user_id'
|
|
||||||
}),
|
|
||||||
users: many(user, {
|
|
||||||
relationName: 'user_pictureId_upload_id'
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const oauth2ClientAuthorizationRelations = relations(
|
|
||||||
oauth2ClientAuthorization,
|
|
||||||
({ one }) => ({
|
|
||||||
user: one(user, {
|
|
||||||
fields: [oauth2ClientAuthorization.userId],
|
|
||||||
references: [user.id]
|
|
||||||
}),
|
|
||||||
o_auth2_client: one(oauth2Client, {
|
|
||||||
fields: [oauth2ClientAuthorization.clientId],
|
|
||||||
references: [oauth2Client.id]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const oauth2ClientUrlRelations = relations(oauth2ClientUrl, ({ one }) => ({
|
|
||||||
o_auth2_client: one(oauth2Client, {
|
|
||||||
fields: [oauth2ClientUrl.clientId],
|
|
||||||
references: [oauth2Client.id]
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const oauth2TokenRelations = relations(oauth2Token, ({ one }) => ({
|
|
||||||
o_auth2_client: one(oauth2Client, {
|
|
||||||
fields: [oauth2Token.clientId],
|
|
||||||
references: [oauth2Client.id]
|
|
||||||
}),
|
|
||||||
user: one(user, {
|
|
||||||
fields: [oauth2Token.userId],
|
|
||||||
references: [user.id]
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const userPrivilegesPrivilegeRelations = relations(
|
|
||||||
userPrivilegesPrivilege,
|
|
||||||
({ one }) => ({
|
|
||||||
user: one(user, {
|
|
||||||
fields: [userPrivilegesPrivilege.userId],
|
|
||||||
references: [user.id]
|
|
||||||
}),
|
|
||||||
privilege: one(privilege, {
|
|
||||||
fields: [userPrivilegesPrivilege.privilegeId],
|
|
||||||
references: [privilege.id]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const privilegeRelations = relations(privilege, ({ many }) => ({
|
|
||||||
user_privileges_privileges: many(userPrivilegesPrivilege)
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const userTokenRelations = relations(userToken, ({ one }) => ({
|
|
||||||
user: one(user, {
|
|
||||||
fields: [userToken.userId],
|
|
||||||
references: [user.id]
|
|
||||||
})
|
|
||||||
}));
|
|
@ -1,4 +1,4 @@
|
|||||||
import { sql } from 'drizzle-orm';
|
import { relations, sql } from 'drizzle-orm';
|
||||||
import {
|
import {
|
||||||
mysqlTable,
|
mysqlTable,
|
||||||
int,
|
int,
|
||||||
@ -225,3 +225,117 @@ export const userToken = mysqlTable('user_token', {
|
|||||||
.default(sql`current_timestamp(6)`)
|
.default(sql`current_timestamp(6)`)
|
||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type UserToken = typeof userToken.$inferSelect;
|
||||||
|
|
||||||
|
export const auditLogRelations = relations(auditLog, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [auditLog.actorId],
|
||||||
|
references: [user.id]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const userRelations = relations(user, ({ one, many }) => ({
|
||||||
|
audit_logs: many(auditLog),
|
||||||
|
documents: many(document),
|
||||||
|
o_auth2_clients: many(oauth2Client),
|
||||||
|
o_auth2_client_authorizations: many(oauth2ClientAuthorization),
|
||||||
|
o_auth2_tokens: many(oauth2Token),
|
||||||
|
uploads: many(upload, {
|
||||||
|
relationName: 'upload_uploaderId_user_id'
|
||||||
|
}),
|
||||||
|
upload: one(upload, {
|
||||||
|
fields: [user.pictureId],
|
||||||
|
references: [upload.id],
|
||||||
|
relationName: 'user_pictureId_upload_id'
|
||||||
|
}),
|
||||||
|
user_privileges_privileges: many(userPrivilegesPrivilege),
|
||||||
|
user_tokens: many(userToken)
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const documentRelations = relations(document, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [document.authorId],
|
||||||
|
references: [user.id]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const oauth2ClientRelations = relations(oauth2Client, ({ one, many }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [oauth2Client.ownerId],
|
||||||
|
references: [user.id]
|
||||||
|
}),
|
||||||
|
upload: one(upload, {
|
||||||
|
fields: [oauth2Client.pictureId],
|
||||||
|
references: [upload.id]
|
||||||
|
}),
|
||||||
|
o_auth2_client_authorizations: many(oauth2ClientAuthorization),
|
||||||
|
o_auth2_client_urls: many(oauth2ClientUrl),
|
||||||
|
o_auth2_tokens: many(oauth2Token)
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const uploadRelations = relations(upload, ({ one, many }) => ({
|
||||||
|
o_auth2_clients: many(oauth2Client),
|
||||||
|
user: one(user, {
|
||||||
|
fields: [upload.uploaderId],
|
||||||
|
references: [user.id],
|
||||||
|
relationName: 'upload_uploaderId_user_id'
|
||||||
|
}),
|
||||||
|
users: many(user, {
|
||||||
|
relationName: 'user_pictureId_upload_id'
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const oauth2ClientAuthorizationRelations = relations(
|
||||||
|
oauth2ClientAuthorization,
|
||||||
|
({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [oauth2ClientAuthorization.userId],
|
||||||
|
references: [user.id]
|
||||||
|
}),
|
||||||
|
o_auth2_client: one(oauth2Client, {
|
||||||
|
fields: [oauth2ClientAuthorization.clientId],
|
||||||
|
references: [oauth2Client.id]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const oauth2ClientUrlRelations = relations(oauth2ClientUrl, ({ one }) => ({
|
||||||
|
o_auth2_client: one(oauth2Client, {
|
||||||
|
fields: [oauth2ClientUrl.clientId],
|
||||||
|
references: [oauth2Client.id]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const oauth2TokenRelations = relations(oauth2Token, ({ one }) => ({
|
||||||
|
o_auth2_client: one(oauth2Client, {
|
||||||
|
fields: [oauth2Token.clientId],
|
||||||
|
references: [oauth2Client.id]
|
||||||
|
}),
|
||||||
|
user: one(user, {
|
||||||
|
fields: [oauth2Token.userId],
|
||||||
|
references: [user.id]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const userPrivilegesPrivilegeRelations = relations(userPrivilegesPrivilege, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [userPrivilegesPrivilege.userId],
|
||||||
|
references: [user.id]
|
||||||
|
}),
|
||||||
|
privilege: one(privilege, {
|
||||||
|
fields: [userPrivilegesPrivilege.privilegeId],
|
||||||
|
references: [privilege.id]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const privilegeRelations = relations(privilege, ({ many }) => ({
|
||||||
|
user_privileges_privileges: many(userPrivilegesPrivilege)
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const userTokenRelations = relations(userToken, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [userToken.userId],
|
||||||
|
references: [user.id]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
64
src/lib/server/email/index.ts
Normal file
64
src/lib/server/email/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
EMAIL_ENABLED,
|
||||||
|
EMAIL_FROM,
|
||||||
|
EMAIL_SMTP_HOST,
|
||||||
|
EMAIL_SMTP_PASS,
|
||||||
|
EMAIL_SMTP_PORT,
|
||||||
|
EMAIL_SMTP_SECURE,
|
||||||
|
EMAIL_SMTP_USER
|
||||||
|
} from '$env/static/private';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import type { EmailTemplate } from './template.interface';
|
||||||
|
|
||||||
|
export class Emails {
|
||||||
|
public transport?: nodemailer.Transporter;
|
||||||
|
public static from = EMAIL_FROM;
|
||||||
|
|
||||||
|
static sender: Emails;
|
||||||
|
|
||||||
|
static getSender() {
|
||||||
|
if (!Emails.sender) {
|
||||||
|
Emails.sender = new Emails();
|
||||||
|
}
|
||||||
|
return Emails.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.createTransport();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async send(
|
||||||
|
to: string,
|
||||||
|
subject: string,
|
||||||
|
text: string,
|
||||||
|
html?: string,
|
||||||
|
from = Emails.from
|
||||||
|
): Promise<unknown> {
|
||||||
|
return this.transport?.sendMail({
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
html,
|
||||||
|
from
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendTemplate(to: string, subject: string, message: EmailTemplate): Promise<unknown> {
|
||||||
|
return this.send(to, subject, message.text, message.html);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createTransport() {
|
||||||
|
if (EMAIL_ENABLED !== 'true') return;
|
||||||
|
this.transport = nodemailer.createTransport({
|
||||||
|
host: EMAIL_SMTP_HOST,
|
||||||
|
port: Number(EMAIL_SMTP_PORT) || undefined,
|
||||||
|
secure: EMAIL_SMTP_SECURE === 'true',
|
||||||
|
auth: {
|
||||||
|
user: EMAIL_SMTP_USER,
|
||||||
|
pass: EMAIL_SMTP_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './templates';
|
4
src/lib/server/email/template.interface.ts
Normal file
4
src/lib/server/email/template.interface.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface EmailTemplate {
|
||||||
|
text: string;
|
||||||
|
html: string;
|
||||||
|
}
|
27
src/lib/server/email/templates/forgot-password.email.ts
Normal file
27
src/lib/server/email/templates/forgot-password.email.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
import type { EmailTemplate } from '../template.interface';
|
||||||
|
|
||||||
|
export const ForgotPasswordEmail = (username: string, url: string): EmailTemplate => ({
|
||||||
|
text: `
|
||||||
|
${PUBLIC_SITE_NAME}
|
||||||
|
|
||||||
|
Hello, ${username}! You have requested a password reset on ${PUBLIC_SITE_NAME}.
|
||||||
|
|
||||||
|
In order to change your password, please click on the following link.
|
||||||
|
|
||||||
|
Change your password: ${url}
|
||||||
|
|
||||||
|
If you did not request a password change on ${PUBLIC_SITE_NAME}, you can safely ignore this email.
|
||||||
|
`,
|
||||||
|
html: /* html */ `
|
||||||
|
<h1>${PUBLIC_SITE_NAME}</h1>
|
||||||
|
|
||||||
|
<p><strong>Hello, ${username}! You have requested a password reset on ${PUBLIC_SITE_NAME}.</strong></p>
|
||||||
|
|
||||||
|
<p>In order to change your password, please click on the following link.</p>
|
||||||
|
|
||||||
|
<p>Change your password: <a href="${url}" target="_blank">${url}</a></p>
|
||||||
|
|
||||||
|
<p>If you did not request a password change on ${PUBLIC_SITE_NAME}, you can safely ignore this email.</p>
|
||||||
|
`
|
||||||
|
});
|
3
src/lib/server/email/templates/index.ts
Normal file
3
src/lib/server/email/templates/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './forgot-password.email';
|
||||||
|
export * from './invitation.email';
|
||||||
|
export * from './registration.email';
|
23
src/lib/server/email/templates/invitation.email.ts
Normal file
23
src/lib/server/email/templates/invitation.email.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
import type { EmailTemplate } from '../template.interface';
|
||||||
|
|
||||||
|
export const InvitationEmail = (url: string): EmailTemplate => ({
|
||||||
|
text: `
|
||||||
|
${PUBLIC_SITE_NAME}
|
||||||
|
|
||||||
|
Please click on the following link to create an account on ${PUBLIC_SITE_NAME}.
|
||||||
|
|
||||||
|
Create your account here: ${url}
|
||||||
|
|
||||||
|
This email was sent to you because you have requested an account on ${PUBLIC_SITE_NAME}. If you did not request this, you may safely ignore this email.
|
||||||
|
`,
|
||||||
|
html: /* html */ `
|
||||||
|
<h1>${PUBLIC_SITE_NAME}</h1>
|
||||||
|
|
||||||
|
<p><b>Please click on the following link to create an account on ${PUBLIC_SITE_NAME}.</b></p>
|
||||||
|
|
||||||
|
<p>Create your account here: <a href="${url}" target="_blank">${url}</a></p>
|
||||||
|
|
||||||
|
<p>This email was sent to you because you have requested an account on ${PUBLIC_SITE_NAME}. If you did not request this, you may safely ignore this email.</p>
|
||||||
|
`
|
||||||
|
});
|
27
src/lib/server/email/templates/registration.email.ts
Normal file
27
src/lib/server/email/templates/registration.email.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
import type { EmailTemplate } from '../template.interface';
|
||||||
|
|
||||||
|
export const RegistrationEmail = (username: string, url: string): EmailTemplate => ({
|
||||||
|
text: `
|
||||||
|
${PUBLIC_SITE_NAME}
|
||||||
|
|
||||||
|
Welcome to ${PUBLIC_SITE_NAME}, ${username}!
|
||||||
|
|
||||||
|
In order to proceed with logging in, please click on the following link to activate your account.
|
||||||
|
|
||||||
|
Activate your account: ${url}
|
||||||
|
|
||||||
|
This email was sent to you because you have created an account on ${PUBLIC_SITE_NAME}. If you did not create an account, you may contact us or just let the account expire.
|
||||||
|
`,
|
||||||
|
html: /* html */ `
|
||||||
|
<h1>${PUBLIC_SITE_NAME}</h1>
|
||||||
|
|
||||||
|
<p><strong>Welcome to ${PUBLIC_SITE_NAME}, ${username}!</strong></p>
|
||||||
|
|
||||||
|
<p>In order to proceed with logging in, please click on the following link to activate your account.</p>
|
||||||
|
|
||||||
|
<p>Activate your account: <a href="${url}" target="_blank">${url}</a></p>
|
||||||
|
|
||||||
|
<p>This email was sent to you because you have created an account on ${PUBLIC_SITE_NAME}. If you did not create an account, you may contact us or just let the account expire.</p>
|
||||||
|
`
|
||||||
|
});
|
@ -124,7 +124,7 @@ export class OAuth2Clients {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (scope.includes('management')) {
|
if (scope.includes('management')) {
|
||||||
allowedScopes.push('management', 'admin');
|
allowedScopes.push('management');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: client picture
|
// TODO: client picture
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { and, eq, or } from 'drizzle-orm';
|
import { and, eq, gt, or, sql } from 'drizzle-orm';
|
||||||
import { db, user, type User } from '../drizzle';
|
import { db, user, userToken, type User } from '../drizzle';
|
||||||
import type { UserSession } from './types';
|
import type { UserSession } from './types';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { CryptoUtils } from '../crypto-utils';
|
||||||
|
import { EMAIL_ENABLED } from '$env/static/private';
|
||||||
|
import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email';
|
||||||
|
import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public';
|
||||||
|
import { UserTokens } from './tokens';
|
||||||
|
|
||||||
export class Users {
|
export class Users {
|
||||||
static async getById(id: number): Promise<User | undefined> {
|
static async getById(id: number): Promise<User | undefined> {
|
||||||
@ -71,6 +76,172 @@ export class Users {
|
|||||||
}
|
}
|
||||||
return currentUser;
|
return currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async checkRegistration(username: string, email: string) {
|
||||||
|
return !(
|
||||||
|
await db
|
||||||
|
.select({ id: user.id })
|
||||||
|
.from(user)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
sql`lower(${user.username}) = ${username.toLowerCase()}`,
|
||||||
|
sql`lower(${user.email}) = ${email.toLowerCase()}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)?.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getActivationToken(token: string) {
|
||||||
|
const [returnedToken] = await db
|
||||||
|
.select({ id: userToken.id, token: userToken.token, userId: userToken.userId })
|
||||||
|
.from(userToken)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userToken.token, token),
|
||||||
|
eq(userToken.type, 'activation'),
|
||||||
|
gt(userToken.expires_at, new Date())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
if (!returnedToken?.userId) return undefined;
|
||||||
|
|
||||||
|
const [userInfo] = await db
|
||||||
|
.select()
|
||||||
|
.from(user)
|
||||||
|
.where(eq(user.id, returnedToken.userId as number));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...returnedToken,
|
||||||
|
user: userInfo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async activateUserBy(token: string, subject: User) {
|
||||||
|
await db
|
||||||
|
.update(user)
|
||||||
|
.set({ activated: 1, activity_at: new Date() })
|
||||||
|
.where(eq(user.id, subject.id));
|
||||||
|
await UserTokens.remove(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getPasswordToken(token: string) {
|
||||||
|
return db.query.userToken.findFirst({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
token: true
|
||||||
|
},
|
||||||
|
where: and(
|
||||||
|
eq(userToken.token, token),
|
||||||
|
eq(userToken.type, 'password'),
|
||||||
|
gt(userToken.expires_at, new Date())
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
user: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async register({
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
activate = false
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
activate?: boolean;
|
||||||
|
}) {
|
||||||
|
const passwordHash = await Users.hashPassword(password);
|
||||||
|
const [retval] = await db.insert(user).values({
|
||||||
|
uuid: CryptoUtils.createUUID(),
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password: passwordHash,
|
||||||
|
display_name: displayName,
|
||||||
|
activated: EMAIL_ENABLED === 'false' ? 1 : Number(activate),
|
||||||
|
activity_at: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
const [newUser] = await db.select().from(user).where(eq(user.id, retval.insertId));
|
||||||
|
|
||||||
|
if (EMAIL_ENABLED !== 'false' && !activate) {
|
||||||
|
await Users.sendRegistrationEmail(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: audit log
|
||||||
|
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendRegistrationEmail(user: User) {
|
||||||
|
const token = await UserTokens.create(
|
||||||
|
'activation',
|
||||||
|
new Date(Date.now() + 3600 * 1000),
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ activate: token.token });
|
||||||
|
const content = RegistrationEmail(user.username, `${PUBLIC_URL}/login?${params.toString()}`);
|
||||||
|
|
||||||
|
// TODO: logging
|
||||||
|
try {
|
||||||
|
await Emails.getSender().sendTemplate(
|
||||||
|
user.email,
|
||||||
|
`Activate your account on ${PUBLIC_SITE_NAME}`,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
await UserTokens.remove(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
const content = ForgotPasswordEmail(
|
||||||
|
user.username,
|
||||||
|
`${PUBLIC_URL}/login/password?${params.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: logging
|
||||||
|
try {
|
||||||
|
await Emails.getSender().sendTemplate(
|
||||||
|
user.email,
|
||||||
|
`Reset your password on ${PUBLIC_SITE_NAME}`,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
await UserTokens.remove(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendInvitationEmail(email: string) {
|
||||||
|
const token = await UserTokens.create(
|
||||||
|
'login',
|
||||||
|
new Date(Date.now() + 3600 * 1000),
|
||||||
|
undefined,
|
||||||
|
email
|
||||||
|
);
|
||||||
|
const params = new URLSearchParams({ token: token.token });
|
||||||
|
const content = InvitationEmail(`${PUBLIC_URL}/register?${params.toString()}`);
|
||||||
|
|
||||||
|
// TODO: logging
|
||||||
|
try {
|
||||||
|
await Emails.getSender().sendTemplate(
|
||||||
|
email,
|
||||||
|
`You have been invited to create an account on ${PUBLIC_SITE_NAME}`,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
await UserTokens.remove(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './totp';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export * from './tokens';
|
||||||
|
28
src/lib/server/users/tokens.ts
Normal file
28
src/lib/server/users/tokens.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { CryptoUtils } from '../crypto-utils';
|
||||||
|
import { db, userToken, type UserToken } from '../drizzle';
|
||||||
|
|
||||||
|
export class UserTokens {
|
||||||
|
static async create(
|
||||||
|
type: (typeof userToken.$inferInsert)['type'],
|
||||||
|
expires: Date,
|
||||||
|
userId?: number,
|
||||||
|
nonce?: string
|
||||||
|
) {
|
||||||
|
const token = CryptoUtils.generateString(64);
|
||||||
|
const obj = <typeof userToken.$inferInsert>{
|
||||||
|
type,
|
||||||
|
token,
|
||||||
|
userId,
|
||||||
|
expires_at: expires,
|
||||||
|
nonce
|
||||||
|
};
|
||||||
|
const [retval] = await db.insert(userToken).values(obj);
|
||||||
|
return { id: retval.insertId, ...obj } as UserToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async remove(token: string | { token: string }) {
|
||||||
|
const removeBy = typeof token === 'string' ? token : token.token;
|
||||||
|
await db.delete(userToken).where(eq(userToken.token, removeBy));
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { authenticator as totp } from 'otplib';
|
import { authenticator as totp } from 'otplib';
|
||||||
import { db, userToken, type User } from '../drizzle';
|
import { db, userToken, type User } from '../drizzle';
|
||||||
import { and, eq, gt, isNull, or } from 'drizzle-orm';
|
import { and, eq, gt, isNull, or } from 'drizzle-orm';
|
||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
|
||||||
totp.options = {
|
totp.options = {
|
||||||
window: 2
|
window: 2
|
||||||
@ -12,7 +13,7 @@ export class TimeOTP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static getUri(secret: string, username: string): string {
|
public static getUri(secret: string, username: string): string {
|
||||||
return totp.keyuri(username, 'Icy Network', secret);
|
return totp.keyuri(username, PUBLIC_SITE_NAME, secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createSecret(): string {
|
public static createSecret(): string {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
||||||
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
|
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
@ -51,8 +52,12 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t('account.title')} - {PUBLIC_SITE_NAME}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<h1>{$t('common.siteName')}</h1>
|
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||||
<SplitView>
|
<SplitView>
|
||||||
<div>
|
<div>
|
||||||
<h2>{$t('account.title')}</h2>
|
<h2>{$t('account.title')}</h2>
|
||||||
@ -93,6 +98,7 @@
|
|||||||
<label for="form-displayName">{$t('account.displayName')}</label>
|
<label for="form-displayName">{$t('account.displayName')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="nickname"
|
||||||
name="displayName"
|
name="displayName"
|
||||||
value={form?.displayName || data.user.name}
|
value={form?.displayName || data.user.name}
|
||||||
id="form-displayName"
|
id="form-displayName"
|
||||||
@ -103,12 +109,17 @@
|
|||||||
<FormSection title={$t('account.changeEmail')}>
|
<FormSection title={$t('account.changeEmail')}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="form-currentEmail">{$t('account.currentEmail')}</label>
|
<label for="form-currentEmail">{$t('account.currentEmail')}</label>
|
||||||
<input type="email" name="currentEmail" id="form-currentEmail" />
|
<input
|
||||||
|
type="email"
|
||||||
|
name="currentEmail"
|
||||||
|
id="form-currentEmail"
|
||||||
|
autocomplete="email"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="form-newEmail">{$t('account.newEmail')}</label>
|
<label for="form-newEmail">{$t('account.newEmail')}</label>
|
||||||
<input type="email" name="newEmail" id="form-newEmail" />
|
<input type="email" name="newEmail" id="form-newEmail" autocomplete="email" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
@ -136,7 +147,7 @@
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="form-repeatPassword">{$t('account.repeatPassword')}</label>
|
<label for="form-repeatPassword">{$t('account.repeatNewPassword')}</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<h1>{$t('common.siteName')}</h1>
|
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||||
<h2>{$t('account.otp.title')}</h2>
|
<h2>{$t('account.otp.title')}</h2>
|
||||||
|
|
||||||
{#if form?.success}
|
{#if form?.success}
|
||||||
|
@ -93,3 +93,32 @@ export const actions = {
|
|||||||
return redirect(303, redirectUrl);
|
return redirect(303, redirectUrl);
|
||||||
}
|
}
|
||||||
} as Actions;
|
} as Actions;
|
||||||
|
|
||||||
|
export const load = async ({ locals, url }) => {
|
||||||
|
if (locals.session.data?.user) {
|
||||||
|
return redirect(301, url.searchParams.get('redirectTo') || '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activation routine
|
||||||
|
if (url.searchParams.has('activate')) {
|
||||||
|
const activationInfo = await Users.getActivationToken(
|
||||||
|
url.searchParams.get('activate') as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!activationInfo?.user) {
|
||||||
|
return {
|
||||||
|
activated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Users.activateUserBy(activationInfo.token, activationInfo.user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activated: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activated: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import type { ActionData, PageData } from './$types';
|
||||||
import Alert from '$lib/components/Alert.svelte';
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import SideContainer from '$lib/components/SideContainer.svelte';
|
import SideContainer from '$lib/components/SideContainer.svelte';
|
||||||
@ -7,19 +9,30 @@
|
|||||||
import FormSection from '$lib/components/form/FormSection.svelte';
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import type { ActionData } from './$types';
|
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t('account.login.title')} - {PUBLIC_SITE_NAME}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<SideContainer>
|
<SideContainer>
|
||||||
<h1>{$t('common.siteName')}</h1>
|
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||||
|
|
||||||
<h2>{$t('account.login.title')}</h2>
|
<h2>{$t('account.login.title')}</h2>
|
||||||
|
|
||||||
<form action="" method="POST" use:enhance>
|
<form action="" method="POST" use:enhance>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
|
{#if data?.activated === false && !form}<Alert type="error"
|
||||||
|
>{$t('account.errors.activationFailed')}</Alert
|
||||||
|
>{/if}
|
||||||
|
{#if data?.activated === true && !form}<Alert type="success"
|
||||||
|
>{$t('account.activateSuccess')}</Alert
|
||||||
|
>{/if}
|
||||||
{#if form?.incorrect}<Alert type="error">{$t('account.errors.invalidLogin')}</Alert>{/if}
|
{#if form?.incorrect}<Alert type="error">{$t('account.errors.invalidLogin')}</Alert>{/if}
|
||||||
|
|
||||||
{#if form?.otpRequired}
|
{#if form?.otpRequired}
|
||||||
<!-- Two-factor code request -->
|
<!-- Two-factor code request -->
|
||||||
<FormSection title={$t('account.login.otp')}>
|
<FormSection title={$t('account.login.otp')}>
|
||||||
@ -49,11 +62,12 @@
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
{/if}
|
{/if}
|
||||||
<Button type="submit" variant="primary">{$t('account.login.submit')}</Button>
|
<Button type="submit" variant="primary">{$t('account.login.submit')}</Button>
|
||||||
|
<div><a href="/register">{$t('account.register.title')}</a></div>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<p class="text-bold">{$t('common.description')}</p>
|
<p class="text-bold">{$t('common.description', { siteName: PUBLIC_SITE_NAME })}</p>
|
||||||
<p>{@html $t('common.cookieDisclaimer')}</p>
|
<p>{@html $t('common.cookieDisclaimer')}</p>
|
||||||
</div>
|
</div>
|
||||||
</SideContainer>
|
</SideContainer>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { assets } from '$app/paths';
|
import { assets } from '$app/paths';
|
||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
import Alert from '$lib/components/Alert.svelte';
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import ColumnView from '$lib/components/ColumnView.svelte';
|
import ColumnView from '$lib/components/ColumnView.svelte';
|
||||||
@ -11,9 +12,13 @@
|
|||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t('oauth2.authorize.title')} "{data.client?.title || ''}" - {PUBLIC_SITE_NAME}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
{#if data.error}
|
{#if data.error}
|
||||||
<h1>{$t('common.siteName')}</h1>
|
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||||
<ColumnView>
|
<ColumnView>
|
||||||
<Alert type="error"
|
<Alert type="error"
|
||||||
>{$t('oauth2.authorize.errorPage')}<br /><br /><code
|
>{$t('oauth2.authorize.errorPage')}<br /><br /><code
|
||||||
@ -25,7 +30,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if data.client}
|
{#if data.client}
|
||||||
<h1 class="title">{$t('common.siteName')}</h1>
|
<h1 class="title">{PUBLIC_SITE_NAME}</h1>
|
||||||
<h2 class="title">{$t('oauth2.authorize.title')}</h2>
|
<h2 class="title">{$t('oauth2.authorize.title')}</h2>
|
||||||
|
|
||||||
<div class="user-client-wrapper">
|
<div class="user-client-wrapper">
|
||||||
|
107
src/routes/register/+page.server.ts
Normal file
107
src/routes/register/+page.server.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { REGISTRATIONS } from '$env/static/private';
|
||||||
|
import { Changesets } from '$lib/server/changesets.js';
|
||||||
|
import { Users } from '$lib/server/users/index.js';
|
||||||
|
import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js';
|
||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
interface RegisterData {
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields: (keyof RegisterData)[] = ['username', 'displayName', 'email', 'password'];
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ request, locals }) => {
|
||||||
|
// Logged in users cannot make more accounts
|
||||||
|
if (locals.session.data?.user || REGISTRATIONS === 'false') {
|
||||||
|
return redirect(303, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.formData();
|
||||||
|
const changes = Changesets.take<RegisterData>(fields, body);
|
||||||
|
const { username, displayName, email, password } = changes;
|
||||||
|
// Each field must be present
|
||||||
|
if (!username || !displayName || !email || !password) {
|
||||||
|
return fail(400, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
errors: ['required'],
|
||||||
|
fields: fields.reduce<string[]>(
|
||||||
|
(missing, field) => (!changes[field] ? [...missing, field] : missing),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usernameRegex.test(username)) {
|
||||||
|
return fail(400, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
errors: ['invalidUsername'],
|
||||||
|
fields: ['username']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayName.length < 3 || displayName.length > 32) {
|
||||||
|
return fail(400, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
errors: ['invalidDisplayName'],
|
||||||
|
fields: ['displayName']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
return fail(400, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
errors: ['invalidEmail'],
|
||||||
|
fields: ['email']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordRegex.test(password)) {
|
||||||
|
return fail(400, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
errors: ['invalidPassword'],
|
||||||
|
fields: ['password']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await Users.checkRegistration(username, email))) {
|
||||||
|
return fail(400, {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
errors: ['existingRegistration'],
|
||||||
|
fields: ['username', 'email']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check for registration token
|
||||||
|
const newUser = await Users.register({ username, displayName, password, email });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: newUser.activated ? 'userCreated' : 'emailSent'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const load = ({ locals }) => {
|
||||||
|
if (locals.session.data?.user) {
|
||||||
|
return redirect(301, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: REGISTRATIONS === 'true'
|
||||||
|
};
|
||||||
|
};
|
134
src/routes/register/+page.svelte
Normal file
134
src/routes/register/+page.svelte
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
import type { SubmitFunction } from '@sveltejs/kit';
|
||||||
|
import type { PageData, ActionData } from './$types';
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import SideContainer from '$lib/components/SideContainer.svelte';
|
||||||
|
import Alert from '$lib/components/Alert.svelte';
|
||||||
|
import ColumnView from '$lib/components/ColumnView.svelte';
|
||||||
|
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||||
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
export let form: ActionData;
|
||||||
|
|
||||||
|
let internalErrors: string[] = [];
|
||||||
|
let submitted = false;
|
||||||
|
$: errors = [...internalErrors, ...(form?.errors?.length ? form.errors : [])];
|
||||||
|
|
||||||
|
const enhanceFn: SubmitFunction = ({ formData, cancel }) => {
|
||||||
|
internalErrors.length = 0;
|
||||||
|
|
||||||
|
const pwd = formData.get('newPassword') as string;
|
||||||
|
const repeat = formData.get('repeatPassword') as string;
|
||||||
|
if (pwd && pwd !== repeat) {
|
||||||
|
internalErrors.push('passwordMismatch');
|
||||||
|
return cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitted = true;
|
||||||
|
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update();
|
||||||
|
submitted = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t('account.register.title')} - {PUBLIC_SITE_NAME}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<SideContainer>
|
||||||
|
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||||
|
<h2>{$t('account.register.title')}</h2>
|
||||||
|
|
||||||
|
<ColumnView>
|
||||||
|
{#if !data.enabled}
|
||||||
|
<Alert type="error">{$t('account.register.disabled')}</Alert>
|
||||||
|
{:else if form?.success}
|
||||||
|
<Alert type="success">{$t(`account.register.${form.success}`)}</Alert>
|
||||||
|
{:else}
|
||||||
|
<form action="" method="POST" use:enhance={enhanceFn}>
|
||||||
|
<FormWrapper>
|
||||||
|
{#if errors.length}
|
||||||
|
{#each errors as error}
|
||||||
|
<Alert type="error">{$t(`account.errors.${error}`)}</Alert>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<FormSection required>
|
||||||
|
<FormControl>
|
||||||
|
<label for="register-username">{$t('account.username')}</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="register-username"
|
||||||
|
autocomplete="username"
|
||||||
|
value={form?.username ?? ''}
|
||||||
|
/>
|
||||||
|
<span>{$t('account.usernameHint')}</span>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="register-displayName">{$t('account.displayName')}</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
name="displayName"
|
||||||
|
autocomplete="nickname"
|
||||||
|
id="register-displayName"
|
||||||
|
value={form?.displayName ?? ''}
|
||||||
|
/>
|
||||||
|
<span>{$t('account.displayNameHint')}</span>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="register-email">{$t('account.email')}</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="register-email"
|
||||||
|
value={form?.email ?? ''}
|
||||||
|
autocomplete="email"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="register-password">{$t('account.password')}</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="register-password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
<span>{$t('account.passwordHint')}</span>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<label for="register-repeatPassword">{$t('account.repeatPassword')}</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
name="repeatPassword"
|
||||||
|
id="register-repeatPassword"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Button type="submit" variant="primary" disabled={submitted}
|
||||||
|
>{$t('account.register.submit')}</Button
|
||||||
|
>
|
||||||
|
</FormWrapper>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
<div><a href="/login">{$t('account.login.title')}</a></div>
|
||||||
|
</ColumnView>
|
||||||
|
</SideContainer>
|
Loading…
Reference in New Issue
Block a user