Added password recovery and login attempt guards.
This commit is contained in:
79
components/emails/recovery.vue
Normal file
79
components/emails/recovery.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Text,
|
||||
} from '@vue-email/components';
|
||||
|
||||
import config from 'config';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => mockUser,
|
||||
},
|
||||
verifyUrl: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100/recovery?userId=0&token=00000000000000000000000000000000',
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// defineProps' default factory (above, in <script setup>) references this. It must live in a plain
|
||||
// <script> block: Vue's compiler forbids defineProps from referencing variables declared inside
|
||||
// <script setup> itself, since that block gets hoisted out of the setup() function.
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'DebuggingLibrarian',
|
||||
email: 'debug@alerts.traxxx.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Html>
|
||||
<Body style="font-family: sans-serif;">
|
||||
<Preview>Recovering your traxxx password.</Preview>
|
||||
|
||||
<Container>
|
||||
<Img
|
||||
:src="`${address}/img/logo.png`"
|
||||
width="150"
|
||||
alt="traxxx"
|
||||
style="margin: 15px auto 25px auto;"
|
||||
/>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="color: #f65596; margin: 0 auto 0 auto;"
|
||||
>Hi, {{ user.username }}</Heading>
|
||||
|
||||
<Text>
|
||||
<strong>You requested to reset the password for your traxxx account.</strong>
|
||||
<br>
|
||||
Please set a new password by clicking the button below:
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
:href="verifyUrl"
|
||||
target="_blank"
|
||||
style="background: #f65596; color: #fff; font-size: 16px; padding: 1rem 1.5rem; border-radius: .5rem; margin: 1rem 0;"
|
||||
><strong>Reset password</strong></Button>
|
||||
|
||||
<Text style="font-size: 16px; margin: 20px 0;">
|
||||
The link will expire in {{ config.auth.passwordTokenExpiry }} minutes, or when you request another password reset.
|
||||
If you did not request a password reset, you can safely ignore this e-mail.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
</template>
|
||||
@@ -154,7 +154,13 @@ module.exports = {
|
||||
siteKey: '10000000-ffff-ffff-ffff-000000000001',
|
||||
secretKey: '0x0000000000000000000000000000000000000000',
|
||||
},
|
||||
loginCredentialAttempts: 5,
|
||||
loginCredentialCooldown: 10, // minutes,
|
||||
loginIpAttempts: 20, // shared IPs might result in more non-malicious failed attempt
|
||||
loginIpCooldown: 10, // minutes
|
||||
tokenCooldown: 10, // minutes, how often a token can be requested
|
||||
emailTokenExpiry: 120, // minutes
|
||||
passwordTokenExpiry: 120, // minutes
|
||||
},
|
||||
bans: {
|
||||
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
export default {
|
||||
meta: {
|
||||
title: {
|
||||
env: {
|
||||
server: true,
|
||||
client: true,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
env: {
|
||||
server: true,
|
||||
|
||||
@@ -16,7 +16,6 @@ export async function onBeforeRender(pageContext) {
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: 'actors',
|
||||
pageProps: {
|
||||
actors,
|
||||
countries,
|
||||
|
||||
1
pages/actors/+title.js
Normal file
1
pages/actors/+title.js
Normal file
@@ -0,0 +1 @@
|
||||
export default 'Actors';
|
||||
@@ -9,10 +9,7 @@ import navigate from '#/src/navigate.js';
|
||||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
const allowSignup = pageContext.env.allowSignup;
|
||||
// const allowRecovery = pageContext.env.emailEnabled;
|
||||
const allowRecovery = false; // TODO: implement
|
||||
|
||||
console.log(pageContext.env);
|
||||
const allowRecovery = pageContext.env.emailEnabled;
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
|
||||
1
pages/auth/login/+title.js
Normal file
1
pages/auth/login/+title.js
Normal file
@@ -0,0 +1 @@
|
||||
export default 'Login';
|
||||
218
pages/auth/recovery/+Page.vue
Normal file
218
pages/auth/recovery/+Page.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<script setup>
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import Password from '#/components/form/password.vue';
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
|
||||
import { post } from '#/src/api.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { userId, token } = pageContext.urlParsed.search;
|
||||
const allowRecovery = pageContext.env.emailEnabled;
|
||||
|
||||
const credential = ref('');
|
||||
|
||||
const submitted = ref(false);
|
||||
const success = ref(false);
|
||||
const errorMsg = ref(null);
|
||||
const password = ref(null);
|
||||
|
||||
async function request() {
|
||||
submitted.value = true;
|
||||
errorMsg.value = null;
|
||||
|
||||
try {
|
||||
await post('/passwords/request', {
|
||||
credential: credential.value,
|
||||
}, {
|
||||
successFeedback: 'Password reset requested, please check your e-mail inbox',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
success.value = true;
|
||||
}
|
||||
catch (error) {
|
||||
errorMsg.value = error.message;
|
||||
}
|
||||
finally {
|
||||
submitted.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
submitted.value = true;
|
||||
errorMsg.value = null;
|
||||
|
||||
try {
|
||||
await post('/passwords/reset', {
|
||||
userId,
|
||||
token,
|
||||
password: password.value,
|
||||
}, {
|
||||
successFeedback: 'Password reset successfully',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
success.value = true;
|
||||
}
|
||||
catch (error) {
|
||||
errorMsg.value = error.message;
|
||||
}
|
||||
finally {
|
||||
submitted.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="recovery-container">
|
||||
<h3 class="heading">
|
||||
<template v-if="userId && token">Password reset</template>
|
||||
<template v-else>Account recovery</template>
|
||||
</h3>
|
||||
|
||||
<template v-if="allowRecovery">
|
||||
<strong
|
||||
v-if="errorMsg"
|
||||
class="error"
|
||||
>{{ errorMsg }}</strong>
|
||||
|
||||
<form
|
||||
v-else-if="userId && token"
|
||||
class="login-panel"
|
||||
@submit.prevent="reset"
|
||||
>
|
||||
<div
|
||||
v-if="success"
|
||||
class="success"
|
||||
>
|
||||
<strong class="success-heading">Password reset successfully</strong>
|
||||
|
||||
<a
|
||||
href="/login"
|
||||
class="link"
|
||||
>Go to login</a>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<Password
|
||||
:password="password"
|
||||
@input="(newPassword) => password = newPassword"
|
||||
/>
|
||||
|
||||
<Ellipsis v-if="submitted" />
|
||||
|
||||
<button
|
||||
v-else
|
||||
class="button button-submit"
|
||||
>Reset password</button>
|
||||
</template>
|
||||
</form>
|
||||
|
||||
<form
|
||||
v-else-if="allowRecovery"
|
||||
class="login-panel"
|
||||
@submit.prevent="request"
|
||||
>
|
||||
<div
|
||||
v-if="success"
|
||||
class="success"
|
||||
>
|
||||
<strong class="success-heading">Password reset requested</strong>
|
||||
If the account exists, instructions have been sent to the associated e-mail address.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<input
|
||||
v-model="credential"
|
||||
placeholder="Username or e-mail"
|
||||
class="input"
|
||||
>
|
||||
|
||||
<Ellipsis v-if="submitted" />
|
||||
|
||||
<button
|
||||
v-else
|
||||
class="button button-submit"
|
||||
>Request password reset</button>
|
||||
</template>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="error"
|
||||
>Password recovery is currently unavailable</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-align: center;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.recovery-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--background-base-10);
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
padding: 1rem;
|
||||
margin: 0 1rem 1rem 1rem;
|
||||
border-radius: .5rem;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
background: var(--background-base);
|
||||
font-size: 1rem;
|
||||
|
||||
.button {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-top: .5rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 1rem;
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.success {
|
||||
justify-content: center;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.success-heading {
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.load-container {
|
||||
justify-content: center;
|
||||
padding: 1rem 1rem .5rem 1rem;
|
||||
}
|
||||
</style>
|
||||
1
pages/auth/recovery/+route.js
Normal file
1
pages/auth/recovery/+route.js
Normal file
@@ -0,0 +1 @@
|
||||
export default '/recovery';
|
||||
1
pages/auth/recovery/+title.js
Normal file
1
pages/auth/recovery/+title.js
Normal file
@@ -0,0 +1 @@
|
||||
export default `Account recovery`;
|
||||
1
pages/auth/signup/+title.js
Normal file
1
pages/auth/signup/+title.js
Normal file
@@ -0,0 +1 @@
|
||||
export default 'Sign up';
|
||||
1
pages/recovery/+title.js
Normal file
1
pages/recovery/+title.js
Normal file
@@ -0,0 +1 @@
|
||||
export default 'Account recovery';
|
||||
@@ -270,7 +270,7 @@ function scrollHorizontal(event) {
|
||||
gap: .5rem;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 0;
|
||||
margin-top: .5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.domain {
|
||||
|
||||
@@ -5,11 +5,15 @@ import { dangerouslySkipEscape, escapeInject } from 'vike/server';
|
||||
|
||||
import { createApp } from './app.js';
|
||||
|
||||
function getTitle(location) {
|
||||
function getTitle(location, pageContext) {
|
||||
if (!location) {
|
||||
return config.title;
|
||||
}
|
||||
|
||||
if (typeof location === 'function') {
|
||||
return getTitle(location(pageContext), pageContext);
|
||||
}
|
||||
|
||||
return `${config.title} - ${location.slice(0, 1).toUpperCase()}${location.slice(1)}`;
|
||||
}
|
||||
|
||||
@@ -44,8 +48,7 @@ async function onRenderHtml(pageContext) {
|
||||
const appHtml = await renderToString(app);
|
||||
|
||||
// See https://vike.dev/head
|
||||
const { documentProps } = pageContext.exports;
|
||||
const title = getTitle(documentProps?.title || pageContext.title);
|
||||
const title = getTitle(pageContext.title || pageContext.config.title, pageContext);
|
||||
|
||||
const documentHtml = escapeInject`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
@@ -413,7 +413,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!filters.includeAliased) {
|
||||
if (!filters.includeAliased && !filters.stashId) {
|
||||
builder.where('alias_for', 0);
|
||||
}
|
||||
|
||||
|
||||
236
src/auth.js
236
src/auth.js
@@ -46,19 +46,71 @@ async function generateAvatar(user) {
|
||||
logger.verbose(`Generated avatar for '${user.username}' (${user.id})`);
|
||||
}
|
||||
|
||||
async function checkLoginCooldown(identifier, attemptThreshold) {
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cooldownKey = `traxxx:login_cooldown:${slugify(identifier, '_')}`;
|
||||
const attempts = JSON.parse(await redis.get(cooldownKey) || '0');
|
||||
|
||||
if (attempts >= attemptThreshold) {
|
||||
throw new HttpError(`Please wait a few minutes before attempting another login`, 409);
|
||||
}
|
||||
}
|
||||
|
||||
async function setLoginCooldown(identifier, attemptCooldown) {
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cooldownKey = `traxxx:login_cooldown:${slugify(identifier, '_')}`;
|
||||
|
||||
await redis.incr(cooldownKey);
|
||||
await redis.expire(cooldownKey, attemptCooldown * 60);
|
||||
}
|
||||
|
||||
export async function login(credentials, userIp) {
|
||||
if (!config.auth.login) {
|
||||
throw new HttpError('Logins are currently disabled', 503);
|
||||
}
|
||||
|
||||
if (!credentials.username) {
|
||||
throw new HttpError('Please provide a username or e-mail', 400);
|
||||
}
|
||||
|
||||
if (!credentials.password) {
|
||||
throw new HttpError('Please provide a password', 400);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
checkLoginCooldown(credentials.username, config.auth.loginCredentialAttempts),
|
||||
checkLoginCooldown(userIp, config.auth.loginIpAttempts),
|
||||
]);
|
||||
|
||||
const { user } = await fetchUser(credentials.username.trim(), {
|
||||
email: true,
|
||||
raw: true,
|
||||
}).catch(() => {
|
||||
}).catch(async () => {
|
||||
await Promise.all([
|
||||
setLoginCooldown(credentials.username, config.auth.loginCredentialCooldown),
|
||||
setLoginCooldown(userIp, config.auth.loginIpCooldown),
|
||||
]);
|
||||
|
||||
throw new HttpError('Username or password incorrect', 401);
|
||||
});
|
||||
|
||||
try {
|
||||
await verifyPassword(credentials.password, user.password);
|
||||
}
|
||||
catch (error) {
|
||||
await Promise.all([
|
||||
setLoginCooldown(credentials.username, config.auth.loginCredentialCooldown),
|
||||
setLoginCooldown(userIp, config.auth.loginIpCooldown),
|
||||
]);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await knex('users')
|
||||
.update('last_login', 'NOW()')
|
||||
@@ -90,6 +142,17 @@ async function hashPassword(password) {
|
||||
};
|
||||
}
|
||||
|
||||
async function tokenCooldown(identifier) {
|
||||
const cooldownKey = `traxxx:token_cooldown:${slugify(identifier, '_')}`;
|
||||
|
||||
if (await redis.exists(cooldownKey)) {
|
||||
throw new HttpError(`Please wait a few minutes before requesting another token`, 409);
|
||||
}
|
||||
|
||||
await redis.set(cooldownKey, JSON.stringify(true));
|
||||
await redis.expire(cooldownKey, config.auth.tokenCooldown * 60);
|
||||
}
|
||||
|
||||
export async function sendEmailVerification(user, customEmail, trigger) {
|
||||
if (!config.email.enabled) {
|
||||
throw new HttpError('E-mail verification is disabled', 503);
|
||||
@@ -105,6 +168,8 @@ export async function sendEmailVerification(user, customEmail, trigger) {
|
||||
throw new HttpError('No e-mail address provided', 400);
|
||||
}
|
||||
|
||||
await tokenCooldown(email);
|
||||
|
||||
const tokenKey = `traxxx:token_email:${user.id}`;
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
@@ -135,6 +200,138 @@ export async function sendEmailVerification(user, customEmail, trigger) {
|
||||
}]);
|
||||
}
|
||||
|
||||
export async function verifyEmail(userId, emailToken) {
|
||||
if (!userId) {
|
||||
throw new HttpError('User ID not provided', 400);
|
||||
}
|
||||
|
||||
if (!emailToken) {
|
||||
throw new HttpError('Token not provided', 400);
|
||||
}
|
||||
|
||||
const tokenKey = `traxxx:token_email:${userId}`;
|
||||
const tokenEntry = await redis.getDel(tokenKey);
|
||||
|
||||
if (!tokenEntry) {
|
||||
throw new HttpError('Invalid verification link', 401);
|
||||
}
|
||||
|
||||
const { token, email } = JSON.parse(tokenEntry);
|
||||
|
||||
if (!token
|
||||
|| !email
|
||||
|| emailToken.length !== token.length
|
||||
|| !crypto.timingSafeEqual(Buffer.from(emailToken, 'hex'), Buffer.from(token, 'hex'))) {
|
||||
throw new HttpError('Invalid verification link', 401);
|
||||
}
|
||||
|
||||
const updated = await knex('users')
|
||||
.where('id', userId)
|
||||
.update({
|
||||
email,
|
||||
email_verified: true,
|
||||
});
|
||||
|
||||
if (updated === 0) {
|
||||
throw new HttpError('E-mail verification failed because the user could not be found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendPasswordReset(credential) {
|
||||
if (!config.email.enabled) {
|
||||
throw new HttpError('Password reset is disabled', 503);
|
||||
}
|
||||
|
||||
if (!credential) {
|
||||
throw new HttpError('No credential provided', 400);
|
||||
}
|
||||
|
||||
await tokenCooldown(credential);
|
||||
|
||||
const user = await fetchUser(credential, {
|
||||
email: true,
|
||||
includePrivate: true,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
// we don't throw errors because the requesting user shouldn't know whether the account exists or not
|
||||
if (!user) {
|
||||
logger.info(`Password reset requested for unknown user '${credential}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.email) {
|
||||
logger.info(`Password reset requested for '${credential}', but user '${user.username}' (${user.id}) has no e-mail address known`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenKey = `traxxx:token_password:${user.id}`;
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
const tokenData = {
|
||||
token,
|
||||
};
|
||||
|
||||
await redis.set(tokenKey, JSON.stringify(tokenData));
|
||||
await redis.expire(tokenKey, config.auth.passwordTokenExpiry * 60);
|
||||
|
||||
const verifyUrl = `${config.web.address}/recovery?${new URLSearchParams({
|
||||
userId: user.id,
|
||||
token,
|
||||
}).toString()}`;
|
||||
|
||||
logger.info(`Password reset sent to ${user.username} (${user.id}): ${verifyUrl}`);
|
||||
|
||||
await sendEmails([{
|
||||
template: 'recovery',
|
||||
to: user.email,
|
||||
subject: 'Reset your traxxx password',
|
||||
props: {
|
||||
user,
|
||||
verifyUrl,
|
||||
},
|
||||
}]);
|
||||
}
|
||||
|
||||
export async function resetPassword(userId, emailToken, newPassword) {
|
||||
if (!userId) {
|
||||
throw new HttpError('User ID not provided', 400);
|
||||
}
|
||||
|
||||
if (!emailToken) {
|
||||
throw new HttpError('Token not provided', 400);
|
||||
}
|
||||
|
||||
if (!newPassword) {
|
||||
throw new HttpError('New password not provided', 400);
|
||||
}
|
||||
|
||||
const tokenKey = `traxxx:token_password:${userId}`;
|
||||
const tokenEntry = await redis.getDel(tokenKey);
|
||||
|
||||
if (!tokenEntry) {
|
||||
throw new HttpError('Invalid recovery link', 401);
|
||||
}
|
||||
|
||||
const { token } = JSON.parse(tokenEntry);
|
||||
|
||||
if (!token
|
||||
|| emailToken.length !== token.length
|
||||
|| !crypto.timingSafeEqual(Buffer.from(emailToken, 'hex'), Buffer.from(token, 'hex'))) {
|
||||
throw new HttpError('Invalid recovery link', 401);
|
||||
}
|
||||
|
||||
const { storedPassword } = await hashPassword(newPassword);
|
||||
|
||||
const updated = await knex('users')
|
||||
.where('id', userId)
|
||||
.update('password', storedPassword);
|
||||
|
||||
if (updated === 0) {
|
||||
throw new HttpError('Password reset failed because the user could not be found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
export async function signup(credentials, userIp) {
|
||||
if (!config.auth.signup) {
|
||||
throw new HttpError('Sign-ups are currently disabled', 503);
|
||||
@@ -256,43 +453,6 @@ async function updateEmail(email, reqUser) {
|
||||
await sendEmailVerification(reqUser, email, 'change');
|
||||
}
|
||||
|
||||
export async function verifyEmail(userId, emailToken) {
|
||||
if (!userId) {
|
||||
throw new HttpError('User ID not provided', 400);
|
||||
}
|
||||
|
||||
if (!emailToken) {
|
||||
throw new HttpError('Token not provided', 400);
|
||||
}
|
||||
|
||||
const tokenKey = `traxxx:token_email:${userId}`;
|
||||
const tokenEntry = await redis.getDel(tokenKey);
|
||||
|
||||
if (!tokenEntry) {
|
||||
throw new HttpError('Invalid verification link', 401);
|
||||
}
|
||||
|
||||
const { token, email } = JSON.parse(tokenEntry);
|
||||
|
||||
if (!token
|
||||
|| !email
|
||||
|| emailToken.length !== token.length
|
||||
|| !crypto.timingSafeEqual(Buffer.from(emailToken, 'hex'), Buffer.from(token, 'hex'))) {
|
||||
throw new HttpError('Invalid verification link', 401);
|
||||
}
|
||||
|
||||
const updated = await knex('users')
|
||||
.where('id', userId)
|
||||
.update({
|
||||
email,
|
||||
email_verified: true,
|
||||
});
|
||||
|
||||
if (updated === 0) {
|
||||
throw new HttpError('E-mail verification failed because the user could not be found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser({ credentials, password }, reqUser, reqSession) {
|
||||
if (!reqUser) {
|
||||
throw new HttpError('You are not logged in', 401);
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
flushUserKeys,
|
||||
login,
|
||||
removeUserKey,
|
||||
resetPassword,
|
||||
sendEmailVerification,
|
||||
sendPasswordReset,
|
||||
signup,
|
||||
updateSettings,
|
||||
updateUser,
|
||||
@@ -184,6 +186,20 @@ async function verifyEmailApi(req, res) {
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function sendPasswordResetApi(req, res) {
|
||||
await sendPasswordReset(req.body.credential);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function resetPasswordApi(req, res) {
|
||||
const userId = Number(req.body.userId) || null;
|
||||
|
||||
await resetPassword(userId, req.body.token, req.body.password);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function updateSettingsApi(req, res) {
|
||||
const updatedSettings = await updateSettings(req.body, req.user);
|
||||
|
||||
@@ -234,6 +250,8 @@ authRouter.patch('/api/me/settings', updateSettingsApi);
|
||||
// EMAIL VERIFICATION
|
||||
authRouter.post('/api/me/email_verification', sendEmailVerificationApi);
|
||||
authRouter.post('/api/emails/verify', verifyEmailApi); // usable without session
|
||||
authRouter.post('/api/passwords/request', sendPasswordResetApi);
|
||||
authRouter.post('/api/passwords/reset', resetPasswordApi);
|
||||
|
||||
// API KEYS
|
||||
authRouter.get('/api/me/keys', fetchUserKeysApi);
|
||||
|
||||
Reference in New Issue
Block a user