feat: implement base db support

feat: implement dev containers
This commit is contained in:
Kakious 2024-07-16 01:05:06 -04:00
parent ca8ee728fb
commit a71c33c085
22 changed files with 1408 additions and 262 deletions

View file

@ -0,0 +1,63 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
{
"name": "waterwolf-auth",
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
// The 'service' property is the name of the service for the container that VS Code should
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
"service": "waterwolf-auth",
// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/cirolosapio/devcontainers-features/alpine-git:0": {}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000, "mysql:3306"],
"portsAttributes": {
"3000": {
"label": "Application port"
},
"9464": {
"label": "Metrics port",
"onAutoForward": "ignore"
},
"mysql:3306": {
"label": "MySQL"
}
},
// Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [],
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",
// Uncomment the next line to run commands after the container is created.
"postCreateCommand": "./.devcontainer/postCreateCommand.sh",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"visualstudioexptteam.vscodeintellicode",
"orta.vscode-jest",
"eamodio.gitlens",
"firsttris.vscode-jest-runner",
"christian-kohler.path-intellisense",
"esbenp.prettier-vscode",
"rvest.vs-code-prettier-eslint",
"steoates.autoimport",
"pmneo.tsimporter",
"christian-kohler.npm-intellisense",
"rohinivsenthil.rabbitrace"
]
}
}
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "devcontainer"
}

View file

@ -0,0 +1,26 @@
version: '3.8'
services:
# Update this to the name of the service you want to work with in your docker-compose.yml file
waterwolf-auth:
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
# array). The sample below assumes your primary file is in the root of your project.
#
# build:
# context: .
# dockerfile: .devcontainer/Dockerfile
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- ..:/workspaces:cached
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
# cap_add:
# - SYS_PTRACE
# security_opt:
# - seccomp:unconfined
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "while sleep 1000; do :; done"

140
.dockerignore Normal file
View file

@ -0,0 +1,140 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
# .next
# out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
bin/
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
keys/
.git
.gitignore
.github
Dockerfile
.dockerignore
**/README.md

58
Dockerfile Normal file
View file

@ -0,0 +1,58 @@
# base environment
FROM node:22.4.1-alpine3.20 AS base-stage
RUN mkdir /app && chown -R node:node /app
WORKDIR /app
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# dependency environment
FROM base-stage AS dependency-stage
USER node
COPY --link --chown=1000:1000 package*.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# test stage to stop at for testing
FROM dependency-stage AS all-source-stage
COPY --link --chown=1000:1000 . .
# lint
FROM all-source-stage AS lint
RUN npm run lint -- --no-fix
# test-e2e
FROM all-source-stage AS test-e2e
USER root
RUN mkdir /keys
USER node
RUN --mount=type=secret,id=cookies,target=/keys/cookies.json,uid=1000,gid=1000,required=true \
--mount=type=secret,id=jwks,target=/keys/jwks.json,uid=1000,gid=1000,required=true \
npm run test:e2e:cov -- --ci --json --testLocationInResults --outputFile=/tmp/report.json
# Just the e2e report file
FROM scratch AS test-stage
COPY --link --from=test-e2e /tmp/report.json /
# build environment
FROM dependency-stage AS build-stage
COPY --link --chown=1000:1000 . .
RUN npm run build
# prod dependency environment
FROM build-stage AS production-dependency-stage
RUN npm prune --production
# production environment
FROM base-stage AS production-stage
RUN apk add --no-cache tini
USER node
COPY --link --chown=1000:1000 --from=production-dependency-stage /app /app
ENV NODE_ENV=production
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "/app/.dist/src/main.js"]
ARG VERSION
ENV VERSION=${VERSION}

View file

