Prettier?
Some checks failed
continuous-integration/drone/pr Build is failing

This commit is contained in:
2023-05-24 10:46:24 -04:00
parent 65e30ba1c7
commit 35b89062e4
36 changed files with 482 additions and 553 deletions

19
.eslintrc.js Normal file
View File

@@ -0,0 +1,19 @@
/* eslint-env node */
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:prettier/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': 'typescript',
},
root: true,
};

View File

@@ -1,8 +1,9 @@
.build.yarnrc.yml
.drone.yml
.prettierrc
.yarnrc.yml
babel.config.*
jest.config.*
src src
tsconfig.json tsconfig.json
tslint.json tslint.json
.prettierrc
.yarnrc.yml
.drone.yml
babel.config.*
jest.config.*

7
.prettierrc.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
parser: 'typescript',
printWidth: 120,
trailingComma: 'all',
tabWidth: 4,
singleQuote: true,
};

View File

@@ -1,17 +1,17 @@
import { Auth, Log } from ".."; import { Auth, Log } from '..';
import { Action } from "../constants/action"; import { Action } from '../constants/action';
import { getLoginToken } from "../utils/getLoginToken"; import { getLoginToken } from '../utils/getLoginToken';
export const authenticate = async (username: string, password: string) => { export const authenticate = async (username: string, password: string) => {
const doc = await Auth.findByUsername(username).catch(); const doc = await Auth.findByUsername(username).catch();
if (!!doc && (await doc.authenticate(password))) { if (!!doc && (await doc.authenticate(password))) {
Log.add(doc.id, Action.AUTHENTICATE); Log.add(doc.id, Action.AUTHENTICATE);
return { ...doc, token: getLoginToken(doc) }; return { ...doc, token: getLoginToken(doc) };
} }
if (doc) { if (doc) {
Log.add(doc.id, Action.AUTHENTICATE_FAILURE); Log.add(doc.id, Action.AUTHENTICATE_FAILURE);
} }
return false; return false;
}; };

View File

@@ -1,15 +1,15 @@
import { StringSchemaDefinition } from "mongoose"; import { StringSchemaDefinition } from 'mongoose';
import { Auth, Strategy } from ".."; import { Auth, Strategy } from '..';
export const deleteStrategy = async (id: StringSchemaDefinition) => { export const deleteStrategy = async (id: StringSchemaDefinition) => {
const strategy = await Strategy.findById(id); const strategy = await Strategy.findById(id);
if (strategy) { if (strategy) {
const parentId = strategy.parent; const parentId = strategy.parent;
await strategy.deleteOne(); await strategy.deleteOne();
await Auth.findOneAndUpdate({ id: parentId, strategies: { $pull: id } }); await Auth.findOneAndUpdate({ id: parentId, strategies: { $pull: id } });
return true; return true;
} }
return false; return false;
}; };

View File

@@ -1,18 +1,18 @@
import { readOneByUsername } from "../dao/readOneByUsername"; import { readOneByUsername } from '../dao/readOneByUsername';
import { Log, Token } from ".."; import { Log, Token } from '..';
import { TokenType } from "../constants/tokens"; import { TokenType } from '../constants/tokens';
import { Action } from "../constants/action"; import { Action } from '../constants/action';
export const resetPasswordGet = async (username: string) => { export const resetPasswordGet = async (username: string) => {
const doc = await readOneByUsername(username); const doc = await readOneByUsername(username);
if (doc) { if (doc) {
Log.add(doc._id, Action.RESET_REQUEST); Log.add(doc._id, Action.RESET_REQUEST);
return { return {
record: doc.record, record: doc.record,
token: Token.getToken(TokenType.RESET, doc._id), token: Token.getToken(TokenType.RESET, doc._id),
}; };
} }
return false; return false;
}; };

View File

@@ -1,41 +1,41 @@
import { Types } from "mongoose"; import { Types } from 'mongoose';
import { Log, Strategy, Token } from ".."; import { Log, Strategy, Token } from '..';
import { STRATEGIES } from "../constants/strategies"; import { STRATEGIES } from '../constants/strategies';
import { AuthDocument } from "../schema/auth"; import { AuthDocument } from '../schema/auth';
import { getLoginToken } from "../utils/getLoginToken"; import { getLoginToken } from '../utils/getLoginToken';
import { StrategyDocument } from "../schema/strategy"; import { StrategyDocument } from '../schema/strategy';
import { Action } from "../constants/action"; import { Action } from '../constants/action';
export const resetPasswordPost = async (token: string, password: string) => { export const resetPasswordPost = async (token: string, password: string) => {
const parentId = await Token.validateResetToken(token); const parentId = await Token.validateResetToken(token);
if (parentId) { if (parentId) {
let parent: AuthDocument; let parent: AuthDocument;
let strategy: StrategyDocument | null = await Strategy.findOne({ let strategy: StrategyDocument | null = await Strategy.findOne({
parent: parentId, parent: parentId,
method: STRATEGIES.LOCAL, method: STRATEGIES.LOCAL,
}); });
if (strategy) { if (strategy) {
parent = await strategy.getAuthRecord(); parent = await strategy.getAuthRecord();
strategy.key = password; strategy.key = password;
await strategy.save(); await strategy.save();
} else { } else {
strategy = await Strategy.create({ strategy = await Strategy.create({
key: password, key: password,
method: STRATEGIES.LOCAL, method: STRATEGIES.LOCAL,
parent: <Types.ObjectId>parentId, parent: <Types.ObjectId>parentId,
}); });
parent = await strategy.getAuthRecord(); parent = await strategy.getAuthRecord();
parent.strategies.push(strategy._id); parent.strategies.push(strategy._id);
await parent.save(); await parent.save();
}
Log.add(parent._id, Action.RESET);
return { record: parent.record, token: getLoginToken(parent) };
} }
Log.add(parent._id, Action.RESET); return false;
return { record: parent.record, token: getLoginToken(parent) };
}
return false;
}; };

