You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

193 lines
6.4 KiB

import { checkStringParam, errorOut, notYet, randomElement, requestIp, reverseString, hours, minutes } from '../logic/utils.js';
import { Animals } from '../misc/animals.js';
import { reissueToken, generateToken, newTokenExpiry, hashPassword, doesPasswordMatch, isEndpointAllowedForBannedUsers, isEndpointProtected, generateRestoreCode, restoreValidity, restoreAttempts } from '../logic/security.js';
import { sendRestorationLink } from '../logic/email.js';
import { UserSession } from '../logic/session.js';
import { SpamCache } from '../logic/cache.js';
/**
* @param {Object} props
* @param {import('fastify').FastifyInstance} props.app
* @param {import('sequelize/types').Sequelize} props.db
*/
function AuthController({app, db}){
const { Users } = db.models;
const signInSpam = new SpamCache();
const signUpSpam = new SpamCache();
const restoreApplySpam = new SpamCache();
{ //validate token header and put .session in every request
app.decorateRequest('session', null);
app.addHook('preHandler', async (request, reply) => {
if(isEndpointProtected(request.url)){
const token = request.headers.authorization;
let session = UserSession.find(token);
if(!session){
const userData = await Users.findOne({where: {token}});
if(userData){
session = new UserSession(userData, request);
}
}
const tokenNotExpired = notYet(session?.user.tokenExpiry);
const userNotBanned = (!notYet(session?.user.bannedUntil)) || isEndpointAllowedForBannedUsers(request.url);
if(session?.user && tokenNotExpired && userNotBanned){
request.session = session;
}else{
errorOut(reply, 'Unauthorized', 401);
return true; //abort
}
}
});
}
app.post('/auth/sign-in', async (request, reply) => {
const {email, paswd} = request.body || {};
if(signInSpam.check(requestIp(request), [[5000, 2*hours]])){
return errorOut(reply, 'error.too_fast');
}
{ //form validation
if(!isValidEmail(email)) return errorOut(reply, 'auth.bad_email');
if(!isValidPassword(paswd)) return errorOut(reply, 'auth.bad_paswd');
}
const fixedEmail = fixEmail(email);
const user = await Users.findOne({where: {email: fixedEmail}});
if(user && doesPasswordMatch(reverseString(paswd), user.paswd)){
let token = user.token;
if(new Date() > user.tokenExpiry){
token = await reissueToken(user);
}
return {token};
}else{
errorOut(reply, 'auth.not_found');
}
});
app.post('/auth/sign-up', async (request, reply) => {
const {email, username, paswd} = request.body || {};
if(signUpSpam.check(requestIp(request), [[5000, 2*hours], [100, 10*minutes]])){
return errorOut(reply, 'error.too_fast');
}
{ //form validation
if(!isValidEmail(email)) return errorOut(reply, 'auth.bad_email');
if(!isValidPassword(paswd)) return errorOut(reply, 'auth.bad_paswd');
if(!isValidUsername(username)) return errorOut(reply, 'auth.bad_username');
}
const fixedEmail = fixEmail(email);
const fixedUsername = fixUsername(username);
{ //check if email is already in use
const existingUser = await Users.findOne({where: {email: fixedEmail}});
if(existingUser) return errorOut(reply, 'auth.email_taken');
}
{ //check if username taken
const existingUser = await Users.findOne({where: {username: fixedUsername}});
if(existingUser) return errorOut(reply, 'auth.username_taken');
}
{ //check if this ip already registered a bunch of accounts
const countByIp = await Users.count({where: {firstIp: request.ip}});
if(countByIp >= 10) return errorOut(reply, 'error.suspicious');
}
const newUser = {
email: fixedEmail,
username: fixedUsername,
token: generateToken(),
tokenExpiry: newTokenExpiry(),
paswd: hashPassword(reverseString(paswd)),
role: 'user',
firstIp: requestIp(request)
};
await Users.create(newUser);
return {token: newUser.token};
});
app.post('/auth/restore/request', async (request, reply) => {
const {email} = request.body || {};
if(!isValidEmail(email)) return errorOut(reply, 'auth.bad_email');
const fixedEmail = fixEmail(email);
const user = await Users.findOne({where: {email: fixedEmail}});
if(!user) return errorOut(reply, 'restore.not_found');
if(user.restoreExpiry && notYet(user.restoreExpiry)){
return errorOut(reply, 'restore.already_sent');
}
const restoreCode = generateRestoreCode();
const restoreExpiry = new Date(Date.now() + restoreValidity);
await user.update({restoreCode, restoreExpiry});
const success = await sendRestorationLink(user, app);
return {success, lifetime: restoreValidity};
});
app.post('/auth/restore/apply', async (request, reply) => {
const {email, code, newpaswd} = request.body || {};
const changeRequested = (newpaswd != null);
if(restoreApplySpam.check(requestIp(request), [[100, 12*hours], [25, 30*minutes]])){
return errorOut(reply, 'error.too_fast');
}
if(!isValidEmail(email)) return errorOut(reply, 'auth.bad_email');
if(changeRequested && (!isValidPassword(newpaswd))){
return errorOut(reply, 'auth.bad_paswd');
}
const fixedEmail = fixEmail(email);
const user = await Users.findOne({where: {email: fixedEmail}});
if(!user) return errorOut(reply, 'restore.not_found');
if((!notYet(user.restoreExpiry)) || (!user.restoreCode)){
return errorOut(reply, 'restore.expired');
}
if(user.restoreCode != code){
const restoreExpiry = user.restoreExpiry - (restoreValidity / restoreAttempts);
await user.update({restoreExpiry});
return errorOut(reply, 'restore.wrong_code');
}
if(changeRequested){
const hash = hashPassword(reverseString(newpaswd));
await user.update({paswd: hash, restoreCode: null, restoreExpiry: null});
}
return '👌';
});
}
function fixEmail(email){
const plusPortion = /\+.*@/m; //gmail exploit
return email.toLowerCase().replace(plusPortion, '');
}
function fixUsername(username){
if(username){
const profanities = /nigger|nigga|faggot/gi;
const badSymbols = /[\x00-\x1F\xCC\xCD]/g;
return username.replace(profanities, '💩').replace(badSymbols, '');
}else{
const nick = randomElement(Animals);
const tag = Date.now().toString().slice(-4);
return `Little ${nick} #${tag}`;
}
}
function isValidEmail(email){
return checkStringParam(email, 3, 128) && !!email.match('@');
}
function isValidUsername(username){
return (!username) || checkStringParam(username, 3, 64);
}
function isValidPassword(paswd){
return checkStringParam(paswd, 6, 256);
}
export default AuthController;