@ -1,8 +1,27 @@
## Waterwolf Identity Solution
# Waterwolf Identity Solution
This is a re-imagination on how BOOP works with an optimized OAUTH 2.0 flow. This will hook into the existing user database to read usernames and passwords.
## Description
This is the identity service for the Waterwolf project. It is a NestJS application that provides an API for managing users and authentication.
## Development
To get started with development, first clone the repository.
Create a GitHub classic PAT with permission to read repos and packages in order to be able to install the private furality npm packages .
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic
Create the token and put it in a file in the root of the repository named `.github_token`.
Then install vscode which supports devcontainers and open the repository in vscode as a devcontainer.
This will set up all the necessary supporting services needed to run, test, and debug this service.
Once it finishes building initially, goto the testing tab of vscode and run the tests to make sure
they pass and that your environment is properly set up.
Next, you can start up the app by going to the run and debug tab and selecting the `nest start watch` task.
Once it is started, in the bottom pane of vscode, goto the PORTS tab and look for the `Application port` row.
Either right click or copy the forwarded address to open it in your browser and then navigate to the path:
http://localhost:{PORT}/v1/api/ to ensure it has started correctly
## Installation
@ -17,3 +36,18 @@ pnpm install
```bash
pnpm start
```
## Configuration
This uses `@nestjs/config` which uses [dotenv](https://github.com/motdotla/dotenv).
Configuration can be done by using a `.env` file or environment variables.
Mailing Configuration
- `POSTAL_BASE_URL` - Base URL for the Postal API
- `POSTAL_API_KEY` - API Key for the Postal API
## Credits
- [ConiCaw](https://github.com/ttshivers) - Teaching Kakious about documentation and base documenation from other projects
- [Kakious](https://rawr.ing) - Main Developer and Documentation Writer

28
docker-compose.yml Normal file
View file

@ -0,0 +1,28 @@
version: '3.8'
services:
mysql:
image: mysql:latest
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: waterwolf-auth
volumes:
- mysql-data:/var/lib/mysql
redis:
image: redis/redis-stack-server:6.2.2-v5
restart: unless-stopped
waterwolf-auth:
depends_on:
- mysql
- redis
build:
context: .
target: all-source-stage
restart: unless-stopped
expose:
- '3000'
stdin_open: true
tty: true
volumes:
mysql-data:

1
keys/cookies.json Normal file
View file

@ -0,0 +1 @@
['THIS IS A SECURE THING']

0
keys/jwks.json Normal file
View file

View file

@ -18,25 +18,34 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "ts-node --project tsconfig.json ./node_modules/typeorm/cli",
"typeorm:run-migrations": "npm run typeorm migration:run -- -d ./src/app.datasource.ts",
"typeorm:generate-migration": "npm run typeorm -- -d ./src/app.datasource.ts migration:generate ./src/database/migrations/$npm_config_name",
"typeorm:create-migration": "npm run typeorm -- migration:create ./src/database/migrations/$npm_config_name",
"typeorm:revert-migration": "npm run typeorm -- -d ./src/app.datasource.ts migration:revert",
"prepare": "ts-patch install && typia patch"
},
"dependencies": {
"@atech/postal": "^1.0.0",
"@nestjs/bullmq": "^10.1.1",
"@nestjs/common": "^10.3.10",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.3.10",
"@nestjs/platform-express": "^10.3.10",
"@nestjs/typeorm": "^10.0.2",
"bullmq": "^5.9.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"ioredis": "^5.4.1",
"mysql2": "^3.10.2",
"nanoid": "^5.0.7",
"nestjs-postal-client": "^0.0.5",
"nestjs-postal-client": "^0.0.6",
"oidc-provider": "^8.5.1",
"pug": "^3.0.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sequelize": "^6.37.3",
"sequelize-typescript": "^2.1.6",
"typeorm": "^0.3.20",
"typia": "^6.5.1"
},
"devDependencies": {
@ -46,7 +55,6 @@
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.10",
"@types/sequelize": "^4.28.20",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
@ -55,8 +63,10 @@
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"sequelize-cli": "^6.6.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"tailwindcss": "^3.4.5",
"ts-jest": "^29.2.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",

File diff suppressed because it is too large Load diff

22
src/app.datasource.ts Normal file
View file

@ -0,0 +1,22 @@
import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
import { DataSource } from 'typeorm';
import { DATABASE_MIGRATION } from './database/database.migration';
import { DATABASE_ENTITIES } from './database/database.entities';
config();
const configService = new ConfigService();
export default new DataSource({
type: 'mysql',
host: configService.getOrThrow<string>('DATABASE_HOST'),
port: configService.getOrThrow<number>('DATABASE_PORT'),
username: configService.getOrThrow<string>('DATABASE_USER'),
password: configService.getOrThrow<string>('DATABASE_PASSWORD', ''),
database: configService.getOrThrow<string>('DATABASE_NAME'),
charset: 'UTF8MB4',
entities: DATABASE_ENTITIES,
migrations: DATABASE_MIGRATION,
});

View file

@ -0,0 +1,12 @@
import { IsEmail } from 'class-validator';
import { Column, Model, Table } from 'sequelize-typescript';
@Table
export class User extends Model<User> {
@Column
username: string;
@Column
@IsEmail()
email: string;
}

View file

@ -0,0 +1,68 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import type Provider from 'oidc-provider';
import type { Configuration, errors, KoaContextWithOIDC } from 'oidc-provider';
// This is an async import for the oidc-provider package as it's now only esm and we need to use it in a commonjs environment.
async function getProvider(): Promise<{
provider: any;
providerErrors: typeof errors;
}> {
let provider: {
default: any;
errors: typeof errors;
};
// Unfortunately, there is a Node/Jest bug that causes segmentation faults when doing such an import in Jest:
// https://github.com/nodejs/node/issues/35889
// To work around that, we do the import differently, in case we are in a Jest test run.
// This can be detected via the env variables: https://jestjs.io/docs/environment-variables.
// There have been reports of `JEST_WORKER_ID` being undefined, so to be sure we check both.
if (process.env['JEST_WORKER_ID'] ?? process.env.NODE_ENV === 'test') {
provider = jest.requireActual('oidc-provider');
} else {
provider = await (eval(`import('oidc-provider')`) as Promise<{
default: any;
errors: typeof errors;
}>);
}
return {
provider: provider.default,
providerErrors: provider.errors,
};
}
@Injectable()
/**
* @class OidcService
* OidcService is the service that handles all interaction with the OIDC library and package
*/
export class OidcService implements OnModuleInit {
private provider!: Provider;
private jwks: any;
private cookies: any;
private readonly logger = new Logger(OidcService.name);
constructor() {}
async onModuleInit() {
const { provider, providerErrors } = await getProvider();
const isOrigin = (value: string): boolean => {
if (typeof value !== 'string') {
return false;
}
try {
const { origin } = new URL(value);
// Origin: <scheme> "://" <hostname> [ ":" <port> ]
return value == origin;
} catch (err) {
return false;
}
};
}
private async loadKeys() {}
}

View file

View file

@ -0,0 +1,15 @@
/**
* OIDC Constants
*/
// OIDC TTLs
export const REFRESH_TOKEN_LIFE = 5 * 24 * 60 * 60; // 5 days
export const ID_TOKEN_LIFE = 15 * 60; // 15 minutes
export const AUTHORIZATION_TOKEN_LIFE = 30 * 60; // 1 hour
export const SESSION_LIFE = 5 * 24 * 60 * 60; // 5 days
export const ACCESS_TOKEN_LIFE = 15 * 60; // 15 minutes
export const DEVICE_CODE_LIFE = 10 * 60; // 10 minutes
export const GRANT_LIFE = 60 * 60 * 24 * 30; // 1 month
export const INTERACTION_LIFE = 1 * 60 * 60; // 1 hour
export const PUSHED_AUTH_REQ_LIFE = 15 * 60; // 15 minutes
export const CLIENT_CREDENTIALS_TOKEN_LIFE = 7 * 24 * 60 * 60; // 7 days

View file

@ -1,6 +1,6 @@
export default async () => ({
mail: {
baseUrl: process.env.MAIL_BASE_URL,
apiKey: process.env.MAIL_API_KEY,
base: {
port: process.env.PORT || 3000,
host: process.env.HOST || 'localhost',
},
});

View file

View file

@ -0,0 +1,8 @@
export const MAX_STRING_LENGTH = 255;
export const FETCH_AFTER_CREATION_FAILED =
'Failed to fetch entity after creation';
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
}

View file

@ -0,0 +1,4 @@
import { User } from './models/user.model';
// Database Entities Array
export const DATABASE_ENTITIES = [User];

View file

@ -0,0 +1,2 @@
// Database Migration Array
export const DATABASE_MIGRATION = [];

View file

@ -0,0 +1,29 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { MAX_STRING_LENGTH, UserRole } from '../database.const';
@Entity('user')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: MAX_STRING_LENGTH })
username: string;
@Column({ length: MAX_STRING_LENGTH })
email: string;
@Column({ length: MAX_STRING_LENGTH })
password: string;
@Column({ length: MAX_STRING_LENGTH, nullable: true })
pronouns: string;
@Column({ length: MAX_STRING_LENGTH, nullable: true })
title: string;
@Column({ length: MAX_STRING_LENGTH, nullable: true })
avatar: string;
@Column({ length: 15, enum: UserRole, default: UserRole.USER })
role: string;
}

9
tailwind.config.js Normal file
View file

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [],
theme: {
extend: {},
},
plugins: [],
}