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",
|
||||
"mime-types": "^2.1.35",
|
||||
"mysql2": "^3.9.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"svelte-kit-cookie-session": "^4.0.0",
|
||||
@ -29,6 +30,7 @@
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
@ -1559,6 +1561,15 @@
|
||||
"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": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
|
||||
@ -4108,6 +4119,14 @@
|
||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
@ -19,6 +19,7 @@
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
@ -44,6 +45,7 @@
|
||||
"jose": "^5.3.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mysql2": "^3.9.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"svelte-kit-cookie-session": "^4.0.0",
|
||||
|
@ -11,10 +11,12 @@
|
||||
--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;
|
||||
|
@ -3,10 +3,13 @@
|
||||
|
||||
export let type: 'button' | 'submit' = 'button';
|
||||
export let variant: 'default' | 'primary' | 'link' = 'default';
|
||||
export let disabled = false;
|
||||
const dispath = createEventDispatcher();
|
||||
</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>
|
||||
.btn {
|
||||
|
@ -26,6 +26,10 @@
|
||||
color: var(--in-input-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:user-invalid {
|
||||
border-color: var(--in-input-border-color-invalid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,4 +48,11 @@
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-control > :global(label):has(+ input[required])::after {
|
||||
content: '*';
|
||||
color: var(--in-input-required-color);
|
||||
margin-left: 4px;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
export let title: string = '';
|
||||
export let required = false;
|
||||
</script>
|
||||
|
||||
<div class="form-section">
|
||||
{#if title}<div class="form-subtitle">{title}</div>{/if}
|
||||
{#if required}<span class="form-required">{$t('common.required')}</span>{/if}
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@ -20,4 +24,15 @@
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-required {
|
||||
font-size: 0.9rem;
|
||||
|
||||
&::before {
|
||||
content: '*';
|
||||
color: var(--in-input-required-color);
|
||||
margin-right: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,17 +1,23 @@
|
||||
{
|
||||
"title": "Manage your account",
|
||||
"username": "Username",
|
||||
"usernameHint": "Only the English alphabet, numbers and _-. are allowed.",
|
||||
"displayName": "Display Name",
|
||||
"displayNameHint": "Must be between 3 and 32 characters.",
|
||||
"changeEmail": "Change email address",
|
||||
"currentEmail": "Current email address",
|
||||
"email": "Email address",
|
||||
"newEmail": "New email address",
|
||||
"password": "Password",
|
||||
"repeatPassword": "Repeat password",
|
||||
"changePassword": "Change password",
|
||||
"currentPassword": "Current password",
|
||||
"newPassword": "New password",
|
||||
"repeatPassword": "Repeat new password",
|
||||
"repeatNewPassword": "Repeat new password",
|
||||
"passwordHint": "At least 8 characters, a capital letter and a number required.",
|
||||
"submit": "Submit",
|
||||
"changeSuccess": "Account settings changed successfully!",
|
||||
"activateSuccess": "Your account has been activated successfully! You may now log in.",
|
||||
"avatar": {
|
||||
"title": "Profile avatar",
|
||||
"change": "Change avatar",
|
||||
@ -30,16 +36,27 @@
|
||||
"otp": "Two-factor authentication is enabled",
|
||||
"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": {
|
||||
"invalidLogin": "Invalid email or password!",
|
||||
"invalidRequest": "Invalid request! Please try again.",
|
||||
"emailRequired": "Email address is required.",
|
||||
"invalidUsername": "The username entered is invalid or not allowed",
|
||||
"invalidEmail": "The email address is invalid.",
|
||||
"passwordRequired": "The password is required.",
|
||||
"passwordMismatch": "The passwords do not match!",
|
||||
"invalidPassword": "The provided password 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": {
|
||||
"title": "Two-factor authentication",
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"siteName": "Icy Network",
|
||||
"description": "Icy Network is a Single-Sign-On service used by other applications.",
|
||||
"description": "{{siteName}} is a Single-Sign-On service used by other applications.",
|
||||
"cookieDisclaimer": "The website may use temporary cookies for storing your login session and ensuring your security. This web service is <a href=\"https://git.icynet.eu/IcyNetwork/icynet-auth-server\" target=\"_blank\">completely open source</a> and can be audited by anyone.",
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
"manage": "Manage",
|
||||
"back": "Go back",
|
||||
"home": "Home page"
|
||||
"home": "Home page",
|
||||
"required": "Required fields"
|
||||
}
|
||||
|
@ -11,8 +11,7 @@
|
||||
"email": "Your email address",
|
||||
"picture": "Your profile picture",
|
||||
"account": "Changing your password or other account settings",
|
||||
"management": "Manage Icy Network on your behalf",
|
||||
"admin": "Commit administrative actions to the extent of your user privileges"
|
||||
"management": "Commit administrative actions to the extent of your user privileges"
|
||||
},
|
||||
"link": {
|
||||
"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: [
|
||||
{
|
||||
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 {
|
||||
mysqlTable,
|
||||
int,
|
||||
@ -225,3 +225,117 @@ export const userToken = mysqlTable('user_token', {
|
||||
.default(sql`current_timestamp(6)`)
|
||||
.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')) {
|
||||
allowedScopes.push('management', 'admin');
|
||||
allowedScopes.push('management');
|
||||
}
|
||||
|
||||
// TODO: client picture
|
||||
|
@ -1,8 +1,13 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { and, eq, or } from 'drizzle-orm';
|
||||
import { db, user, type User } from '../drizzle';
|
||||
import { and, eq, gt, or, sql } from 'drizzle-orm';
|
||||
import { db, user, userToken, type User } from '../drizzle';
|
||||
import type { UserSession } from './types';
|
||||
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 {
|
||||
static async getById(id: number): Promise<User | undefined> {
|
||||
@ -71,6 +76,172 @@ export class Users {
|
||||
}
|
||||
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 './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 { db, userToken, type User } from '../drizzle';
|
||||
import { and, eq, gt, isNull, or } from 'drizzle-orm';
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
|
||||
totp.options = {
|
||||
window: 2
|
||||
@ -12,7 +13,7 @@ export class TimeOTP {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -15,6 +15,7 @@
|
||||
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
||||
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
|
||||
export let data: PageData;
|
||||
export let form: ActionData;
|
||||
@ -51,8 +52,12 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('account.title')} - {PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<MainContainer>
|
||||
<h1>{$t('common.siteName')}</h1>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<SplitView>
|
||||
<div>
|
||||
<h2>{$t('account.title')}</h2>
|
||||
@ -93,6 +98,7 @@
|
||||
<label for="form-displayName">{$t('account.displayName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="nickname"
|
||||
name="displayName"
|
||||
value={form?.displayName || data.user.name}
|
||||
id="form-displayName"
|
||||
@ -103,12 +109,17 @@
|
||||
<FormSection title={$t('account.changeEmail')}>
|
||||
<FormControl>
|
||||
<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>
|
||||
<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>
|
||||
</FormSection>
|
||||
|
||||
@ -136,7 +147,7 @@
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<label for="form-repeatPassword">{$t('account.repeatPassword')}</label>
|
||||
<label for="form-repeatPassword">{$t('account.repeatNewPassword')}</label>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
|
@ -13,7 +13,7 @@
|
||||
</script>
|
||||
|
||||
<MainContainer>
|
||||
<h1>{$t('common.siteName')}</h1>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<h2>{$t('account.otp.title')}</h2>
|
||||
|
||||
{#if form?.success}
|
||||
|
@ -93,3 +93,32 @@ export const actions = {
|
||||
return redirect(303, redirectUrl);
|
||||
}
|
||||
} 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">
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import Alert from '$lib/components/Alert.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import SideContainer from '$lib/components/SideContainer.svelte';
|
||||
@ -7,19 +9,30 @@
|
||||
import FormSection from '$lib/components/form/FormSection.svelte';
|
||||
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let form: ActionData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('account.login.title')} - {PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SideContainer>
|
||||
<h1>{$t('common.siteName')}</h1>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
|
||||
<h2>{$t('account.login.title')}</h2>
|
||||
|
||||
<form action="" method="POST" use:enhance>
|
||||
<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?.otpRequired}
|
||||
<!-- Two-factor code request -->
|
||||
<FormSection title={$t('account.login.otp')}>
|
||||
@ -49,11 +62,12 @@
|
||||
</FormControl>
|
||||
{/if}
|
||||
<Button type="submit" variant="primary">{$t('account.login.submit')}</Button>
|
||||
<div><a href="/register">{$t('account.register.title')}</a></div>
|
||||
</FormWrapper>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</SideContainer>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { assets } from '$app/paths';
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import Alert from '$lib/components/Alert.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import ColumnView from '$lib/components/ColumnView.svelte';
|
||||
@ -11,9 +12,13 @@
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('oauth2.authorize.title')} "{data.client?.title || ''}" - {PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<MainContainer>
|
||||
{#if data.error}
|
||||
<h1>{$t('common.siteName')}</h1>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<ColumnView>
|
||||
<Alert type="error"
|
||||
>{$t('oauth2.authorize.errorPage')}<br /><br /><code
|
||||
@ -25,7 +30,7 @@
|
||||
{/if}
|
||||
|
||||
{#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>
|
||||
|
||||
<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