View File

@@ -1,9 +1,9 @@
export enum Action { export enum Action {
AUTHENTICATE = "AUTHENTICATE", AUTHENTICATE = 'AUTHENTICATE',
AUTHENTICATE_FAILURE = "AUTHENTICATE_FAILURE", AUTHENTICATE_FAILURE = 'AUTHENTICATE_FAILURE',
CREATE = "CREATE", CREATE = 'CREATE',
DELETE = "DELETE", DELETE = 'DELETE',
RESET = "RESET", RESET = 'RESET',
RESET_REQUEST = "RESET_REQUEST", RESET_REQUEST = 'RESET_REQUEST',
UPDATE = "UPDATE", UPDATE = 'UPDATE',
} }

View File

@@ -1,8 +1,8 @@
export enum Status { export enum Status {
ACTIVE, ACTIVE,
BLOCK_HARD, BLOCK_HARD,
BLOCK_SOFT, BLOCK_SOFT,
DELETED, DELETED,
INACTIVE, INACTIVE,
UNVERIFIED, UNVERIFIED,
} }

View File

@@ -4,7 +4,7 @@ export const DB_USERNAME = process.env.DB_USERNAME;
export const DB_PASSWORD = process.env.DB_PASSWORD; export const DB_PASSWORD = process.env.DB_PASSWORD;
export const DB_NAME = process.env.DB_NAME; export const DB_NAME = process.env.DB_NAME;
export const COLL_AUTH = "Auth"; export const COLL_AUTH = 'Auth';
export const COLL_LOG = "Log"; export const COLL_LOG = 'Log';
export const COLL_STRATEGY = "Strategy"; export const COLL_STRATEGY = 'Strategy';
export const COLL_TOKEN = "Token"; export const COLL_TOKEN = 'Token';

View File

@@ -1,21 +1,20 @@
export const PACKAGE_NAME = "@mifi/auth"; export const PACKAGE_NAME = '@mifi/auth';
export const PORT = process.env.PORT || 9000; export const PORT = process.env.PORT || 9000;
export const SESSION_KEY = process.env.SESSION_KEY || "secret-key"; export const SESSION_KEY = process.env.SESSION_KEY || 'secret-key';
export const JWT_AUDIENCE = process.env.JWT_AUDIENCE || "mifi.dev"; export const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'mifi.dev';
export const JWT_ISSUER = process.env.JWT_ISSUER || PACKAGE_NAME; export const JWT_ISSUER = process.env.JWT_ISSUER || PACKAGE_NAME;
export const JWT_SECRET = process.env.JWT_SECRET || "secret"; export const JWT_SECRET = process.env.JWT_SECRET || 'secret';
export const LOGIN_VALID_TIMEOUT = process.env.LOGIN_VALID_TIMEOUT || "12h"; // ###d|h|m export const LOGIN_VALID_TIMEOUT = process.env.LOGIN_VALID_TIMEOUT || '12h'; // ###d|h|m
export const RESET_VALID_TIMEOUT = process.env.RESET_VALID_TIMEOUT || "15m"; // ###d|h|m export const RESET_VALID_TIMEOUT = process.env.RESET_VALID_TIMEOUT || '15m'; // ###d|h|m
export const VERIFY_VALID_TIMEOUT = process.env.VERIFY_VALID_TIMEOUT || "60d"; // ###d|h|m export const VERIFY_VALID_TIMEOUT = process.env.VERIFY_VALID_TIMEOUT || '60d'; // ###d|h|m
export const DEFAULT_TOKEN_DAYS = process.env.DEFAULT_TOKEN_DAYS || 365; export const DEFAULT_TOKEN_DAYS = process.env.DEFAULT_TOKEN_DAYS || 365;
export const ROUTE_PREFIX = process.env.ROUTE_PREFIX || "/auth"; export const ROUTE_PREFIX = process.env.ROUTE_PREFIX || '/auth';
export const LOGIN_ROUTE = process.env.LOGIN_ROUTE || "/login"; export const LOGIN_ROUTE = process.env.LOGIN_ROUTE || '/login';
export const RESET_ROUTE = process.env.RESET_ROUTE || "/reset"; export const RESET_ROUTE = process.env.RESET_ROUTE || '/reset';
export const VERIFICATION_ROUTE = export const VERIFICATION_ROUTE = process.env.VERIFICATION_ROUTE || '/verification';
process.env.VERIFICATION_ROUTE || "/verification";
export const REQUIRE_VERIFICATION = process.env.REQUIRE_VERIFICATION || true; export const REQUIRE_VERIFICATION = process.env.REQUIRE_VERIFICATION || true;

View File

@@ -1,13 +1,12 @@
export enum ErrorCodes { export enum ErrorCodes {
RESET_REQUEST_DATA = "RESET_REQUEST_DATA", RESET_REQUEST_DATA = 'RESET_REQUEST_DATA',
} }
export const ErrorMessages = { export const ErrorMessages = {
[ErrorCodes.RESET_REQUEST_DATA]: [ErrorCodes.RESET_REQUEST_DATA]: 'A valid username and password must be provided',
"A valid username and password must be provided",
}; };
export const getErrorBody = (code: ErrorCodes) => ({ export const getErrorBody = (code: ErrorCodes) => ({
code, code,
message: ErrorMessages[code], message: ErrorMessages[code],
}); });

