add remove method for aliases
This commit is contained in:
		
							
								
								
									
										16
									
								
								index.ts
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								index.ts
									
									
									
									
									
								
							| @@ -1,11 +1,13 @@ | ||||
| import { app } from './app' | ||||
| import { logger } from './logger' | ||||
| import { app } from "./app"; | ||||
| import { logger } from "./logger"; | ||||
|  | ||||
| const port = app.get('port') | ||||
| const host = app.get('host') | ||||
| const port = app.get("port"); | ||||
| const host = app.get("host"); | ||||
|  | ||||
| process.on('unhandledRejection', (reason) => logger.error('Unhandled Rejection %O', reason)) | ||||
| process.on("unhandledRejection", (reason) => | ||||
|   logger.error("Unhandled Rejection %O", reason), | ||||
| ); | ||||
|  | ||||
| app.listen(port).then(() => { | ||||
|   logger.info(`Feathers app listening on http://${host}:${port}`) | ||||
| }) | ||||
|   logger.info(`Feathers app listening on http://${host}:${port}`); | ||||
| }); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| // For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html | ||||
| import { createLogger, format, transports } from 'winston' | ||||
| 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', | ||||
|   level: "info", | ||||
|   format: format.combine(format.splat(), format.simple()), | ||||
|   transports: [new transports.Console()] | ||||
| }) | ||||
|   transports: [new transports.Console()], | ||||
| }); | ||||
|   | ||||
							
								
								
									
										94
									
								
								src/app.ts
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								src/app.ts
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| import { feathers } from '@feathersjs/feathers' | ||||
| import { feathers } from "@feathersjs/feathers"; | ||||
| import express, { | ||||
|   rest, | ||||
|   json, | ||||
| @@ -6,80 +6,84 @@ import express, { | ||||
|   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'; | ||||
|   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 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'; | ||||
| 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()) | ||||
| const app: Application = express(feathers()); | ||||
|  | ||||
| // Load app configuration | ||||
| app.configure(configuration()) | ||||
| app.use(cors()) | ||||
| app.use(json({ | ||||
|   limit: '20mb' | ||||
| })) | ||||
| 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 } | ||||
| })); | ||||
| 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() | ||||
|     session: req.session, | ||||
|   }; | ||||
|   next(); | ||||
| }); | ||||
|  | ||||
| app.use(urlencoded({ extended: true })) | ||||
| app.use(urlencoded({ extended: true })); | ||||
| // Host the public folder | ||||
| app.use('/', serveStatic(app.get('public'))) | ||||
| app.use("/", serveStatic(app.get("public"))); | ||||
|  | ||||
| // Configure services and real-time functionality | ||||
| app.configure(rest()) | ||||
| app.configure(rest()); | ||||
| app.configure( | ||||
|   socketio({ | ||||
|     cors: { | ||||
|       origin: app.get('origins') | ||||
|     } | ||||
|   }) | ||||
| ) | ||||
| app.configure(services) | ||||
| app.configure(channels) | ||||
|       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 })) | ||||
| app.use(notFound()); | ||||
| app.use(errorHandler({ logger })); | ||||
|  | ||||
| // Register hooks that run on all service methods | ||||
| app.hooks({ | ||||
|   around: { | ||||
|     all: [logError] | ||||
|     all: [logError], | ||||
|   }, | ||||
|   before: {}, | ||||
|   after: {}, | ||||
|   error: {} | ||||
| }) | ||||
|   error: {}, | ||||
| }); | ||||
| // Register application setup and teardown hooks here | ||||
| app.hooks({ | ||||
|   setup: [], | ||||
|   teardown: [] | ||||
| }) | ||||
|   teardown: [], | ||||
| }); | ||||
|  | ||||
| export { app } | ||||
| export { app }; | ||||
|   | ||||
| @@ -1,31 +1,34 @@ | ||||
| // 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' | ||||
| 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.' | ||||
|   ) | ||||
|     "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) => { | ||||
|   app.on("connection", (connection: RealTimeConnection) => { | ||||
|     // On a new real-time connection, add it to the anonymous channel | ||||
|     app.channel('anonymous').join(connection) | ||||
|   }) | ||||
|     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) | ||||
|   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) | ||||
|     } | ||||
|   }) | ||||
|         // 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) => { | ||||
| @@ -33,6 +36,6 @@ export const channels = (app: Application) => { | ||||
|     // 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') | ||||
|   }) | ||||
| } | ||||
|     return app.channel("authenticated"); | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import axios from 'axios'; | ||||
| import config from 'config'; | ||||
| 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', | ||||
|   baseURL: config.get("wildDuck.url"), | ||||
|   headers: { | ||||
|     "X-Access-Token": config.get("wildDuck.token"), | ||||
|   }, | ||||
|   responseType: "json", | ||||
| }); | ||||
|  | ||||
| export default wildDuckClient; | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| // 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 | ||||
| import { | ||||
|   HookContext as FeathersHookContext, | ||||
|   NextFunction, | ||||
| } from "@feathersjs/feathers"; | ||||
| import { Application as FeathersApplication } from "@feathersjs/express"; | ||||
| type ApplicationConfiguration = any; | ||||
|  | ||||
| export { NextFunction } | ||||
| export { NextFunction }; | ||||
|  | ||||
| // The types for app.get(name) and app.set(name) | ||||
| // eslint-disable-next-line @typescript-eslint/no-empty-interface | ||||
| @@ -14,7 +17,7 @@ export interface Configuration extends ApplicationConfiguration {} | ||||
| export interface ServiceTypes {} | ||||
|  | ||||
| // The application instance type that will be used everywhere else | ||||
| export type Application = FeathersApplication<ServiceTypes, Configuration> | ||||
| 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> | ||||
| export type HookContext<S = any> = FeathersHookContext<Application, S>; | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import type { HookContext, NextFunction } from '../declarations' | ||||
| import { logger } from '../logger' | ||||
| import type { HookContext, NextFunction } from "../declarations"; | ||||
| import { logger } from "../logger"; | ||||
|  | ||||
| export const logError = async (context: HookContext, next: NextFunction) => { | ||||
|   try { | ||||
|     await next() | ||||
|     await next(); | ||||
|   } catch (error: any) { | ||||
|     logger.error(error.stack) | ||||
|     logger.error(error.stack); | ||||
|  | ||||
|     // Log validation errors | ||||
|     if (error.data) { | ||||
|       logger.error('Data: %O', error.data) | ||||
|       logger.error("Data: %O", error.data); | ||||
|     } | ||||
|  | ||||
|     throw error | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { NotAuthenticated } from '@feathersjs/errors' | ||||
| import type { HookContext, NextFunction } from '../declarations' | ||||
| 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') | ||||
|     } | ||||
| } | ||||
|   if (!context.params.session?.user) { | ||||
|     throw new NotAuthenticated("Not authenticated"); | ||||
|   } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/index.ts
									
									
									
									
									
								
							| @@ -1,11 +1,13 @@ | ||||
