first commit
This commit is contained in:
		
							
								
								
									
										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 | ||||
| ) | ||||
		Reference in New Issue
	
	Block a user