add remove method for aliases

This commit is contained in:
Sergo 2023-07-30 00:50:42 +03:00
parent fb29813345
commit 70d95be227
23 changed files with 446 additions and 376 deletions

View File

@ -1,11 +1,13 @@
import { app } from './app' import { app } from "./app";
import { logger } from './logger' import { logger } from "./logger";
const port = app.get('port') const port = app.get("port");
const host = app.get('host') 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(() => { app.listen(port).then(() => {
logger.info(`Feathers app listening on http://${host}:${port}`) logger.info(`Feathers app listening on http://${host}:${port}`);
}) });

View File

@ -1,10 +1,10 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html // 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 // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston
export const logger = createLogger({ export const logger = createLogger({
// To see more detailed errors, change this to 'debug' // To see more detailed errors, change this to 'debug'
level: 'info', level: "info",
format: format.combine(format.splat(), format.simple()), format: format.combine(format.splat(), format.simple()),
transports: [new transports.Console()] transports: [new transports.Console()],
}) });

View File

@ -1,4 +1,4 @@
import { feathers } from '@feathersjs/feathers' import { feathers } from "@feathersjs/feathers";
import express, { import express, {
rest, rest,
json, json,
@ -6,80 +6,84 @@ import express, {
cors, cors,
serveStatic, serveStatic,
notFound, notFound,
errorHandler errorHandler,
} from '@feathersjs/express' } from "@feathersjs/express";
import configuration from '@feathersjs/configuration' import configuration from "@feathersjs/configuration";
import socketio from '@feathersjs/socketio' import socketio from "@feathersjs/socketio";
import session from 'express-session'; import session from "express-session";
import cookieParser from 'cookie-parser'; import cookieParser from "cookie-parser";
import type { Application } from './declarations' import type { Application } from "./declarations";
import { logger } from './logger' import { logger } from "./logger";
import { logError } from './hooks/log-error' import { logError } from "./hooks/log-error";
import { services } from './services/index' import { services } from "./services/index";
import { channels } from './channels' import { channels } from "./channels";
import { randomUUID } from 'crypto'; import { randomUUID } from "crypto";
const app: Application = express(feathers()) const app: Application = express(feathers());
// Load app configuration // Load app configuration
app.configure(configuration()) app.configure(configuration());
app.use(cors()) app.use(cors());
app.use(json({ app.use(
limit: '20mb' json({
})) limit: "20mb",
}),
);
app.use(cookieParser()); app.use(cookieParser());
app.use(session({ app.use(
session({
secret: randomUUID(), secret: randomUUID(),
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { secure: false } cookie: { secure: false },
})); }),
);
// Propagate session to request.params in feathers services // Propagate session to request.params in feathers services
app.use(function (req, _res, next) { app.use(function (req, _res, next) {
req.feathers = { req.feathers = {
...req.feathers, ...req.feathers,
session: req.session session: req.session,
} };
next() next();
}); });
app.use(urlencoded({ extended: true })) app.use(urlencoded({ extended: true }));
// Host the public folder // Host the public folder
app.use('/', serveStatic(app.get('public'))) app.use("/", serveStatic(app.get("public")));
// Configure services and real-time functionality // Configure services and real-time functionality
app.configure(rest()) app.configure(rest());
app.configure( app.configure(
socketio({ socketio({
cors: { cors: {
origin: app.get('origins') origin: app.get("origins"),
} },
}) }),
) );
app.configure(services) app.configure(services);
app.configure(channels) app.configure(channels);
// Configure a middleware for 404s and the error handler // Configure a middleware for 404s and the error handler
app.use(notFound()) app.use(notFound());
app.use(errorHandler({ logger })) app.use(errorHandler({ logger }));
// Register hooks that run on all service methods // Register hooks that run on all service methods
app.hooks({ app.hooks({
around: { around: {
all: [logError] all: [logError],
}, },
before: {}, before: {},
after: {}, after: {},
error: {} error: {},
}) });
// Register application setup and teardown hooks here // Register application setup and teardown hooks here
app.hooks({ app.hooks({
setup: [], setup: [],
teardown: [] teardown: [],
}) });
export { app } export { app };

View File

@ -1,31 +1,34 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/channels.html // For more information about this file see https://dove.feathersjs.com/guides/cli/channels.html
import type { RealTimeConnection, Params } from '@feathersjs/feathers' import type { RealTimeConnection, Params } from "@feathersjs/feathers";
import type { AuthenticationResult } from '@feathersjs/authentication' import type { AuthenticationResult } from "@feathersjs/authentication";
import '@feathersjs/transport-commons' import "@feathersjs/transport-commons";
import type { Application, HookContext } from './declarations' import type { Application, HookContext } from "./declarations";
import { logger } from './logger' import { logger } from "./logger";
export const channels = (app: Application) => { export const channels = (app: Application) => {
logger.warn( 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 // 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) => { app.on(
"login",
(authResult: AuthenticationResult, { connection }: Params) => {
// connection can be undefined if there is no // connection can be undefined if there is no
// real-time connection, e.g. when logging in via REST // real-time connection, e.g. when logging in via REST
if (connection) { if (connection) {
// The connection is no longer anonymous, remove it // The connection is no longer anonymous, remove it
app.channel('anonymous').leave(connection) app.channel("anonymous").leave(connection);
// Add it to the authenticated user channel // Add it to the authenticated user channel
app.channel('authenticated').join(connection) app.channel("authenticated").join(connection);
} }
}) },
);
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
app.publish((data: any, context: HookContext) => { 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, () => {})` // To publish only for a specific event use `app.publish(eventname, () => {})`
// e.g. to publish all service events to all authenticated users use // e.g. to publish all service events to all authenticated users use
return app.channel('authenticated') return app.channel("authenticated");
}) });
} };

View File

@ -1,12 +1,12 @@
import axios from 'axios'; import axios from "axios";
import config from 'config'; import config from "config";
const wildDuckClient = axios.create({ const wildDuckClient = axios.create({
baseURL: config.get('wildDuck.url'), baseURL: config.get("wildDuck.url"),
headers: { headers: {
'X-Access-Token': config.get('wildDuck.token'), "X-Access-Token": config.get("wildDuck.token"),
}, },
responseType: 'json', responseType: "json",
}); });
export default wildDuckClient; export default wildDuckClient;

View File

@ -1,9 +1,12 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/typescript.html // For more information about this file see https://dove.feathersjs.com/guides/cli/typescript.html
import { HookContext as FeathersHookContext, NextFunction } from '@feathersjs/feathers' import {
import { Application as FeathersApplication } from '@feathersjs/express' HookContext as FeathersHookContext,
type ApplicationConfiguration = any 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) // The types for app.get(name) and app.set(name)
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
@ -14,7 +17,7 @@ export interface Configuration extends ApplicationConfiguration {}
export interface ServiceTypes {} export interface ServiceTypes {}
// The application instance type that will be used everywhere else // 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 // 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>;

View File

@ -1,17 +1,17 @@
import type { HookContext, NextFunction } from '../declarations' import type { HookContext, NextFunction } from "../declarations";
import { logger } from '../logger' import { logger } from "../logger";
export const logError = async (context: HookContext, next: NextFunction) => { export const logError = async (context: HookContext, next: NextFunction) => {
try { try {
await next() await next();
} catch (error: any) { } catch (error: any) {
logger.error(error.stack) logger.error(error.stack);
// Log validation errors // Log validation errors
if (error.data) { if (error.data) {
logger.error('Data: %O', error.data) logger.error("Data: %O", error.data);
} }
throw error throw error;
} }
} };

View File

@ -1,9 +1,9 @@
import { NotAuthenticated } from '@feathersjs/errors' import { NotAuthenticated } from "@feathersjs/errors";
import type { HookContext, NextFunction } from '../declarations' import type { HookContext, NextFunction } from "../declarations";
// Check if user is stored in session // Check if user is stored in session
export const validateAuth = async (context: HookContext) => { export const validateAuth = async (context: HookContext) => {
if (!context.params.session?.user) { if (!context.params.session?.user) {
throw new NotAuthenticated('Not authenticated') throw new NotAuthenticated("Not authenticated");
} }
} };

View File

@ -1,11 +1,13 @@
import { app } from './app' import { app } from "./app";
import { logger } from './logger' import { logger } from "./logger";
const port = app.get('port') const port = app.get("port");
const host = app.get('host') 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(() => { app.listen(port).then(() => {
logger.info(`Feathers app listening on http://${host}:${port}`) logger.info(`Feathers app listening on http://${host}:${port}`);
}) });