View File

@@ -1,7 +1,7 @@
export enum STRATEGIES { export enum STRATEGIES {
LOCAL, LOCAL,
APPLE, APPLE,
FACEBOOK, FACEBOOK,
FIDO2, FIDO2,
GOOGLE, GOOGLE,
} }

View File

@@ -1,4 +1,4 @@
export enum TokenType { export enum TokenType {
RESET = "RESET", RESET = 'RESET',
VERIFICATION = "VERIFICATION", VERIFICATION = 'VERIFICATION',
} }

View File

@@ -1,51 +1,45 @@
import { DatabaseError } from "@mifi/services-common/lib/domain/errors/DatabaseError"; import { DatabaseError } from '@mifi/services-common/lib/domain/errors/DatabaseError';
import { Auth, Log, Strategy, Token } from ".."; import { Auth, Log, Strategy, Token } from '..';
import { Auth as AuthProps } from "../schema/auth"; import { Auth as AuthProps } from '../schema/auth';
import { STRATEGIES } from "../constants/strategies"; import { STRATEGIES } from '../constants/strategies';
import { REQUIRE_VERIFICATION } from "../constants/env"; import { REQUIRE_VERIFICATION } from '../constants/env';
import { TokenType } from "../constants/tokens"; import { TokenType } from '../constants/tokens';
import { Status } from "../constants/auth"; import { Status } from '../constants/auth';
import { Action } from "../constants/action"; import { Action } from '../constants/action';
export const create = async ({ export const create = async ({ record, username, password }: AuthProps & { password: string }) => {
record, const status = REQUIRE_VERIFICATION ? Status.UNVERIFIED : Status.ACTIVE;
username, const doc = await Auth.create({
password, record,
}: AuthProps & { password: string }) => { status,
const status = REQUIRE_VERIFICATION ? Status.UNVERIFIED : Status.ACTIVE; username,
const doc = await Auth.create({
record,
status,
username,
}).catch((err) => {
throw new DatabaseError("failed to create user", { err });
});
if (doc) {
const strategy = await Strategy.create({
method: STRATEGIES.LOCAL,
key: password,
parent: doc._id,
}).catch((err) => { }).catch((err) => {
throw new DatabaseError("failed to create strategy", { err }); throw new DatabaseError('failed to create user', { err });
}); });
if (strategy) { if (doc) {
doc.strategies.push(strategy._id); const strategy = await Strategy.create({
await doc.save(); method: STRATEGIES.LOCAL,
Log.add(doc._id, Action.CREATE); key: password,
return { parent: doc._id,
doc, }).catch((err) => {
token: throw new DatabaseError('failed to create strategy', { err });
REQUIRE_VERIFICATION && });
(await Token.getToken(TokenType.VERIFICATION, doc._id)), if (strategy) {
}; doc.strategies.push(strategy._id);
await doc.save();
Log.add(doc._id, Action.CREATE);
return {
doc,
token: REQUIRE_VERIFICATION && (await Token.getToken(TokenType.VERIFICATION, doc._id)),
};
}
await doc.deleteOne((err) => {
throw new DatabaseError('failed to remove invalid auth record', {
err,
doc,
});
});
} }
await doc.deleteOne((err) => { return null;
throw new DatabaseError("failed to remove invalid auth record", {
err,
doc,
});
});
}
return null;
}; };

View File

@@ -1,20 +1,20 @@
import { StringSchemaDefinition } from "mongoose"; import { StringSchemaDefinition } from 'mongoose';
import { Auth, Log, Strategy, Token } from ".."; import { Auth, Log, Strategy, Token } from '..';
import { Status } from "../constants/auth"; import { Status } from '../constants/auth';
import { Action } from "../constants/action"; import { Action } from '../constants/action';
export const deleteById = async (id: StringSchemaDefinition) => { export const deleteById = async (id: StringSchemaDefinition) => {
if ( if (
await Auth.findByIdAndUpdate(id, { await Auth.findByIdAndUpdate(id, {
status: Status.DELETED, status: Status.DELETED,
strategies: [], strategies: [],
}).catch() }).catch()
) { ) {
await Strategy.deleteMany({ parent: id }); await Strategy.deleteMany({ parent: id });
await Token.deleteMany({ auth: id }); await Token.deleteMany({ auth: id });
Log.add(id, Action.DELETE); Log.add(id, Action.DELETE);
return true; return true;
} }
return false; return false;
}; };

View File

@@ -1,11 +1,9 @@
import { FilterQuery } from "mongoose"; import { FilterQuery } from 'mongoose';
import { Auth } from "../model/auth"; import { Auth } from '../model/auth';
import { Status } from "../constants/auth"; import { Status } from '../constants/auth';
import { AuthDocument } from "../schema/auth"; import { AuthDocument } from '../schema/auth';
export const readAll = async (query: FilterQuery<AuthDocument> = {}) => export const readAll = async (query: FilterQuery<AuthDocument> = {}) => Auth.find(query);
Auth.find(query);
export const readAllActive = async () => export const readAllActive = async () => readAll({ status: { $ne: Status.DELETED } });
readAll({ status: { $ne: Status.DELETED } });

View File

