first commit
This commit is contained in:
commit
27efd26d6c
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
lib/
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM node:18-alpine as dev
|
||||||
|
RUN apk add netcat-openbsd
|
||||||
|
|
||||||
|
RUN npm config set update-notifier false
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
COPY src ./
|
||||||
|
|
||||||
|
RUN npm ci --silent
|
||||||
|
RUN npm run compile
|
||||||
|
|
||||||
|
ENTRYPOINT npm run start
|
||||||
|
|
||||||
|
FROM node:18-alpine AS prod
|
||||||
|
RUN npm config set update-notifier false
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=dev /app/package.json /app/package-lock.json /app/
|
||||||
|
COPY config /app/config
|
||||||
|
COPY --from=dev /app/lib /app/lib
|
||||||
|
|
||||||
|
RUN npm ci --only=production --silent
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
93
app.ts
Normal file
93
app.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { feathers } from '@feathersjs/feathers'
|
||||||
|
import express, {
|
||||||
|
rest,
|
||||||
|
json,
|
||||||
|
urlencoded,
|
||||||
|
cors,
|
||||||
|
serveStatic,
|
||||||
|
notFound,
|
||||||
|
errorHandler
|
||||||
|
} from '@feathersjs/express'
|
||||||
|
import configuration from '@feathersjs/configuration'
|
||||||
|
import socketio from '@feathersjs/socketio'
|
||||||
|
import session from 'express-session';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
|
import type { Application } from './declarations'
|
||||||
|
|
||||||
|
import { logger } from './logger'
|
||||||
|
import { logError } from './hooks/log-error'
|
||||||
|
import { services } from './services/index'
|
||||||
|
import { channels } from './channels'
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
const app: Application = express(feathers())
|
||||||
|
|
||||||
|
// Load app configuration
|
||||||
|
app.configure(configuration())
|
||||||
|
app.use(cors())
|
||||||
|
app.use(json({
|
||||||
|
limit: '20mb'
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(session({
|
||||||
|
secret: randomUUID(),
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: { secure: false }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Propagate session to request.params in feathers services
|
||||||
|
app.use(function (req, _res, next) {
|
||||||
|
req.feathers = {
|
||||||
|
...req.feathers,
|
||||||
|
session: req.session
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/authme', (req, res) => {
|
||||||
|
//@ts-ignore
|
||||||
|
req.session.user = {
|
||||||
|
email: "her@va.mm"
|
||||||
|
}
|
||||||
|
res.send("done locally")
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(urlencoded({ extended: true }))
|
||||||
|
// Host the public folder
|
||||||
|
app.use('/', serveStatic(app.get('public')))
|
||||||
|
|
||||||
|
// Configure services and real-time functionality
|
||||||
|
app.configure(rest())
|
||||||
|
app.configure(
|
||||||
|
socketio({
|
||||||
|
cors: {
|
||||||
|
origin: app.get('origins')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.configure(services)
|
||||||
|
app.configure(channels)
|
||||||
|
|
||||||
|
// Configure a middleware for 404s and the error handler
|
||||||
|
app.use(notFound())
|
||||||
|
app.use(errorHandler({ logger }))
|
||||||
|
|
||||||
|
// Register hooks that run on all service methods
|
||||||
|
app.hooks({
|
||||||
|
around: {
|
||||||
|
all: [logError]
|
||||||
|
},
|
||||||
|
before: {},
|
||||||
|
after: {},
|
||||||
|
error: {}
|
||||||
|
})
|
||||||
|
// Register application setup and teardown hooks here
|
||||||
|
app.hooks({
|
||||||
|
setup: [],
|
||||||
|
teardown: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export { app }
|
38
channels.ts
Normal file
38
channels.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/channels.html
|
||||||
|
import type { RealTimeConnection, Params } from '@feathersjs/feathers'
|
||||||
|
import type { AuthenticationResult } from '@feathersjs/authentication'
|
||||||
|
import '@feathersjs/transport-commons'
|
||||||
|
import type { Application, HookContext } from './declarations'
|
||||||
|
import { logger } from './logger'
|
||||||
|
|
||||||
|
export const channels = (app: Application) => {
|
||||||
|
logger.warn(
|
||||||
|
'Publishing all events to all authenticated users. See `channels.ts` and https://dove.feathersjs.com/api/channels.html for more information.'
|
||||||
|
)
|
||||||
|
|
||||||
|
app.on('connection', (connection: RealTimeConnection) => {
|
||||||
|
// On a new real-time connection, add it to the anonymous channel
|
||||||
|
app.channel('anonymous').join(connection)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('login', (authResult: AuthenticationResult, { connection }: Params) => {
|
||||||
|
// connection can be undefined if there is no
|
||||||
|
// real-time connection, e.g. when logging in via REST
|
||||||
|
if (connection) {
|
||||||
|
// The connection is no longer anonymous, remove it
|
||||||
|
app.channel('anonymous').leave(connection)
|
||||||
|
|
||||||
|
// Add it to the authenticated user channel
|
||||||
|
app.channel('authenticated').join(connection)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
app.publish((data: any, context: HookContext) => {
|
||||||
|
// Here you can add event publishers to channels set up in `channels.js`
|
||||||
|
// To publish only for a specific event use `app.publish(eventname, () => {})`
|
||||||
|
|
||||||
|
// e.g. to publish all service events to all authenticated users use
|
||||||
|
return app.channel('authenticated')
|
||||||
|
})
|
||||||
|
}
|
12
clients/wildduck.client.ts
Normal file
12
clients/wildduck.client.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
|
const wildDuckClient = axios.create({
|
||||||
|
baseURL: config.get('wildDuck.url'),
|
||||||
|
headers: {
|
||||||
|
'X-Access-Token': config.get('wildDuck.token'),
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default wildDuckClient;
|
10
config/custom-environment-variables.json
Normal file
10
config/custom-environment-variables.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"port": {
|
||||||
|
"__name": "PORT",
|
||||||
|
"__format": "number"
|
||||||
|
},
|
||||||
|
"host": "HOSTNAME",
|
||||||
|
"authentication": {
|
||||||
|
"secret": "FEATHERS_SECRET"
|
||||||
|
}
|
||||||
|
}
|
16
config/default.json
Normal file
16
config/default.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 3030,
|
||||||
|
"public": "./public/",
|
||||||
|
"origins": [
|
||||||
|
"http://localhost:3030"
|
||||||
|
],
|
||||||
|
"paginate": {
|
||||||
|
"default": 10,
|
||||||
|
"max": 50
|
||||||
|
},
|
||||||
|
"wildDuck": {
|
||||||
|
"url": "http://localhost",
|
||||||
|
"token": "aaaaa"
|
||||||
|
}
|
||||||
|
}
|
13
config/prod.js
Normal file
13
config/prod.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
clientUrl: process.env.CLIENT_URL,
|
||||||
|
oidc: {
|
||||||
|
gatewayUri: process.env.OIDC_GATEWAY_URI,
|
||||||
|
clientId: process.env.OIDC_CLIENT_ID,
|
||||||
|
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||||
|
redirectUris: process.env.OIDC_REDIRECT_URIS
|
||||||
|
},
|
||||||
|
wildDuck: {
|
||||||
|
url: process.env.WILDDUCK_URL,
|
||||||
|
token: process.env.WILDDUCK_TOKEN
|
||||||
|
}
|
||||||
|
};
|
3
config/test.json
Normal file
3
config/test.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"port": 8998
|
||||||
|
}
|
20
declarations.ts
Normal file
20
declarations.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/typescript.html
|
||||||
|
import { HookContext as FeathersHookContext, NextFunction } from '@feathersjs/feathers'
|
||||||
|
import { Application as FeathersApplication } from '@feathersjs/express'
|
||||||
|
type ApplicationConfiguration = any
|
||||||
|
|
||||||
|
export { NextFunction }
|
||||||
|
|
||||||
|
// The types for app.get(name) and app.set(name)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface Configuration extends ApplicationConfiguration {}
|
||||||
|
|
||||||
|
// A mapping of service names to types. Will be extended in service files.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface ServiceTypes {}
|
||||||
|
|
||||||
|
// The application instance type that will be used everywhere else
|
||||||
|
export type Application = FeathersApplication<ServiceTypes, Configuration>
|
||||||
|
|
||||||
|
// The context for hook functions - can be typed with a service class
|
||||||
|
export type HookContext<S = any> = FeathersHookContext<Application, S>
|
88
deployment.yaml
Normal file
88
deployment.yaml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
apiVersion: codemowers.io/v1alpha1
|
||||||
|
kind: OIDCGWClient
|
||||||
|
metadata:
|
||||||
|
name: walias
|
||||||
|
spec:
|
||||||
|
uri: "https://walias-msergo.codemowers.ee/auth-oidc"
|
||||||
|
redirectUris:
|
||||||
|
- "https://walias-msergo.codemowers.ee/auth-oidc/callback"
|
||||||
|
grantTypes:
|
||||||
|
- "authorization_code"
|
||||||
|
- "refresh_token" # might be supported by some implementations
|
||||||
|
responseTypes:
|
||||||
|
- "code"
|
||||||
|
availableScopes:
|
||||||
|
- "openid"
|
||||||
|
- "profile"
|
||||||
|
- "offline_access"
|
||||||
|
tokenEndpointAuthMethod: "client_secret_basic"
|
||||||
|
pkce: true
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: walias
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: shared
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
|
external-dns.alpha.kubernetes.io/target: traefik.codemowers.ee
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: walias-msergo.codemowers.ee
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: walias
|
||||||
|
port:
|
||||||
|
number: 3030
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- "*.codemowers.ee"
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: walias
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: walias
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3030
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: walias
|
||||||
|
labels:
|
||||||
|
app: walias
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: walias
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: walias
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: walias
|
||||||
|
image: walias
|
||||||
|
ports:
|
||||||
|
- containerPort: 3030
|
||||||
|
env:
|
||||||
|
- name: CLIENT_URL
|
||||||
|
value: https://walias-msergo.codemowers.ee
|
||||||
|
- name: NODE_ENV
|
||||||
|
value: prod
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: oidc-client-walias-owner-secrets
|
17
hooks/log-error.ts
Normal file
17
hooks/log-error.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { HookContext, NextFunction } from '../declarations'
|
||||||
|
import { logger } from '../logger'
|
||||||
|
|
||||||
|
export const logError = async (context: HookContext, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
await next()
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(error.stack)
|
||||||
|
|
||||||
|
// Log validation errors
|
||||||
|
if (error.data) {
|
||||||
|
logger.error('Data: %O', error.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
9
hooks/validate-auth.ts
Normal file
9
hooks/validate-auth.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { NotAuthenticated } from '@feathersjs/errors'
|
||||||
|
import type { HookContext, NextFunction } from '../declarations'
|
||||||
|
|
||||||
|
// Check if user is stored in session
|
||||||
|
export const validateAuth = async (context: HookContext) => {
|
||||||
|
if (!context.params.session?.user) {
|
||||||
|
throw new NotAuthenticated('Not authenticated')
|
||||||
|
}
|
||||||
|
}
|
11
index.ts
Normal file
11
index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { app } from './app'
|
||||||
|
import { logger } from './logger'
|
||||||
|
|
||||||
|
const port = app.get('port')
|
||||||
|
const host = app.get('host')
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason) => logger.error('Unhandled Rejection %O', reason))
|
||||||
|
|
||||||
|
app.listen(port).then(() => {
|
||||||
|
logger.info(`Feathers app listening on http://${host}:${port}`)
|
||||||
|
})
|
10
logger.ts
Normal file
10
logger.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html
|
||||||
|
import { createLogger, format, transports } from 'winston'
|
||||||
|
|
||||||
|
// Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston
|
||||||
|
export const logger = createLogger({
|
||||||
|
// To see more detailed errors, change this to 'debug'
|
||||||
|
level: 'info',
|
||||||
|
format: format.combine(format.splat(), format.simple()),
|
||||||
|
transports: [new transports.Console()]
|
||||||
|
})
|
3919
package-lock.json
generated
Normal file
3919
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
package.json
Normal file
78
package.json
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "walias",
|
||||||
|
"description": "Aliases for Wild Duck",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"homepage": "",
|
||||||
|
"private": false,
|
||||||
|
"keywords": [
|
||||||
|
"feathers"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"url": "github.com/msergo"
|
||||||
|
},
|
||||||
|
"contributors": [],
|
||||||
|
"bugs": {},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.19.1"
|
||||||
|
},
|
||||||
|
"feathers": {
|
||||||
|
"language": "ts",
|
||||||
|
"packager": "npm",
|
||||||
|
"database": "other",
|
||||||
|
"framework": "express",
|
||||||
|
"transports": [
|
||||||
|
"rest",
|
||||||
|
"websockets"
|
||||||
|
],
|
||||||
|
"schema": false
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"lib": "src",
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
|
"main": "lib/index",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon -x ts-node src/index.ts",
|
||||||
|
"compile": "shx rm -rf lib/ && tsc",
|
||||||
|
"start": "node lib/",
|
||||||
|
"prettier": "npx prettier \"**/*.ts\" --write",
|
||||||
|
"mocha": "cross-env NODE_ENV=test mocha test/ --require ts-node/register --recursive --extension .ts --exit",
|
||||||
|
"test": "npm run mocha",
|
||||||
|
"bundle:client": "npm run compile && npm pack --pack-destination ./public"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@faker-js/faker": "^8.0.2",
|
||||||
|
"@feathersjs/adapter-commons": "^5.0.8",
|
||||||
|
"@feathersjs/authentication": "^5.0.8",
|
||||||
|
"@feathersjs/authentication-client": "^5.0.8",
|
||||||
|
"@feathersjs/configuration": "^5.0.8",
|
||||||
|
"@feathersjs/errors": "^5.0.8",
|
||||||
|
"@feathersjs/express": "^5.0.8",
|
||||||
|
"@feathersjs/feathers": "^5.0.8",
|
||||||
|
"@feathersjs/schema": "^5.0.8",
|
||||||
|
"@feathersjs/socketio": "^5.0.8",
|
||||||
|
"@feathersjs/transport-commons": "^5.0.8",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"config": "^3.3.9",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
|
"openid-client": "^5.4.3",
|
||||||
|
"winston": "^3.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@feathersjs/cli": "^5.0.8",
|
||||||
|
"@feathersjs/rest-client": "^5.0.8",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
|
"@types/express-session": "^1.17.7",
|
||||||
|
"@types/mocha": "^10.0.1",
|
||||||
|
"@types/node": "^20.4.5",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"shx": "^0.3.4",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^5.1.6"
|
||||||
|
}
|
||||||
|
}
|
37
public/index.html
Normal file
37
public/index.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>walias</title>
|
||||||
|
<meta name="description" content="Aliases for Wild Duck">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.logo {
|
||||||
|
display: block;
|
||||||
|
margin: auto auto;
|
||||||
|
width: 30%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img class="logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTEyOCA5LjEwMmM2NS42NjUgMCAxMTguODk4IDUzLjIzMyAxMTguODk4IDExOC44OTggMCA2NS42NjUtNTMuMjMzIDExOC44OTgtMTE4Ljg5OCAxMTguODk4QzYyLjMzNSAyNDYuODk4IDkuMTAyIDE5My42NjUgOS4xMDIgMTI4IDkuMTAyIDYyLjMzNSA2Mi4zMzUgOS4xMDIgMTI4IDkuMTAyTTEyOCAwQzU3LjQyMSAwIDAgNTcuNDIxIDAgMTI4YzAgNzAuNTc5IDU3LjQyMSAxMjggMTI4IDEyOCA3MC41NzkgMCAxMjgtNTcuNDIxIDEyOC0xMjhDMjU2IDU3LjQyMSAxOTguNTc5IDAgMTI4IDBtMjAuODMgMjUuNTI0Yy0xMC40My0xLjg5Ni0zNS42NTEgMzYuNDA5LTQzLjk5NCA1OS43MzQtLjYzNCAxLjc2OS0yLjA4NiA4LjI0OS0yLjA4NiA5Ljk1NSAwIDAgNi41MzEgMTQuMDU1IDguMzQzIDE3LjM1MS0zLjAzNC0xLjU4LTkuMzIzLTEzLjc1Ni05LjMyMy0xMy43NTYtMy4wMzQgNS43ODQtNS45NDIgMzIuMzQtNC45OTQgMzcuMjcxIDAgMCA2Ljc2MiAxMC4wNjIgOS4zODcgMTIuNTc4LTMuNjAzLTEuMjAxLTkuNjcxLTkuMzU1LTkuNjcxLTkuMzU1LTEuMTM4IDMuNTA4LS45MTYgMTAuODA3LS4zNzkgMTMuMjc0IDQuNTUxIDYuNjM3IDEwLjYxOSA3LjM5NiAxMC42MTkgNy4zOTZzLTYuNjM3IDY2LjE4MSAzLjQxMyA3MS4xMTFjNi4yNTgtMS4zMjcgNy43NzUtNzMuOTU2IDcuNzc1LTczLjk1NnM3LjU4NS41NjkgOS4yOTItMS4zMjdjMy44NTYtMi42NTUgMTIuODI2LTMwLjIyNCAxMi45NTgtMzQuMjAyIDAgMC0xMC40MSAxLjk1Mi0xNS40ODcgMy45MjQgMy44MjYtMy44IDE2LjA0OS02LjM1MiAxNi4wNDktNi4zNTIgMy4zMTUtMy45NzkgMTAuMjkxLTMxLjA0NyAxMC45OTQtMzkuMzkxLjE3Ni0yLjA5My41ODMtNC42NTcuMjY4LTguMzk4IDAgMC05Ljk0MSAyLjE3Ny0xMi4wMTQgMS40MjQgMi4xMDQtLjIzNyAxMi4yNjMtNC4xNCAxMi4yNjMtNC4xNCAxLjgwMS0xNi4yMTMgMi4zNTgtNDIuMDkxLTMuNDEzLTQzLjE0MXptLTM2LjM4IDE3MS42OTFjLS43OTUgMTkuNDk2LTEuMjk0IDI1LjAwNC0yLjExNSAyOS42MDEtLjM3OS44NTctLjc1OC45OTctMS4xMzgtLjA5NS0zLjQ3Ny0xNS45OTItMy4yMjQtMTM2LjQzOCAzNi40MDktMTkxLjI0MS0yMy4wNSA0Mi4wOTItMzMuNTM1IDEyMi44NjEtMzMuMTU2IDE2MS43MzV6IiBmaWxsPSIjMzMzIi8+PC9zdmc+" />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
42
readme.md
Normal file
42
readme.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# walias
|
||||||
|
|
||||||
|
> Aliases for Wild Duck
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
This project uses [Feathers](http://feathersjs.com). An open source framework for building APIs and real-time applications.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed.
|
||||||
|
2. Install your dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
cd path/to/walias
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start your app
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run compile # Compile TypeScript source
|
||||||
|
npm run migrate # Run migrations to set up the database
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run `npm test` and all your tests in the `test/` directory will be run.
|
||||||
|
|
||||||
|
## Scaffolding
|
||||||
|
|
||||||
|
This app comes with a powerful command line interface for Feathers. Here are a few things it can do:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npx feathers help # Show all commands
|
||||||
|
$ npx feathers generate service # Generate a new Service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com).
|
84
services/aliases/aliases.class.ts
Normal file
84
services/aliases/aliases.class.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||||
|
|
||||||
|
import type { Application } from '../../declarations'
|
||||||
|
import wildDuckClient from '../../clients/wildduck.client'
|
||||||
|
import { faker } from '@faker-js/faker'
|
||||||
|
import { BadRequest } from '@feathersjs/errors'
|
||||||
|
|
||||||
|
interface Alias {
|
||||||
|
success: boolean,
|
||||||
|
id: string,
|
||||||
|
address: string,
|
||||||
|
main: boolean,
|
||||||
|
user: string,
|
||||||
|
tags: string[],
|
||||||
|
created: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAddressInfoResponse {
|
||||||
|
success: boolean,
|
||||||
|
results: Alias[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateAddressResponse {
|
||||||
|
success: boolean,
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasesData = any
|
||||||
|
type AliasesPatch = any
|
||||||
|
type AliasesQuery = any
|
||||||
|
|
||||||
|
export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery }
|
||||||
|
|
||||||
|
export interface AliasesServiceOptions {
|
||||||
|
app: Application
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AliasesParams extends Params<AliasesQuery> {
|
||||||
|
session?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
|
||||||
|
implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch>
|
||||||
|
{
|
||||||
|
constructor(public options: AliasesServiceOptions) { }
|
||||||
|
|
||||||
|
async find(params: ServiceParams): Promise<Alias[]> {
|
||||||
|
const userId = await this.getUserIdByEmailAddress(params)
|
||||||
|
const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`)
|
||||||
|
|
||||||
|
return userAddressesResponse.results
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: AliasesData, params: ServiceParams): Promise<Alias>
|
||||||
|
async create(data: AliasesData, params: ServiceParams): Promise<Alias | Alias[]> {
|
||||||
|
const userId = await this.getUserIdByEmailAddress(params)
|
||||||
|
const alias = `${faker.animal.crocodilia().replace(/\s/, '').slice(10)}-${faker.git.commitSha({ length: 5 })}`;
|
||||||
|
|
||||||
|
const createResult = await wildDuckClient.post<CreateAddressResponse>(`/users/${userId}/addresses`, {
|
||||||
|
address: alias
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!createResult.data.success) {
|
||||||
|
throw new BadRequest('Failed to create alias')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`)
|
||||||
|
|
||||||
|
return userAddressesResponse.results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUserIdByEmailAddress(params: ServiceParams): Promise<string> {
|
||||||
|
const emails = params.session?.user?.emails;
|
||||||
|
|
||||||
|
const addressInfoResponse = await Promise.any(emails.map((email: string) => wildDuckClient.get<Alias>(`addresses/resolve/${email}`)))
|
||||||
|
|
||||||
|
return addressInfoResponse.data.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptions = (app: Application) => {
|
||||||
|
return { app }
|
||||||
|
}
|
41
services/aliases/aliases.ts
Normal file
41
services/aliases/aliases.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { Application } from '../../declarations'
|
||||||
|
import { validateAuth } from '../../hooks/validate-auth'
|
||||||
|
import { AliasesService, getOptions } from './aliases.class'
|
||||||
|
|
||||||
|
export const aliasesPath = 'aliases'
|
||||||
|
export const aliasesMethods = ['find', 'create'] as const
|
||||||
|
|
||||||
|
export * from './aliases.class'
|
||||||
|
|
||||||
|
export const aliases = (app: Application) => {
|
||||||
|
app.use(aliasesPath, new AliasesService(getOptions(app)), {
|
||||||
|
methods: aliasesMethods,
|
||||||
|
events: []
|
||||||
|
})
|
||||||
|
|
||||||
|
app.service(aliasesPath).hooks({
|
||||||
|
around: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
all: [
|
||||||
|
validateAuth
|
||||||
|
],
|
||||||
|
find: [],
|
||||||
|
create: [],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this service to the service type index
|
||||||
|
declare module '../../declarations' {
|
||||||
|
interface ServiceTypes {
|
||||||
|
[aliasesPath]: AliasesService
|
||||||
|
}
|
||||||
|
}
|
53
services/auth-oidc/auth-oidc.class.ts
Normal file
53
services/auth-oidc/auth-oidc.class.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||||
|
|
||||||
|
import type { Application } from '../../declarations'
|
||||||
|
|
||||||
|
import { Issuer, generators } from 'openid-client'
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
|
|
||||||
|
type AuthOidcResponse = string
|
||||||
|
type AuthOidcQuery = any
|
||||||
|
|
||||||
|
export type { AuthOidcResponse as AuthOidc, AuthOidcQuery }
|
||||||
|
|
||||||
|
export interface AuthOidcServiceOptions {
|
||||||
|
app: Application
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthOidcParams extends Params<AuthOidcQuery> {
|
||||||
|
session?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthOidcService<ServiceParams extends AuthOidcParams = AuthOidcParams>
|
||||||
|
implements ServiceInterface<AuthOidcResponse, ServiceParams>
|
||||||
|
{
|
||||||
|
constructor(public options: AuthOidcServiceOptions) { }
|
||||||
|
|
||||||
|
async find(params: ServiceParams): Promise<AuthOidcResponse> {
|
||||||
|
const issuer = await Issuer.discover(config.get('oidc.gatewayUri'));
|
||||||
|
const client = new issuer.Client({
|
||||||
|
client_id: config.get('oidc.clientId'),
|
||||||
|
client_secret: config.get('oidc.clientSecret'),
|
||||||
|
redirect_uris: [config.get('oidc.redirectUris')],
|
||||||
|
response_types: ['code'],
|
||||||
|
})
|
||||||
|
const codeVerifier = generators.codeVerifier();
|
||||||
|
const codeChallenge = generators.codeChallenge(codeVerifier);
|
||||||
|
|
||||||
|
const url = client.authorizationUrl({
|
||||||
|
redirect_uri: config.get('clientUrl') + '/auth-oidc/callback',
|
||||||
|
scope: 'openid profile offline_access',
|
||||||
|
response_type: 'code',
|
||||||
|
code_challenge: codeChallenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
});
|
||||||
|
|
||||||
|
params.session.codeVerifier = codeVerifier;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptions = (app: Application) => {
|
||||||
|
return { app }
|
||||||
|
}
|
41
services/auth-oidc/auth-oidc.ts
Normal file
41
services/auth-oidc/auth-oidc.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { Application } from '../../declarations'
|
||||||
|
import { AuthOidcService, getOptions } from './auth-oidc.class'
|
||||||
|
|
||||||
|
export const authOidcPath = 'auth-oidc'
|
||||||
|
export const authOidcMethods = ['find'] as const
|
||||||
|
|
||||||
|
export * from './auth-oidc.class'
|
||||||
|
|
||||||
|
export const authOidc = (app: Application) => {
|
||||||
|
// TODO: fix this to use the correct type
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(authOidcPath, new AuthOidcService(getOptions(app)), {
|
||||||
|
methods: authOidcMethods,
|
||||||
|
events: []
|
||||||
|
}, (req: any, res: any) => {
|
||||||
|
|
||||||
|
return res.redirect(res.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
app.service(authOidcPath).hooks({
|
||||||
|
around: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
all: [],
|
||||||
|
find: [],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '../../declarations' {
|
||||||
|
interface ServiceTypes {
|
||||||
|
[authOidcPath]: AuthOidcService
|
||||||
|
}
|
||||||
|
}
|
52
services/auth-oidc/callback/auth-oidc-callback.class.ts
Normal file
52
services/auth-oidc/callback/auth-oidc-callback.class.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||||
|
import type { Application } from '../../../declarations'
|
||||||
|
import { Issuer } from 'openid-client'
|
||||||
|
|
||||||
|
import config from 'config'
|
||||||
|
|
||||||
|
type AuthOidcCallback = string
|
||||||
|
type AuthOidcCallbackData = any
|
||||||
|
type AuthOidcCallbackPatch = any
|
||||||
|
type AuthOidcCallbackQuery = any
|
||||||
|
|
||||||
|
export type { AuthOidcCallback, AuthOidcCallbackData, AuthOidcCallbackPatch, AuthOidcCallbackQuery }
|
||||||
|
|
||||||
|
export interface AuthOidcCallbackServiceOptions {
|
||||||
|
app: Application
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthOidcCallbackParams extends Params<AuthOidcCallbackQuery> {
|
||||||
|
session?: any
|
||||||
|
query: {
|
||||||
|
iss: string,
|
||||||
|
code: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthOidcCallbackService<ServiceParams extends AuthOidcCallbackParams = AuthOidcCallbackParams>
|
||||||
|
implements ServiceInterface<AuthOidcCallback, AuthOidcCallbackData, ServiceParams, AuthOidcCallbackPatch>
|
||||||
|
{
|
||||||
|
constructor(public options: AuthOidcCallbackServiceOptions) { }
|
||||||
|
|
||||||
|
async find(params: ServiceParams): Promise<AuthOidcCallback> {
|
||||||
|
const issuer = await Issuer.discover(config.get('oidc.gatewayUri'));
|
||||||
|
const client = new issuer.Client({
|
||||||
|
client_id: config.get('oidc.clientId'),
|
||||||
|
client_secret: config.get('oidc.clientSecret'),
|
||||||
|
redirect_uris: [config.get('oidc.redirectUris')],
|
||||||
|
response_types: ['code'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const codeVerifier = params.session.codeVerifier;
|
||||||
|
const tokenSet = await client.callback(config.get('clientUrl') + '/auth-oidc/callback', { code: params.query.code, iss: params.query.iss }, { code_verifier: codeVerifier });
|
||||||
|
const userinfo = await client.userinfo(tokenSet.access_token as string);
|
||||||
|
|
||||||
|
params.session.user = userinfo;
|
||||||
|
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptions = (app: Application) => {
|
||||||
|
return { app }
|
||||||
|
}
|
42
services/auth-oidc/callback/auth-oidc-callback.ts
Normal file
42
services/auth-oidc/callback/auth-oidc-callback.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { http } from '@feathersjs/transport-commons'
|
||||||
|
import type { Application } from '../../../declarations'
|
||||||
|
import { AuthOidcCallbackService, getOptions } from './auth-oidc-callback.class'
|
||||||
|
|
||||||
|
export const authOidcCallbackPath = 'auth-oidc/callback'
|
||||||
|
export const authOidcCallbackMethods = ['find'] as const
|
||||||
|
|
||||||
|
export * from './auth-oidc-callback.class'
|
||||||
|
|
||||||
|
export const authOidcCallback = (app: Application) => {
|
||||||
|
// TODO: fix this to use the correct type
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(authOidcCallbackPath, new AuthOidcCallbackService(getOptions(app)), {
|
||||||
|
methods: authOidcCallbackMethods,
|
||||||
|
events: []
|
||||||
|
}, (req: any, res: any) => {
|
||||||
|
|
||||||
|
return res.redirect(res.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
app.service(authOidcCallbackPath).hooks({
|
||||||
|
around: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
all: [],
|
||||||
|
find: [],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '../../../declarations' {
|
||||||
|
interface ServiceTypes {
|
||||||
|
[authOidcCallbackPath]: AuthOidcCallbackService
|
||||||
|
}
|
||||||
|
}
|
10
services/index.ts
Normal file
10
services/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { authOidcCallback } from './auth-oidc/callback/auth-oidc-callback'
|
||||||
|
import { authOidc } from './auth-oidc/auth-oidc'
|
||||||
|
import { aliases } from './aliases/aliases'
|
||||||
|
import type { Application } from '../declarations'
|
||||||
|
|
||||||
|
export const services = (app: Application) => {
|
||||||
|
app.configure(authOidcCallback)
|
||||||
|
app.configure(authOidc)
|
||||||
|
app.configure(aliases)
|
||||||
|
}
|
25
skaffold.yaml
Normal file
25
skaffold.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: skaffold/v4beta1
|
||||||
|
kind: Config
|
||||||
|
build:
|
||||||
|
artifacts:
|
||||||
|
- image: walias
|
||||||
|
|
||||||
|
manifests:
|
||||||
|
rawYaml:
|
||||||
|
- deployment.yaml
|
||||||
|
|
||||||
|
profiles:
|
||||||
|
- name: dev
|
||||||
|
activation:
|
||||||
|
- command: dev
|
||||||
|
build:
|
||||||
|
artifacts:
|
||||||
|
- image: walias
|
||||||
|
docker:
|
||||||
|
target: dev
|
||||||
|
sync:
|
||||||
|
manual:
|
||||||
|
- src: .
|
||||||
|
dest: /app/
|
||||||
|
deploy:
|
||||||
|
kubectl: {}
|
93
src/app.ts
Normal file
93
src/app.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { feathers } from '@feathersjs/feathers'
|
||||||
|
import express, {
|
||||||
|
rest,
|
||||||
|
json,
|
||||||
|
urlencoded,
|
||||||
|
cors,
|
||||||
|
serveStatic,
|
||||||
|
notFound,
|
||||||
|
errorHandler
|
||||||
|
} from '@feathersjs/express'
|
||||||
|
import configuration from '@feathersjs/configuration'
|
||||||
|
import socketio from '@feathersjs/socketio'
|
||||||
|
import session from 'express-session';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
|
import type { Application } from './declarations'
|
||||||
|
|
||||||
|
import { logger } from './logger'
|
||||||
|
import { logError } from './hooks/log-error'
|
||||||
|
import { services } from './services/index'
|
||||||
|
import { channels } from './channels'
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
const app: Application = express(feathers())
|
||||||
|
|
||||||
|
// Load app configuration
|
||||||
|
app.configure(configuration())
|
||||||
|
app.use(cors())
|
||||||
|
app.use(json({
|
||||||
|
limit: '20mb'
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(session({
|
||||||
|
secret: randomUUID(),
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: { secure: false }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Propagate session to request.params in feathers services
|
||||||
|
app.use(function (req, _res, next) {
|
||||||
|
req.feathers = {
|
||||||
|
...req.feathers,
|
||||||
|
session: req.session
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/authme', (req, res) => {
|
||||||
|
//@ts-ignore
|
||||||
|
req.session.user = {
|
||||||
|
email: "her@va.mm"
|
||||||
|
}
|
||||||
|
res.send("done locally")
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(urlencoded({ extended: true }))
|
||||||
|
// Host the public folder
|
||||||
|
app.use('/', serveStatic(app.get('public')))
|
||||||
|
|
||||||
|
// Configure services and real-time functionality
|
||||||
|
app.configure(rest())
|
||||||
|
app.configure(
|
||||||
|
socketio({
|
||||||
|
cors: {
|
||||||
|
origin: app.get('origins')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.configure(services)
|
||||||
|
app.configure(channels)
|
||||||
|
|
||||||
|
// Configure a middleware for 404s and the error handler
|
||||||
|
app.use(notFound())
|
||||||
|
app.use(errorHandler({ logger }))
|
||||||
|
|
||||||
|
// Register hooks that run on all service methods
|
||||||
|
app.hooks({
|
||||||
|
around: {
|
||||||
|
all: [logError]
|
||||||
|
},
|
||||||
|
before: {},
|
||||||
|
after: {},
|
||||||
|
error: {}
|
||||||
|
})
|
||||||
|
// Register application setup and teardown hooks here
|
||||||
|
app.hooks({
|
||||||
|
setup: [],
|
||||||
|
teardown: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export { app }
|
38
src/channels.ts
Normal file
38
src/channels.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/channels.html
|
||||||
|
import type { RealTimeConnection, Params } from '@feathersjs/feathers'
|
||||||
|
import type { AuthenticationResult } from '@feathersjs/authentication'
|
||||||
|
import '@feathersjs/transport-commons'
|
||||||
|
import type { Application, HookContext } from './declarations'
|
||||||
|
import { logger } from './logger'
|
||||||
|
|
||||||
|
export const channels = (app: Application) => {
|
||||||
|
logger.warn(
|
||||||
|
'Publishing all events to all authenticated users. See `channels.ts` and https://dove.feathersjs.com/api/channels.html for more information.'
|
||||||
|
)
|
||||||
|
|
||||||
|
app.on('connection', (connection: RealTimeConnection) => {
|
||||||
|
// On a new real-time connection, add it to the anonymous channel
|
||||||
|
app.channel('anonymous').join(connection)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('login', (authResult: AuthenticationResult, { connection }: Params) => {
|
||||||
|
// connection can be undefined if there is no
|
||||||
|
// real-time connection, e.g. when logging in via REST
|
||||||
|
if (connection) {
|
||||||
|
// The connection is no longer anonymous, remove it
|
||||||
|
app.channel('anonymous').leave(connection)
|
||||||
|
|
||||||
|
// Add it to the authenticated user channel
|
||||||
|
app.channel('authenticated').join(connection)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
app.publish((data: any, context: HookContext) => {
|
||||||
|
// Here you can add event publishers to channels set up in `channels.js`
|
||||||
|
// To publish only for a specific event use `app.publish(eventname, () => {})`
|
||||||
|
|
||||||
|
// e.g. to publish all service events to all authenticated users use
|
||||||
|
return app.channel('authenticated')
|
||||||
|
})
|
||||||
|
}
|
12
src/clients/wildduck.client.ts
Normal file
12
src/clients/wildduck.client.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
|
const wildDuckClient = axios.create({
|
||||||
|
baseURL: config.get('wildDuck.url'),
|
||||||
|
headers: {
|
||||||
|
'X-Access-Token': config.get('wildDuck.token'),
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default wildDuckClient;
|
20
src/declarations.ts
Normal file
20
src/declarations.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/typescript.html
|
||||||
|
import { HookContext as FeathersHookContext, NextFunction } from '@feathersjs/feathers'
|
||||||
|
import { Application as FeathersApplication } from '@feathersjs/express'
|
||||||
|
type ApplicationConfiguration = any
|
||||||
|
|
||||||
|
export { NextFunction }
|
||||||
|
|
||||||
|
// The types for app.get(name) and app.set(name)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface Configuration extends ApplicationConfiguration {}
|
||||||
|
|
||||||
|
// A mapping of service names to types. Will be extended in service files.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface ServiceTypes {}
|
||||||
|
|
||||||
|
// The application instance type that will be used everywhere else
|
||||||
|
export type Application = FeathersApplication<ServiceTypes, Configuration>
|
||||||
|
|
||||||
|
// The context for hook functions - can be typed with a service class
|
||||||
|
export type HookContext<S = any> = FeathersHookContext<Application, S>
|
17
src/hooks/log-error.ts
Normal file
17
src/hooks/log-error.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { HookContext, NextFunction } from '../declarations'
|
||||||
|
import { logger } from '../logger'
|
||||||
|
|
||||||
|
export const logError = async (context: HookContext, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
await next()
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(error.stack)
|
||||||
|
|
||||||
|
// Log validation errors
|
||||||
|
if (error.data) {
|
||||||
|
logger.error('Data: %O', error.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
9
src/hooks/validate-auth.ts
Normal file
9
src/hooks/validate-auth.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { NotAuthenticated } from '@feathersjs/errors'
|
||||||
|
import type { HookContext, NextFunction } from '../declarations'
|
||||||
|
|
||||||
|
// Check if user is stored in session
|
||||||
|
export const validateAuth = async (context: HookContext) => {
|
||||||
|
if (!context.params.session?.user) {
|
||||||
|
throw new NotAuthenticated('Not authenticated')
|
||||||
|
}
|
||||||
|
}
|
11
src/index.ts
Normal file
11
src/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { app } from './app'
|
||||||
|
import { logger } from './logger'
|
||||||
|
|
||||||
|
const port = app.get('port')
|
||||||
|
const host = app.get('host')
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason) => logger.error('Unhandled Rejection %O', reason))
|
||||||
|
|
||||||
|
app.listen(port).then(() => {
|
||||||
|
logger.info(`Feathers app listening on http://${host}:${port}`)
|
||||||
|
})
|
10
src/logger.ts
Normal file
10
src/logger.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html
|
||||||
|
import { createLogger, format, transports } from 'winston'
|
||||||
|
|
||||||
|
// Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston
|
||||||
|
export const logger = createLogger({
|
||||||
|
// To see more detailed errors, change this to 'debug'
|
||||||
|
level: 'info',
|
||||||
|
format: format.combine(format.splat(), format.simple()),
|
||||||
|
transports: [new transports.Console()]
|
||||||
|
})
|
92
src/services/aliases/aliases.class.ts
Normal file
92
src/services/aliases/aliases.class.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||||
|
|
||||||
|
import type { Application } from '../../declarations'
|
||||||
|
import wildDuckClient from '../../clients/wildduck.client'
|
||||||
|
import { faker } from '@faker-js/faker'
|
||||||
|
import { BadRequest } from '@feathersjs/errors'
|
||||||
|
import config from 'config'
|
||||||
|
|
||||||
|
interface Alias {
|
||||||
|
success: boolean,
|
||||||
|
id: string,
|
||||||
|
address: string,
|
||||||
|
main: boolean,
|
||||||
|
user: string,
|
||||||
|
tags: string[],
|
||||||
|
created: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAddressInfoResponse {
|
||||||
|
success: boolean,
|
||||||
|
results: Alias[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateAddressResponse {
|
||||||
|
success: boolean,
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasesData = any
|
||||||
|
type AliasesPatch = any
|
||||||
|
type AliasesQuery = any
|
||||||
|
|
||||||
|
export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery }
|
||||||
|
|
||||||
|
export interface AliasesServiceOptions {
|
||||||
|
app: Application
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AliasesParams extends Params<AliasesQuery> {
|
||||||
|
session?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
|
||||||
|
implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch>
|
||||||
|
{
|
||||||
|
constructor(public options: AliasesServiceOptions) { }
|
||||||
|
|
||||||
|
async find(params: ServiceParams): Promise<Alias[]> {
|
||||||
|
const userId = await this.getUserIdByEmailAddress(params)
|
||||||
|
const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`)
|
||||||
|
|
||||||
|
return userAddressesResponse.results
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: AliasesData, params: ServiceParams): Promise<Alias>
|
||||||
|
async create(data: AliasesData, params: ServiceParams): Promise<Alias | Alias[]> {
|
||||||
|
const userId = await this.getUserIdByEmailAddress(params)
|
||||||
|
const aliasFirstPart = faker.animal.crocodilia()
|
||||||
|
.replace(/\D/, '')
|
||||||
|
.replace(/\s/, '')
|
||||||
|
.slice(10);
|
||||||
|
|
||||||
|
const aliasSecondPart = faker.git.commitSha({ length: 5 });
|
||||||
|
const alias = `${aliasFirstPart}-${aliasSecondPart}@${config.get('wildDuck.domain')}`;
|
||||||
|
// const alias = `${faker.animal.crocodilia().replace(/\s/, '').slice(10)}-${faker.git.commitSha({ length: 5 })}`;
|
||||||
|
|
||||||
|
const createResult = await wildDuckClient.post<CreateAddressResponse>(`/users/${userId}/addresses`, {
|
||||||
|
address: alias
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!createResult.data.success) {
|
||||||
|
throw new BadRequest('Failed to create alias')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`)
|
||||||
|
|
||||||
|
return userAddressesResponse.results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUserIdByEmailAddress(params: ServiceParams): Promise<string> {
|
||||||
|
const emails = params.session?.user?.emails;
|
||||||
|
|
||||||
|
const addressInfoResponse = await Promise.any(emails.map((email: string) => wildDuckClient.get<Alias>(`addresses/resolve/${email}`)))
|
||||||
|
|
||||||
|
return addressInfoResponse.data.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptions = (app: Application) => {
|
||||||
|
return { app }
|
||||||
|
}
|
41
src/services/aliases/aliases.ts
Normal file
41
src/services/aliases/aliases.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { Application } from '../../declarations'
|
||||||
|
import { validateAuth } from '../../hooks/validate-auth'
|
||||||
|
import { AliasesService, getOptions } from './aliases.class'
|
||||||
|
|
||||||
|
export const aliasesPath = 'aliases'
|
||||||
|
export const aliasesMethods = ['find', 'create'] as const
|
||||||
|
|
||||||
|
export * from './aliases.class'
|
||||||
|
|
||||||
|
export const aliases = (app: Application) => {
|
||||||
|
app.use(aliasesPath, new AliasesService(getOptions(app)), {
|
||||||
|
methods: aliasesMethods,
|
||||||
|
events: []
|
||||||
|
})
|
||||||
|
|
||||||
|
app.service(aliasesPath).hooks({
|
||||||
|
around: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
all: [
|
||||||
|
validateAuth
|
||||||
|
],
|
||||||
|
find: [],
|
||||||
|
create: [],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this service to the service type index
|
||||||
|
declare module '../../declarations' {
|
||||||
|
interface ServiceTypes {
|
||||||
|
[aliasesPath]: AliasesService
|
||||||
|
}
|
||||||
|
}
|
53
src/services/auth-oidc/auth-oidc.class.ts
Normal file
53
src/services/auth-oidc/auth-oidc.class.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||||
|
|
||||||
|
import type { Application } from '../../declarations'
|
||||||
|
|
||||||
|
import { Issuer, generators } from 'openid-client'
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
|
|
||||||
|
type AuthOidcResponse = string
|
||||||
|
type AuthOidcQuery = any
|
||||||
|
|
||||||
|
export type { AuthOidcResponse as AuthOidc, AuthOidcQuery }
|
||||||
|
|
||||||
|
export interface AuthOidcServiceOptions {
|
||||||
|
app: Application
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthOidcParams extends Params<AuthOidcQuery> {
|
||||||
|
session?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthOidcService<ServiceParams extends AuthOidcParams = AuthOidcParams>
|
||||||
|
implements ServiceInterface<AuthOidcResponse, ServiceParams>
|
||||||
|
{
|
||||||
|
constructor(public options: AuthOidcServiceOptions) { }
|
||||||
|
|
||||||
|
async find(params: ServiceParams): Promise<AuthOidcResponse> {
|
||||||
|
const issuer = await Issuer.discover(config.get('oidc.gatewayUri'));
|
||||||
|
const client = new issuer.Client({
|
||||||
|
client_id: config.get('oidc.clientId'),
|
||||||
|
client_secret: config.get('oidc.clientSecret'),
|
||||||
|
redirect_uris: [config.get('oidc.redirectUris')],
|
||||||
|
response_types: ['code'],
|
||||||
|
})
|
||||||
|
const codeVerifier = generators.codeVerifier();
|
||||||
|
const codeChallenge = generators.codeChallenge(codeVerifier);
|
||||||
|
|
||||||
|
const url = client.authorizationUrl({
|
||||||
|
redirect_uri: config.get('clientUrl') + '/auth-oidc/callback',
|
||||||
|
scope: 'openid profile offline_access',
|
||||||
|
response_type: 'code',
|
||||||
|
code_challenge: codeChallenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
});
|
||||||
|
|
||||||
|
params.session.codeVerifier = codeVerifier;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptions = (app: Application) => {
|
||||||
|
return { app }
|
||||||
|
}
|
41
src/services/auth-oidc/auth-oidc.ts
Normal file
41
src/services/auth-oidc/auth-oidc.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { Application } from '../../declarations'
|
||||||
|
import { AuthOidcService, getOptions } from './auth-oidc.class'
|
||||||
|
|
||||||
|
export const authOidcPath = 'auth-oidc'
|
||||||
|
export const authOidcMethods = ['find'] as const
|
||||||
|
|
||||||
|
export * from './auth-oidc.class'
|
||||||
|
|
||||||
|
export const authOidc = (app: Application) => {
|
||||||
|
// TODO: fix this to use the correct type
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(authOidcPath, new AuthOidcService(getOptions(app)), {
|
||||||
|
methods: authOidcMethods,
|
||||||
|
events: []
|
||||||
|
}, (req: any, res: any) => {
|
||||||
|
|
||||||
|
return res.redirect(res.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
app.service(authOidcPath).hooks({
|
||||||
|
around: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
all: [],
|
||||||
|
find: [],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '../../declarations' {
|
||||||
|
interface ServiceTypes {
|
||||||
|
[authOidcPath]: AuthOidcService
|
||||||
|
}
|
||||||
|
}
|
52
src/services/auth-oidc/callback/auth-oidc-callback.class.ts
Normal file
52
src/services/auth-oidc/callback/auth-oidc-callback.class.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||||
|
import type { Application } from '../../../declarations'
|
||||||
|
import { Issuer } from 'openid-client'
|
||||||
|
|
||||||
|
import config from 'config'
|
||||||
|
|
||||||
|
type AuthOidcCallback = string
|
||||||
|
type AuthOidcCallbackData = any
|
||||||
|
type AuthOidcCallbackPatch = any
|
||||||
|
type AuthOidcCallbackQuery = any
|
||||||
|
|
||||||
|
export type { AuthOidcCallback, AuthOidcCallbackData, AuthOidcCallbackPatch, AuthOidcCallbackQuery }
|
||||||
|
|
||||||
|
export interface AuthOidcCallbackServiceOptions {
|
||||||
|
app: Application
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthOidcCallbackParams extends Params<AuthOidcCallbackQuery> {
|
||||||
|
session?: any
|
||||||
|
query: {
|
||||||
|
iss: string,
|
||||||
|
code: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthOidcCallbackService<ServiceParams extends AuthOidcCallbackParams = AuthOidcCallbackParams>
|
||||||
|
implements ServiceInterface<AuthOidcCallback, AuthOidcCallbackData, ServiceParams, AuthOidcCallbackPatch>
|
||||||
|
{
|
||||||
|
constructor(public options: AuthOidcCallbackServiceOptions) { }
|
||||||
|
|
||||||
|
async find(params: ServiceParams): Promise<AuthOidcCallback> {
|
||||||
|
const issuer = await Issuer.discover(config.get('oidc.gatewayUri'));
|
||||||
|
const client = new issuer.Client({
|
||||||
|
client_id: config.get('oidc.clientId'),
|
||||||
|
client_secret: config.get('oidc.clientSecret'),
|
||||||
|
redirect_uris: [config.get('oidc.redirectUris')],
|
||||||
|
response_types: ['code'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const codeVerifier = params.session.codeVerifier;
|
||||||
|
const tokenSet = await client.callback(config.get('clientUrl') + '/auth-oidc/callback', { code: params.query.code, iss: params.query.iss }, { code_verifier: codeVerifier });
|
||||||
|
const userinfo = await client.userinfo(tokenSet.access_token as string);
|
||||||
|
|
||||||
|
params.session.user = userinfo;
|
||||||
|
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptions = (app: Application) => {
|
||||||
|
return { app }
|
||||||
|
}
|
42
src/services/auth-oidc/callback/auth-oidc-callback.ts
Normal file
42
src/services/auth-oidc/callback/auth-oidc-callback.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { http } from '@feathersjs/transport-commons'
|
||||||
|
import type { Application } from '../../../declarations'
|
||||||
|
import { AuthOidcCallbackService, getOptions } from './auth-oidc-callback.class'
|
||||||
|
|
||||||
|
export const authOidcCallbackPath = 'auth-oidc/callback'
|
||||||
|
export const authOidcCallbackMethods = ['find'] as const
|
||||||
|
|
||||||
|
export * from './auth-oidc-callback.class'
|
||||||
|
|
||||||
|
export const authOidcCallback = (app: Application) => {
|
||||||
|
// TODO: fix this to use the correct type
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(authOidcCallbackPath, new AuthOidcCallbackService(getOptions(app)), {
|
||||||
|
methods: authOidcCallbackMethods,
|
||||||
|
events: []
|
||||||
|
}, (req: any, res: any) => {
|
||||||
|
|
||||||
|
return res.redirect(res.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
app.service(authOidcCallbackPath).hooks({
|
||||||
|
around: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
all: [],
|
||||||
|
find: [],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
all: []
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '../../../declarations' {
|
||||||
|
interface ServiceTypes {
|
||||||
|
[authOidcCallbackPath]: AuthOidcCallbackService
|
||||||
|
}
|
||||||
|
}
|
10
src/services/index.ts
Normal file
10
src/services/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { authOidcCallback } from './auth-oidc/callback/auth-oidc-callback'
|
||||||
|
import { authOidc } from './auth-oidc/auth-oidc'
|
||||||
|
import { aliases } from './aliases/aliases'
|
||||||
|
import type { Application } from '../declarations'
|
||||||
|
|
||||||
|
export const services = (app: Application) => {
|
||||||
|
app.configure(authOidcCallback)
|
||||||
|
app.configure(authOidc)
|
||||||
|
app.configure(aliases)
|
||||||
|
}
|
29
src/validators.ts
Normal file
29
src/validators.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html
|
||||||
|
import { Ajv, addFormats } from '@feathersjs/schema'
|
||||||
|
import type { FormatsPluginOptions } from '@feathersjs/schema'
|
||||||
|
|
||||||
|
const formats: FormatsPluginOptions = [
|
||||||
|
'date-time',
|
||||||
|
'time',
|
||||||
|
'date',
|
||||||
|
'email',
|
||||||
|
'hostname',
|
||||||
|
'ipv4',
|
||||||
|
'ipv6',
|
||||||
|
'uri',
|
||||||
|
'uri-reference',
|
||||||
|
'uuid',
|
||||||
|
'uri-template',
|
||||||
|
'json-pointer',
|
||||||
|
'relative-json-pointer',
|
||||||
|
'regex'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const dataValidator: Ajv = addFormats(new Ajv({}), formats)
|
||||||
|
|
||||||
|
export const queryValidator: Ajv = addFormats(
|
||||||
|
new Ajv({
|
||||||
|
coerceTypes: true
|
||||||
|
}),
|
||||||
|
formats
|
||||||
|
)
|
40
test/app.test.ts
Normal file
40
test/app.test.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/app.test.html
|
||||||
|
import assert from 'assert'
|
||||||
|
import axios from 'axios'
|
||||||
|
import type { Server } from 'http'
|
||||||
|
import { app } from '../src/app'
|
||||||
|
|
||||||
|
const port = app.get('port')
|
||||||
|
const appUrl = `http://${app.get('host')}:${port}`
|
||||||
|
|
||||||
|
describe('Feathers application tests', () => {
|
||||||
|
let server: Server
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
server = await app.listen(port)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await app.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('starts and shows the index page', async () => {
|
||||||
|
const { data } = await axios.get<string>(appUrl)
|
||||||
|
|
||||||
|
assert.ok(data.indexOf('<html lang="en">') !== -1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a 404 JSON error', async () => {
|
||||||
|
try {
|
||||||
|
await axios.get(`${appUrl}/path/to/nowhere`, {
|
||||||
|
responseType: 'json'
|
||||||
|
})
|
||||||
|
assert.fail('should never get here')
|
||||||
|
} catch (error: any) {
|
||||||
|
const { response } = error
|
||||||
|
assert.strictEqual(response?.status, 404)
|
||||||
|
assert.strictEqual(response?.data?.code, 404)
|
||||||
|
assert.strictEqual(response?.data?.name, 'NotFound')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
11
test/services/aliases/aliases.test.ts
Normal file
11
test/services/aliases/aliases.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html
|
||||||
|
import assert from 'assert'
|
||||||
|
import { app } from '../../../src/app'
|
||||||
|
|
||||||
|
describe('aliases service', () => {
|
||||||
|
it('registered the service', () => {
|
||||||
|
const service = app.service('aliases')
|
||||||
|
|
||||||
|
assert.ok(service, 'Registered the service')
|
||||||
|
})
|
||||||
|
})
|
11
test/services/auth-oidc/auth-oidc.test.ts
Normal file
11
test/services/auth-oidc/auth-oidc.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html
|
||||||
|
import assert from 'assert'
|
||||||
|
import { app } from '../../../src/app'
|
||||||
|
|
||||||
|
describe('auth-oidc service', () => {
|
||||||
|
it('registered the service', () => {
|
||||||
|
const service = app.service('auth-oidc')
|
||||||
|
|
||||||
|
assert.ok(service, 'Registered the service')
|
||||||
|
})
|
||||||
|
})
|
11
test/services/auth-oidc/callback/callback.test.ts
Normal file
11
test/services/auth-oidc/callback/callback.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html
|
||||||
|
import assert from 'assert'
|
||||||
|
import { app } from '../../../../src/app'
|
||||||
|
|
||||||
|
describe('auth-oidc/callback service', () => {
|
||||||
|
it('registered the service', () => {
|
||||||
|
const service = app.service('auth-oidc/callback')
|
||||||
|
|
||||||
|
assert.ok(service, 'Registered the service')
|
||||||
|
})
|
||||||
|
})
|
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"ts-node": {
|
||||||
|
"files": true
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
],
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
29
validators.ts
Normal file
29
validators.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html
|
||||||
|
import { Ajv, addFormats } from '@feathersjs/schema'
|
||||||
|
import type { FormatsPluginOptions } from '@feathersjs/schema'
|
||||||
|
|
||||||
|
const formats: FormatsPluginOptions = [
|
||||||
|
'date-time',
|
||||||
|
'time',
|
||||||
|
'date',
|
||||||
|
'email',
|
||||||
|
'hostname',
|
||||||
|
'ipv4',
|
||||||
|
'ipv6',
|
||||||
|
'uri',
|
||||||
|
'uri-reference',
|
||||||
|
'uuid',
|
||||||
|
'uri-template',
|
||||||
|
'json-pointer',
|
||||||
|
'relative-json-pointer',
|
||||||
|
'regex'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const dataValidator: Ajv = addFormats(new Ajv({}), formats)
|
||||||
|
|
||||||
|
export const queryValidator: Ajv = addFormats(
|
||||||
|
new Ajv({
|
||||||
|
coerceTypes: true
|
||||||
|
}),
|
||||||
|
formats
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user