sso-core/src/routes/register/+page.server.ts

167 lines
3.6 KiB
TypeScript

import { env } from '$env/dynamic/private';
import { AuditAction } from '$lib/server/audit';
import { Audit } from '$lib/server/audit/audit.js';
import { Changesets } from '$lib/server/changesets.js';
import { Users } from '$lib/server/users/index.js';
import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js';
import { error, fail, redirect } from '@sveltejs/kit';
import { RateLimiter } from 'sveltekit-rate-limiter/server';
interface RegisterData {
username: string;
displayName: string;
email: string;
password: string;
'h-captcha-response': string;
}
const fields: (keyof RegisterData)[] = [
'username',
'displayName',
'email',
'password',
'h-captcha-response'
];
const limiter = new RateLimiter({
IP: [6, 'm']
});
export const actions = {
default: async (event) => {
const { request, locals } = event;
if (await limiter.isLimited(event)) throw error(429);
// Logged in users cannot make more accounts
if (locals.session.data?.user || env.REGISTRATIONS === 'false') {
return redirect(303, '/');
}
const body = await request.formData();
const changes = Changesets.only(fields, body);
const {
username,
displayName,
email,
password,
['h-captcha-response']: captchaToken
} = 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 (env.HCAPTCHA_SECRET) {
if (!captchaToken) {
return fail(400, {
username,
displayName,
email,
errors: ['invalidCaptcha'],
fields: ['hcaptcha']
});
}
const requestBody = new FormData();
requestBody.append('response', captchaToken);
requestBody.append('secret', env.HCAPTCHA_SECRET);
requestBody.append('remoteip', event.getClientAddress());
const testResultRequest = await fetch('https://api.hcaptcha.com/siteverify', {
method: 'POST',
body: requestBody
});
const testResult = await testResultRequest.json();
if (!testResult.success) {
return fail(400, {
username,
displayName,
email,
errors: ['invalidCaptcha'],
fields: ['hcaptcha']
});
}
}
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 });
await Audit.insertRequest(AuditAction.REGISTRATION, event, newUser);
return {
success: newUser.activated ? 'userCreated' : 'emailSent'
};
}
};
export const load = ({ locals }) => {
if (locals.session.data?.user) {
return redirect(301, '/');
}
return {
enabled: env.REGISTRATIONS === 'true'
};
};