| import { app } from './app' | ||||
| import { logger } from './logger' | ||||
| import { app } from "./app"; | ||||
| import { logger } from "./logger"; | ||||
|  | ||||
| const port = app.get('port') | ||||
| const host = app.get('host') | ||||
| const port = app.get("port"); | ||||
| const host = app.get("host"); | ||||
|  | ||||
| process.on('unhandledRejection', (reason) => logger.error('Unhandled Rejection %O', reason)) | ||||
| process.on("unhandledRejection", (reason) => | ||||
|   logger.error("Unhandled Rejection %O", reason), | ||||
| ); | ||||
|  | ||||
| app.listen(port).then(() => { | ||||
|   logger.info(`Feathers app listening on http://${host}:${port}`) | ||||
| }) | ||||
|   logger.info(`Feathers app listening on http://${host}:${port}`); | ||||
| }); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| // For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html | ||||
| import { createLogger, format, transports } from 'winston' | ||||
| 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', | ||||
|   level: "info", | ||||
|   format: format.combine(format.splat(), format.simple()), | ||||
|   transports: [new transports.Console()] | ||||
| }) | ||||
|   transports: [new transports.Console()], | ||||
| }); | ||||
|   | ||||
| @@ -1,92 +1,123 @@ | ||||
| import type { Params, ServiceInterface } from '@feathersjs/feathers' | ||||
| import type { | ||||
|   NullableId, | ||||
|   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' | ||||
| 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, | ||||
|   success: boolean; | ||||
|   id: string; | ||||
|   address: string; | ||||
|   main: boolean; | ||||
|   user: string; | ||||
|   tags: string[]; | ||||
|   created: string; | ||||
| } | ||||
|  | ||||
| interface GetAddressInfoResponse { | ||||
|   success: boolean, | ||||
|   results: Alias[] | ||||
|   success: boolean; | ||||
|   results: Alias[]; | ||||
| } | ||||
|  | ||||
| interface CreateAddressResponse { | ||||
|   success: boolean, | ||||
|   id: string, | ||||
|   success: boolean; | ||||
|   id: string; | ||||
| } | ||||
|  | ||||
| type AliasesData = any | ||||
| type AliasesPatch = any | ||||
| type AliasesQuery = any | ||||
| type AliasesData = any; | ||||
| type AliasesPatch = any; | ||||
| type AliasesQuery = any; | ||||
|  | ||||
| export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery } | ||||
| export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery }; | ||||
|  | ||||
| export interface AliasesServiceOptions { | ||||
|   app: Application | ||||
|   app: Application; | ||||
| } | ||||
|  | ||||
| export interface AliasesParams extends Params<AliasesQuery> { | ||||
|   session?: any | ||||
|   session?: any; | ||||
| } | ||||
|  | ||||
| export class AliasesService<ServiceParams extends AliasesParams = AliasesParams> | ||||
|   implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch> | ||||
| { | ||||
|   constructor(public options: AliasesServiceOptions) { } | ||||
|   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`) | ||||
|     const userId = await this.getUserIdByEmailAddress(params); | ||||
|  | ||||
|     return userAddressesResponse.results | ||||
|     return this.getUserAddresses(userId); | ||||
|   } | ||||
|  | ||||
|   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/g, '') | ||||
|       .replace(/\s/g, '') | ||||
|   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/g, "") | ||||
|       .replace(/\s/g, "") | ||||
|       .slice(0, 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 alias = `${aliasFirstPart}-${aliasSecondPart}@${config.get( | ||||
|       "wildDuck.domain", | ||||
|     )}`; | ||||
|  | ||||
|     const createResult = await wildDuckClient.post<CreateAddressResponse>(`/users/${userId}/addresses`, { | ||||
|       address: alias | ||||
|     }) | ||||
|     const createResult = await wildDuckClient.post<CreateAddressResponse>( | ||||
|       `/users/${userId}/addresses`, | ||||
|       { | ||||
|         address: alias, | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     if (!createResult.data.success) { | ||||
|       throw new BadRequest('Failed to create alias') | ||||
|       throw new BadRequest("Failed to create alias"); | ||||
|     } | ||||
|  | ||||
|     const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`) | ||||
|  | ||||
|     return userAddressesResponse.results | ||||
|  | ||||
|     return this.getUserAddresses(userId); | ||||
|   } | ||||
|  | ||||
|   private async getUserIdByEmailAddress(params: ServiceParams): Promise<string> { | ||||
|   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}`))) | ||||
|     const addressInfoResponse = await Promise.any( | ||||
|       emails.map((email: string) => | ||||
|         wildDuckClient.get<Alias>(`addresses/resolve/${email}`), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     return addressInfoResponse.data.user | ||||
|     return addressInfoResponse.data.user; | ||||
|   } | ||||
|  | ||||
|   private async getUserAddresses(userId: string): Promise<Alias[]> { | ||||
|     const { data: userAddressesResponse } = | ||||
|       await wildDuckClient.get<GetAddressInfoResponse>( | ||||
|         `/users/${userId}/addresses`, | ||||
|       ); | ||||
|  | ||||
|     return userAddressesResponse.results; | ||||
|   } | ||||
|  | ||||
|   async remove(id: NullableId, params: ServiceParams): Promise<Alias[]> { | ||||
|     await wildDuckClient.delete<Alias>(`/addresses/${id}`); | ||||
|  | ||||
|     const userId = await this.getUserIdByEmailAddress(params); | ||||
|  | ||||
|     return this.getUserAddresses(userId); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const getOptions = (app: Application) => { | ||||
|   return { app } | ||||
| } | ||||
|   return { app }; | ||||
| }; | ||||
|   | ||||
| @@ -1,41 +1,39 @@ | ||||
| import type { Application } from '../../declarations' | ||||
| import { validateAuth } from '../../hooks/validate-auth' | ||||
| import { AliasesService, getOptions } from './aliases.class' | ||||
| 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 const aliasesPath = "aliases"; | ||||
| export const aliasesMethods = ["find", "create"] as const; | ||||
|  | ||||
| export * from './aliases.class' | ||||
| export * from "./aliases.class"; | ||||
|  | ||||
| export const aliases = (app: Application) => { | ||||
|   app.use(aliasesPath, new AliasesService(getOptions(app)), { | ||||
|     methods: aliasesMethods, | ||||
|     events: [] | ||||
|   }) | ||||
|     events: [], | ||||
|   }); | ||||
|  | ||||
|   app.service(aliasesPath).hooks({ | ||||
|     around: { | ||||
|       all: [] | ||||
|       all: [], | ||||
|     }, | ||||
|     before: { | ||||
|       all: [ | ||||
|         validateAuth | ||||
|       ], | ||||
|       all: [validateAuth], | ||||
|       find: [], | ||||
|       create: [], | ||||
|     }, | ||||
|     after: { | ||||
|       all: [] | ||||
|       all: [], | ||||
|     }, | ||||
|     error: { | ||||
|       all: [] | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|       all: [], | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| // Add this service to the service type index | ||||
| declare module '../../declarations' { | ||||
| declare module "../../declarations" { | ||||
|   interface ServiceTypes { | ||||
|     [aliasesPath]: AliasesService | ||||
|     [aliasesPath]: AliasesService; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,46 +1,46 @@ | ||||
| import type { Params, ServiceInterface } from '@feathersjs/feathers' | ||||
| import type { Params, ServiceInterface } from "@feathersjs/feathers"; | ||||
|  | ||||
| import type { Application } from '../../declarations' | ||||
| import type { Application } from "../../declarations"; | ||||
|  | ||||
| import { Issuer, generators } from 'openid-client' | ||||
| import config from 'config'; | ||||
| import { Issuer, generators } from "openid-client"; | ||||
| import config from "config"; | ||||
|  | ||||
| type AuthOidcResponse = string; | ||||
| type AuthOidcQuery = any; | ||||
|  | ||||
| type AuthOidcResponse = string | ||||
| type AuthOidcQuery = any | ||||
|  | ||||
| export type { AuthOidcResponse as AuthOidc, AuthOidcQuery } | ||||
| export type { AuthOidcResponse as AuthOidc, AuthOidcQuery }; | ||||
|  | ||||
| export interface AuthOidcServiceOptions { | ||||
|   app: Application | ||||
|   app: Application; | ||||
| } | ||||
|  | ||||
| export interface AuthOidcParams extends Params<AuthOidcQuery> { | ||||
|   session?: any | ||||
|   session?: any; | ||||
| } | ||||
|  | ||||
| export class AuthOidcService<ServiceParams extends AuthOidcParams = AuthOidcParams> | ||||
|   implements ServiceInterface<AuthOidcResponse, ServiceParams> | ||||
| export class AuthOidcService< | ||||
|   ServiceParams extends AuthOidcParams = AuthOidcParams, | ||||
| > implements ServiceInterface<AuthOidcResponse, ServiceParams> | ||||
| { | ||||
|   constructor(public options: AuthOidcServiceOptions) { } | ||||
|   constructor(public options: AuthOidcServiceOptions) {} | ||||
|  | ||||
|   async find(params: ServiceParams): Promise<AuthOidcResponse> { | ||||
|     const issuer = await Issuer.discover(config.get('oidc.gatewayUri')); | ||||
|     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'], | ||||
|     }) | ||||
|       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', | ||||
|       redirect_uri: config.get("clientUrl") + "/auth-oidc/callback", | ||||
|       scope: "openid profile offline_access", | ||||
|       response_type: "code", | ||||
|       code_challenge: codeChallenge, | ||||
|       code_challenge_method: 'S256', | ||||
|       code_challenge_method: "S256", | ||||
|     }); | ||||
|  | ||||
|     params.session.codeVerifier = codeVerifier; | ||||
| @@ -49,5 +49,5 @@ export class AuthOidcService<ServiceParams extends AuthOidcParams = AuthOidcPara | ||||
| } | ||||
|  | ||||
| export const getOptions = (app: Application) => { | ||||
|   return { app } | ||||
| } | ||||
|   return { app }; | ||||
| }; | ||||
|   | ||||
| @@ -1,41 +1,45 @@ | ||||
| import type { Application } from '../../declarations' | ||||
| import { AuthOidcService, getOptions } from './auth-oidc.class' | ||||
| import type { Application } from "../../declarations"; | ||||
| import { AuthOidcService, getOptions } from "./auth-oidc.class"; | ||||
|  | ||||
| export const authOidcPath = 'auth-oidc' | ||||
| export const authOidcMethods = ['find'] as const | ||||
| export const authOidcPath = "auth-oidc"; | ||||
| export const authOidcMethods = ["find"] as const; | ||||
|  | ||||
| export * from './auth-oidc.class' | ||||
| 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.use( | ||||
|     authOidcPath, | ||||
|     new AuthOidcService(getOptions(app)), | ||||
|     { | ||||
|       methods: authOidcMethods, | ||||
|       events: [], | ||||
|     }, | ||||
|     (req: any, res: any) => { | ||||
|       return res.redirect(res.data); | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   app.service(authOidcPath).hooks({ | ||||
|     around: { | ||||
|       all: [] | ||||
|       all: [], | ||||
|     }, | ||||
|     before: { | ||||
|       all: [], | ||||
|       find: [], | ||||
|     }, | ||||
|     after: { | ||||
|       all: [] | ||||
|       all: [], | ||||
|     }, | ||||
|     error: { | ||||
|       all: [] | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|       all: [], | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| declare module '../../declarations' { | ||||
| declare module "../../declarations" { | ||||
|   interface ServiceTypes { | ||||
|     [authOidcPath]: AuthOidcService | ||||
|     [authOidcPath]: AuthOidcService; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,52 +1,68 @@ | ||||
| import type { Params, ServiceInterface } from '@feathersjs/feathers' | ||||
| import type { Application } from '../../../declarations' | ||||
| import { Issuer } from 'openid-client' | ||||
| import type { Params, ServiceInterface } from "@feathersjs/feathers"; | ||||
| import type { Application } from "../../../declarations"; | ||||
| import { Issuer } from "openid-client"; | ||||
|  | ||||
| import config from 'config' | ||||
| import config from "config"; | ||||
|  | ||||
| type AuthOidcCallback = string | ||||
| type AuthOidcCallbackData = any | ||||
| type AuthOidcCallbackPatch = any | ||||
| type AuthOidcCallbackQuery = any | ||||
| type AuthOidcCallback = string; | ||||
| type AuthOidcCallbackData = any; | ||||
| type AuthOidcCallbackPatch = any; | ||||
| type AuthOidcCallbackQuery = any; | ||||
|  | ||||
| export type { AuthOidcCallback, AuthOidcCallbackData, AuthOidcCallbackPatch, AuthOidcCallbackQuery } | ||||
| export type { | ||||
|   AuthOidcCallback, | ||||
|   AuthOidcCallbackData, | ||||
|   AuthOidcCallbackPatch, | ||||
|   AuthOidcCallbackQuery, | ||||
| }; | ||||
|  | ||||
| export interface AuthOidcCallbackServiceOptions { | ||||
|   app: Application | ||||
|   app: Application; | ||||
| } | ||||
|  | ||||
| export interface AuthOidcCallbackParams extends Params<AuthOidcCallbackQuery> { | ||||
|   session?: any | ||||
|   session?: any; | ||||
|   query: { | ||||
|     iss: string, | ||||
|     code: string, | ||||
|   } | ||||
|     iss: string; | ||||
|     code: string; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export class AuthOidcCallbackService<ServiceParams extends AuthOidcCallbackParams = AuthOidcCallbackParams> | ||||
|   implements ServiceInterface<AuthOidcCallback, AuthOidcCallbackData, ServiceParams, AuthOidcCallbackPatch> | ||||
| export class AuthOidcCallbackService< | ||||
|   ServiceParams extends AuthOidcCallbackParams = AuthOidcCallbackParams, | ||||
| > implements | ||||
|     ServiceInterface< | ||||
|       AuthOidcCallback, | ||||
|       AuthOidcCallbackData, | ||||
|       ServiceParams, | ||||
|       AuthOidcCallbackPatch | ||||
|     > | ||||
| { | ||||
|   constructor(public options: AuthOidcCallbackServiceOptions) { } | ||||
|   constructor(public options: AuthOidcCallbackServiceOptions) {} | ||||
|  | ||||
|   async find(params: ServiceParams): Promise<AuthOidcCallback> { | ||||
|     const issuer = await Issuer.discover(config.get('oidc.gatewayUri')); | ||||
|     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'], | ||||
|     }) | ||||
|       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 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 '/' | ||||
|     return "/"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const getOptions = (app: Application) => { | ||||
|   return { app } | ||||
| } | ||||
|   return { app }; | ||||
| }; | ||||
|   | ||||
| @@ -1,42 +1,49 @@ | ||||
| import { http } from '@feathersjs/transport-commons' | ||||
| import type { Application } from '../../../declarations' | ||||
| import { AuthOidcCallbackService, getOptions } from './auth-oidc-callback.class' | ||||
| 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 const authOidcCallbackPath = "auth-oidc/callback"; | ||||
| export const authOidcCallbackMethods = ["find"] as const; | ||||
|  | ||||
| export * from './auth-oidc-callback.class' | ||||
| 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.use( | ||||
|     authOidcCallbackPath, | ||||
|     new AuthOidcCallbackService(getOptions(app)), | ||||
|     { | ||||
|       methods: authOidcCallbackMethods, | ||||
|       events: [], | ||||
|     }, | ||||
|     (req: any, res: any) => { | ||||
|       return res.redirect(res.data); | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   app.service(authOidcCallbackPath).hooks({ | ||||
|     around: { | ||||
|       all: [] | ||||
|       all: [], | ||||
|     }, | ||||
|     before: { | ||||
|       all: [], | ||||
|       find: [], | ||||
|     }, | ||||
|     after: { | ||||
|       all: [] | ||||
|       all: [], | ||||
|     }, | ||||
|     error: { | ||||
|       all: [] | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|       all: [], | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| declare module '../../../declarations' { | ||||
| declare module "../../../declarations" { | ||||
|   interface ServiceTypes { | ||||
|     [authOidcCallbackPath]: AuthOidcCallbackService | ||||
|     [authOidcCallbackPath]: AuthOidcCallbackService; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,10 +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' | ||||
| 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) | ||||
| } | ||||
|   app.configure(authOidcCallback); | ||||
|   app.configure(authOidc); | ||||
|   app.configure(aliases); | ||||
| }; | ||||
|   | ||||
| @@ -1,29 +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' | ||||
| 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' | ||||
| ] | ||||
|   "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 dataValidator: Ajv = addFormats(new Ajv({}), formats); | ||||
|  | ||||
| export const queryValidator: Ajv = addFormats( | ||||
|   new Ajv({ | ||||
|     coerceTypes: true | ||||
|     coerceTypes: true, | ||||
|   }), | ||||
|   formats | ||||
| ) | ||||
|   formats, | ||||
| ); | ||||
|   | ||||
| @@ -1,40 +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' | ||||
| 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}` | ||||
| const port = app.get("port"); | ||||
| const appUrl = `http://${app.get("host")}:${port}`; | ||||
|  | ||||
| describe('Feathers application tests', () => { | ||||
|   let server: Server | ||||
| describe("Feathers application tests", () => { | ||||
|   let server: Server; | ||||
|  | ||||
|   before(async () => { | ||||
|     server = await app.listen(port) | ||||
|   }) | ||||
|     server = await app.listen(port); | ||||
|   }); | ||||
|  | ||||
|   after(async () => { | ||||
|     await app.teardown() | ||||
|   }) | ||||
|     await app.teardown(); | ||||
|   }); | ||||
|  | ||||
|   it('starts and shows the index page', async () => { | ||||
|     const { data } = await axios.get<string>(appUrl) | ||||
|   it("starts and shows the index page", async () => { | ||||
|     const { data } = await axios.get<string>(appUrl); | ||||
|  | ||||
|     assert.ok(data.indexOf('<html lang="en">') !== -1) | ||||
|   }) | ||||
|     assert.ok(data.indexOf('<html lang="en">') !== -1); | ||||
|   }); | ||||
|  | ||||
|   it('shows a 404 JSON error', async () => { | ||||
|   it("shows a 404 JSON error", async () => { | ||||
|     try { | ||||
|       await axios.get(`${appUrl}/path/to/nowhere`, { | ||||
|         responseType: 'json' | ||||
|       }) | ||||
|       assert.fail('should never get here') | ||||
|         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') | ||||
|       const { response } = error; | ||||
|       assert.strictEqual(response?.status, 404); | ||||
|       assert.strictEqual(response?.data?.code, 404); | ||||
|       assert.strictEqual(response?.data?.name, "NotFound"); | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,11 +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' | ||||
| import assert from "assert"; | ||||
| import { app } from "../../../src/app"; | ||||
|  | ||||
| describe('aliases service', () => { | ||||
|   it('registered the service', () => { | ||||
|     const service = app.service('aliases') | ||||
| describe("aliases service", () => { | ||||
|   it("registered the service", () => { | ||||
|     const service = app.service("aliases"); | ||||
|  | ||||
|     assert.ok(service, 'Registered the service') | ||||
|   }) | ||||
| }) | ||||
|     assert.ok(service, "Registered the service"); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,11 +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' | ||||
| import assert from "assert"; | ||||
| import { app } from "../../../src/app"; | ||||
|  | ||||
| describe('auth-oidc service', () => { | ||||
|   it('registered the service', () => { | ||||
|     const service = app.service('auth-oidc') | ||||
| describe("auth-oidc service", () => { | ||||
|   it("registered the service", () => { | ||||
|     const service = app.service("auth-oidc"); | ||||
|  | ||||
|     assert.ok(service, 'Registered the service') | ||||
|   }) | ||||
| }) | ||||
|     assert.ok(service, "Registered the service"); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,11 +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' | ||||
| 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') | ||||
| describe("auth-oidc/callback service", () => { | ||||
|   it("registered the service", () => { | ||||
|     const service = app.service("auth-oidc/callback"); | ||||
|  | ||||
|     assert.ok(service, 'Registered the service') | ||||
|   }) | ||||
| }) | ||||
|     assert.ok(service, "Registered the service"); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,29 +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' | ||||
| 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' | ||||
| ] | ||||
|   "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 dataValidator: Ajv = addFormats(new Ajv({}), formats); | ||||
|  | ||||
| export const queryValidator: Ajv = addFormats( | ||||
|   new Ajv({ | ||||
|     coerceTypes: true | ||||
|     coerceTypes: true, | ||||
|   }), | ||||
|   formats | ||||
| ) | ||||
|   formats, | ||||
| ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user