Skip to content

Commit

Permalink
feat: csrf and session integration using redis (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcdo29 committed Jun 9, 2023
2 parents 2e79980 + af3108c commit 736ae40
Show file tree
Hide file tree
Showing 94 changed files with 2,090 additions and 159 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ apps/site/public/images
docker/
!libs/docker/
*.log
dump
22 changes: 22 additions & 0 deletions .swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2017",
"keepClassNames": true,
"baseUrl": ".",
"paths": {
"@unteris/server/*": ["libs/server/*/src/"]
}
},
"module": {
"type": "commonjs",
"strictMode": true
}
}
5 changes: 5 additions & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:80 {
root * ./dist/apps/site
file_server
try_files {path} /index.html
}
16 changes: 12 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
FROM node:18.15-alpine AS node-base
RUN npm i -g pnpm
RUN apk add dumb-init
RUN npm i -g pnpm && \
apk add --no-cache dumb-init=1.2.5-r2

FROM node-base AS common
WORKDIR /src
RUN apk add python3 make gcc g++
RUN apk add --no-cache \
python3=3.10.11-r0\
make=4.3-r1 \
gcc=12.2.1_git20220924-r4 \
g++=12.2.1_git20220924-r4
COPY package.json \
tsconfig* \
nx.json \
Expand Down Expand Up @@ -33,6 +37,7 @@ WORKDIR /src
COPY --from=server-build --chown=node:node /src/dist/apps/server ./
ENV NODE_ENV=production
RUN pnpm i
RUN pnpx node-prune
CMD ["dumb-init", "node", "main.js"]

####################
Expand All @@ -57,6 +62,7 @@ COPY --from=migrations-build --chown=node:node /src/dist ./dist
RUN cp ./dist/apps/kysely-cli/package.json ./package.json
ENV NODE_ENV=production
RUN pnpm i
RUN pnpx node-prune
CMD ["dumb-init", "node", "dist/apps/kysely-cli/main", "migrate"]

##############
Expand All @@ -76,4 +82,6 @@ RUN VITE_SERVER_URL="https://api.unteris.com" pnpm nx run site:build:production

FROM caddy:2.6.4-alpine as site-prod
WORKDIR /src
COPY --from=site-build /src/dist/apps/site/ /usr/share/caddy
COPY --from=site-build /src/dist/apps/site/ ./dist/apps/site
COPY Caddyfile ./Caddyfile
RUN ["caddy", "run", "--config", "Caddyfile"]
2 changes: 1 addition & 1 deletion apps/kysely-cli/src/app/kysely.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class KyselyCliCommand extends CommandRunner {
throw new Error(migrationErrors.join('\n'));
}
} catch (err) {
this.logger.error(err);
this.logger.printError(err as Error);
} finally {
this.logger.log('Migrations Finished');
}
Expand Down
6 changes: 6 additions & 0 deletions apps/server-e2e/.env.e2e
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DATABASE_NAME=test
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_HOST=localhost
DATABASE_PASSWORD=postgres
REDIS_URL=redis://localhost:6379
18 changes: 18 additions & 0 deletions apps/server-e2e/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
28 changes: 28 additions & 0 deletions apps/server-e2e/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "server-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/server-e2e/src",
"projectType": "application",
"targets": {
"e2e": {
"executor": "nx:run-commands",
"options": {
"commands": [
"docker compose -f docker-compose.test.yml up -d",
"while ! nc -q0 localhost 6379 < /dev/null > /dev/null 2>&1; do sleep 10; done",
"while ! nc -q0 localhost 5432 < /dev/null > /dev/null 2>&1; do sleep 10; done",
"NODE_ENV=development node -r @swc/register apps/server-e2e/src/main.ts",
"docker compose -f docker-compose.test.yml down"
]
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/server-e2e/**/*.ts"]
}
}
},
"tags": ["server", "e2e", "test"]
}
File renamed without changes.
28 changes: 28 additions & 0 deletions apps/server-e2e/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { OgmaService } from '@ogma/nestjs-module';
import { RootModule } from '@unteris/server/root';
import { request, spec } from 'pactum';
import { suite } from 'uvu';
import { csrfTests } from './tests/csrf';

type UvuContext = { app: INestApplication };

const ApplicationE2E = suite<UvuContext>('Unteris E2E test suite');

ApplicationE2E.before(async (context) => {
const app = await NestFactory.create(RootModule, { bufferLogs: true });
app.useLogger(app.get(OgmaService));
await app.listen(0);
const reqURL = await app.getUrl();
request.setBaseUrl(reqURL.replace('[::1]', 'localhost'));
context.app = app;
});

