Finally have prettier and linting maybe working
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
10
lib/app.ts
10
lib/app.ts
@@ -1,8 +1,8 @@
|
||||
import Koa from 'koa';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import cookie from 'koa-cookie';
|
||||
import passport from 'koa-passport';
|
||||
import session from 'koa-session';
|
||||
import koa from 'koa';
|
||||
import koaBodyparser from 'koa-bodyparser';
|
||||
import koaCookie from 'koa-cookie';
|
||||
import koaPassport from 'koa-passport';
|
||||
import koaSession from 'koa-session';
|
||||
|
||||
import { performanceLogger, perfromanceTimer } from './middleware/performance';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import passport from 'koa-passport';
|
||||
import koaPassport from 'koa-passport';
|
||||
|
||||
// import Users from 'grow-db/lib/models/users';
|
||||
// import { User } from 'grow-db/lib/schemas/user';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export enum STRATEGIES {
|
||||
LOCAL,
|
||||
APPLE,
|
||||
FACEBOOK,
|
||||
FIDO2,
|
||||
GOOGLE,
|
||||
LOCAL,
|
||||
APPLE,
|
||||
FACEBOOK,
|
||||
FIDO2,
|
||||
GOOGLE,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,4 @@ const DB_HOST = process.env.DB_HOST || 'mongodb';
|
||||
const DB_PORT = process.env.DB_PORT || 27017;
|
||||
const DB_NAME = process.env.DB_NAME || 'auth';
|
||||
|
||||
export const connection = mongoose.connect(
|
||||
`${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}`
|
||||
);
|
||||
export const connection = mongoose.connect(`${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}`);
|
||||
|
||||
@@ -2,13 +2,12 @@ import { StatusCodes } from 'http-status-codes';
|
||||
import { Context, Next } from 'koa';
|
||||
|
||||
export const errorHandler = async (ctx: Context, next: Next) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (error: any) {
|
||||
ctx.status =
|
||||
error.statusCode || error.status || StatusCodes.INTERNAL_SERVER_ERROR;
|
||||
error.status = ctx.status;
|
||||
ctx.body = { error };
|
||||
ctx.app.emit('error', error, ctx);
|
||||
}
|
||||
try {
|
||||
await next();
|
||||
} catch (error: any) {
|
||||
ctx.status = error.statusCode || error.status || StatusCodes.INTERNAL_SERVER_ERROR;
|
||||
error.status = ctx.status;
|
||||
ctx.body = { error };
|
||||
ctx.app.emit('error', error, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@ import { Next } from 'koa';
|
||||
import { KoaContext } from '../types/KoaContext';
|
||||
|
||||
export const performanceLogger = async (ctx: KoaContext, next: Next) => {
|
||||
await next();
|
||||
const rt = ctx.response.get('X-Response-Time');
|
||||
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
|
||||
await next();
|
||||
const rt = ctx.response.get('X-Response-Time');
|
||||
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
|
||||
};
|
||||
|
||||
export const perfromanceTimer = async (ctx: KoaContext, next: Next) => {
|
||||
const start = Date.now();
|
||||
await next();
|
||||
const ms = Date.now() - start;
|
||||
ctx.set('X-Response-Time', `${ms}ms`);
|
||||
const start = Date.now();
|
||||
await next();
|
||||
const ms = Date.now() - start;
|
||||
ctx.set('X-Response-Time', `${ms}ms`);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { JwtPayload } from 'jsonwebtoken';
|
||||
import {
|
||||
InferSchemaType,
|
||||
Model,
|
||||
Schema,
|
||||
StringSchemaDefinition,
|
||||
Types,
|
||||
} from 'mongoose';
|
||||
import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
|
||||
|
||||
import { Strategy } from './strategy';
|
||||
import { STRATEGIES } from '../constants/strategies';
|
||||
@@ -14,139 +8,132 @@ import { encrypt, verify as verifyPassword } from '../utils/password';
|
||||
import { generateResetToken } from '../utils/tokens';
|
||||
|
||||
export type Auth = {
|
||||
is2FA?: boolean;
|
||||
record: StringSchemaDefinition;
|
||||
username: string;
|
||||
is2FA?: boolean;
|
||||
record: StringSchemaDefinition;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type AuthPrivate = Auth & {
|
||||
strategies: Types.ArraySubdocument<Strategy>;
|
||||
strategies: Types.ArraySubdocument<Strategy>;
|
||||
};
|
||||
|
||||
export interface AuthMethods {
|
||||
authenticate(password: string): boolean;
|
||||
getAuthStrategy(method?: STRATEGIES): Strategy | false;
|
||||
getResetLink(route: string): Promise<string | undefined>;
|
||||
getResetToken(): Promise<string | undefined>;
|
||||
getToken(props?: Omit<TokenProps, 'sub'>): string;
|
||||
setPassword(password: string): Promise<boolean>;
|
||||
authenticate(password: string): boolean;
|
||||
getAuthStrategy(method?: STRATEGIES): Strategy | false;
|
||||
getResetLink(route: string): Promise<string | undefined>;
|
||||
getResetToken(): Promise<string | undefined>;
|
||||
getToken(props?: Omit<TokenProps, 'sub'>): string;
|
||||
setPassword(password: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface AuthModel extends Model<AuthPrivate, {}, AuthMethods> {
|
||||
authenticate(password: any): boolean;
|
||||
findByUsername(username: string): Promise<AuthModel & AuthPrivate>;
|
||||
isUsernameAvailable(username: string): Promise<boolean>;
|
||||
findUserForReset(
|
||||
strategy: STRATEGIES,
|
||||
token: string
|
||||
): Promise<Strategy | undefined>;
|
||||
resetPassword(token: string, password: string): Promise<boolean>;
|
||||
authenticate(password: any): boolean;
|
||||
findByUsername(username: string): Promise<AuthModel & AuthPrivate>;
|
||||
isUsernameAvailable(username: string): Promise<boolean>;
|
||||
findUserForReset(strategy: STRATEGIES, token: string): Promise<Strategy | undefined>;
|
||||
resetPassword(token: string, password: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export const AuthSchema = new Schema<AuthPrivate, AuthModel, AuthMethods>(
|
||||
{
|
||||
is2FA: { type: Boolean, default: false },
|
||||
record: { type: Types.ObjectId },
|
||||
strategies: { type: Types.ArraySubdocument<Strategy>, required: true },
|
||||
username: { type: String, required: true, unique: true },
|
||||
},
|
||||
{
|
||||
minimize: true,
|
||||
timestamps: true,
|
||||
}
|
||||
{
|
||||
is2FA: { type: Boolean, default: false },
|
||||
record: { type: Types.ObjectId },
|
||||
strategies: { type: Types.ArraySubdocument<Strategy>, required: true },
|
||||
username: { type: String, required: true, unique: true },
|
||||
},
|
||||
{
|
||||
minimize: true,
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
AuthSchema.methods = {
|
||||
authenticate: function (password: string) {
|
||||
const strategy = this.getAuthStrategy(STRATEGIES.LOCAL);
|
||||
return !!strategy && verifyPassword(password, strategy.key);
|
||||
},
|
||||
authenticate(password: string) {
|
||||
const strategy = this.getAuthStrategy(STRATEGIES.LOCAL);
|
||||
return !!strategy && verifyPassword(password, strategy.key);
|
||||
},
|
||||
|
||||
getAuthStrategy: function (method = STRATEGIES.LOCAL) {
|
||||
return (
|
||||
this.strategies
|
||||
.filter((strategy: Strategy) => strategy.method === method)
|
||||
.pop() || false
|
||||
);
|
||||
},
|
||||
getAuthStrategy(method = STRATEGIES.LOCAL) {
|
||||
return this.strategies.filter((strategy: Strategy) => strategy.method === method).pop() || false;
|
||||
},
|
||||
|
||||
getToken: function (props = {}) {
|
||||
return sign({
|
||||
sub: this._id,
|
||||
...props,
|
||||
});
|
||||
},
|
||||
getToken(props = {}) {
|
||||
return sign({
|
||||
sub: this._id,
|
||||
...props,
|
||||
});
|
||||
},
|
||||
|
||||
getResetLink: async function (route) {
|
||||
const resetToken = await this.getResetToken();
|
||||
if (resetToken) {
|
||||
let resetRoute = route;
|
||||
resetRoute = resetRoute.replace(':user_id', this._id);
|
||||
resetRoute = resetRoute.replace(':reset_token?', resetToken);
|
||||
const resetUrl = `${process.env.URL}${resetRoute}`;
|
||||
console.log('[sendPasswordReset] resetUrl:', resetUrl);
|
||||
return resetUrl;
|
||||
}
|
||||
},
|
||||
async getResetLink(route) {
|
||||
const resetToken = await this.getResetToken();
|
||||
if (resetToken) {
|
||||
let resetRoute = route;
|
||||
resetRoute = resetRoute.replace(':user_id', this._id);
|
||||
resetRoute = resetRoute.replace(':reset_token?', resetToken);
|
||||
const resetUrl = `${process.env.URL}${resetRoute}`;
|
||||
console.log('[sendPasswordReset] resetUrl:', resetUrl);
|
||||
return resetUrl;
|
||||
}
|
||||
},
|
||||
|
||||
getResetToken: async function () {
|
||||
const { key, token } = generateResetToken(this._id);
|
||||
this.resetCheckBit = key;
|
||||
await this.save().catch(() => undefined);
|
||||
return token;
|
||||
},
|
||||
async getResetToken() {
|
||||
const { key, token } = generateResetToken(this._id);
|
||||
this.resetCheckBit = key;
|
||||
await this.save().catch(() => undefined);
|
||||
return token;
|
||||
},
|
||||
|
||||
setPassword: async function (password) {
|
||||
const key = encrypt(password);
|
||||
const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL);
|
||||
const strategy = {
|
||||
key,
|
||||
method: STRATEGIES.LOCAL,
|
||||
resetToken: undefined,
|
||||
};
|
||||
async setPassword(password) {
|
||||
const key = encrypt(password);
|
||||
const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL);
|
||||
const strategy = {
|
||||
key,
|
||||
method: STRATEGIES.LOCAL,
|
||||
resetToken: undefined,
|
||||
};
|
||||
|
||||
if (hasLocalStrategy) {
|
||||
await this.model('User')
|
||||
.findOneAndUpdate(
|
||||
{ _id: this._id, 'strategies.method': STRATEGIES.LOCAL },
|
||||
{ $set: { 'strategies.$': strategy } },
|
||||
{ upsert: true },
|
||||
)
|
||||
.catch();
|
||||
return true;
|
||||
}
|
||||
this.credentials.push(strategy);
|
||||
await this.save().catch(() => false);
|
||||
return true;
|
||||
}
|
||||
if (hasLocalStrategy) {
|
||||
await this.model('User')
|
||||
.findOneAndUpdate(
|
||||
{ _id: this._id, 'strategies.method': STRATEGIES.LOCAL },
|
||||
{ $set: { 'strategies.$': strategy } },
|
||||
{ upsert: true },
|
||||
)
|
||||
.catch();
|
||||
return true;
|
||||
}
|
||||
this.credentials.push(strategy);
|
||||
await this.save().catch(() => false);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
AuthSchema.statics = {
|
||||
// authenticateAndGetRecordLocator: async function (username, password) {
|
||||
// const auth = await this.findByUserName(username);
|
||||
// if (auth && auth.authenticate(password)) {
|
||||
// return auth?.record;
|
||||
// }
|
||||
// return false;
|
||||
// },
|
||||
// authenticateAndGetRecordLocator: async function (username, password) {
|
||||
// const auth = await this.findByUserName(username);
|
||||
// if (auth && auth.authenticate(password)) {
|
||||
// return auth?.record;
|
||||
// }
|
||||
// return false;
|
||||
// },
|
||||
|
||||
findByUsername: async function (username) {
|
||||
return this.findOne({ username });
|
||||
},
|
||||
async findByUsername(username) {
|
||||
return this.findOne({ username });
|
||||
},
|
||||
|
||||
isUsernameAvailable: async function (username) {
|
||||
return !this.findByUsername(username);
|
||||
},
|
||||
async isUsernameAvailable(username) {
|
||||
return !this.findByUsername(username);
|
||||
},
|
||||
|
||||
resetPassword: async function (token, password) {
|
||||
const decoded = verifyJwt(token);
|
||||
const { sub, key } = decoded as JwtPayload;
|
||||
const auth = await this.findOne({
|
||||
_id: sub,
|
||||
'strategies.resetToken': key,
|
||||
}).catch();
|
||||
return !!auth && auth.setPassword(password);
|
||||
}
|
||||
async resetPassword(token, password) {
|
||||
const decoded = verifyJwt(token);
|
||||
const { sub, key } = decoded as JwtPayload;
|
||||
const auth = await this.findOne({
|
||||
_id: sub,
|
||||
'strategies.resetToken': key,
|
||||
}).catch();
|
||||
return !!auth && auth.setPassword(password);
|
||||
},
|
||||
};
|
||||
|
||||
export type AuthSchema = InferSchemaType<typeof AuthSchema>;
|
||||
|
||||
@@ -2,24 +2,24 @@ import { InferSchemaType, Schema, Types } from 'mongoose';
|
||||
import { STRATEGIES } from '../constants/strategies';
|
||||
|
||||
export const Strategy = new Schema(
|
||||
{
|
||||
method: {
|
||||
type: Number,
|
||||
enum: Object.values(STRATEGIES),
|
||||
index: true,
|
||||
required: true,
|
||||
unique: true
|
||||
{
|
||||
method: {
|
||||
type: Number,
|
||||
enum: Object.values(STRATEGIES),
|
||||
index: true,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
externalId: { type: String, index: true },
|
||||
key: { type: String, required: true, trim: true },
|
||||
profile: {},
|
||||
resetToken: { type: String },
|
||||
forceReset: { type: Boolean },
|
||||
},
|
||||
{
|
||||
minimize: true,
|
||||
timestamps: true,
|
||||
},
|
||||
externalId: { type: String, index: true },
|
||||
key: { type: String, required: true, trim: true },
|
||||
profile: {},
|
||||
resetToken: { type: String },
|
||||
forceReset: { type: Boolean }
|
||||
},
|
||||
{
|
||||
minimize: true,
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export type Strategy = InferSchemaType<typeof Strategy>;
|
||||
|
||||
@@ -2,11 +2,12 @@ import dotenv from 'dotenv';
|
||||
|
||||
import app from './app';
|
||||
import { connection } from './database/database.connection';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const PORT: number = Number(process.env.PORT) || 9000;
|
||||
|
||||
connection.then(
|
||||
() => app.listen(PORT),
|
||||
(err) => console.error('ERROR!', err),
|
||||
() => app.listen(PORT),
|
||||
(err) => console.error('ERROR!', err),
|
||||
);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import passport from 'koa-passport';
|
||||
import koaPassport from 'koa-passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
|
||||
import Auth from '../model/auth';
|
||||
|
||||
export const localStrategy = passport.use(
|
||||
new Strategy(async (username, password, done) => {
|
||||
const user = await Auth.findOne({
|
||||
where: {
|
||||
username,
|
||||
}
|
||||
}).catch();
|
||||
if (user && user.authenticate(password)) {
|
||||
done(null, user);
|
||||
} else {
|
||||
done(null, false);
|
||||
}
|
||||
})
|
||||
new Strategy(async (username, password, done) => {
|
||||
const user = await Auth.findOne({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
}).catch();
|
||||
if (user && user.authenticate(password)) {
|
||||
done(null, user);
|
||||
} else {
|
||||
done(null, false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import Auth from '../model/auth';
|
||||
import auth from '../model/auth';
|
||||
import { AuthModel, AuthPrivate } from '../schema/auth';
|
||||
import { sign } from './jwt';
|
||||
|
||||
export const getAuthenticationBundle = async (
|
||||
username: string,
|
||||
password: string,
|
||||
) => {
|
||||
const auth = await Auth.findByUsername(username).catch();
|
||||
const isAuthenticated = !!auth && (auth as AuthModel).authenticate(password);
|
||||
const record = isAuthenticated ? (auth as AuthPrivate).record : null;
|
||||
const token = isAuthenticated ? (auth as AuthModel).getToken() : sign();
|
||||
return {
|
||||
record,
|
||||
token,
|
||||
};
|
||||
export const getAuthenticationBundle = async (username: string, password: string) => {
|
||||
const auth = await Auth.findByUsername(username).catch();
|
||||
const isAuthenticated = !!auth && (auth as AuthModel).authenticate(password);
|
||||
const record = isAuthenticated ? (auth as AuthPrivate).record : null;
|
||||
const token = isAuthenticated ? (auth as AuthModel).getToken() : sign();
|
||||
return {
|
||||
record,
|
||||
token,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,40 +1,35 @@
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import jsonwebtoken, { JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
export interface TokenProps {
|
||||
aud?: string;
|
||||
exp?: number | Date;
|
||||
iss?: string;
|
||||
sub: string | null;
|
||||
[key: string]: any;
|
||||
aud?: string;
|
||||
exp?: number | Date;
|
||||
iss?: string;
|
||||
sub: string | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type SignProps = string | TokenProps | void;
|
||||
|
||||
export const sign = (props: SignProps) => {
|
||||
const today = new Date();
|
||||
const { sub = null, ...rest }: TokenProps =
|
||||
typeof props === 'string' || typeof props === 'undefined'
|
||||
? { sub: props || null }
|
||||
: props;
|
||||
let exp = rest.exp;
|
||||
if (!exp) {
|
||||
exp = new Date(today);
|
||||
exp.setDate(
|
||||
today.getDate() + parseInt(process.env.JWT_DAYS_VALID as string),
|
||||
const today = new Date();
|
||||
const { sub = null, ...rest }: TokenProps =
|
||||
typeof props === 'string' || typeof props === 'undefined' ? { sub: props || null } : props;
|
||||
let { exp } = rest;
|
||||
if (!exp) {
|
||||
exp = new Date(today);
|
||||
exp.setDate(today.getDate() + parseInt(process.env.JWT_DAYS_VALID as string));
|
||||
exp = exp.getTime() / 1000;
|
||||
}
|
||||
return jwt.sign(
|
||||
{
|
||||
exp,
|
||||
sub,
|
||||
aud: rest.aud || process.env.JWT_AUDIENCE,
|
||||
iat: today.getTime(),
|
||||
iss: rest.iss || process.env.JWT_ISSUER,
|
||||
},
|
||||
process.env.JWT_SECRET || 'secret',
|
||||
);
|
||||
exp = exp.getTime() / 1000;
|
||||
}
|
||||
return jwt.sign(
|
||||
{
|
||||
exp,
|
||||
sub,
|
||||
aud: rest.aud || process.env.JWT_AUDIENCE,
|
||||
iat: today.getTime(),
|
||||
iss: rest.iss || process.env.JWT_ISSUER,
|
||||
},
|
||||
process.env.JWT_SECRET || 'secret',
|
||||
);
|
||||
};
|
||||
|
||||
export const verify = (token: string) =>
|
||||
jwt.verify(token, process.env.JWT_SECRET || 'secret');
|
||||
export const verify = (token: string) => jwt.verify(token, process.env.JWT_SECRET || 'secret');
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { pbkdf2Sync, randomBytes } from 'crypto';
|
||||
|
||||
export const encrypt = (password: string) => {
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
const hash = pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
|
||||
return `${salt}:${hash}`;
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
const hash = pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
|
||||
return `${salt}:${hash}`;
|
||||
};
|
||||
|
||||
export const verify = (test: string, secret: string) => {
|
||||
const [salt, hash] = secret.split(':');
|
||||
return pbkdf2Sync(test, salt, 10000, 512, 'sha512').toString('hex') === hash;
|
||||
const [salt, hash] = secret.split(':');
|
||||
return pbkdf2Sync(test, salt, 10000, 512, 'sha512').toString('hex') === hash;
|
||||
};
|
||||
|
||||
@@ -3,11 +3,11 @@ import crypto from 'crypto';
|
||||
import { sign } from './jwt';
|
||||
|
||||
export const generateResetToken = (sub: string) => {
|
||||
const key = crypto.randomBytes(16).toString('hex');
|
||||
const token = sign({
|
||||
sub,
|
||||
key,
|
||||
exp: Date.now() + 24 * 60 * 60 * 1000,
|
||||
});
|
||||
return { key, token };
|
||||
const key = crypto.randomBytes(16).toString('hex');
|
||||
const token = sign({
|
||||
sub,
|
||||
key,
|
||||
exp: Date.now() + 24 * 60 * 60 * 1000,
|
||||
});
|
||||
return { key, token };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user