This commit is contained in:
8
lib/constants/auth.ts
Normal file
8
lib/constants/auth.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export enum Status {
|
||||||
|
ACTIVE,
|
||||||
|
BLOCK_HARD,
|
||||||
|
BLOCK_SOFT,
|
||||||
|
DELETED,
|
||||||
|
INACTIVE,
|
||||||
|
UNVERIFIED,
|
||||||
|
}
|
||||||
15
lib/constants/constants.ts
Normal file
15
lib/constants/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
export const PACKAGE_NAME = '@mifi/latch';
|
||||||
|
export const PORT = process.env.PORT || 9000;
|
||||||
|
|
||||||
|
export const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'mifi.dev';
|
||||||
|
export const JWT_ISSUER = process.env.JWT_ISSUER || PACKAGE_NAME;
|
||||||
|
export const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||||
|
|
||||||
|
export const LOGIN_VALID_TIME = process.env.LOGIN_VALID_TIME || '12H'; // ###D|H|M
|
||||||
|
export const RESET_VALID_MINUTES = process.env.RESET_VALID_MINUTES || 24;
|
||||||
|
export const DEFAULT_TOKEN_DAYS = process.env.DEFAULT_TOKEN_DAYS || 365;
|
||||||
|
|
||||||
|
export const ROUTE_PREFIX = process.env.ROUTE_PREFIX || '/auth';
|
||||||
|
export const LOGIN_ROUTE = process.env.LOGIN_ROUTE || '/login';
|
||||||
|
export const RESET_ROUTE = process.env.RESET_ROUTE || '/reset';
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export const PORT = 9000;
|
|
||||||
export const API_PATH = '/api';
|
|
||||||
export const AUTH_ROUTE = '/auth';
|
|
||||||
export const RESET_ROUTE = `${AUTH_ROUTE}/reset`;
|
|
||||||
|
|
||||||
export const JWT_SECRET = 'secret';
|
|
||||||
12
lib/constants/errors.ts
Normal file
12
lib/constants/errors.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export enum ErrorCodes {
|
||||||
|
RESET_REQUEST_DATA = 'RESET_REQUEST_DATA',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorMessages = {
|
||||||
|
[ErrorCodes.RESET_REQUEST_DATA]: 'A valid username and password must be provided',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getErrorBody = (code: ErrorCodes) => ({
|
||||||
|
code,
|
||||||
|
message: ErrorMessages[code],
|
||||||
|
});
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import Koa from 'koa';
|
|
||||||
import Router from 'koa-router';
|
|
||||||
import { StatusCodes } from 'http-status-codes';
|
|
||||||
import { API_PATH } from '../constants/defaults';
|
|
||||||
|
|
||||||
import Auth from '../model/auth';
|
|
||||||
import passport from '../passport';
|
|
||||||
import { sign } from '../utils/jwt';
|
|
||||||
|
|
||||||
const routerOpts: Router.IRouterOptions = {
|
|
||||||
prefix: process.env.API_PATH || API_PATH,
|
|
||||||
};
|
|
||||||
|
|
||||||
const router: Router = new Router(routerOpts);
|
|
||||||
|
|
||||||
router.post('/', async (ctx: Koa.Context) => {
|
|
||||||
const data = await Auth.create(ctx.body);
|
|
||||||
data.save();
|
|
||||||
ctx.body = { success: true, data };
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/login', async (ctx: Koa.Context, next) => {
|
|
||||||
return passport.authenticate('local', (err, user) => {
|
|
||||||
if (user === false) {
|
|
||||||
ctx.body = { token: sign() };
|
|
||||||
ctx.throw(StatusCodes.UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
ctx.body = { token: sign(user) };
|
|
||||||
return ctx.login(user);
|
|
||||||
})(ctx, next);
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.patch('/:customer_id', async (ctx: Koa.Context) => {
|
|
||||||
const data = await Auth.findByIdAndUpdate(ctx.params.customer_id);
|
|
||||||
if (!data) {
|
|
||||||
ctx.throw(StatusCodes.NOT_FOUND);
|
|
||||||
}
|
|
||||||
ctx.body = { success: true, data };
|
|
||||||
});
|
|
||||||
@@ -2,11 +2,12 @@ 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 { Strategy } from './strategy';
|
||||||
import { STRATEGIES } from '../constants/strategies';
|
import { STRATEGIES } from '../../constants/strategies';
|
||||||
import { TokenProps, sign, verify as verifyJwt } from '../utils/jwt';
|
import { TokenProps, verify as verifyJwt } from '../../utils/jwt';
|
||||||
import { encrypt, verify as verifyPassword } from '../utils/password';
|
import { encrypt, verify as verifyPassword } from '../../utils/password';
|
||||||
import { generateResetToken } from '../utils/tokens';
|
import { generateLoginToken, generateResetToken } from '../../utils/tokens';
|
||||||
import { getPasswordResetLink } from '../utils/links';
|
import { getPasswordResetLink } from '../../utils/links';
|
||||||
|
import { Status } from '../../constants/auth';
|
||||||
|
|
||||||
export type Auth = {
|
export type Auth = {
|
||||||
is2FA?: boolean;
|
is2FA?: boolean;
|
||||||
@@ -15,6 +16,7 @@ export type Auth = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AuthPrivate = Auth & {
|
export type AuthPrivate = Auth & {
|
||||||
|
status: Status;
|
||||||
strategies: Types.ArraySubdocument<Strategy>;
|
strategies: Types.ArraySubdocument<Strategy>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,15 +26,16 @@ export interface AuthMethods {
|
|||||||
getResetLink(route: string): Promise<string | undefined>;
|
getResetLink(route: string): Promise<string | undefined>;
|
||||||
getResetToken(): Promise<string | undefined>;
|
getResetToken(): Promise<string | undefined>;
|
||||||
getToken(props?: Omit<TokenProps, 'sub'> | void): string;
|
getToken(props?: Omit<TokenProps, 'sub'> | void): string;
|
||||||
|
isActive(): boolean;
|
||||||
setPassword(password: string): Promise<boolean>;
|
setPassword(password: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthModel extends Model<AuthPrivate, void, AuthMethods> {
|
export interface AuthModel extends Model<AuthPrivate, void, AuthMethods> {
|
||||||
authenticate(password: any): boolean;
|
authenticate(username: string, password?: string): string | false;
|
||||||
findByUsername(username: string): Promise<AuthModel & AuthPrivate>;
|
findByUsername(username: string): Promise<AuthModel & AuthPrivate>;
|
||||||
isUsernameAvailable(username: string): Promise<boolean>;
|
isUsernameAvailable(username: string): Promise<boolean>;
|
||||||
findUserForReset(strategy: STRATEGIES, token: string): Promise<Strategy | undefined>;
|
findUserForReset(strategy: STRATEGIES, token: string): Promise<Strategy | undefined>;
|
||||||
resetPassword(token: string, password: string): Promise<boolean>;
|
resetPassword(token: string, password: string): Promise<string | false>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthSchema = new Schema<AuthPrivate, AuthModel, AuthMethods>(
|
export const AuthSchema = new Schema<AuthPrivate, AuthModel, AuthMethods>(
|
||||||
@@ -40,6 +43,7 @@ export const AuthSchema = new Schema<AuthPrivate, AuthModel, AuthMethods>(
|
|||||||
is2FA: { type: Boolean, default: false },
|
is2FA: { type: Boolean, default: false },
|
||||||
record: { type: Types.ObjectId },
|
record: { type: Types.ObjectId },
|
||||||
strategies: { type: Types.ArraySubdocument<Strategy>, required: true },
|
strategies: { type: Types.ArraySubdocument<Strategy>, required: true },
|
||||||
|
status: { type: Number, enum: Object.values(Status), default: Status.UNVERIFIED },
|
||||||
username: { type: String, required: true, unique: true },
|
username: { type: String, required: true, unique: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,10 +63,7 @@ AuthSchema.methods = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getToken(props = {}) {
|
getToken(props = {}) {
|
||||||
return sign({
|
return generateLoginToken(this._id, this.status);
|
||||||
sub: this._id,
|
|
||||||
...props,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getResetLink(route) {
|
async getResetLink(route) {
|
||||||
@@ -81,6 +82,10 @@ AuthSchema.methods = {
|
|||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isActive() {
|
||||||
|
return this.status === Status.ACTIVE;
|
||||||
|
},
|
||||||
|
|
||||||
async setPassword(password) {
|
async setPassword(password) {
|
||||||
const key = encrypt(password);
|
const key = encrypt(password);
|
||||||
const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL);
|
const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL);
|
||||||
@@ -107,7 +112,7 @@ AuthSchema.methods = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AuthSchema.statics = {
|
AuthSchema.statics = {
|
||||||
authenticateAndGetRecordLocator: async function (username, password) {
|
authenticate: async function (username, password) {
|
||||||
const auth = await this.findByUsername(username);
|
const auth = await this.findByUsername(username);
|
||||||
if (auth && auth.authenticate(password)) {
|
if (auth && auth.authenticate(password)) {
|
||||||
return auth.record;
|
return auth.record;
|
||||||
@@ -130,7 +135,11 @@ AuthSchema.statics = {
|
|||||||
_id: sub,
|
_id: sub,
|
||||||
'strategies.resetToken': key,
|
'strategies.resetToken': key,
|
||||||
}).catch();
|
}).catch();
|
||||||
return !!auth && auth.setPassword(password);
|
if (auth) {
|
||||||
|
await auth.setPassword(password).catch();
|
||||||
|
return auth.getToken();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { InferSchemaType, Schema, Types } from 'mongoose';
|
import { InferSchemaType, Schema, Types } from 'mongoose';
|
||||||
import { STRATEGIES } from '../constants/strategies';
|
import { STRATEGIES } from '../../constants/strategies';
|
||||||
|
|
||||||
export const Strategy = new Schema(
|
export const Strategy = new Schema(
|
||||||
{
|
{
|
||||||
@@ -6,6 +6,7 @@ import session from 'koa-session';
|
|||||||
import passport from './passport';
|
import passport from './passport';
|
||||||
import { performanceLogger, perfromanceTimer } from './middleware/performance';
|
import { performanceLogger, perfromanceTimer } from './middleware/performance';
|
||||||
import { errorHandler } from './middleware/errorHandler';
|
import { errorHandler } from './middleware/errorHandler';
|
||||||
|
import { authRouter } from './controllers/auth';
|
||||||
|
|
||||||
const app: Koa = new Koa();
|
const app: Koa = new Koa();
|
||||||
|
|
||||||
@@ -21,6 +22,9 @@ app.use(session({}, app));
|
|||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
|
|
||||||
|
app.use(authRouter.routes());
|
||||||
|
app.use(authRouter.allowedMethods());
|
||||||
|
|
||||||
// Application error logging.
|
// Application error logging.
|
||||||
app.on('error', console.error);
|
app.on('error', console.error);
|
||||||
|
|
||||||
50
lib/server/controllers/auth.ts
Normal file
50
lib/server/controllers/auth.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Koa from 'koa';
|
||||||
|
import Router from 'koa-router';
|
||||||
|
import { StatusCodes } from 'http-status-codes';
|
||||||
|
|
||||||
|
import { ROUTE_PREFIX as prefix, RESET_ROUTE } from '../../constants/constants';
|
||||||
|
import Auth from '../../db/model/auth';
|
||||||
|
import { sign } from '../../utils/jwt';
|
||||||
|
import passport from '../passport';
|
||||||
|
import { ErrorCodes, getErrorBody } from '../../constants/errors';
|
||||||
|
|
||||||
|
const routerOpts: Router.IRouterOptions = { prefix };
|
||||||
|
const router: Router = new Router(routerOpts);
|
||||||
|
|
||||||
|
router.post('/', async (ctx) => {
|
||||||
|
const data = (await Auth.create(ctx.body)).save();
|
||||||
|
ctx.body = { success: true, data: { ...data, strategies: undefined } };
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/login', async (ctx, next) => {
|
||||||
|
return passport.authenticate('local', (err, user) => {
|
||||||
|
if (user === false) {
|
||||||
|
ctx.body = { token: null };
|
||||||
|
ctx.throw(StatusCodes.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
ctx.body = { token: sign(user) };
|
||||||
|
return ctx.login(user);
|
||||||
|
})(ctx, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post(process.env.RESET_ROUTE || RESET_ROUTE, async (ctx, next) => {
|
||||||
|
const { token = null, password = null } = ctx.request.body as { token?: string, password?: string };
|
||||||
|
if (token && password) {
|
||||||
|
const loginToken = await Auth.resetPassword(token, password).catch();
|
||||||
|
ctx.body({ token: loginToken });
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
ctx.body = { success: false, ...getErrorBody(ErrorCodes.RESET_REQUEST_DATA) };
|
||||||
|
});
|
||||||
|
|
||||||
|
router.patch('/:record', (ctx: Koa.Context) => {
|
||||||
|
const data = Auth.findOneAndUpdate(
|
||||||
|
{ record: ctx.params.record },
|
||||||
|
);
|
||||||
|
if (!data) {
|
||||||
|
ctx.throw(StatusCodes.NOT_FOUND);
|
||||||
|
}
|
||||||
|
ctx.body = { success: true, data };
|
||||||
|
});
|
||||||
|
|
||||||
|
export { router as authRouter };
|
||||||
12
lib/server/middleware/authenication.ts
Normal file
12
lib/server/middleware/authenication.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Middleware } from 'koa';
|
||||||
|
import { LOGIN_ROUTE } from '../../constants/constants';
|
||||||
|
|
||||||
|
export const authenticated = (): Middleware => {
|
||||||
|
return (ctx, next) => {
|
||||||
|
if (ctx.isAuthenticated()) {
|
||||||
|
return next();
|
||||||
|
} else {
|
||||||
|
ctx.redirect(process.env.LOGIN_ROUTE || LOGIN_ROUTE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import passport from 'koa-passport';
|
import passport from 'koa-passport';
|
||||||
|
|
||||||
import Auth from '../model/auth';
|
import Auth from '../../model/auth';
|
||||||
import { Auth as AuthRecord } from '../schema/auth';
|
import { Auth as AuthRecord } from '../../db/schema/auth';
|
||||||
import LocalStrategy from './strategies/local';
|
import LocalStrategy from './strategies/local';
|
||||||
import JwtStrategy from './strategies/jwt';
|
import JwtStrategy from './strategies/jwt';
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// eslint-disable-next-line import/named
|
// eslint-disable-next-line import/named
|
||||||
import { ExtractJwt, Strategy as JwtStrategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy as JwtStrategy } from 'passport-jwt';
|
||||||
|
|
||||||
import Auth from '../../model/auth';
|
import Auth from '../../../model/auth';
|
||||||
import { getJwtSecret } from '../../utils/jwt';
|
import { getJwtSecret } from '../../../utils/jwt';
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import passport from 'koa-passport';
|
|
||||||
// eslint-disable-next-line import/named
|
// eslint-disable-next-line import/named
|
||||||
import { Strategy as LocalStrategy } from 'passport-local';
|
import { Strategy as LocalStrategy } from 'passport-local';
|
||||||
|
|
||||||
import Auth from '../../model/auth';
|
import Auth from '../../../model/auth';
|
||||||
|
|
||||||
export default new LocalStrategy(async (username: string, password: string, done: any) => {
|
export default new LocalStrategy(async (username: string, password: string, done: any) => {
|
||||||
const user = await Auth.findOne({
|
const user = await Auth.findOne({
|
||||||
@@ -2,12 +2,10 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
import app from './app';
|
import app from './app';
|
||||||
import { connection } from './database/database.connection';
|
import { connection } from './database/database.connection';
|
||||||
import { PORT as DEFAULT_PORT } from './constants/defaults';
|
import { PORT } from '../constants/constants';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const PORT: number = Number(process.env.PORT) || DEFAULT_PORT;
|
|
||||||
|
|
||||||
connection.then(
|
connection.then(
|
||||||
() => app.listen(PORT),
|
() => app.listen(PORT),
|
||||||
(err) => console.error('ERROR!', err),
|
(err) => console.error('ERROR!', err),
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import Auth from '../model/auth';
|
import Auth from '../db/model/auth';
|
||||||
import { AuthModel, AuthPrivate } from '../schema/auth';
|
import { AuthModel, AuthPrivate } from '../db/schema/auth';
|
||||||
import { sign } from './jwt';
|
import { sign } from './jwt';
|
||||||
|
|
||||||
export const getAuthenticationBundle = async (username: string, password: string) => {
|
export const getAuthenticationBundle = async (username: string, password: string) => {
|
||||||
const auth = await Auth.findByUsername(username).catch();
|
const auth = await Auth.findByUsername(username).catch();
|
||||||
const isAuthenticated = !!auth && (auth as AuthModel).authenticate(password);
|
const isAuthenticated = !!auth && (<AuthModel>auth).authenticate(password);
|
||||||
const record = isAuthenticated ? ((auth as AuthPrivate).record as string) : null;
|
const record = isAuthenticated ? <string>(<AuthPrivate>auth).record : null;
|
||||||
const token = sign(record || undefined);
|
const token = sign(record || undefined);
|
||||||
return {
|
return {
|
||||||
record,
|
record,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { JWT_SECRET } from '../constants/defaults';
|
import { JWT_AUDIENCE, JWT_ISSUER, JWT_SECRET } from '../constants/constants';
|
||||||
|
|
||||||
export const getJwtSecret = () => process.env.JWT_SECRET || JWT_SECRET;
|
|
||||||
export interface TokenProps {
|
export interface TokenProps {
|
||||||
aud?: string;
|
aud?: string;
|
||||||
exp?: number | Date;
|
exp?: number | Date;
|
||||||
@@ -26,12 +24,12 @@ export const sign = (props: SignProps) => {
|
|||||||
{
|
{
|
||||||
exp,
|
exp,
|
||||||
sub,
|
sub,
|
||||||
aud: rest.aud || process.env.JWT_AUDIENCE,
|
aud: rest.aud || JWT_AUDIENCE,
|
||||||
iat: today.getTime(),
|
iat: today.getTime(),
|
||||||
iss: rest.iss || process.env.JWT_ISSUER,
|
iss: rest.iss || JWT_ISSUER,
|
||||||
},
|
},
|
||||||
getJwtSecret(),
|
JWT_SECRET,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verify = (token: string) => jwt.verify(token, getJwtSecret());
|
export const verify = (token: string) => jwt.verify(token, JWT_SECRET);
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import { API_PATH, PORT, RESET_ROUTE } from '../constants/defaults';
|
import { RESET_ROUTE, ROUTE_PREFIX } from '../constants/constants';
|
||||||
|
|
||||||
export const getPasswordResetLink = (token: string) => {
|
export const getPasswordResetPath = (token: string) => `${ROUTE_PREFIX}${RESET_ROUTE}?t=${token}`;
|
||||||
const hostname = process.env.HOST_NAME || `localhost:${process.env.PORT || PORT}`;
|
|
||||||
const path = `${process.env.API_PATH || API_PATH}${process.env.RESET_ROUTE || RESET_ROUTE}`;
|
|
||||||
return `https://${hostname}${path}?t=${token}`;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,13 +1,34 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { sign } from './jwt';
|
import { sign } from './jwt';
|
||||||
|
import { LOGIN_VALID_TIME, RESET_VALID_MINUTES } from '../constants/constants';
|
||||||
|
import { Status } from '../constants/auth';
|
||||||
|
|
||||||
|
const parseLoginValid = () => {
|
||||||
|
const [number, unit] = process.env.LOGIN_VALID_TIME || LOGIN_VALID_TIME;
|
||||||
|
return [
|
||||||
|
unit === 'd' ? parseInt(number) : 1,
|
||||||
|
unit === 'h' ? parseInt(number) : (unit === 'm' && 1) || 24,
|
||||||
|
unit === 'm' ? parseInt(number) : 60,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateLoginToken = (sub: string, status: Status) => {
|
||||||
|
const [days, hours, mins] = parseLoginValid();
|
||||||
|
return sign({
|
||||||
|
sub,
|
||||||
|
status,
|
||||||
|
exp: Date.now() + days * hours * mins * 60 * 1000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const generateResetToken = (sub: string) => {
|
export const generateResetToken = (sub: string) => {
|
||||||
|
const hoursValid = <number>(process.env.RESET_VALID_HOURS || RESET_VALID_MINUTES);
|
||||||
const key = crypto.randomBytes(16).toString('hex');
|
const key = crypto.randomBytes(16).toString('hex');
|
||||||
const token = sign({
|
const token = sign({
|
||||||
sub,
|
sub,
|
||||||
key,
|
key,
|
||||||
exp: Date.now() + 24 * 60 * 60 * 1000,
|
exp: Date.now() + hoursValid * 60 * 60 * 1000,
|
||||||
});
|
});
|
||||||
return { key, token };
|
return { key, token };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user