store express sessions in redis, delete only aliases from preferred domain #3
9
.prettierrc.json
Normal file
9
.prettierrc.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"quoteProps": "as-needed",
|
||||
"arrowParens": "avoid"
|
||||
}
|
24
index.ts
24
index.ts
@ -1,33 +1,33 @@
|
||||
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');
|
||||
const server = app.listen(port);
|
||||
|
||||
app.listen(port).then(() => {
|
||||
logger.info(`Walias app listening on http://${host}:${port}`);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
logger.info("Received SIGINT signal. Shutting down gracefully.");
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('Received SIGINT signal. Shutting down gracefully.');
|
||||
|
||||
server.close(() => {
|
||||
logger.info("HTTP server closed.");
|
||||
logger.info('HTTP server closed.');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
logger.info("Received SIGTERM signal. Shutting down gracefully.");
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('Received SIGTERM signal. Shutting down gracefully.');
|
||||
|
||||
server.close(() => {
|
||||
logger.info("HTTP server closed.");
|
||||
logger.info('HTTP server closed.');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
logger.error("Unhandled rejection", reason);
|
||||
process.on('unhandledRejection', reason => {
|
||||
logger.error('Unhandled rejection', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
|
@ -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()],
|
||||
});
|
||||
|
56
src/app.ts
56
src/app.ts
@ -1,27 +1,19 @@
|
||||
import { randomUUID } from "crypto";
|
||||
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 RedisStore from "connect-redis";
|
||||
import { createClient } from "redis";
|
||||
import config from "config";
|
||||
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 { Env, getEnv } from "./helpers/get-env";
|
||||
import { randomUUID } from 'crypto';
|
||||
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 RedisStore from 'connect-redis';
|
||||
import { createClient } from 'redis';
|
||||
import config from 'config';
|
||||
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 { Env, getEnv } from './helpers/get-env';
|
||||
|
||||
const app: Application = express(feathers());
|
||||
|
||||
@ -30,8 +22,8 @@ app.configure(configuration());
|
||||
app.use(cors());
|
||||
app.use(
|
||||
json({
|
||||
limit: "20mb",
|
||||
}),
|
||||
limit: '20mb',
|
||||
})
|
||||
);
|
||||
|
||||
app.use(cookieParser());
|
||||
@ -39,9 +31,9 @@ app.use(cookieParser());
|
||||
const sessionStore =
|
||||
getEnv() === Env.prod
|
||||
? new RedisStore({
|
||||
prefix: "walias:",
|
||||
prefix: 'walias:',
|
||||
client: createClient({
|
||||
url: config.get("redis.url"),
|
||||
url: config.get('redis.url'),
|
||||
}),
|
||||
})
|
||||
: undefined;
|
||||
@ -53,7 +45,7 @@ app.use(
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { secure: false },
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// Propagate session to request.params in feathers services
|
||||
@ -67,16 +59,16 @@ app.use(function (req, _res, next) {
|
||||
|
||||
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(
|
||||
socketio({
|
||||
cors: {
|
||||
origin: app.get("origins"),
|
||||
origin: app.get('origins'),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
app.configure(services);
|
||||
app.configure(channels);
|
||||
|
@ -1,34 +1,31 @@
|
||||
// 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) => {
|
||||
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.channel('anonymous').leave(connection);
|
||||
|
||||
// Add it to the authenticated user channel
|
||||
app.channel("authenticated").join(connection);
|
||||
app.channel('authenticated').join(connection);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
app.publish((data: any, context: HookContext) => {
|
||||
@ -36,6 +33,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"),
|
||||
baseURL: config.get('wildDuck.url'),
|
||||
headers: {
|
||||
"X-Access-Token": config.get("wildDuck.token"),
|
||||
'X-Access-Token': config.get('wildDuck.token'),
|
||||
},
|
||||
responseType: "json",
|
||||
responseType: 'json',
|
||||
});
|
||||
|
||||
export default wildDuckClient;
|
||||
|
@ -1,9 +1,6 @@
|
||||
// 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";
|
||||
import { HookContext as FeathersHookContext, NextFunction } from '@feathersjs/feathers';
|
||||
import { Application as FeathersApplication } from '@feathersjs/express';
|
||||
type ApplicationConfiguration = any;
|
||||
|
||||
export { NextFunction };
|
||||
|
@ -1,14 +1,14 @@
|
||||
export enum Env {
|
||||
dev = "dev",
|
||||
prod = "prod",
|
||||
test = "test",
|
||||
dev = 'dev',
|
||||
prod = 'prod',
|
||||
test = 'test',
|
||||
}
|
||||
|
||||
export const getEnv = (): Env => {
|
||||
const env = process.env.NODE_ENV;
|
||||
if (env === "prod") {
|
||||
if (env === 'prod') {
|
||||
return Env.prod;
|
||||
} else if (env === "test") {
|
||||
} else if (env === 'test') {
|
||||
return Env.test;
|
||||
} else {
|
||||
return Env.dev;
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
@ -9,7 +9,7 @@ export const logError = async (context: HookContext, next: NextFunction) => {
|
||||
|
||||
// Log validation errors
|
||||
if (error.data) {
|
||||
logger.error("Data: %O", error.data);
|
||||
logger.error('Data: %O', error.data);
|
||||
}
|
||||
|
||||
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");
|
||||
throw new NotAuthenticated('Not authenticated');
|
||||
}
|
||||
};
|
||||
|
12
src/index.ts
12
src/index.ts
@ -1,12 +1,10 @@
|
||||
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}`);
|
||||
|
@ -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()],
|
||||
});
|
||||
|
@ -1,14 +1,10 @@
|
||||
import type {
|
||||
NullableId,
|
||||
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, th } 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, th } from '@faker-js/faker';
|
||||
import { BadRequest } from '@feathersjs/errors';
|
||||
import config from 'config';
|
||||
|
||||
interface WildDuckAddress {
|
||||
success: boolean;
|
||||
@ -41,12 +37,7 @@ type AliasesData = any;
|
||||
type AliasesPatch = any;
|
||||
type AliasesQuery = any;
|
||||
|
||||
export type {
|
||||
WildDuckAddress as Aliases,
|
||||
AliasesData,
|
||||
AliasesPatch,
|
||||
AliasesQuery,
|
||||
};
|
||||
export type { WildDuckAddress as Aliases, AliasesData, AliasesPatch, AliasesQuery };
|
||||
|
||||
export interface AliasesServiceOptions {
|
||||
app: Application;
|
||||
@ -57,13 +48,7 @@ export interface AliasesParams extends Params<AliasesQuery> {
|
||||
}
|
||||
|
||||
export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
|
||||
implements
|
||||
ServiceInterface<
|
||||
AliasApiResponse,
|
||||
AliasesData,
|
||||
ServiceParams,
|
||||
AliasesPatch
|
||||
>
|
||||
implements ServiceInterface<AliasApiResponse, AliasesData, ServiceParams, AliasesPatch>
|
||||
{
|
||||
constructor(public options: AliasesServiceOptions) {}
|
||||
|
||||
@ -73,104 +58,75 @@ export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
|
||||
return this.getUserAddresses(userId);
|
||||
}
|
||||
|
||||
async create(
|
||||
data: AliasesData,
|
||||
params: ServiceParams,
|
||||
): Promise<AliasApiResponse>;
|
||||
async create(
|
||||
data: AliasesData,
|
||||
params: ServiceParams,
|
||||
): Promise<AliasApiResponse | AliasApiResponse[]> {
|
||||
async create(data: AliasesData, params: ServiceParams): Promise<AliasApiResponse>;
|
||||
async create(data: AliasesData, params: ServiceParams): Promise<AliasApiResponse | AliasApiResponse[]> {
|
||||
const userId = await this.getUserIdByEmailAddress(params);
|
||||
|
||||
const randomString = faker.git.commitSha({ length: 4 });
|
||||
|
||||
// Replace all non-alphanumeric characters with nothing and spaces with dashes
|
||||
const alias =
|
||||
`${faker.color.human()}-${faker.animal.snake()}-${randomString}`
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-zA-Z0-9-]/g, "")
|
||||
const alias = `${faker.color.human()}-${faker.animal.snake()}-${randomString}`
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-zA-Z0-9-]/g, '')
|
||||
.toLowerCase();
|
||||
|
||||
const emailDomain = config.get("wildDuck.domain");
|
||||
const emailDomain = config.get('wildDuck.domain');
|
||||
|
||||
const createResult =
|
||||
await wildDuckClient.post<CreateWildDuckAddressResponse>(
|
||||
`/users/${userId}/addresses`,
|
||||
{
|
||||
const createResult = await wildDuckClient.post<CreateWildDuckAddressResponse>(`/users/${userId}/addresses`, {
|
||||
address: `${alias}@${emailDomain}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!createResult.data.success) {
|
||||
throw new BadRequest("Failed to create alias");
|
||||
throw new BadRequest('Failed to create alias');
|
||||
}
|
||||
|
||||
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 preferredDomain = config.get("wildDuck.preferredDomain");
|
||||
const preferredDomain = config.get('wildDuck.preferredDomain');
|
||||
|
||||
if (!emails.length || !preferredDomain) {
|
||||
throw new BadRequest("Unable to find user");
|
||||
throw new BadRequest('Unable to find user');
|
||||
}
|
||||
|
||||
const addressInfoResponse = await Promise.any(
|
||||
emails
|
||||
.filter((email: string) =>
|
||||
email.endsWith(config.get("wildDuck.preferredDomain")),
|
||||
)
|
||||
.map((email: string) =>
|
||||
wildDuckClient.get<WildDuckAddress>(`addresses/resolve/${email}`),
|
||||
),
|
||||
.filter((email: string) => email.endsWith(config.get('wildDuck.preferredDomain')))
|
||||
.map((email: string) => wildDuckClient.get<WildDuckAddress>(`addresses/resolve/${email}`))
|
||||
);
|
||||
|
||||
return addressInfoResponse.data.user;
|
||||
}
|
||||
|
||||
private async getUserAddresses(userId: string): Promise<AliasApiResponse[]> {
|
||||
const { data: userAddressesResponse } =
|
||||
await wildDuckClient.get<GetWildDuckAddressInfoResponse>(
|
||||
`/users/${userId}/addresses`,
|
||||
const { data: userAddressesResponse } = await wildDuckClient.get<GetWildDuckAddressInfoResponse>(
|
||||
`/users/${userId}/addresses`
|
||||
);
|
||||
|
||||
return userAddressesResponse.results.map(this.sanitizeAliasResponse);
|
||||
}
|
||||
|
||||
async remove(
|
||||
id: NullableId,
|
||||
params: ServiceParams,
|
||||
): Promise<AliasApiResponse[]> {
|
||||
const { data: addressInfoResponse } =
|
||||
await wildDuckClient.get<WildDuckAddress>(`addresses/resolve/${id}`);
|
||||
const allowedDomain: string = config.get("wildDuck.domain");
|
||||
async remove(id: NullableId, params: ServiceParams): Promise<AliasApiResponse[]> {
|
||||
const { data: addressInfoResponse } = await wildDuckClient.get<WildDuckAddress>(`addresses/resolve/${id}`);
|
||||
const allowedDomain: string = config.get('wildDuck.domain');
|
||||
|
||||
// If address does not match the allowed domain, throw an error
|
||||
if (
|
||||
!allowedDomain ||
|
||||
!addressInfoResponse.address.endsWith(allowedDomain)
|
||||
) {
|
||||
throw new BadRequest("Unable to delete address");
|
||||
if (!allowedDomain || !addressInfoResponse.address.endsWith(allowedDomain)) {
|
||||
throw new BadRequest('Unable to delete address');
|
||||
}
|
||||
const userId = await this.getUserIdByEmailAddress(params);
|
||||
|
||||
await wildDuckClient.delete<WildDuckAddress>(
|
||||
`users/${userId}/addresses/${id}`,
|
||||
);
|
||||
await wildDuckClient.delete<WildDuckAddress>(`users/${userId}/addresses/${id}`);
|
||||
|
||||
return this.getUserAddresses(userId);
|
||||
}
|
||||
|
||||
sanitizeAliasResponse(alias: WildDuckAddress): AliasApiResponse {
|
||||
// Hide the id if the alias is not removable
|
||||
const isRemovable =
|
||||
alias.main ||
|
||||
!alias.address.endsWith(config.get("wildDuck.preferredDomain"));
|
||||
const isRemovable = alias.main || !alias.address.endsWith(config.get('wildDuck.preferredDomain'));
|
||||
|
||||
return {
|
||||
id: isRemovable ? null : alias.id,
|
||||
|
@ -1,11 +1,11 @@
|
||||
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", "remove"] as const;
|
||||
export const aliasesPath = 'aliases';
|
||||
export const aliasesMethods = ['find', 'create', 'remove'] as const;
|
||||
|
||||
export * from "./aliases.class";
|
||||
export * from './aliases.class';
|
||||
|
||||
export const aliases = (app: Application) => {
|
||||
app.use(aliasesPath, new AliasesService(getOptions(app)), {
|
||||
@ -32,7 +32,7 @@ export const aliases = (app: Application) => {
|
||||
};
|
||||
|
||||
// Add this service to the service type index
|
||||
declare module "../../declarations" {
|
||||
declare module '../../declarations' {
|
||||
interface ServiceTypes {
|
||||
[aliasesPath]: AliasesService;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
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;
|
||||
@ -18,29 +18,28 @@ export interface AuthOidcParams extends Params<AuthOidcQuery> {
|
||||
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) {}
|
||||
|
||||
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;
|
||||
|
@ -1,10 +1,10 @@
|
||||
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
|
||||
@ -18,7 +18,7 @@ export const authOidc = (app: Application) => {
|
||||
},
|
||||
(req: any, res: any) => {
|
||||
return res.redirect(res.data);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
app.service(authOidcPath).hooks({
|
||||
@ -38,7 +38,7 @@ export const authOidc = (app: Application) => {
|
||||
});
|
||||
};
|
||||
|
||||
declare module "../../declarations" {
|
||||
declare module '../../declarations' {
|
||||
interface ServiceTypes {
|
||||
[authOidcPath]: AuthOidcService;
|
||||
}
|
||||
|
@ -1,20 +1,15 @@
|
||||
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;
|
||||
|
||||
export type {
|
||||
AuthOidcCallback,
|
||||
AuthOidcCallbackData,
|
||||
AuthOidcCallbackPatch,
|
||||
AuthOidcCallbackQuery,
|
||||
};
|
||||
export type { AuthOidcCallback, AuthOidcCallbackData, AuthOidcCallbackPatch, AuthOidcCallbackQuery };
|
||||
|
||||
export interface AuthOidcCallbackServiceOptions {
|
||||
app: Application;
|
||||
@ -28,38 +23,31 @@ export interface AuthOidcCallbackParams extends Params<AuthOidcCallbackQuery> {
|
||||
};
|
||||
}
|
||||
|
||||
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) {}
|
||||
|
||||
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",
|
||||
config.get('clientUrl') + '/auth-oidc/callback',
|
||||
{ code: params.query.code, iss: params.query.iss },
|
||||
{ code_verifier: codeVerifier },
|
||||
{ code_verifier: codeVerifier }
|
||||
);
|
||||
const userinfo = await client.userinfo(tokenSet.access_token as string);
|
||||
|
||||
params.session.user = userinfo;
|
||||
|
||||
return "/";
|
||||
return '/';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,11 @@
|
||||
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
|
||||
@ -22,7 +19,7 @@ export const authOidcCallback = (app: Application) => {
|
||||
},
|
||||
(req: any, res: any) => {
|
||||
return res.redirect(res.data);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
app.service(authOidcCallbackPath).hooks({
|
||||
@ -42,7 +39,7 @@ export const authOidcCallback = (app: Application) => {
|
||||
});
|
||||
};
|
||||
|
||||
declare module "../../../declarations" {
|
||||
declare module '../../../declarations' {
|
||||
interface ServiceTypes {
|
||||
[authOidcCallbackPath]: AuthOidcCallbackService;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
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);
|
||||
|
@ -1,22 +1,22 @@
|
||||
// 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);
|
||||
@ -25,5 +25,5 @@ export const queryValidator: Ajv = addFormats(
|
||||
new Ajv({
|
||||
coerceTypes: true,
|
||||
}),
|
||||
formats,
|
||||
formats
|
||||
);
|
||||
|
@ -1,13 +1,13 @@
|
||||
// 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", () => {
|
||||
describe('Feathers application tests', () => {
|
||||
let server: Server;
|
||||
|
||||
before(async () => {
|
||||
@ -18,23 +18,23 @@ describe("Feathers application tests", () => {
|
||||
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);
|
||||
|
||||
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",
|
||||
responseType: 'json',
|
||||
});
|
||||
assert.fail("should never get here");
|
||||
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");
|
||||
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,22 +1,22 @@
|
||||
// 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);
|
||||
@ -25,5 +25,5 @@ export const queryValidator: Ajv = addFormats(
|
||||
new Ajv({
|
||||
coerceTypes: true,
|
||||
}),
|
||||
formats,
|
||||
formats
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user