@@ -1,5 +1,5 @@
import { Types } from "mongoose"; import { Types } from 'mongoose';
import { Auth } from "../model/auth"; import { Auth } from '../model/auth';
export const readOneById = async (id: Types.ObjectId) => Auth.findById(id); export const readOneById = async (id: Types.ObjectId) => Auth.findById(id);

View File

@@ -1,6 +1,5 @@
import { Types } from "mongoose"; import { Types } from 'mongoose';
import { Auth } from "../model/auth"; import { Auth } from '../model/auth';
export const readOneByRecord = async (record: Types.ObjectId) => export const readOneByRecord = async (record: Types.ObjectId) => Auth.findOne({ record });
Auth.findOne({ record });

View File

@@ -1,4 +1,3 @@
import { Auth } from "../model/auth"; import { Auth } from '../model/auth';
export const readOneByUsername = async (username: string) => export const readOneByUsername = async (username: string) => Auth.findOne({ username });
Auth.findOne({ username });

View File

@@ -1,27 +1,17 @@
import mongoose from "mongoose"; import mongoose from 'mongoose';
import { import { DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USERNAME } from './constants/db';
DB_HOST, import { Auth } from './model/auth';
DB_NAME, import { Log } from './model/log';
DB_PASSWORD, import { Strategy } from './model/strategy';
DB_PORT, import { Token } from './model/token';
DB_USERNAME,
} from "./constants/db";
import { Auth } from "./model/auth";
import { Log } from "./model/log";
import { Strategy } from "./model/strategy";
import { Token } from "./model/token";
const connection = mongoose const connection = mongoose
.connect( .connect(`mongodb://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}`)
`mongodb://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}` .then((c) => console.debug('Database connection established', { connection: c }))
) .catch((error) => {
.then((c) => console.error('Mongo connection failure', error);
console.debug("Database connection established", { connection: c }) process.exit(1);
) });
.catch((error) => {
console.error("Mongo connection failure", error);
process.exit(1);
});
export { connection, Auth, Log, Strategy, Token }; export { connection, Auth, Log, Strategy, Token };

View File

@@ -1,9 +1,6 @@
import mongoose from "mongoose"; import mongoose from 'mongoose';
import { AuthDocument, AuthModel, AuthSchema } from "../schema/auth"; import { AuthDocument, AuthModel, AuthSchema } from '../schema/auth';
import { COLL_AUTH } from "../constants/db"; import { COLL_AUTH } from '../constants/db';
export const Auth = mongoose.model<AuthDocument, AuthModel>( export const Auth = mongoose.model<AuthDocument, AuthModel>(COLL_AUTH, AuthSchema);
COLL_AUTH,
AuthSchema
);

View File

@@ -1,6 +1,6 @@
import mongoose from "mongoose"; import mongoose from 'mongoose';
import { LogModel, Log as LogDocument, LogSchema } from "../schema/log"; import { LogModel, Log as LogDocument, LogSchema } from '../schema/log';
import { COLL_LOG } from "../constants/db"; import { COLL_LOG } from '../constants/db';
export const Log = mongoose.model<LogDocument, LogModel>(COLL_LOG, LogSchema); export const Log = mongoose.model<LogDocument, LogModel>(COLL_LOG, LogSchema);

View File

@@ -1,13 +1,6 @@
import mongoose from "mongoose"; import mongoose from 'mongoose';
import { import { StrategyDocument, StrategyModel, StrategySchema } from '../schema/strategy';
StrategyDocument, import { COLL_STRATEGY } from '../constants/db';
StrategyModel,
StrategySchema,
} from "../schema/strategy";
import { COLL_STRATEGY } from "../constants/db";
export const Strategy = mongoose.model<StrategyDocument, StrategyModel>( export const Strategy = mongoose.model<StrategyDocument, StrategyModel>(COLL_STRATEGY, StrategySchema);
COLL_STRATEGY,
StrategySchema
);

View File

@@ -1,13 +1,6 @@
import mongoose from "mongoose"; import mongoose from 'mongoose';
import { import { TokenModel, Token as TokenDocument, TokenSchema } from '../schema/token';
TokenModel, import { COLL_TOKEN } from '../constants/db';
Token as TokenDocument,
TokenSchema,
} from "../schema/token";
import { COLL_TOKEN } from "../constants/db";
export const Token = mongoose.model<TokenDocument, TokenModel>( export const Token = mongoose.model<TokenDocument, TokenModel>(COLL_TOKEN, TokenSchema);
COLL_TOKEN,
TokenSchema
);

View File

