35 Commits

Author SHA1 Message Date
ee398feeb9 Update pipelines
Some checks are pending
continuous-integration/drone/push Build is running
2023-05-04 11:23:28 -04:00
c2d9f8be5a Update to v 0.0.4 2023-05-04 11:17:52 -04:00
5f69193b81 Annoying ass shit 2023-05-03 23:07:44 -04:00
2a13a433e1 this 2023-05-03 23:06:37 -04:00
b094ae2c9a Deploy 2023-05-03 23:00:55 -04:00
e48037a7d4 Tweaks 2023-05-03 22:57:22 -04:00
ecfa12154d Up version / publish 2023-05-03 22:37:21 -04:00
aa2611b68d Updates
Some checks failed
continuous-integration/drone/push Build encountered an error
2023-05-03 19:46:24 -04:00
0a7bbb96c0 Why not?
Some checks failed
continuous-integration/drone/push Build encountered an error
2023-05-03 17:05:53 -04:00
96c7061ace try this 2023-05-03 16:52:19 -04:00
6b39d138de Deploy reporting
Some checks failed
continuous-integration/drone/push Build encountered an error
2023-05-03 16:40:30 -04:00
0d3571e859 last try then pause
Some checks failed
continuous-integration/drone/push Build encountered an error
2023-05-03 16:36:07 -04:00
2a7172ddfe ?? 2023-05-03 16:33:00 -04:00
97ed5f1f43 ? 2023-05-03 16:31:42 -04:00
0e53dd8132 brokeb? 2023-05-03 16:29:03 -04:00
06c9c1d896 test 2023-05-03 16:26:26 -04:00
f29b50120f typo 2023-05-03 15:34:07 -04:00
cb75c651b9 Done 2023-05-03 15:33:24 -04:00
c83db307ad Dammit 2023-05-03 15:29:42 -04:00
45964e4f13 Ugh 2023-05-03 15:23:13 -04:00
0e3ff90e09 Done 2023-05-03 15:17:31 -04:00
8031b12940 Hmmm. 2023-05-03 15:13:56 -04:00
9073408f6d What about... 2023-05-03 15:11:41 -04:00
943ffe96d1 Erm..
Some checks failed
continuous-integration/drone/push Build was killed
2023-05-03 15:05:46 -04:00
6d07bbd9ad - Changes 2023-05-03 15:03:13 -04:00
51b3d2c7c6 A stab at deploying
Some checks failed
continuous-integration/drone/push Build was killed
2023-05-03 15:00:10 -04:00
32bfbd7adc Finishing touches to publish
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-03 11:38:41 -04:00
dc72cefece Reorganizing
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-03 11:12:59 -04:00
27a78dd471 Better build pipeline?
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-02 22:55:02 -04:00
8ca639058b - stuff and things
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-02 22:50:23 -04:00
39ba4965e0 Tweaks
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-02 22:10:47 -04:00
e3a0e36560 Adding jest
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-02 21:58:29 -04:00
68f58329e8 - Connecting the dots?
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-02 21:47:45 -04:00
5fba4c6643 Linty fresh and pretty
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-02 20:54:14 -04:00
f105ce537f Prettier! 2023-05-02 20:36:40 -04:00
34 changed files with 1944 additions and 408 deletions

View File

