Breaking down mega-package. Hello auth-db 1.0.0!
This commit is contained in:
75
lib/schema/auth.ts
Normal file
75
lib/schema/auth.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Document, InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
|
||||
|
||||
import { Status } from '../constants/auth';
|
||||
import { COLL_STRATEGY } from '../constants/db';
|
||||
import { STRATEGIES } from '../constants/strategies';
|
||||
import { StrategyDocument } from './strategy';
|
||||
import { verify } from '../utils/password';
|
||||
|
||||
export interface Auth {
|
||||
is2FA?: boolean;
|
||||
record: StringSchemaDefinition;
|
||||
username: string;
|
||||
status: Status;
|
||||
strategies: Types.ObjectId[] | StrategyDocument[];
|
||||
}
|
||||
|
||||
interface AuthBaseDocument extends Auth, Document {
|
||||
authenticate(password: string): Promise<boolean>;
|
||||
getStrategy(method?: STRATEGIES): Promise<StrategyDocument | null>;
|
||||
}
|
||||
|
||||
export interface AuthDocument extends AuthBaseDocument {
|
||||
strategies: Types.ObjectId[];
|
||||
}
|
||||
|
||||
export interface AuthPopulatedDocument extends AuthBaseDocument {
|
||||
strategies: StrategyDocument[];
|
||||
}
|
||||
|
||||
export interface AuthModel extends Model<AuthDocument> {
|
||||
findByUsername(username: string): Promise<AuthDocument>;
|
||||
getLocalStrategyForUsername(username: string): Promise<StrategyDocument>;
|
||||
isUsernameAvailable(username: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export const AuthSchema = new Schema<AuthDocument, AuthModel>(
|
||||
{
|
||||
is2FA: { type: Boolean, default: false },
|
||||
record: { type: Types.ObjectId, unique: true },
|
||||
status: { type: Number, enum: Status, default: Status.UNVERIFIED, index: 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 (this: AuthBaseDocument, password: string) {
|
||||
const strategy = await this.getStrategy();
|
||||
return !!strategy && verify(password, strategy.key);
|
||||
};
|
||||
|
||||
AuthSchema.methods.getStrategy = async function (this: AuthBaseDocument, method = STRATEGIES.LOCAL) {
|
||||
const doc = await this.populate<{ strategies: StrategyDocument[] }>('strategies');
|
||||
return doc.strategies.filter((strategy) => strategy.method === method).pop() || null;
|
||||
};
|
||||
|
||||
AuthSchema.statics = {
|
||||
async findByUsername(username) {
|
||||
return this.findOne({ username });
|
||||
},
|
||||
|
||||
async getLocalStrategyForUsername(username) {
|
||||
const doc = await this.findByUsername(username);
|
||||
return !!doc && doc.getStrategy();
|
||||
},
|
||||
|
||||
async isUsernameAvailable(username) {
|
||||
return !this.findByUsername(username);
|
||||
},
|
||||
};
|
||||
|
||||
export type AuthSchema = InferSchemaType<typeof AuthSchema>;
|
||||
45
lib/schema/log.ts
Normal file
45
lib/schema/log.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
|
||||
|
||||
import { Payload } from '@mifi/services-common/types/Payload';
|
||||
|
||||
import { Action } from '../constants/action';
|
||||
|
||||
export interface Log {
|
||||
action: Action;
|
||||
auth: StringSchemaDefinition;
|
||||
payload?: Payload;
|
||||
}
|
||||
|
||||
export interface LogModel extends Model<Log> {
|
||||
add(id: StringSchemaDefinition, action: Action, payload?: Payload): void;
|
||||
historyForUser(id: StringSchemaDefinition, action?: Action): Array<Log>;
|
||||
loginsForUser(id: StringSchemaDefinition): Array<Log>;
|
||||
}
|
||||
|
||||
export const LogSchema = new Schema<Log, LogModel>(
|
||||
{
|
||||
action: { type: String, enum: Action, required: true },
|
||||
auth: { type: Types.ObjectId, index: true, required: true },
|
||||
payload: { type: Object },
|
||||
},
|
||||
{
|
||||
minimize: true,
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
LogSchema.statics = {
|
||||
add(id, action, payload) {
|
||||
this.create({ action, auth: id, payload }).catch();
|
||||
},
|
||||
|
||||
async historyForUser(id, action) {
|
||||
return this.find({ auth: id, action });
|
||||
},
|
||||
|
||||
async loginsForUser(id) {
|
||||
return this.find({ auth: id, action: Action.AUTHENTICATE });
|
||||
},
|
||||
};
|
||||
|
||||
export type LogSchema = InferSchemaType<typeof LogSchema>;
|
||||
81
lib/schema/strategy.ts
Normal file
81
lib/schema/strategy.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Document, InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
|
||||
|
||||
import { STRATEGIES } from '../constants/strategies';
|
||||
import { encrypt } from '../utils/password';
|
||||
import { COLL_AUTH } from '../constants/db';
|
||||
import { AuthDocument } from './auth';
|
||||
import { Strategy } from '..';
|
||||
|
||||
export interface Strategy {
|
||||
method: STRATEGIES;
|
||||
parent: StringSchemaDefinition | AuthDocument;
|
||||
externalId?: string;
|
||||
key: string;
|
||||
profile?: { [key: string]: string | boolean | number };
|
||||
forceReset?: boolean;
|
||||
}
|
||||
|
||||
interface StrategyBaseDocument extends Strategy, Document {
|
||||
getAuthRecord(): Promise<AuthDocument>;
|
||||
getPopulatedStrategy(): Promise<StrategyPopulatedDocument>;
|
||||
}
|
||||
|
||||
export interface StrategyDocument extends StrategyBaseDocument {
|
||||
parent: StringSchemaDefinition;
|
||||
}
|
||||
|
||||
export interface StrategyPopulatedDocument extends StrategyBaseDocument {
|
||||
parent: AuthDocument;
|
||||
}
|
||||
|
||||
export type StrategyModel = Model<StrategyDocument>;
|
||||
|
||||
export const StrategySchema = new Schema<StrategyDocument, StrategyModel>(
|
||||
{
|
||||
method: {
|
||||
type: Number,
|
||||
enum: STRATEGIES,
|
||||
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: {},
|
||||
},
|
||||
{
|
||||
minimize: true,
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
StrategySchema.methods.getPopulatedStrategy = async function (this: StrategyModel) {
|
||||
return this.populate<StrategyPopulatedDocument>('parent');
|
||||
};
|
||||
|
||||
StrategySchema.methods.getAuthRecord = async function (this: StrategyModel) {
|
||||
return (await this.getPopulatedStrategy()).parent;
|
||||
};
|
||||
|
||||
StrategySchema.pre('save', async function save(next) {
|
||||
if (typeof this.method === 'undefined') {
|
||||
return next(new Error(`Strategy requires a method.`));
|
||||
}
|
||||
|
||||
if (await Strategy.findOne({ method: this.method, parent: this.parent })) {
|
||||
return next(new Error(`${this.method} strategy already exists for this user.`));
|
||||
}
|
||||
|
||||
if (this.method !== STRATEGIES.LOCAL || !this.isModified('key')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
this.key = encrypt(this.key);
|
||||
return next();
|
||||
});
|
||||
|
||||
export type StrategySchema = InferSchemaType<typeof StrategySchema>;
|
||||
65
lib/schema/token.ts
Normal file
65
lib/schema/token.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
|
||||
|
||||
import { TokenType } from '../constants/tokens';
|
||||
import { getDefaultExpiresFor } from '../utils/getDefaultExpiresFor';
|
||||
import { sign, verify } from '../utils/jwt';
|
||||
|
||||
export interface Token {
|
||||
auth: StringSchemaDefinition;
|
||||
expires?: number;
|
||||
type: TokenType;
|
||||
}
|
||||
|
||||
export interface TokenModel extends Model<Token> {
|
||||
cleanupExpiredTokens(): { success: boolean; deletedCount: number };
|
||||
getToken(type: TokenType, auth: Types.ObjectId, expires?: number): string;
|
||||
validateResetToken(token: string): Types.ObjectId | false;
|
||||
}
|
||||
|
||||
export const TokenSchema = new Schema<Token, TokenModel>(
|
||||
{
|
||||
auth: { type: Types.ObjectId, index: true },
|
||||
expires: { type: Number, required: true },
|
||||
type: { type: String, enum: TokenType, required: true },
|
||||
},
|
||||
{
|
||||
minimize: true,
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
TokenSchema.statics = {
|
||||
async cleanupExpiredTokens() {
|
||||
const { acknowledged, deletedCount } = await this.deleteMany({ expires: { $lte: Date.now() } });
|
||||
return { success: acknowledged, deletedCount };
|
||||
},
|
||||
|
||||
async getToken(type: TokenType, auth: StringSchemaDefinition, expires?: number) {
|
||||
const existing = await this.findOne({ type, auth });
|
||||
if (existing) {
|
||||
await existing.deleteOne();
|
||||
}
|
||||
|
||||
const doc = await this.create({ type, auth, expires: expires || getDefaultExpiresFor(type) });
|
||||
return sign({
|
||||
sub: `${doc._id}`,
|
||||
exp: doc.expires,
|
||||
});
|
||||
},
|
||||
|
||||
async validateResetToken(token: string) {
|
||||
const { sub } = verify(token);
|
||||
|
||||
if (sub) {
|
||||
const record = await this.findById(sub);
|
||||
if (record) {
|
||||
await record.deleteOne();
|
||||
return !!record?.expires && record.expires >= Date.now() && record.auth;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export type TokenSchema = InferSchemaType<typeof TokenSchema>;
|
||||
Reference in New Issue
Block a user