ApplicationE2E.after(async ({ app }) => {
await app.close();
});

ApplicationE2E('CSRF Testing', csrfTests);

ApplicationE2E.run();
53 changes: 53 additions & 0 deletions apps/server-e2e/src/tests/csrf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { parse } from 'lightcookie';
import { spec } from 'pactum';
import { Callback } from 'uvu';
import { ok, unreachable } from 'uvu/assert';

export const csrfTests: Callback<any> = async (_context: any) => {
await spec()
.get('/csrf')
.expectStatus(200)
.expectJsonLike({ csrfToken: /\w+/ })
.expect(({ res }) => {
const cookies = res.headers['set-cookie'];
if (!cookies || cookies.length === 0) {
unreachable('Received no cookies from the server');
return;
}
cookies.forEach((cookie) => {
const parsed = parse(cookie);
ok(
['sessionId', 'refreshId'].some((cookieName) => {
return cookieName in parsed;
})
);
});
})
.stores((_req, res) => {
const { csrfToken } = res.body;
const cookies = res.headers['set-cookie'];
if (!cookies || cookies.length === 0) {
unreachable('Received no cookies from the server');
return;
}
const sessionId = cookies
.map((c) => parse(c))
.find((cookie) => 'sessionId' in cookie)?.sessionId;
const refreshId = cookies
.map((c) => parse(c))
.find((cookie) => 'refreshId' in cookie)?.refreshId;
return {
csrfToken,
sessionId,
refreshId,
};
})
.toss();
await spec()
.post('/csrf/verify')
.withHeaders('X-UNTERIS-CSRF-PROTECTION', '$S{csrfToken}')
.withCookies('sessionId', '$S{sessionId}')
.expectStatus(201)
.expectJson({ success: true })
.toss();
};
10 changes: 10 additions & 0 deletions apps/server-e2e/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}
13 changes: 13 additions & 0 deletions apps/server-e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}
20 changes: 20 additions & 0 deletions apps/server/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2017",
"keepClassNames": true,
"baseUrl": "."
},
"module": {
"type": "commonjs",
"strictMode": true
}
}
18 changes: 0 additions & 18 deletions apps/server/src/app/app.module.ts

This file was deleted.

11 changes: 2 additions & 9 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
/**
* This is not a production server yet!
* This is only a minimal backend to get started.
*/

import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { OgmaService } from '@ogma/nestjs-module';
import { ServerConfigService } from '@unteris/server/config';

import { AppModule } from './app/app.module';
import { RootModule } from '@unteris/server/root';

async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
const app = await NestFactory.create(RootModule, { bufferLogs: true });
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
app.useLogger(app.get(OgmaService));
Expand Down
9 changes: 8 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
ports:
- '8080:80'
volumes:
- './images:/usr/share/caddy/images'
- './images:/src/images'
labels:
- 'com.centurylinklabs.watchtower.enable=true'
server:
Expand Down Expand Up @@ -50,3 +50,10 @@ services:
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_DB: ${DATABASE_NAME}
redis:
image: redis
ports:
- '6380:6379'
volumes:
- './redis.conf:/usr/local/etc/redis/redis.conf'
command: redis-server /usr/local/etc/redis/redis.conf
14 changes: 14 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: '3'
services:
postgres:
image: postgres
ports:
- '5432:5432'
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
redis:
image: redis
ports:
- '6379:6379'
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: unteris
redis:
image: redis
ports:
- '6379:6379'
9 changes: 9 additions & 0 deletions libs/server/config/src/lib/config.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { z } from 'zod';

const hourInSeconds = 60 * 60;
const dayInSeconds = hourInSeconds * 24;

export const Config = z.object({
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
Expand All @@ -10,5 +13,11 @@ export const Config = z.object({
.optional(z.string().transform((val) => Number.parseInt(val, 10)))
.default('3333'),
CORS: z.optional(z.string()).default('http://localhost:4200'),
REDIS_URL: z.string(),
NODE_ENV: z.enum(['development', 'production']),
SESSION_EXPIRES_IN: z.number().optional().default(hourInSeconds),
REFRESH_EXPIRES_IN: z
.number()
.optional()
.default(7 * dayInSeconds),
});
18 changes: 18 additions & 0 deletions libs/server/csrf/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
Loading

0 comments on commit 736ae40

Please sign in to comment.