@@ -55,6 +55,11 @@ workspace:
path: /drone/grow
steps:
- name: yarn install
image: node:latest
commands:
- yarn install
- name: Build
image: node:latest
commands:
@@ -84,3 +89,129 @@ trigger:
- main
event:
- push
---
kind: pipeline
type: docker
name: Publish Pipeline
workspace:
path: /drone/grow
steps:
- name: Build Service
image: node:latest
commands:
- cp /drone/grow/package.json ./
- cp /drone/grow/yarn.lock ./
- yarn install --frozen-lockfile
- cp -r /drone/grow/* .
- yarn build
- name: Publish NPM
image: plugins/npm
settings:
username: mifi
registry: git.mifi.dev
token:
- from_secret: gitea_token
- name: Publish Image
image: plugins/docker
settings:
auto_tag: true
squash: true
repo: git.mifi.dev/mifi/auth
registry: git.mifi.dev
ssh-agent-key:
from_secret: gitea_token
volumes:
- name: dockersock
Path: /var/run/docker.sock
- name: dockerconfig
Path: /.docker/config.json
- name: Send Status Notifications
image: plugins/webhook
settings:
urls: https://lab.mifi.dev/hooks/ccw34hdf7tgbjmzp96nptn938r
content_type: application/json
template: |
{
"icon_url":"https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/apple/198/freezing-face_1f976.png",
"text": "[{{ repo.name }} - New tagged docker image release {{tag}} from # {{ build.number }}] Deploy {{ build.status }} {{#success build.status}}:tada:{{else}}:poop:{{/success}}",
"username":"DroneBot"
}
when:
status:
- success
- failure
volumes:
- name: dockersock
path: /var/run/docker.sock
- name: dockerconfig
path: /volume1/docker/labs/grow-auth/dockerconfig.json
depends_on:
- Build Pipeline
trigger:
branch:
- main
event:
- tag
---
kind: pipeline
type: docker
name: Deploy Pipeline
workspace:
path: /drone/grow
# image_pull_secrets:
# - from_secret: gitea_token
steps:
- name: Deploy Container
image: docker
commands:
- compose build .
- compose up --wait
volumes:
- name: env
path: /.env
- name: dockersock
Path: /var/run/docker.sock
- name: dockerconfig
Path: /.docker/config.json
- name: Send Status Notifications
image: plugins/webhook
settings:
urls: https://lab.mifi.dev/hooks/ccw34hdf7tgbjmzp96nptn938r
content_type: application/json
template: |
{
"icon_url":"https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/apple/198/freezing-face_1f976.png",
"text": "[{{ repo.name }} - Build # {{ build.number }}] Deploy {{ build.status }} {{#success build.status}}:tada:{{else}}:poop:{{/success}}",
"username":"DroneBot"
}
when:
status:
- success
- failure
volumes:
- name: env
path: /volume1/docker/labs/grow-auth/.env
- name: dockersock
path: /var/run/docker.sock
- name: dockerconfig
path: /volume1/docker/labs/grow-auth/dockerconfig.json
depends_on:
- Build Pipeline
trigger:
branch:
- main
event:
- tag

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# FROM node:20-alpine AS build
# RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
# WORKDIR /home/node/app
# COPY package*.json .
# COPY dist/lib .
# USER node
# RUN yarn install --frozen-lockfile --production
# COPY --chown=node:node node_modules ./node_modules
# CMD ["node", "dist/lib/server/index.js"]
FROM node:20-alpine AS build
WORKDIR /home/node/app
COPY package*.json ./
COPY tsconfig.json ./
COPY lib ./lib
RUN ls -a
RUN yarn install
RUN yarn build:production
## this is stage two , where the app actually runs
FROM node:20-alpine AS containerize
WORKDIR /home/node/app
COPY package*.json ./
RUN yarn install --frozen-lockfile --production
COPY --from=0 /home/node/app/dist .
EXPOSE 80
CMD ["node","server/index.js"]

6
babel.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

56
docker-compose.yml Normal file
View File

@@ -0,0 +1,56 @@
version: '3.8'
services:
auth-service_mongo:
container_name: ${CONTAINER_PREFIX}${SERVICE_NAME}_${DB_TYPE}
environment:
- ALLOW_EMPTY_PASSWORD=yes
- MONGO_INITDB_ROOT_USERNAME=${DB_USER}
- MONGO_INITDB_ROOT_PASSWORD=${DB_PASS}
- MONGO_INITDB_DATABASE=${DB_NAME}
networks:
- docknet
volumes:
- '/volume1/docker/labs/grow-auth/mongo:/data/db'
restart: unless-stopped
image: mongo
auth-service:
build: .
container_name: ${CONTAINER_PREFIX}-${SERVICE_NAME}
environment:
- PORT=${PORT}
- ROUTE_PREFIX=${ROUTE_PREFIX}
- LOGIN_ROUTE=${LOGIN_ROUTE}
- RESET_ROUTE=${RESET_ROUTE}
- DB_HOST=${CONTAINER_PREFIX}-${SERVICE_NAME}_${DB_TYPE}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
- DB_NAME=${DB_NAME}
- SESSION_KEY=${SESSION_KEY}
- JWT_AUDIENCE=${JWT_AUDIENCE}
- JWT_ISSUER=${JWT_ISSUER}
- JWT_SECRET=${JWT_SECRET}
- LOGIN_VALID_TIME=${LOGIN_VALID_TIME}
- RESET_VALID_MINUTES=${RESET_VALID_MINUTES}
- DEFAULT_TOKEN_DAYS=${DEFAULT_TOKEN_DAYS}
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.grow.rule=Host(`${HOST}`)'
- 'traefik.http.routers.grow.rule=Path(`${ROUTE_PREFIX}`)'
- 'traefik.http.routers.grow.entrypoints=websecure'
- 'traefik.http.routers.grow.tls=true'
- 'traefik.http.routers.grow.tls.certresolver=letsencrypt'
- 'traefik.http.routers.grow.service=gitea'
- 'traefik.http.services.gitea.loadbalancer.server.port=${PORT}'
networks:
- docknet
volumes:
- '/Volume1/docker/.yarnrc.yml:/.yarnrc.yml'
- '/Volume1/docker/labs/grow-auth/.env:/.env'
restart: unless-stopped
image: node
depends_on:
- auth-service_mongo
networks:
docknet:
name: docknet

195
jest.config.ts Normal file
View File

@@ -0,0 +1,195 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/75/3cyx0pq133n7gk3ysqf0mj4r0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

View File

@@ -1,16 +0,0 @@
// import koaPassport from 'koa-passport';
// import Users from 'grow-db/lib/models/users';
// import { User } from 'grow-db/lib/schemas/user';
// passport.serializeUser((user: User, done) => { done(null, user._id); });
// passport.deserializeUser(async (id, done) => {
// const user = await Users.findById(id);
// if (user) {
// done(null, user);
// }
// done('user not found', null);
// });

8
lib/constants/auth.ts Normal file
View File

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

5
lib/constants/db.ts Normal file
View File

@@ -0,0 +1,5 @@
export const DB_HOST = process.env.DB_HOST || 'mongodb';
export const DB_PORT = process.env.DB_PORT || 27017;
export const DB_USER = process.env.DB_USER || 'test';
export const DB_PASS = process.env.DB_PASSWORD || 'test';
export const DB_NAME = process.env.DB_NAME || 'auth';

16
lib/constants/env.ts Normal file
View File

@@ -0,0 +1,16 @@
export const PACKAGE_NAME = '@mifi/latch';
export const PORT = process.env.PORT || 9000;
export const SESSION_KEY = process.env.SESSION_KEY || 'secret-key';
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';

12
lib/constants/errors.ts Normal file
View 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],
});

View File

@@ -1,227 +0,0 @@
// // const errors = require('restify-errors');
// // const config = require('../config');
// // const handlePassportResponse = (req, res, next) => (err, user, info) => {
// // if (err) {
// // return next(err);
// // }
// // const isVerifiedUser = user &&
// // user.isRegistrationVerified();
// // if (user && isVerifiedUser) {
// // return res.send({ ...user.toAuthJSON() });
// // } else if (user && !isVerifiedUser){
// // return res.send({
// // registrationSuccess: true,
// // nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.'
// // });
// // }
// // return res.send(400, info);
// // };
// // module.exports = function (server, auth) {
// // const { passport } = auth;
// // /* Local Auth */
// // server.post('/auth', (req, res, next) => {
// // const { body: { username = null, password = null } = {} } = req;
// // if (!username || !password) {
// // let errors = {};
// // if (!username) {
// // errors.username = 'is required';
// // }
// // if (!password) {
// // errors.password = 'is required';
// // }
// // return res.send(422, { errors });
// // }
// // const callback = handlePassportResponse(req, res, next);
// // return passport.authenticate('local', { session: false }.then(callback)(req, res, next);
// // });
// // /**
// // * SERVICES
// // */
// // /* Google */
// // server.get(
// // '/auth/google',
// // passport.authenticate('google', { scope: 'profile email', session: false }),
// // );
// // server.get(
// // '/auth/google/callback',
// // (req, res, next) => {
// // const callback = handlePassportResponse(req, res, next);
// // return passport.authenticate(
// // 'google',
// // { failureRedirect: '/login' },
// // callback,
// // )(req, res, next);
// // },
// // );
// // /* Facebook */
// // server.get(
// // '/auth/facebook/login',
// // passport.authenticate('facebook', {
// // scope: ['email', 'public_profile'],
// // session: false,
// // }),
// // );
// // server.get(
// // '/auth/facebook/loggedin',
// // (req, res, next) => {
// // const callback = handlePassportResponse(req, res, next);
// // return passport.authenticate(
// // 'facebook',
// // { failureRedirect: '/login' },
// // callback,
// // )(req, res, next);
// // }
// // );
// // server.get(
// // '/auth/facebook/link',
// // auth.secure,
// // (req, res, next) => {
// // req.user.record.setLinkCheckBit((err, linkCheckBit) => {
// // passport.authenticate('facebookLink', {
// // scope: ['email', 'public_profile'],
// // session: false,
// // state: linkCheckbit,
// // })(req, res, next);
// // });
// // },
// // );
// //
// // server.get(
// // '/auth/facebook/linked',
// // (req, res, next) => {
// // const linkCheckBit = req.query.state;
// //
// // return passport.authenticate(
// // 'facebook',
// // { failureRedirect: '/profile' },
// // (err, profile) => {
// // if (err) {
// // return next(err);
// // }
// //
// // User.linkFacebookProfile(linkCheckBit, profile, (err, user) => {
// // if (err) {
// // return next(err);
// // }
// //
// // if (!user) {
// // return next(err, false, 'Linking the account to Facebook was unsuccessful, please try again.');
// // }
// //
// // res.send({
// // success: true,
// // info: 'Facerbook account successfully linked',
// // });
// // });
// // },
// // )(req, res, next);
// // }
// // );
// };
// import Koa from 'koa';
// import Router from 'koa-router';
// import { StatusCodes } from 'http-status-codes';
// import Users from 'grow-db/lib/models/users';
// const handlePassportResponse = (ctx: Koa.Context) => (err, user, info) => {
// if (err) {
// return next(err);
// }
// const isVerifiedUser = user &&
// user.isRegistrationVerified();
// if (user && isVerifiedUser) {
// return res.send({ ...user.toAuthJSON() });
// } else if (user && !isVerifiedUser){
// return res.send({
// registrationSuccess: true,
// nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.'
// });
// }
// return res.send(400, info);
// };
// const routerOpts: Router.IRouterOptions = {
// prefix: '/auth',
// };
// const router: Router = new Router(routerOpts);
// router.get('/', async (ctx: Koa.Context) => {
// const data = await Customers.find({}).exec();
// ctx.body = { data };
// });
// router.get('/:customer_id', async (ctx: Koa.Context) => {
// const data = await Customers.findById(ctx.params.customer_id).populate('person').exec();
// if (!data) {
// ctx.throw(StatusCodes.NOT_FOUND);
// }
// ctx.body = { data };
// });
// router.delete('/:customer_id', async (ctx: Koa.Context) => {
// const data = await Customers.findByIdAndDelete(ctx.params.customer_id).exec();
// if (!data) {
// ctx.throw(StatusCodes.NOT_FOUND);
// }
// ctx.body = { success: true, data };
// });
// router.post('/', async (ctx: Koa.Context) => {
// const data = await Customers.create(ctx.body);
// data.save();
// ctx.body = { success: true, data };
// });
// router.post('/', async (ctx: Koa.Context) => {
// const { body: { username = null, password = null } = {} } = ctx;
// if (!username || !password) {
// let errors = {};
// if (!username) {
// errors.username = 'is required';
// }
// if (!password) {
// errors.password = 'is required';
// }
// ctx.status = StatusCodes.UNPROCESSABLE_ENTITY;
// ctx.throw(422, { errors });
// }
// const callback = handlePassportResponse(req, res, next);
// return passport.authenticate('local', { session: false }, callback)(req, res, next);
// });
// router.patch('/:customer_id', async (ctx: Koa.Context) => {
// const data = await Customers.findByIdAndUpdate(ctx.params.customer_id);
// if (!data) {
// ctx.throw(StatusCodes.NOT_FOUND);
// }
// ctx.body = { success: true, data };
// });

