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.
128 lines
3.9 KiB
128 lines
3.9 KiB
|
4 years ago
|
import { checkStringParam, errorOut, randomElement, reverseString } from '../logic/common.js';
|
||
|
|
import { Animals } from '../misc/animals.js';
|
||
|
|
import { reissueToken, generateToken, newTokenExpiry, hashPassword, doesPasswordMatch } from '../logic/security.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;
|
||
|
|
|
||
|
|
{ //validate token header and put .user in every request
|
||
|
|
app.decorateRequest('user', null);
|
||
|
|
app.addHook('preHandler', async (request, reply) => {
|
||
|
|
const unprotectedUrl = (request.url.substring(0, 6) == '/auth/') || (request.url === '/');
|
||
|
|
|
||
|
|
if(!unprotectedUrl){
|
||
|
|
const token = request.headers.authorization;
|
||
|
|
//todo: local cache
|
||
|
|
|
||
|
|
const user = await Users.findOne({where: {token}});
|
||
|
|
const tokenNotExpired = user && (new Date() <= user.tokenExpiry);
|
||
|
|
const userNotBanned = true;
|
||
|
|
if(tokenNotExpired && userNotBanned){
|
||
|
|
request.user = user;
|
||
|
|
}else{
|
||
|
|
errorOut(reply, 'Unauthorized', 401);
|
||
|
|
return true; //abort
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
app.post('/auth/sign-in', async (request) => {
|
||
|
|
const {email, paswd} = request.body || {};
|
||
|
|
//todo: spam check
|
||
|
|
|
||
|
|
{ //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, Users);
|
||
|
|
}
|
||
|
|
return {token};
|
||
|
|
}else{
|
||
|
|
errorOut(reply, 'auth.not_found');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
app.post('/auth/sign-up', async (request, reply) => {
|
||
|
|
const {email, username, paswd} = request.body || {};
|
||
|
|
//todo: spam check
|
||
|
|
|
||
|
|
{ //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: request.ip
|
||
|
|
firstIp: request.hostname
|
||
|
|
};
|
||
|
|
await Users.create(newUser);
|
||
|
|
return {token: newUser.token};
|
||
|
|
});
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
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(10).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;
|