View File

@ -1,10 +1,10 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html // 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 // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston
export const logger = createLogger({ export const logger = createLogger({
// To see more detailed errors, change this to 'debug' // To see more detailed errors, change this to 'debug'
level: 'info', level: "info",
format: format.combine(format.splat(), format.simple()), format: format.combine(format.splat(), format.simple()),
transports: [new transports.Console()] transports: [new transports.Console()],
}) });

View File

@ -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 type { Application } from "../../declarations";
import wildDuckClient from '../../clients/wildduck.client' import wildDuckClient from "../../clients/wildduck.client";
import { faker } from '@faker-js/faker' import { faker } from "@faker-js/faker";
import { BadRequest } from '@feathersjs/errors' import { BadRequest } from "@feathersjs/errors";
import config from 'config' import config from "config";
interface Alias { interface Alias {
success: boolean, success: boolean;
id: string, id: string;
address: string, address: string;
main: boolean, main: boolean;
user: string, user: string;
tags: string[], tags: string[];
created: string, created: string;
} }
interface GetAddressInfoResponse { interface GetAddressInfoResponse {
success: boolean, success: boolean;
results: Alias[] results: Alias[];
} }
interface CreateAddressResponse { interface CreateAddressResponse {
success: boolean, success: boolean;
id: string, id: string;
} }
type AliasesData = any type AliasesData = any;
type AliasesPatch = any type AliasesPatch = any;
type AliasesQuery = any type AliasesQuery = any;
export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery } export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery };
export interface AliasesServiceOptions { export interface AliasesServiceOptions {
app: Application app: Application;
} }
export interface AliasesParams extends Params<AliasesQuery> { export interface AliasesParams extends Params<AliasesQuery> {
session?: any session?: any;
} }
export class AliasesService<ServiceParams extends AliasesParams = AliasesParams> export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch> implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch>
{ {
constructor(public options: AliasesServiceOptions) { } constructor(public options: AliasesServiceOptions) {}
async find(params: ServiceParams): Promise<Alias[]> { async find(params: ServiceParams): Promise<Alias[]> {
const userId = await this.getUserIdByEmailAddress(params) const userId = await this.getUserIdByEmailAddress(params);
const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`)
return userAddressesResponse.results return this.getUserAddresses(userId);
} }
async create(data: AliasesData, params: ServiceParams): Promise<Alias> async create(data: AliasesData, params: ServiceParams): Promise<Alias>;
async create(data: AliasesData, params: ServiceParams): Promise<Alias | Alias[]> { async create(
const userId = await this.getUserIdByEmailAddress(params) data: AliasesData,
const aliasFirstPart = faker.animal.crocodilia() params: ServiceParams,
.replace(/\D/g, '') ): Promise<Alias | Alias[]> {
.replace(/\s/g, '') const userId = await this.getUserIdByEmailAddress(params);
const aliasFirstPart = faker.animal
.crocodilia()
.replace(/\D/g, "")
.replace(/\s/g, "")
.slice(0, 10); .slice(0, 10);
const aliasSecondPart = faker.git.commitSha({ length: 5 }); const aliasSecondPart = faker.git.commitSha({ length: 5 });
const alias = `${aliasFirstPart}-${aliasSecondPart}@${config.get('wildDuck.domain')}`; const alias = `${aliasFirstPart}-${aliasSecondPart}@${config.get(
// const alias = `${faker.animal.crocodilia().replace(/\s/, '').slice(10)}-${faker.git.commitSha({ length: 5 })}`; "wildDuck.domain",
)}`;
const createResult = await wildDuckClient.post<CreateAddressResponse>(`/users/${userId}/addresses`, { const createResult = await wildDuckClient.post<CreateAddressResponse>(
address: alias `/users/${userId}/addresses`,
}) {
address: alias,
},
);
if (!createResult.data.success) { 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 this.getUserAddresses(userId);
return userAddressesResponse.results
} }
private async getUserIdByEmailAddress(params: ServiceParams): Promise<string> { private async getUserIdByEmailAddress(
params: ServiceParams,
): Promise<string> {
const emails = params.session?.user?.emails; 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) => { export const getOptions = (app: Application) => {
return { app } return { app };
} };

View File

@ -1,41 +1,39 @@
import type { Application } from '../../declarations' import type { Application } from "../../declarations";
import { validateAuth } from '../../hooks/validate-auth' import { validateAuth } from "../../hooks/validate-auth";
import { AliasesService, getOptions } from './aliases.class' import { AliasesService, getOptions } from "./aliases.class";
export const aliasesPath = 'aliases' export const aliasesPath = "aliases";
export const aliasesMethods = ['find', 'create'] as const export const aliasesMethods = ["find", "create"] as const;
export * from './aliases.class' export * from "./aliases.class";
export const aliases = (app: Application) => { export const aliases = (app: Application) => {
app.use(aliasesPath, new AliasesService(getOptions(app)), { app.use(aliasesPath, new AliasesService(getOptions(app)), {
methods: aliasesMethods, methods: aliasesMethods,
events: [] events: [],
}) });
app.service(aliasesPath).hooks({ app.service(aliasesPath).hooks({
around: { around: {
all: [] all: [],
}, },
before: { before: {
all: [ all: [validateAuth],
validateAuth
],
find: [], find: [],
create: [], create: [],
}, },
after: { after: {
all: [] all: [],
}, },
error: { error: {
all: [] all: [],
} },
}) });
} };
// Add this service to the service type index // Add this service to the service type index
declare module '../../declarations' { declare module "../../declarations" {
interface ServiceTypes { interface ServiceTypes {
[aliasesPath]: AliasesService [aliasesPath]: AliasesService;
} }
} }

View File

@ -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 { Issuer, generators } from "openid-client";
import config from 'config'; import config from "config";
type AuthOidcResponse = string;
type AuthOidcQuery = any;
type AuthOidcResponse = string export type { AuthOidcResponse as AuthOidc, AuthOidcQuery };
type AuthOidcQuery = any
export type { AuthOidcResponse as AuthOidc, AuthOidcQuery }
export interface AuthOidcServiceOptions { export interface AuthOidcServiceOptions {
app: Application app: Application;
} }
export interface AuthOidcParams extends Params<AuthOidcQuery> { export interface AuthOidcParams extends Params<AuthOidcQuery> {
session?: any session?: any;
} }
export class AuthOidcService<ServiceParams extends AuthOidcParams = AuthOidcParams> export class AuthOidcService<
implements ServiceInterface<AuthOidcResponse, ServiceParams> ServiceParams extends AuthOidcParams = AuthOidcParams,
> implements ServiceInterface<AuthOidcResponse, ServiceParams>
{ {
constructor(public options: AuthOidcServiceOptions) { } constructor(public options: AuthOidcServiceOptions) {}
async find(params: ServiceParams): Promise<AuthOidcResponse> { 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({ const client = new issuer.Client({
client_id: config.get('oidc.clientId'), client_id: config.get("oidc.clientId"),
client_secret: config.get('oidc.clientSecret'), client_secret: config.get("oidc.clientSecret"),
redirect_uris: [config.get('oidc.redirectUris')], redirect_uris: [config.get("oidc.redirectUris")],
response_types: ['code'], response_types: ["code"],
}) });
const codeVerifier = generators.codeVerifier(); const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier); const codeChallenge = generators.codeChallenge(codeVerifier);
const url = client.authorizationUrl({ const url = client.authorizationUrl({
redirect_uri: config.get('clientUrl') + '/auth-oidc/callback', redirect_uri: config.get("clientUrl") + "/auth-oidc/callback",
scope: 'openid profile offline_access', scope: "openid profile offline_access",
response_type: 'code', response_type: "code",
code_challenge: codeChallenge, code_challenge: codeChallenge,
code_challenge_method: 'S256', code_challenge_method: "S256",
}); });
params.session.codeVerifier = codeVerifier; params.session.codeVerifier = codeVerifier;
@ -49,5 +49,5 @@ export class AuthOidcService<ServiceParams extends AuthOidcParams = AuthOidcPara
} }
export const getOptions = (app: Application) => { export const getOptions = (app: Application) => {
return { app } return { app };
} };

View File

@ -1,41 +1,45 @@
import type { Application } from '../../declarations' import type { Application } from "../../declarations";
import { AuthOidcService, getOptions } from './auth-oidc.class' import { AuthOidcService, getOptions } from "./auth-oidc.class";
export const authOidcPath = 'auth-oidc' export const authOidcPath = "auth-oidc";
export const authOidcMethods = ['find'] as const export const authOidcMethods = ["find"] as const;
export * from './auth-oidc.class' export * from "./auth-oidc.class";
export const authOidc = (app: Application) => { export const authOidc = (app: Application) => {
// TODO: fix this to use the correct type // TODO: fix this to use the correct type
// @ts-ignore // @ts-ignore
app.use(authOidcPath, new AuthOidcService(getOptions(app)), { app.use(
authOidcPath,
new AuthOidcService(getOptions(app)),
{
methods: authOidcMethods, methods: authOidcMethods,
events: [] events: [],
}, (req: any, res: any) => { },
(req: any, res: any) => {
return res.redirect(res.data); return res.redirect(res.data);
}) },
);
app.service(authOidcPath).hooks({ app.service(authOidcPath).hooks({
around: { around: {
all: [] all: [],
}, },
before: { before: {
all: [], all: [],
find: [], find: [],
}, },
after: { after: {
all: [] all: [],
}, },
error: { error: {
all: [] all: [],
} },
}) });
} };
declare module '../../declarations' { declare module "../../declarations" {
interface ServiceTypes { interface ServiceTypes {
[authOidcPath]: AuthOidcService [authOidcPath]: AuthOidcService;
} }
} }

View File

@ -1,52 +1,68 @@
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 } from 'openid-client' import { Issuer } from "openid-client";
import config from 'config' import config from "config";
type AuthOidcCallback = string type AuthOidcCallback = string;
type AuthOidcCallbackData = any type AuthOidcCallbackData = any;
type AuthOidcCallbackPatch = any type AuthOidcCallbackPatch = any;
type AuthOidcCallbackQuery = any type AuthOidcCallbackQuery = any;
export type { AuthOidcCallback, AuthOidcCallbackData, AuthOidcCallbackPatch, AuthOidcCallbackQuery } export type {
AuthOidcCallback,
AuthOidcCallbackData,
AuthOidcCallbackPatch,
AuthOidcCallbackQuery,
};
export interface AuthOidcCallbackServiceOptions { export interface AuthOidcCallbackServiceOptions {
app: Application app: Application;
} }
export interface AuthOidcCallbackParams extends Params<AuthOidcCallbackQuery> { export interface AuthOidcCallbackParams extends Params<AuthOidcCallbackQuery> {
session?: any session?: any;
query: { query: {
iss: string, iss: string;
code: string, code: string;
} };
} }
export class AuthOidcCallbackService<ServiceParams extends AuthOidcCallbackParams = AuthOidcCallbackParams> export class AuthOidcCallbackService<
implements ServiceInterface<AuthOidcCallback, AuthOidcCallbackData, ServiceParams, AuthOidcCallbackPatch> ServiceParams extends AuthOidcCallbackParams = AuthOidcCallbackParams,
> implements
ServiceInterface<
AuthOidcCallback,
AuthOidcCallbackData,
ServiceParams,
AuthOidcCallbackPatch
>
{ {
constructor(public options: AuthOidcCallbackServiceOptions) { } constructor(public options: AuthOidcCallbackServiceOptions) {}
async find(params: ServiceParams): Promise<AuthOidcCallback> { 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({ const client = new issuer.Client({
client_id: config.get('oidc.clientId'), client_id: config.get("oidc.clientId"),
client_secret: config.get('oidc.clientSecret'), client_secret: config.get("oidc.clientSecret"),
redirect_uris: [config.get('oidc.redirectUris')], redirect_uris: [config.get("oidc.redirectUris")],
response_types: ['code'], response_types: ["code"],
}) });
const codeVerifier = params.session.codeVerifier; 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); const userinfo = await client.userinfo(tokenSet.access_token as string);
params.session.user = userinfo; params.session.user = userinfo;
return '/' return "/";
} }
} }
export const getOptions = (app: Application) => { export const getOptions = (app: Application) => {
return { app } return { app };
} };

View File

@ -1,42 +1,49 @@
import { http } from '@feathersjs/transport-commons' import { http } from "@feathersjs/transport-commons";
import type { Application } from '../../../declarations' import type { Application } from "../../../declarations";
import { AuthOidcCallbackService, getOptions } from './auth-oidc-callback.class' import {
AuthOidcCallbackService,
getOptions,
} from "./auth-oidc-callback.class";
export const authOidcCallbackPath = 'auth-oidc/callback' export const authOidcCallbackPath = "auth-oidc/callback";
export const authOidcCallbackMethods = ['find'] as const export const authOidcCallbackMethods = ["find"] as const;
export * from './auth-oidc-callback.class' export * from "./auth-oidc-callback.class";
export const authOidcCallback = (app: Application) => { export const authOidcCallback = (app: Application) => {
// TODO: fix this to use the correct type // TODO: fix this to use the correct type
// @ts-ignore // @ts-ignore
app.use(authOidcCallbackPath, new AuthOidcCallbackService(getOptions(app)), { app.use(
authOidcCallbackPath,
new AuthOidcCallbackService(getOptions(app)),
{
methods: authOidcCallbackMethods, methods: authOidcCallbackMethods,
events: [] events: [],
}, (req: any, res: any) => { },
(req: any, res: any) => {
return res.redirect(res.data); return res.redirect(res.data);
}) },
);
app.service(authOidcCallbackPath).hooks({ app.service(authOidcCallbackPath).hooks({
around: { around: {
all: [] all: [],
}, },
before: { before: {
all: [], all: [],
find: [], find: [],
}, },
after: { after: {
all: [] all: [],
}, },
error: { error: {
all: [] all: [],
} },
}) });
} };
declare module '../../../declarations' { declare module "../../../declarations" {
interface ServiceTypes { interface ServiceTypes {
[authOidcCallbackPath]: AuthOidcCallbackService [authOidcCallbackPath]: AuthOidcCallbackService;
} }
} }

View File

@ -1,10 +1,10 @@
import { authOidcCallback } from './auth-oidc/callback/auth-oidc-callback' import { authOidcCallback } from "./auth-oidc/callback/auth-oidc-callback";
import { authOidc } from './auth-oidc/auth-oidc' import { authOidc } from "./auth-oidc/auth-oidc";
import { aliases } from './aliases/aliases' import { aliases } from "./aliases/aliases";
import type { Application } from '../declarations' import type { Application } from "../declarations";
export const services = (app: Application) => { export const services = (app: Application) => {
app.configure(authOidcCallback) app.configure(authOidcCallback);
app.configure(authOidc) app.configure(authOidc);
app.configure(aliases) app.configure(aliases);
} };

View File

@ -1,29 +1,29 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html // For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html
import { Ajv, addFormats } from '@feathersjs/schema' import { Ajv, addFormats } from "@feathersjs/schema";
import type { FormatsPluginOptions } from '@feathersjs/schema' import type { FormatsPluginOptions } from "@feathersjs/schema";
const formats: FormatsPluginOptions = [ const formats: FormatsPluginOptions = [
'date-time', "date-time",
'time', "time",
'date', "date",
'email', "email",
'hostname', "hostname",
'ipv4', "ipv4",
'ipv6', "ipv6",
'uri', "uri",
'uri-reference', "uri-reference",
'uuid', "uuid",
'uri-template', "uri-template",
'json-pointer', "json-pointer",
'relative-json-pointer', "relative-json-pointer",
'regex' "regex",
] ];
export const dataValidator: Ajv = addFormats(new Ajv({}), formats) export const dataValidator: Ajv = addFormats(new Ajv({}), formats);
export const queryValidator: Ajv = addFormats( export const queryValidator: Ajv = addFormats(
new Ajv({ new Ajv({
coerceTypes: true coerceTypes: true,
}), }),
formats formats,
) );

View File

@ -1,40 +1,40 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/app.test.html // For more information about this file see https://dove.feathersjs.com/guides/cli/app.test.html
import assert from 'assert' import assert from "assert";
import axios from 'axios' import axios from "axios";
import type { Server } from 'http' import type { Server } from "http";
import { app } from '../src/app' import { app } from "../src/app";
const port = app.get('port') const port = app.get("port");
const appUrl = `http://${app.get('host')}:${port}` const appUrl = `http://${app.get("host")}:${port}`;
describe('Feathers application tests', () => { describe("Feathers application tests", () => {
let server: Server let server: Server;
before(async () => { before(async () => {
server = await app.listen(port) server = await app.listen(port);
}) });
after(async () => { after(async () => {
await app.teardown() await app.teardown();
}) });
it('starts and shows the index page', async () => { it("starts and shows the index page", async () => {
const { data } = await axios.get<string>(appUrl) 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 { try {
await axios.get(`${appUrl}/path/to/nowhere`, { await axios.get(`${appUrl}/path/to/nowhere`, {
responseType: 'json' responseType: "json",
}) });
assert.fail('should never get here') assert.fail("should never get here");
} catch (error: any) { } catch (error: any) {
const { response } = error const { response } = error;
assert.strictEqual(response?.status, 404) assert.strictEqual(response?.status, 404);
assert.strictEqual(response?.data?.code, 404) assert.strictEqual(response?.data?.code, 404);
assert.strictEqual(response?.data?.name, 'NotFound') assert.strictEqual(response?.data?.name, "NotFound");
} }
}) });
}) });

View File

@ -1,11 +1,11 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html // For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html
import assert from 'assert' import assert from "assert";
import { app } from '../../../src/app' import { app } from "../../../src/app";
describe('aliases service', () => { describe("aliases service", () => {
it('registered the service', () => { it("registered the service", () => {
const service = app.service('aliases') const service = app.service("aliases");
assert.ok(service, 'Registered the service') assert.ok(service, "Registered the service");
}) });
}) });

View File

@ -1,11 +1,11 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html // For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html
import assert from 'assert' import assert from "assert";
import { app } from '../../../src/app' import { app } from "../../../src/app";
describe('auth-oidc service', () => { describe("auth-oidc service", () => {
it('registered the service', () => { it("registered the service", () => {
const service = app.service('auth-oidc') const service = app.service("auth-oidc");
assert.ok(service, 'Registered the service') assert.ok(service, "Registered the service");
}) });
}) });

View File

@ -1,11 +1,11 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html // For more information about this file see https://dove.feathersjs.com/guides/cli/service.test.html
import assert from 'assert' import assert from "assert";
import { app } from '../../../../src/app' import { app } from "../../../../src/app";
describe('auth-oidc/callback service', () => { describe("auth-oidc/callback service", () => {
it('registered the service', () => { it("registered the service", () => {
const service = app.service('auth-oidc/callback') const service = app.service("auth-oidc/callback");
assert.ok(service, 'Registered the service') assert.ok(service, "Registered the service");
}) });
}) });

View File

@ -1,29 +1,29 @@
// For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html // For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html
import { Ajv, addFormats } from '@feathersjs/schema' import { Ajv, addFormats } from "@feathersjs/schema";
import type { FormatsPluginOptions } from '@feathersjs/schema' import type { FormatsPluginOptions } from "@feathersjs/schema";
const formats: FormatsPluginOptions = [ const formats: FormatsPluginOptions = [
'date-time', "date-time",
'time', "time",
'date', "date",
'email', "email",
'hostname', "hostname",
'ipv4', "ipv4",
'ipv6', "ipv6",
'uri', "uri",
'uri-reference', "uri-reference",
'uuid', "uuid",
'uri-template', "uri-template",
'json-pointer', "json-pointer",
'relative-json-pointer', "relative-json-pointer",
'regex' "regex",
] ];
export const dataValidator: Ajv = addFormats(new Ajv({}), formats) export const dataValidator: Ajv = addFormats(new Ajv({}), formats);
export const queryValidator: Ajv = addFormats( export const queryValidator: Ajv = addFormats(
new Ajv({ new Ajv({
coerceTypes: true coerceTypes: true,
}), }),
formats formats,
) );