View File

@@ -1,9 +0,0 @@
import mongoose from 'mongoose';
const DB_USER = process.env.DB_USER || 'test';
const DB_PASS = process.env.DB_PASSWORD || 'test';
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}`);

5
lib/db/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import mongoose from 'mongoose';
import { DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER } from '../constants/db';
export const connection = mongoose.connect(`mongodb://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}`);

View File

@@ -2,10 +2,12 @@ import { JwtPayload } from 'jsonwebtoken';
import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
import { Strategy } from './strategy';
import { STRATEGIES } from '../constants/strategies';
import { TokenProps, sign, verify as verifyJwt } from '../utils/jwt';
import { encrypt, verify as verifyPassword } from '../utils/password';
import { generateResetToken } from '../utils/tokens';
import { STRATEGIES } from '../../constants/strategies';
import { TokenProps, verify as verifyJwt } from '../../utils/jwt';
import { encrypt, verify as verifyPassword } from '../../utils/password';
import { generateLoginToken, generateResetToken } from '../../utils/tokens';
import { getPasswordResetPath } from '../../utils/links';
import { Status } from '../../constants/auth';
export type Auth = {
is2FA?: boolean;
@@ -14,6 +16,7 @@ export type Auth = {
};
export type AuthPrivate = Auth & {
status: Status;
strategies: Types.ArraySubdocument<Strategy>;
};
@@ -23,22 +26,24 @@ export interface AuthMethods {
getResetLink(route: string): Promise<string | undefined>;
getResetToken(): Promise<string | undefined>;
getToken(props?: Omit<TokenProps, 'sub'> | void): string;
isActive(): boolean;
setPassword(password: string): Promise<boolean>;
}
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>;
isUsernameAvailable(username: string): Promise<boolean>;
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>(
{
is2FA: { type: Boolean, default: false },
record: { type: Types.ObjectId },
strategies: { type: Types.ArraySubdocument<Strategy>, required: true },
strategies: { type: Array<Strategy>, required: true },
status: { type: Number, enum: Object.values(Status), default: Status.UNVERIFIED },
username: { type: String, required: true, unique: true },
},
{
@@ -58,19 +63,13 @@ AuthSchema.methods = {
},
getToken(props = {}) {
return sign({
sub: this._id,
...props,
});
return generateLoginToken(this._id, this.status);
},
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}`;
const token = await this.getResetToken();
if (token) {
const resetUrl = getPasswordResetPath(token);
console.log('[sendPasswordReset] resetUrl:', resetUrl);
return resetUrl;
}
@@ -83,6 +82,10 @@ AuthSchema.methods = {
return token;
},
isActive() {
return this.status === Status.ACTIVE;
},
async setPassword(password) {
const key = encrypt(password);
const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL);
@@ -109,13 +112,13 @@ AuthSchema.methods = {
};
AuthSchema.statics = {
// authenticateAndGetRecordLocator: async function (username, password) {
// const auth = await this.findByUserName(username);
// if (auth && auth.authenticate(password)) {
// return auth?.record;
// }
// return false;
// },
authenticate: async function (username, password) {
const auth = await this.findByUsername(username);
if (auth && auth.authenticate(password)) {
return auth.record;
}
return false;
},
async findByUsername(username) {
return this.findOne({ username });
@@ -132,7 +135,11 @@ AuthSchema.statics = {
_id: sub,
'strategies.resetToken': key,
}).catch();
return !!auth && auth.setPassword(password);
if (auth) {
await auth.setPassword(password).catch();
return auth.getToken();
}
return false;
},
};

View File

@@ -1,5 +1,5 @@
import { InferSchemaType, Schema, Types } from 'mongoose';
import { STRATEGIES } from '../constants/strategies';
import { STRATEGIES } from '../../constants/strategies';
export const Strategy = new Schema(
{

View File

@@ -1,11 +1,13 @@
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 passport from './passport';
import { performanceLogger, perfromanceTimer } from './middleware/performance';
import { errorHandler } from './middleware/errorHandler';
import { authRouter } from './controllers/auth';
import { SESSION_KEY } from '../constants/env';
const app: Koa = new Koa();
@@ -15,12 +17,15 @@ app.use(performanceLogger);
app.use(bodyparser());
app.use(cookie());
app.keys = [process.env.SESSION_KEYS as string];
app.keys = [SESSION_KEY];
app.use(session({}, app));
app.use(passport.initialize());
app.use(passport.session());
app.use(authRouter.routes());
app.use(authRouter.allowedMethods());
// Application error logging.
app.on('error', console.error);

View File

@@ -0,0 +1,48 @@
import Koa from 'koa';
import Router from 'koa-router';
import { StatusCodes } from 'http-status-codes';
import { ROUTE_PREFIX as prefix, RESET_ROUTE } from '../../constants/env';
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 };

View File

@@ -1,12 +1,11 @@
import dotenv from 'dotenv';
import app from './app';
import { connection } from './database/database.connection';
import { connection } from '../db';
import { PORT } from '../constants/env';
dotenv.config();
const PORT: number = Number(process.env.PORT) || 9000;
connection.then(
() => app.listen(PORT),
(err) => console.error('ERROR!', err),

View File

@@ -0,0 +1,12 @@
import { Middleware } from 'koa';
import { LOGIN_ROUTE } from '../../constants/env';
export const authenticated = (): Middleware => {
return (ctx, next) => {
if (ctx.isAuthenticated()) {
return next();
} else {
ctx.redirect(process.env.LOGIN_ROUTE || LOGIN_ROUTE);
}
};
};

View File

@@ -0,0 +1,23 @@
import passport from 'koa-passport';
import Auth from '../../db/model/auth';
import { Auth as AuthRecord } from '../../db/schema/auth';
import LocalStrategy from './strategies/local';
import JwtStrategy from './strategies/jwt';
passport.serializeUser((user, done) => done(null, (user as AuthRecord).record));
passport.deserializeUser(async (id, done) => {
const user = await Auth.findOne({ record: id });
if (user) {
done(null, user);
}
done('user not found', null);
});
passport.use(LocalStrategy);
passport.use(JwtStrategy);
export default passport;

View File

@@ -0,0 +1,22 @@
// eslint-disable-next-line import/named
import { ExtractJwt, Strategy as JwtStrategy } from 'passport-jwt';
import Auth from '../../../db/model/auth';
import { JWT_SECRET } from '../../../constants/env';
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JWT_SECRET,
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
};
export default new JwtStrategy(opts, async (jwt_payload, done) => {
const auth = await Auth.findOne({ record: jwt_payload.sub }).catch();
if (auth) {
return done(null, auth);
}
return done(null, false);
});

View File

@@ -0,0 +1,17 @@
// eslint-disable-next-line import/named
import { Strategy as LocalStrategy } from 'passport-local';
import Auth from '../../../db/model/auth';
export default new LocalStrategy(async (username: string, password: string, done: any) => {
const user = await Auth.findOne({
where: {
username,
},
}).catch();
if (user && user.authenticate(password)) {
done(null, user);
} else {
done(null, false);
}
});

View File

@@ -1,20 +0,0 @@
import passport from 'koa-passport';
// eslint-disable-next-line import/named
import { Strategy as LocalStrategy } from 'passport-local';
import Auth from '../model/auth';
export const localStrategy = passport.use(
new LocalStrategy(async (username: string, password: string, done: any) => {
const user = await Auth.findOne({
where: {
username,
},
}).catch();
if (user && user.authenticate(password)) {
done(null, user);
} else {
done(null, false);
}
}),
);

View File

@@ -1,11 +1,11 @@
import Auth from '../model/auth';
import { AuthModel, AuthPrivate } from '../schema/auth';
import Auth from '../db/model/auth';
import { AuthModel, AuthPrivate } from '../db/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 as string : null;
const isAuthenticated = !!auth && (<AuthModel>auth).authenticate(password);
const record = isAuthenticated ? <string>(<AuthPrivate>auth).record : null;
const token = sign(record || undefined);
return {
record,

View File

@@ -1,5 +1,5 @@
import jwt from 'jsonwebtoken';
import { JWT_AUDIENCE, JWT_ISSUER, JWT_SECRET } from '../constants/env';
export interface TokenProps {
aud?: string;
exp?: number | Date;
@@ -24,12 +24,12 @@ export const sign = (props: SignProps) => {
{
exp,
sub,
aud: rest.aud || process.env.JWT_AUDIENCE,
aud: rest.aud || JWT_AUDIENCE,
iat: today.getTime(),
iss: rest.iss || process.env.JWT_ISSUER,
iss: rest.iss || JWT_ISSUER,
},
process.env.JWT_SECRET || 'secret',
JWT_SECRET,
);
};
export const verify = (token: string) => jwt.verify(token, process.env.JWT_SECRET || 'secret');
export const verify = (token: string) => jwt.verify(token, JWT_SECRET);

3
lib/utils/links.ts Normal file
View File

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

View File

@@ -1,13 +1,34 @@
import crypto from 'crypto';
import { sign } from './jwt';
import { LOGIN_VALID_TIME, RESET_VALID_MINUTES } from '../constants/env';
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) => {
const hoursValid = <number>(process.env.RESET_VALID_HOURS || RESET_VALID_MINUTES);
const key = crypto.randomBytes(16).toString('hex');
const token = sign({
sub,
key,
exp: Date.now() + 24 * 60 * 60 * 1000,
exp: Date.now() + hoursValid * 60 * 60 * 1000,
});
return { key, token };
};

View File

@@ -1,20 +1,26 @@
{
"name": "@mifi/auth",
"version": "0.0.1",
"version": "0.0.4",
"author": "mifi (Mike Fitzpatrick)",
"license": "MIT",
"scripts": {
"build": "tsc",
"build:production": "tsc -p .",
"format": "prettier:fix && lint:fix",
"lint": "eslint --ext .ts,.tsx lib/",
"lint:fix": "eslint --fix --ext .ts,.tsx lib/",
"prettier": "prettier --check 'lib/**/*.ts'",
"prettier:fix": "prettier --write 'lib/**/*.ts'",
"serve": "ts-node src/server.ts",
"start": "nodemon"
"serve": "node dist/lib/server/index.js",
"start": "nodemon",
"test": "jest --passWithNoTests"
},
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@tsconfig/node16": "^1.0.3",
"@types/jest": "^29.5.1",
"@types/jsonwebtoken": "^9.0.1",
"@types/koa": "^2.13.5",
"@types/koa-bodyparser": "^4.3.10",
@@ -32,15 +38,15 @@
"@types/passport-local": "^1.0.35",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"babel-jest": "^29.5.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-import-resolver-webpack": "^0.13.2",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.0.0",
"jest": "^29.4.2",
"jest": "^29.5.0",
"nodemon": "^2.0.20",
"prettier": "^2.8.4",
"prettier-eslint": "^15.0.1",
@@ -66,6 +72,7 @@
"passport-facebook": "^3.0.0",
"passport-fido2-webauthn": "^0.1.0",
"passport-google-oauth": "^2.0.0",
"passport-http-bearer": "^1.0.1",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0"
},

View File

@@ -6,7 +6,7 @@
"emitDecoratorMetadata": true,
"noImplicitAny": true,
"outDir": "./dist/",
"rootDirs": ["lib", "../"],
"rootDirs": ["lib"],
"sourceMap": true
}
}

1361
yarn.lock

File diff suppressed because it is too large Load Diff