@@ -1,98 +1,80 @@
import { import { Document, InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
Document,
InferSchemaType,
Model,
Schema,
StringSchemaDefinition,
Types,
} from "mongoose";
import { Status } from "../constants/auth"; import { Status } from '../constants/auth';
import { COLL_STRATEGY } from "../constants/db"; import { COLL_STRATEGY } from '../constants/db';
import { STRATEGIES } from "../constants/strategies"; import { STRATEGIES } from '../constants/strategies';
import { StrategyDocument } from "./strategy"; import { StrategyDocument } from './strategy';
import { verify } from "../utils/password"; import { verify } from '../utils/password';
export interface Auth { export interface Auth {
is2FA?: boolean; is2FA?: boolean;
record: StringSchemaDefinition; record: StringSchemaDefinition;
username: string; username: string;
status: Status; status: Status;
strategies: Types.ObjectId[] | StrategyDocument[]; strategies: Types.ObjectId[] | StrategyDocument[];
} }
interface AuthBaseDocument extends Auth, Document { interface AuthBaseDocument extends Auth, Document {
authenticate(password: string): Promise<boolean>; authenticate(password: string): Promise<boolean>;
getStrategy(method?: STRATEGIES): Promise<StrategyDocument | null>; getStrategy(method?: STRATEGIES): Promise<StrategyDocument | null>;
} }
export interface AuthDocument extends AuthBaseDocument { export interface AuthDocument extends AuthBaseDocument {
strategies: Types.ObjectId[]; strategies: Types.ObjectId[];
} }
export interface AuthPopulatedDocument extends AuthBaseDocument { export interface AuthPopulatedDocument extends AuthBaseDocument {
strategies: StrategyDocument[]; strategies: StrategyDocument[];
} }
export interface AuthModel extends Model<AuthDocument> { export interface AuthModel extends Model<AuthDocument> {
findByUsername(username: string): Promise<AuthDocument>; findByUsername(username: string): Promise<AuthDocument>;
getLocalStrategyForUsername(username: string): Promise<StrategyDocument>; getLocalStrategyForUsername(username: string): Promise<StrategyDocument>;
isUsernameAvailable(username: string): Promise<boolean>; isUsernameAvailable(username: string): Promise<boolean>;
} }
export const AuthSchema = new Schema<AuthDocument, AuthModel>( export const AuthSchema = new Schema<AuthDocument, AuthModel>(
{ {
is2FA: { type: Boolean, default: false }, is2FA: { type: Boolean, default: false },
record: { type: Types.ObjectId, unique: true }, record: { type: Types.ObjectId, unique: true },
status: { status: {
type: Number, type: Number,
enum: Status, enum: Status,
default: Status.UNVERIFIED, default: Status.UNVERIFIED,
index: true, index: true,
},
strategies: [{ type: Types.ObjectId, ref: COLL_STRATEGY, default: [] }],
username: { type: String, required: true, unique: true },
},
{
minimize: true,
timestamps: true,
}, },
strategies: [{ type: Types.ObjectId, ref: COLL_STRATEGY, default: [] }],
username: { type: String, required: true, unique: true },
},
{
minimize: true,
timestamps: true,
}
); );
AuthSchema.methods.authenticate = async function ( AuthSchema.methods.authenticate = async function (this: AuthBaseDocument, password: string) {
this: AuthBaseDocument, const strategy = await this.getStrategy();
password: string return !!strategy && verify(password, strategy.key);
) {
const strategy = await this.getStrategy();
return !!strategy && verify(password, strategy.key);
}; };
AuthSchema.methods.getStrategy = async function ( AuthSchema.methods.getStrategy = async function (this: AuthBaseDocument, method = STRATEGIES.LOCAL) {
this: AuthBaseDocument, const doc = await this.populate<{ strategies: StrategyDocument[] }>('strategies');
method = STRATEGIES.LOCAL return doc.strategies.filter((strategy) => strategy.method === method).pop() || null;
) {
const doc = await this.populate<{ strategies: StrategyDocument[] }>(
"strategies"
);
return (
doc.strategies.filter((strategy) => strategy.method === method).pop() ||
null
);
}; };
AuthSchema.statics = { AuthSchema.statics = {
async findByUsername(username) { async findByUsername(username) {
return this.findOne({ username }); return this.findOne({ username });
}, },
async getLocalStrategyForUsername(username) { async getLocalStrategyForUsername(username) {
const doc = await this.findByUsername(username); const doc = await this.findByUsername(username);
return !!doc && doc.getStrategy(); return !!doc && doc.getStrategy();
}, },
async isUsernameAvailable(username) { async isUsernameAvailable(username) {
return !this.findByUsername(username); return !this.findByUsername(username);
}, },
}; };
export type AuthSchema = InferSchemaType<typeof AuthSchema>; export type AuthSchema = InferSchemaType<typeof AuthSchema>;

View File

@@ -1,51 +1,45 @@
import { import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
InferSchemaType,
Model,
Schema,
StringSchemaDefinition,
Types,
} from "mongoose";
import { Payload } from "@mifi/services-common/lib/types/Payload"; import { Payload } from '@mifi/services-common/lib/types/Payload';
import { Action } from "../constants/action"; import { Action } from '../constants/action';
export interface Log { export interface Log {
action: Action; action: Action;
auth: StringSchemaDefinition; auth: StringSchemaDefinition;
payload?: Payload; payload?: Payload;
} }
export interface LogModel extends Model<Log> { export interface LogModel extends Model<Log> {
add(id: StringSchemaDefinition, action: Action, payload?: Payload): void; add(id: StringSchemaDefinition, action: Action, payload?: Payload): void;
historyForUser(id: StringSchemaDefinition, action?: Action): Array<Log>; historyForUser(id: StringSchemaDefinition, action?: Action): Array<Log>;
loginsForUser(id: StringSchemaDefinition): Array<Log>; loginsForUser(id: StringSchemaDefinition): Array<Log>;
} }
export const LogSchema = new Schema<Log, LogModel>( export const LogSchema = new Schema<Log, LogModel>(
{ {
action: { type: String, enum: Action, required: true }, action: { type: String, enum: Action, required: true },
auth: { type: Types.ObjectId, index: true, required: true }, auth: { type: Types.ObjectId, index: true, required: true },
payload: { type: Object }, payload: { type: Object },
}, },
{ {
minimize: true, minimize: true,
timestamps: true, timestamps: true,
} },
); );
LogSchema.statics = { LogSchema.statics = {
add(id, action, payload) { add(id, action, payload) {
this.create({ action, auth: id, payload }).catch(); this.create({ action, auth: id, payload }).catch();
}, },
async historyForUser(id, action) { async historyForUser(id, action) {
return this.find({ auth: id, action }); return this.find({ auth: id, action });
}, },
async loginsForUser(id) { async loginsForUser(id) {
return this.find({ auth: id, action: Action.AUTHENTICATE }); return this.find({ auth: id, action: Action.AUTHENTICATE });
}, },
}; };
export type LogSchema = InferSchemaType<typeof LogSchema>; export type LogSchema = InferSchemaType<typeof LogSchema>;

View File

@@ -1,92 +1,81 @@
import { import { Document, InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
Document,
InferSchemaType,
Model,
Schema,
StringSchemaDefinition,
Types,
} from "mongoose";
import { STRATEGIES } from "../constants/strategies"; import { STRATEGIES } from '../constants/strategies';
import { encrypt } from "../utils/password"; import { encrypt } from '../utils/password';
import { COLL_AUTH } from "../constants/db"; import { COLL_AUTH } from '../constants/db';
import { AuthDocument } from "./auth"; import { AuthDocument } from './auth';
import { Strategy } from ".."; import { Strategy } from '..';
export interface Strategy { export interface Strategy {
method: STRATEGIES; method: STRATEGIES;
parent: StringSchemaDefinition | AuthDocument; parent: StringSchemaDefinition | AuthDocument;
externalId?: string; externalId?: string;
key: string; key: string;
profile?: { [key: string]: string | boolean | number }; profile?: { [key: string]: string | boolean | number };
forceReset?: boolean; forceReset?: boolean;
} }
interface StrategyBaseDocument extends Strategy, Document { interface StrategyBaseDocument extends Strategy, Document {
getAuthRecord(): Promise<AuthDocument>; getAuthRecord(): Promise<AuthDocument>;
getPopulatedStrategy(): Promise<StrategyPopulatedDocument>; getPopulatedStrategy(): Promise<StrategyPopulatedDocument>;
} }
export interface StrategyDocument extends StrategyBaseDocument { export interface StrategyDocument extends StrategyBaseDocument {
parent: StringSchemaDefinition; parent: StringSchemaDefinition;
} }
export interface StrategyPopulatedDocument extends StrategyBaseDocument { export interface StrategyPopulatedDocument extends StrategyBaseDocument {
parent: AuthDocument; parent: AuthDocument;
} }
export type StrategyModel = Model<StrategyDocument>; export type StrategyModel = Model<StrategyDocument>;
export const StrategySchema = new Schema<StrategyDocument, StrategyModel>( export const StrategySchema = new Schema<StrategyDocument, StrategyModel>(
{ {
method: { method: {
type: Number, type: Number,
enum: STRATEGIES, enum: STRATEGIES,
index: true, index: true,
},
externalId: { type: String, index: true },
forceReset: { type: Boolean },
key: { type: String, required: true, trim: true },
parent: {
type: Types.ObjectId,
ref: COLL_AUTH,
required: true,
},
profile: {},
}, },
externalId: { type: String, index: true }, {
forceReset: { type: Boolean }, minimize: true,
key: { type: String, required: true, trim: true }, timestamps: true,
parent: {
type: Types.ObjectId,
ref: COLL_AUTH,
required: true,
}, },
profile: {},
},
{
minimize: true,
timestamps: true,
}
); );
StrategySchema.methods.getPopulatedStrategy = async function ( StrategySchema.methods.getPopulatedStrategy = async function (this: StrategyDocument) {
this: StrategyDocument return this.populate<StrategyPopulatedDocument>('parent');
) {
return this.populate<StrategyPopulatedDocument>("parent");
}; };
StrategySchema.methods.getAuthRecord = async function (this: StrategyDocument) { StrategySchema.methods.getAuthRecord = async function (this: StrategyDocument) {
return (await this.getPopulatedStrategy()).parent; return (await this.getPopulatedStrategy()).parent;
}; };
StrategySchema.pre("save", async function save(next) { StrategySchema.pre('save', async function save(next) {
if (typeof this.method === "undefined") { if (typeof this.method === 'undefined') {
return next(new Error(`Strategy requires a method.`)); return next(new Error(`Strategy requires a method.`));
} }
if (await Strategy.findOne({ method: this.method, parent: this.parent })) { if (await Strategy.findOne({ method: this.method, parent: this.parent })) {
return next( return next(new Error(`${this.method} strategy already exists for this user.`));
new Error(`${this.method} strategy already exists for this user.`) }
);
}
if (this.method !== STRATEGIES.LOCAL || !this.isModified("key")) { if (this.method !== STRATEGIES.LOCAL || !this.isModified('key')) {
return next();
}
this.key = encrypt(this.key);
return next(); return next();
}
this.key = encrypt(this.key);
return next();
}); });
export type StrategySchema = InferSchemaType<typeof StrategySchema>; export type StrategySchema = InferSchemaType<typeof StrategySchema>;

View File

@@ -1,81 +1,71 @@
import { import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
InferSchemaType,
Model,
Schema,
StringSchemaDefinition,
Types,
} from "mongoose";
import { TokenType } from "../constants/tokens"; import { TokenType } from '../constants/tokens';
import { getDefaultExpiresFor } from "../utils/getDefaultExpiresFor"; import { getDefaultExpiresFor } from '../utils/getDefaultExpiresFor';
import { sign, verify } from "../utils/jwt"; import { sign, verify } from '../utils/jwt';
export interface Token { export interface Token {
auth: StringSchemaDefinition; auth: StringSchemaDefinition;
expires?: number; expires?: number;
type: TokenType; type: TokenType;
} }
export interface TokenModel extends Model<Token> { export interface TokenModel extends Model<Token> {
cleanupExpiredTokens(): { success: boolean; deletedCount: number }; cleanupExpiredTokens(): { success: boolean; deletedCount: number };
getToken(type: TokenType, auth: Types.ObjectId, expires?: number): string; getToken(type: TokenType, auth: Types.ObjectId, expires?: number): string;
validateResetToken(token: string): Types.ObjectId | false; validateResetToken(token: string): Types.ObjectId | false;
} }
export const TokenSchema = new Schema<Token, TokenModel>( export const TokenSchema = new Schema<Token, TokenModel>(
{ {
auth: { type: Types.ObjectId, index: true }, auth: { type: Types.ObjectId, index: true },
expires: { type: Number, required: true }, expires: { type: Number, required: true },
type: { type: String, enum: TokenType, required: true }, type: { type: String, enum: TokenType, required: true },
}, },
{ {
minimize: true, minimize: true,
timestamps: true, timestamps: true,
} },
); );
TokenSchema.statics = { TokenSchema.statics = {
async cleanupExpiredTokens() { async cleanupExpiredTokens() {
const { acknowledged, deletedCount } = await this.deleteMany({ const { acknowledged, deletedCount } = await this.deleteMany({
expires: { $lte: Date.now() }, expires: { $lte: Date.now() },
}); });
return { success: acknowledged, deletedCount }; return { success: acknowledged, deletedCount };
}, },
async getToken( async getToken(type: TokenType, auth: StringSchemaDefinition, expires?: number) {
type: TokenType, const existing = await this.findOne({ type, auth });
auth: StringSchemaDefinition, if (existing) {
expires?: number await existing.deleteOne();
) { }
const existing = await this.findOne({ type, auth });
if (existing) {
await existing.deleteOne();
}
const doc = await this.create({ const doc = await this.create({
type, type,
auth, auth,
expires: expires || getDefaultExpiresFor(type), expires: expires || getDefaultExpiresFor(type),
}); });
return sign({ return sign({
sub: `${doc._id}`, sub: `${doc._id}`,
exp: doc.expires, exp: doc.expires,
}); });
}, },
async validateResetToken(token: string) { async validateResetToken(token: string) {
const { sub } = verify(token); const { sub } = verify(token);
if (sub) { if (sub) {
const record = await this.findById(sub); const record = await this.findById(sub);
if (record) { if (record) {
await record.deleteOne(); await record.deleteOne();
return !!record?.expires && record.expires >= Date.now() && record.auth; return !!record?.expires && record.expires >= Date.now() && record.auth;
} }
} }
return false; return false;
}, },
}; };
export type TokenSchema = InferSchemaType<typeof TokenSchema>; export type TokenSchema = InferSchemaType<typeof TokenSchema>;

View File

@@ -1,19 +1,15 @@
import { import { LOGIN_VALID_TIMEOUT, RESET_VALID_TIMEOUT, VERIFY_VALID_TIMEOUT } from '../constants/env';
LOGIN_VALID_TIMEOUT, import { TokenType } from '../constants/tokens';
RESET_VALID_TIMEOUT, import { parseTimeoutToMs } from '../utils/parseTimeoutToMs';
VERIFY_VALID_TIMEOUT,
} from "../constants/env";
import { TokenType } from "../constants/tokens";
import { parseTimeoutToMs } from "../utils/parseTimeoutToMs";
export const getDefaultExpiresFor = (type: TokenType | void) => { export const getDefaultExpiresFor = (type: TokenType | void) => {
if (type === TokenType.RESET) { if (type === TokenType.RESET) {
return Date.now() + parseTimeoutToMs(RESET_VALID_TIMEOUT); return Date.now() + parseTimeoutToMs(RESET_VALID_TIMEOUT);
} }
if (type === TokenType.VERIFICATION) { if (type === TokenType.VERIFICATION) {
return Date.now() + parseTimeoutToMs(VERIFY_VALID_TIMEOUT); return Date.now() + parseTimeoutToMs(VERIFY_VALID_TIMEOUT);
} }
return Date.now() + parseTimeoutToMs(LOGIN_VALID_TIMEOUT); return Date.now() + parseTimeoutToMs(LOGIN_VALID_TIMEOUT);
}; };

View File

@@ -1,11 +1,11 @@
import { sign } from "../utils/jwt"; import { sign } from '../utils/jwt';
import { LOGIN_VALID_TIMEOUT } from "../constants/env"; import { LOGIN_VALID_TIMEOUT } from '../constants/env';
import { parseTimeoutToMs } from "../utils/parseTimeoutToMs"; import { parseTimeoutToMs } from '../utils/parseTimeoutToMs';
import { AuthDocument } from "../schema/auth"; import { AuthDocument } from '../schema/auth';
export const getLoginToken = ({ record: sub, status }: AuthDocument) => export const getLoginToken = ({ record: sub, status }: AuthDocument) =>
sign({ sign({
sub: <string>sub, sub: <string>sub,
status, status,
exp: Date.now() + parseTimeoutToMs(LOGIN_VALID_TIMEOUT), exp: Date.now() + parseTimeoutToMs(LOGIN_VALID_TIMEOUT),
}); });

View File

@@ -1,39 +1,35 @@
import jwt from "jsonwebtoken"; import jwt from 'jsonwebtoken';
import { JWT_AUDIENCE, JWT_ISSUER, JWT_SECRET } from "../constants/env"; import { JWT_AUDIENCE, JWT_ISSUER, JWT_SECRET } from '../constants/env';
export interface TokenProps { export interface TokenProps {
aud?: string; aud?: string;
exp?: number | Date; exp?: number | Date;
iss?: string; iss?: string;
sub: string | null; sub: string | null;
[key: string]: any; [key: string]: any;
} }
export type SignProps = string | TokenProps | void; export type SignProps = string | TokenProps | void;
export const sign = (props: SignProps) => { export const sign = (props: SignProps) => {
const today = new Date(); const today = new Date();
const { sub = null, ...rest }: TokenProps = const { sub = null, ...rest }: TokenProps =
typeof props === "string" || typeof props === "undefined" typeof props === 'string' || typeof props === 'undefined' ? { sub: props || null } : props;
? { sub: props || null } let { exp } = rest;
: props; if (!exp) {
let { exp } = rest; exp = new Date(today);
if (!exp) { exp.setDate(today.getDate() + parseInt(process.env.JWT_DAYS_VALID as string));
exp = new Date(today); exp = exp.getTime() / 1000;
exp.setDate( }
today.getDate() + parseInt(process.env.JWT_DAYS_VALID as string) return jwt.sign(
{
exp,
sub,
aud: rest.aud || JWT_AUDIENCE,
iat: today.getTime(),
iss: rest.iss || JWT_ISSUER,
},
JWT_SECRET,
); );
exp = exp.getTime() / 1000;
}
return jwt.sign(
{
exp,
sub,
aud: rest.aud || JWT_AUDIENCE,
iat: today.getTime(),
iss: rest.iss || JWT_ISSUER,
},
JWT_SECRET
);
}; };
export const verify = (token: string) => jwt.verify(token, JWT_SECRET); export const verify = (token: string) => jwt.verify(token, JWT_SECRET);

View File

@@ -1,11 +1,5 @@
import { import { RESET_ROUTE, ROUTE_PREFIX, VERIFICATION_ROUTE } from '../constants/env';
RESET_ROUTE,
ROUTE_PREFIX,
VERIFICATION_ROUTE,
} from "../constants/env";
export const getPasswordResetPath = (token: string) => export const getPasswordResetPath = (token: string) => `${ROUTE_PREFIX}${RESET_ROUTE}?t=${token}`;
`${ROUTE_PREFIX}${RESET_ROUTE}?t=${token}`;
export const getVerificationPath = (token: string) => export const getVerificationPath = (token: string) => `${ROUTE_PREFIX}${VERIFICATION_ROUTE}?t=${token}`;
`${ROUTE_PREFIX}${VERIFICATION_ROUTE}?t=${token}`;

View File

@@ -1,13 +1,13 @@
export const parseTimeoutToMs = (timeout: string) => { export const parseTimeoutToMs = (timeout: string) => {
const match = timeout.match(/(?<number>\d+)(?<unit>d|h|m)/gi)?.groups || {}; const match = timeout.match(/(?<number>\d+)(?<unit>d|h|m)/gi)?.groups || {};
const { number, unit } = match; const { number, unit } = match;
switch (unit) { switch (unit) {
case "d": case 'd':
return 1000 * 60 * 60 * 24 * parseInt(number); return 1000 * 60 * 60 * 24 * parseInt(number);
case "h": case 'h':
return 1000 * 60 * 60 * parseInt(number); return 1000 * 60 * 60 * parseInt(number);
case "m": case 'm':
default: default:
return 1000 * 60 * parseInt(number) || 1; return 1000 * 60 * parseInt(number) || 1;
} }
}; };

View File

@@ -1,12 +1,12 @@
import { pbkdf2Sync, randomBytes } from "crypto"; import { pbkdf2Sync, randomBytes } from 'crypto';
export const encrypt = (password: string) => { export const encrypt = (password: string) => {
const salt = randomBytes(16).toString("hex"); const salt = randomBytes(16).toString('hex');
const hash = pbkdf2Sync(password, salt, 10000, 512, "sha512").toString("hex"); const hash = pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
return `${salt}:${hash}`; return `${salt}:${hash}`;
}; };
export const verify = (test: string, secret: string) => { export const verify = (test: string, secret: string) => {
const [salt, hash] = secret.split(":"); const [salt, hash] = secret.split(':');
return pbkdf2Sync(test, salt, 10000, 512, "sha512").toString("hex") === hash; return pbkdf2Sync(test, salt, 10000, 512, 'sha512').toString('hex') === hash;
}; };

View File

@@ -1,11 +1,11 @@
import { sign } from "./jwt"; import { sign } from './jwt';
import { LOGIN_VALID_TIMEOUT } from "../constants/env"; import { LOGIN_VALID_TIMEOUT } from '../constants/env';
import { Status } from "../constants/auth"; import { Status } from '../constants/auth';
import { parseTimeoutToMs } from "./parseTimeoutToMs"; import { parseTimeoutToMs } from './parseTimeoutToMs';
export const generateLoginToken = (sub: string, status: Status) => export const generateLoginToken = (sub: string, status: Status) =>
sign({ sign({
sub, sub,
status, status,
exp: Date.now() + parseTimeoutToMs(LOGIN_VALID_TIMEOUT), exp: Date.now() + parseTimeoutToMs(LOGIN_VALID_TIMEOUT),
}); });