working on basic service setup, auth, conf
This commit is contained in:
commit
e26ba0297a
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/*
|
||||
build/*
|
||||
data/*
|
86
Dockerfile
Normal file
86
Dockerfile
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
ARG NODE_VERSION="20"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# =====
|
||||
# 1. Build Stage
|
||||
# =====
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine AS build_stage
|
||||
ENV BUILD_PATH="/app.build"
|
||||
|
||||
# Make a directory to build the application in
|
||||
RUN mkdir -p $BUILD_PATH
|
||||
WORKDIR $BUILD_PATH
|
||||
|
||||
# Install common build dependencies
|
||||
# see: https://github.com/gliderlabs/docker-alpine/blob/master/docs/usage.md (re: `--no-cache` flag)
|
||||
RUN apk add --no-cache \
|
||||
python3 \
|
||||
g++ \
|
||||
make \
|
||||
bash
|
||||
|
||||
# Copy over the application files
|
||||
COPY package.json package-lock.json tsconfig.json ${BUILD_PATH}/
|
||||
COPY src/ ${BUILD_PATH}/src/
|
||||
|
||||
# Install dependencies and build the application source
|
||||
RUN npm ci
|
||||
RUN npm run tsc
|
||||
|
||||
# Remove any dev dependencies now that the app is built
|
||||
RUN npm prune --production
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# =====
|
||||
# 2. Distribution Stage
|
||||
# =====
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine AS dist_stage
|
||||
ENV BUILD_PATH="/app.build"
|
||||
|
||||
# Environment variables to control where the various application
|
||||
# components are located. You probably don't need to change these
|
||||
ENV APP_PATH="/app"
|
||||
ENV DATA_PATH="/app.data"
|
||||
ENV CONF_PATH="/app.conf"
|
||||
|
||||
# Create a new user/group to run the application
|
||||
RUN addgroup appuser && \
|
||||
adduser \
|
||||
--no-create-home \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--ingroup appuser \
|
||||
appuser
|
||||
|
||||
# Make a directory to store configuration
|
||||
VOLUME [ "${DATA_PATH}" ]
|
||||
RUN mkdir -p ${DATA_PATH} && \
|
||||
chown appuser:appuser ${DATA_PATH} && \
|
||||
chmod 0700 ${DATA_PATH}
|
||||
|
||||
# Make a directory to store configuration
|
||||
VOLUME [ "${CONF_PATH}" ]
|
||||
RUN mkdir -p ${CONF_PATH} && \
|
||||
chown appuser:appuser ${CONF_PATH} && \
|
||||
chmod 0500 ${CONF_PATH}
|
||||
|
||||
# Copy over the built application code
|
||||
COPY --from=build_stage --chown=appuser:appuser --chmod=500 ${BUILD_PATH}/build/ ${APP_PATH}/
|
||||
|
||||
# Copy over the production node_modules
|
||||
COPY --from=build_stage --chown=appuser:appuser --chmod=500 ${BUILD_PATH}/node_modules/ /node_modules/
|
||||
|
||||
# Run the application with the user/group given permissions above
|
||||
USER appuser:appuser
|
||||
|
||||
# Run the application by default
|
||||
ENTRYPOINT [ "node", "${APP_PATH}/start.js" ]
|
34
conf/00-default.yaml
Normal file
34
conf/00-default.yaml
Normal file
@ -0,0 +1,34 @@
|
||||
$schema: ../schemas/config.json
|
||||
web:
|
||||
address: 0.0.0.0
|
||||
port: 8080
|
||||
exposed_url: https://me.local.jbrumond.me:8080
|
||||
tls: false
|
||||
# tls:
|
||||
# key: /tls/tls.key
|
||||
# cert: /tls/tls.cert
|
||||
etag:
|
||||
static_assets: strong
|
||||
cache_control:
|
||||
static_assets: public, max-age=3600
|
||||
metadata:
|
||||
address: 0.0.0.0
|
||||
port: 8081
|
||||
tls: false
|
||||
# tls:
|
||||
# key: /tls/tls.key
|
||||
# cert: /tls/tls.cert
|
||||
oidc:
|
||||
server_url: https://sso.jbrumond.me/realms/public
|
||||
signing_algorithm: ES512
|
||||
client_id: ""
|
||||
client_secret: ""
|
||||
pkce_cookie:
|
||||
name: app_pkce_code
|
||||
secure: true
|
||||
ttl: 300
|
||||
session_cookie:
|
||||
name: app_session_key
|
||||
secure: true
|
||||
ttl: 7200
|
||||
|
1644
package-lock.json
generated
Normal file
1644
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@templates/nodejs-typescript-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Template project for creating new Node.js / TypeScript services",
|
||||
"main": "src/index.ts",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.jbrumond.me:templates/nodejs-typescript-service.git"
|
||||
},
|
||||
"scripts": {
|
||||
"tsc": "tsc --build"
|
||||
},
|
||||
"author": "James Brumond <https://jbrumond.me>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.2",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/compress": "^6.4.0",
|
||||
"@fastify/etag": "^4.2.0",
|
||||
"@fastify/formbody": "^7.4.0",
|
||||
"fastify": "^4.19.2",
|
||||
"openid-client": "^5.4.3",
|
||||
"yaml": "^2.3.1"
|
||||
}
|
||||
}
|
42
readme.md
Normal file
42
readme.md
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
Template project for creating new Node.js / TypeScript services
|
||||
|
||||
---
|
||||
|
||||
## Get Started
|
||||
|
||||
### Pull down the code
|
||||
|
||||
```bash
|
||||
git init
|
||||
git pull https://gitea.jbrumond.me/templates/nodejs-typescript-service.git master
|
||||
```
|
||||
|
||||
### Update configuration
|
||||
|
||||
- In `package.json`, update any fields like `name`, `description`, `repository`, etc.
|
||||
|
||||
|
||||
|
||||
## Building from source
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run tsc
|
||||
|
||||
# Make a directory to store data in
|
||||
mkdir ./data
|
||||
|
||||
# Run the server
|
||||
APP_PATH="./build" DATA_PATH="./data" CONF_PATH="./conf" node ./build/start.js
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Building container image
|
||||
|
||||
```bash
|
||||
docker build . -f Dockerfile -t nodejs-template-service:latest
|
||||
```
|
||||
|
||||
|
217
schemas/config.json
Normal file
217
schemas/config.json
Normal file
@ -0,0 +1,217 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://json-schema.jbrumond.me/config",
|
||||
"title": "Configuration for app service",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"web": {
|
||||
"title": "Web Server Config",
|
||||
"description": "Configuration for the main HTTP(S) server",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"title": "Web Listener Address",
|
||||
"description": "Address to listen on for inbound connections",
|
||||
"type": "string",
|
||||
"pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
|
||||
"default": "0.0.0.0",
|
||||
"example": "0.0.0.0"
|
||||
},
|
||||
"port": {
|
||||
"title": "Web Listener Port",
|
||||
"description": "Port number to listen on for inbound connections",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"default": 8080,
|
||||
"example": 8080
|
||||
},
|
||||
"exposed_url": {
|
||||
"title": "Web Exposed URL",
|
||||
"description": "",
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"example": "https://example.com"
|
||||
},
|
||||
"tls": {
|
||||
"title": "Web TLS Config",
|
||||
"description": "Configuration for TLS/SSL for the HTTP API",
|
||||
"oneOf": [
|
||||
{ "type": "boolean", "const": false },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": { },
|
||||
"cert": { }
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"cert"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"etag": {
|
||||
"title": "Web Etag Config",
|
||||
"description": "Controls the generation and validation of `Etag` headers. Each request type can have etags set to `weak`, `strong`, or `none`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"static_assets": { "$ref": "#/$defs/etag_type" }
|
||||
}
|
||||
},
|
||||
"cache_control": {
|
||||
"title": "Web Cache-Control Config",
|
||||
"description": "Controls the generation of `Cache-Control` headers. Each request type has a full `Cache-Control` directive string defined",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"static_assets": { "$ref": "#/$defs/cache_control_directives" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"address",
|
||||
"port"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Metadata API Config",
|
||||
"description": "Configuration for the secondary metadata HTTP(S) server, used for health checks and other service meta-APIs",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "Address to listen on for inbound connections",
|
||||
"type": "string",
|
||||
"pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
|
||||
"default": "0.0.0.0",
|
||||
"example": "0.0.0.0"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number to listen on for inbound connections",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"default": 8081,
|
||||
"example": 8081
|
||||
},
|
||||
"tls": {
|
||||
"title": "TLS Config",
|
||||
"description": "Configuration for TLS/SSL for the HTTP API",
|
||||
"oneOf": [
|
||||
{ "type": "boolean", "const": false },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": { },
|
||||
"required": [ ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"address",
|
||||
"port"
|
||||
]
|
||||
},
|
||||
"oidc": {
|
||||
"title": "OpenID Connect (OIDC) Config",
|
||||
"description": "Configuration for the OpenID Connect (OIDC) provider and client",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_url": {
|
||||
"title": "OIDC Server Location",
|
||||
"description": "URL pointing to the OIDC provider service",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"signing_algorithm": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ES512"
|
||||
]
|
||||
},
|
||||
"client_id": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"type": "string"
|
||||
},
|
||||
"client_secret": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ ]
|
||||
},
|
||||
"pkce_cookie": {
|
||||
"title": "PKCE Cookie Config",
|
||||
"description": "Configuration for the cookie used in the Proof Key for Code Exchange (PKCE) flow",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "PKCE Cookie Name",
|
||||
"description": "The name of the cookie to store the PKCE code in",
|
||||
"type": "string",
|
||||
"default": "pkce_code"
|
||||
},
|
||||
"secure": {
|
||||
"title": "PKCE Cookie Secure",
|
||||
"description": "Sets the `Secure` directive on the PKCE code cookie (this should always be `true` in production)",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"ttl": {
|
||||
"title": "PKCE Cookie TTL",
|
||||
"description": "Time-to-live for the PKCE code cookie (in seconds)",
|
||||
"type": "integer",
|
||||
"default": 600
|
||||
}
|
||||
}
|
||||
},
|
||||
"session_cookie": {
|
||||
"title": "Session Cookie Config",
|
||||
"description": "Configuration for the cookie used in to store login session keys",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Session Cookie Name",
|
||||
"description": "The name of the cookie to store the session key in",
|
||||
"type": "string",
|
||||
"default": "pkce_code"
|
||||
},
|
||||
"secure": {
|
||||
"title": "Session Cookie Secure",
|
||||
"description": "Sets the `Secure` directive on the session key cookie (this should always be `true` in production)",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"ttl": {
|
||||
"title": "Session Cookie TTL",
|
||||
"description": "Time-to-live for the session key cookie (in seconds)",
|
||||
"type": "integer",
|
||||
"default": 7200
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"web",
|
||||
"metadata",
|
||||
"oidc"
|
||||
],
|
||||
"$defs": {
|
||||
"etag_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"weak",
|
||||
"strong"
|
||||
]
|
||||
},
|
||||
"cache_control_directives": {
|
||||
"description": "A full `Cache-Control` directives string",
|
||||
"type": "string",
|
||||
"example": "public, max-age=3600"
|
||||
}
|
||||
}
|
||||
}
|
87
src/conf.ts
Normal file
87
src/conf.ts
Normal file
@ -0,0 +1,87 @@
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { join as path_join } from 'path';
|
||||
import { parse as parse_yaml } from 'yaml';
|
||||
import { deep_merge } from './utilities/deep-merge';
|
||||
import { OIDCConfig, validate_oidc_conf } from './security/openid-connect';
|
||||
import { PKCECookieConfig, validate_pkce_cookie_conf } from './security/pkce-cookie';
|
||||
|
||||
const conf_dir = process.env.CONF_PATH;
|
||||
|
||||
if (! conf_dir) {
|
||||
console.error('No CONF_PATH defined');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export async function load_conf() : Promise<unknown> {
|
||||
const conf = Object.create(null);
|
||||
const files = await fs.readdir(conf_dir, { recursive: true });
|
||||
|
||||
// Sort configuration files by file name for consistent load order (users should
|
||||
// number prefix their config file names)
|
||||
files.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
// Load each config file in order, parse as YAML, and merge the contents into the
|
||||
// results object
|
||||
for (const file of files) {
|
||||
const conf_yaml = await fs.readFile(path_join(conf_dir, file), 'utf8');
|
||||
const conf_parsed = parse_yaml(conf_yaml);
|
||||
|
||||
deep_merge(conf, conf_parsed);
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
export function validate_conf(conf: unknown) : asserts conf is Conf {
|
||||
if (typeof conf !== 'object' || ! conf) {
|
||||
throw new Error('`conf` is not an object');
|
||||
}
|
||||
|
||||
if ('oidc' in conf) {
|
||||
validate_oidc_conf(conf.oidc)
|
||||
}
|
||||
|
||||
else {
|
||||
throw new Error('`conf.oidc` is missing');
|
||||
}
|
||||
|
||||
if ('pkce_cookie' in conf) {
|
||||
validate_pkce_cookie_conf(conf.pkce_cookie);
|
||||
}
|
||||
|
||||
else {
|
||||
throw new Error('`conf.pkce_cookie` is missing');
|
||||
}
|
||||
|
||||
// todo: validate other config
|
||||
}
|
||||
|
||||
export interface Conf {
|
||||
web: {
|
||||
address: string;
|
||||
port: number;
|
||||
exposed_url: string;
|
||||
tls?: false | {
|
||||
key: string;
|
||||
cert: string;
|
||||
};
|
||||
etag?: {
|
||||
static_assets?: 'none' | 'weak' | 'strong';
|
||||
};
|
||||
cache_control?: {
|
||||
static_assets?: string;
|
||||
};
|
||||
};
|
||||
metadata: {
|
||||
address: string;
|
||||
port: number;
|
||||
tls?: false | {
|
||||
key: string;
|
||||
cert: string;
|
||||
};
|
||||
};
|
||||
oidc: OIDCConfig;
|
||||
pkce_cookie: PKCECookieConfig;
|
||||
// session_cookie: SessionCookieConfig;
|
||||
}
|
62
src/security/openid-connect.ts
Normal file
62
src/security/openid-connect.ts
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
import { FastifyReply } from 'fastify';
|
||||
// import { redirect_302_found } from '../http';
|
||||
import { BaseClient, Issuer, TypeOfGenericClient } from 'openid-client';
|
||||
import { PKCECookieConfig } from './pkce-cookie';
|
||||
|
||||
const scopes = 'openid profile email';
|
||||
|
||||
export interface OIDCConfig {
|
||||
server_url: string;
|
||||
signing_algorithm: 'ES512';
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
pkce: PKCECookieConfig;
|
||||
}
|
||||
|
||||
export function validate_oidc_conf(conf: unknown) : asserts conf is OIDCConfig {
|
||||
// todo: validate config
|
||||
}
|
||||
|
||||
export class OIDCProvider {
|
||||
#conf: OIDCConfig;
|
||||
#issuer: Issuer;
|
||||
#Client: TypeOfGenericClient<BaseClient>;
|
||||
#client: BaseClient;
|
||||
#init_promise: Promise<void>;
|
||||
|
||||
constructor(conf: OIDCConfig) {
|
||||
this.#conf = conf;
|
||||
this.#init_promise = this.#init();
|
||||
}
|
||||
|
||||
public get ready() {
|
||||
return this.#init_promise;
|
||||
}
|
||||
|
||||
async #init() {
|
||||
this.#issuer = await Issuer.discover(this.#conf.server_url);
|
||||
this.#Client = this.#issuer.Client;
|
||||
this.#client = new this.#Client({
|
||||
client_id: this.#conf.client_id,
|
||||
client_secret: this.#conf.client_secret,
|
||||
id_token_signed_response_alg: this.#conf.signing_algorithm,
|
||||
authorization_signed_response_alg: this.#conf.signing_algorithm,
|
||||
});
|
||||
}
|
||||
|
||||
async redirect_to_auth_endpoint(res: FastifyReply, code_challenge: string, redirect_uri: string) {
|
||||
await this.#init_promise;
|
||||
|
||||
const uri = this.#client.authorizationUrl({
|
||||
response_type: 'code',
|
||||
scope: scopes,
|
||||
redirect_uri,
|
||||
code_challenge,
|
||||
code_challenge_method: 'S256'
|
||||
});
|
||||
|
||||
// todo: redirect
|
||||
// return redirect_302_found(res, uri, 'Logging in with OpenID Connect');
|
||||
}
|
||||
}
|
53
src/security/pkce-cookie.ts
Normal file
53
src/security/pkce-cookie.ts
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import { rand } from '../utilities/rand';
|
||||
import { set_cookie } from '../utilities/http-cookies';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
export interface PKCECookieConfig {
|
||||
name: string;
|
||||
secure: boolean;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
export function validate_pkce_cookie_conf(conf: unknown) : asserts conf is PKCECookieConfig {
|
||||
// todo: validate config
|
||||
}
|
||||
|
||||
export class PKCECookieProvider {
|
||||
#conf: PKCECookieConfig;
|
||||
|
||||
constructor(conf: PKCECookieConfig) {
|
||||
this.#conf = conf;
|
||||
}
|
||||
|
||||
async setup_pkce_challenge(res: FastifyReply) {
|
||||
const pkce_code_verifier = await generate_code_verifier();
|
||||
const pkce_code_challenge = sha256_base64_url(pkce_code_verifier);
|
||||
const pkce_expire = new Date(Date.now() + (this.#conf.ttl * 1000));
|
||||
|
||||
// "Lax" rather than "Strict", so the PKCE verifier will be included on the
|
||||
// redirect to the login callback endpoint, which comes from the OpenID server,
|
||||
// which is likely on a different site
|
||||
set_cookie(res, this.#conf.name, pkce_code_verifier, pkce_expire, this.#conf.secure, 'Lax');
|
||||
|
||||
return pkce_code_challenge;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function generate_code_verifier() {
|
||||
const base64 = await rand(50, 'base64');
|
||||
return replace_base64_url_chars(base64);
|
||||
}
|
||||
|
||||
function sha256_base64_url(input: string) {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(input);
|
||||
const base64 = hash.digest('base64');
|
||||
return replace_base64_url_chars(base64);
|
||||
}
|
||||
|
||||
function replace_base64_url_chars(input: string) {
|
||||
return input.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
0
src/security/session-cookie.ts
Normal file
0
src/security/session-cookie.ts
Normal file
20
src/start.ts
Normal file
20
src/start.ts
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
import { load_conf, validate_conf } from './conf';
|
||||
import { OIDCProvider } from './security/openid-connect';
|
||||
import { PKCECookieProvider } from './security/pkce-cookie';
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
// Load and validate configuration
|
||||
const conf = await load_conf();
|
||||
validate_conf(conf);
|
||||
|
||||
// Create all of the core feature providers
|
||||
const oidc = new OIDCProvider(conf.oidc);
|
||||
const pkce_cookie = new PKCECookieProvider(conf.pkce_cookie);
|
||||
// const session_cookie = new SessionCookieProvider(conf.session_cookie);
|
||||
|
||||
// Wait for any async init steps
|
||||
await oidc.ready;
|
||||
}
|
13
src/utilities/deep-merge.ts
Normal file
13
src/utilities/deep-merge.ts
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
export function deep_merge<T>(host: T, donor: T) : T {
|
||||
for (const [key, value] of Object.entries(donor)) {
|
||||
if (value != null && host[key] != null && typeof value === 'object' && typeof host[key] === 'object') {
|
||||
deep_merge(host[key], value);
|
||||
continue;
|
||||
}
|
||||
|
||||
host[key] = value;
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
41
src/utilities/http-cookies.ts
Normal file
41
src/utilities/http-cookies.ts
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
export type SameSite = 'Strict' | 'Lax' | 'None';
|
||||
|
||||
export function set_cookie(res: FastifyReply, name: string, value: string, expires: Date, secure: boolean, same_site: SameSite = 'Strict', path?: string) {
|
||||
const cookie
|
||||
= `${name}=${value}; `
|
||||
+ `Expires=${expires.toUTCString()}; `
|
||||
+ (path ? ` Path=${path}; ` : '')
|
||||
+ `HttpOnly; `
|
||||
+ `SameSite=${same_site};`
|
||||
+ (secure ? ' Secure;' : '')
|
||||
;
|
||||
|
||||
res.header('set-cookie', cookie);
|
||||
}
|
||||
|
||||
export function invalidate_cookie(res: FastifyReply, name: string, secure: boolean, path?: string) {
|
||||
set_cookie(res, name, 'invalidate', new Date(0), secure, 'Strict', path);
|
||||
}
|
||||
|
||||
export function parse_req_cookies(req: FastifyRequest) : Record<string, string> {
|
||||
const result: Record<string, string> = { };
|
||||
const cookies = req.headers.cookie || '';
|
||||
|
||||
for (const cookie of cookies.split(';')) {
|
||||
const index = cookie.indexOf('=');
|
||||
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = cookie.slice(0, index).trim();
|
||||
const value = cookie.slice(index + 1).trim();
|
||||
|
||||
result[name] = decodeURIComponent(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
21
src/utilities/rand.ts
Normal file
21
src/utilities/rand.ts
Normal file
@ -0,0 +1,21 @@
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export type BufferEncoded<T extends BufferEncoding> = T extends 'binary' ? Buffer : string;
|
||||
|
||||
export function rand<T extends BufferEncoding = 'binary'>(size: number, encoding?: T) : Promise<BufferEncoded<T>> {
|
||||
return new Promise<BufferEncoded<T>>((resolve, reject) => {
|
||||
randomBytes(size, (error, buffer) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
if (encoding && encoding !== 'binary') {
|
||||
resolve(buffer.toString(encoding) as BufferEncoded<T>);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(buffer as BufferEncoded<T>);
|
||||
});
|
||||
});
|
||||
}
|
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build",
|
||||
"target": "ES2022",
|
||||
"moduleResolution": "NodeNext"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user