first commit
This commit is contained in:
93
src/app.ts
Normal file
93
src/app.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { feathers } from '@feathersjs/feathers'
|
||||
import express, {
|
||||
rest,
|
||||
json,
|
||||
urlencoded,
|
||||
cors,
|
||||
serveStatic,
|
||||
notFound,
|
||||
errorHandler
|
||||
} from '@feathersjs/express'
|
||||
import configuration from '@feathersjs/configuration'
|
||||
import socketio from '@feathersjs/socketio'
|
||||
import session from 'express-session';
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
import type { Application } from './declarations'
|
||||
|
||||
import { logger } from './logger'
|
||||
import { logError } from './hooks/log-error'
|
||||
import { services } from './services/index'
|
||||
import { channels } from './channels'
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const app: Application = express(feathers())
|
||||
|
||||
// Load app configuration
|
||||
app.configure(configuration())
|
||||
app.use(cors())
|
||||
app.use(json({
|
||||
limit: '20mb'
|
||||
}))
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(session({
|
||||
secret: randomUUID(),
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: { secure: false }
|
||||
}));
|
||||
|
||||
// Propagate session to request.params in feathers services
|
||||
app.use(function (req, _res, next) {
|
||||
req.feathers = {
|
||||
...req.feathers,
|
||||
session: req.session
|
||||
}
|
||||
next()
|
||||
});
|
||||
|
||||
app.use('/authme', (req, res) => {
|
||||
//@ts-ignore
|
||||
req.session.user = {
|
||||
email: "her@va.mm"
|
||||
}
|
||||
res.send("done locally")
|
||||
})
|
||||
|
||||
app.use(urlencoded({ extended: true }))
|
||||
// Host the public folder
|
||||
app.use('/', serveStatic(app.get('public')))
|
||||
|
||||
// Configure services and real-time functionality
|
||||
app.configure(rest())
|
||||
app.configure(
|
||||
socketio({
|
||||
cors: {
|
||||
origin: app.get('origins')
|
||||
}
|
||||
})
|
||||
)
|
||||
app.configure(services)
|
||||
app.configure(channels)
|
||||
|
||||
// Configure a middleware for 404s and the error handler
|
||||
app.use(notFound())
|
||||
app.use(errorHandler({ logger }))
|
||||
|
||||
// Register hooks that run on all service methods
|
||||
app.hooks({
|
||||
around: {
|
||||
all: [logError]
|
||||
},
|
||||
before: {},
|
||||
after: {},
|
||||
error: {}
|
||||
})
|
||||
// Register application setup and teardown hooks here
|
||||
app.hooks({
|
||||
setup: [],
|
||||
teardown: []
|
||||
})
|
||||
|
||||
export { app }
|
38
src/channels.ts
Normal file
38
src/channels.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// For more information about this file see https://dove.feathersjs.com/guides/cli/channels.html
|
||||
import type { RealTimeConnection, Params } from '@feathersjs/feathers'
|
||||
import type { AuthenticationResult } from '@feathersjs/authentication'
|
||||
import '@feathersjs/transport-commons'
|
||||
import type { Application, HookContext } from './declarations'
|
||||
import { logger } from './logger'
|
||||
|
||||
export const channels = (app: Application) => {
|
||||
logger.warn(
|
||||
'Publishing all events to all authenticated users. See `channels.ts` and https://dove.feathersjs.com/api/channels.html for more information.'
|
||||
)
|
||||
|
||||
app.on('connection', (connection: RealTimeConnection) => {
|
||||
// On a new real-time connection, add it to the anonymous channel
|
||||
app.channel('anonymous').join(connection)
|
||||
})
|
||||
|
||||
app.on('login', (authResult: AuthenticationResult, { connection }: Params) => {
|
||||
// connection can be undefined if there is no
|
||||
// real-time connection, e.g. when logging in via REST
|
||||
if (connection) {
|
||||
// The connection is no longer anonymous, remove it
|
||||
app.channel('anonymous').leave(connection)
|
||||
|
||||
// Add it to the authenticated user channel
|
||||
app.channel('authenticated').join(connection)
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
app.publish((data: any, context: HookContext) => {
|
||||
// Here you can add event publishers to channels set up in `channels.js`
|
||||
// To publish only for a specific event use `app.publish(eventname, () => {})`
|
||||
|
||||
// e.g. to publish all service events to all authenticated users use
|
||||
return app.channel('authenticated')
|
||||
})
|
||||
}
|
12
src/clients/wildduck.client.ts
Normal file
12
src/clients/wildduck.client.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import axios from 'axios';
|
||||
import config from 'config';
|
||||
|
||||
const wildDuckClient = axios.create({
|
||||
baseURL: config.get('wildDuck.url'),
|
||||
headers: {
|
||||
'X-Access-Token': config.get('wildDuck.token'),
|
||||
},
|
||||
responseType: 'json',
|
||||
});
|
||||
|
||||
export default wildDuckClient;
|
20
src/declarations.ts
Normal file
20
src/declarations.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// For more information about this file see https://dove.feathersjs.com/guides/cli/typescript.html
|
||||
import { HookContext as FeathersHookContext, NextFunction } from '@feathersjs/feathers'
|
||||
import { Application as FeathersApplication } from '@feathersjs/express'
|
||||
type ApplicationConfiguration = any
|
||||
|
||||
export { NextFunction }
|
||||
|
||||
// The types for app.get(name) and app.set(name)
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Configuration extends ApplicationConfiguration {}
|
||||
|
||||
// A mapping of service names to types. Will be extended in service files.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ServiceTypes {}
|
||||
|
||||
// The application instance type that will be used everywhere else
|
||||
export type Application = FeathersApplication<ServiceTypes, Configuration>
|
||||
|
||||
// The context for hook functions - can be typed with a service class
|
||||
export type HookContext<S = any> = FeathersHookContext<Application, S>
|
17
src/hooks/log-error.ts
Normal file
17
src/hooks/log-error.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { HookContext, NextFunction } from '../declarations'
|
||||
import { logger } from '../logger'
|
||||
|
||||
export const logError = async (context: HookContext, next: NextFunction) => {
|
||||
try {
|
||||
await next()
|
||||
} catch (error: any) {
|
||||
logger.error(error.stack)
|
||||
|
||||
// Log validation errors
|
||||
if (error.data) {
|
||||
logger.error('Data: %O', error.data)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
9
src/hooks/validate-auth.ts
Normal file
9
src/hooks/validate-auth.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NotAuthenticated } from '@feathersjs/errors'
|
||||
import type { HookContext, NextFunction } from '../declarations'
|
||||
|
||||
// Check if user is stored in session
|
||||
export const validateAuth = async (context: HookContext) => {
|
||||
if (!context.params.session?.user) {
|
||||
throw new NotAuthenticated('Not authenticated')
|
||||
}
|
||||
}
|
11
src/index.ts
Normal file
11
src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { app } from './app'
|
||||
import { logger } from './logger'
|
||||
|
||||
const port = app.get('port')
|
||||
const host = app.get('host')
|
||||
|
||||
process.on('unhandledRejection', (reason) => logger.error('Unhandled Rejection %O', reason))
|
||||
|
||||
app.listen(port).then(() => {
|
||||
logger.info(`Feathers app listening on http://${host}:${port}`)
|
||||
})
|
10
src/logger.ts
Normal file
10
src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html
|
||||
import { createLogger, format, transports } from 'winston'
|
||||
|
||||
// Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston
|
||||
export const logger = createLogger({
|
||||
// To see more detailed errors, change this to 'debug'
|
||||
level: 'info',
|
||||
format: format.combine(format.splat(), format.simple()),
|
||||
transports: [new transports.Console()]
|
||||
})
|
92
src/services/aliases/aliases.class.ts
Normal file
92
src/services/aliases/aliases.class.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||
|
||||
import type { Application } from '../../declarations'
|
||||
import wildDuckClient from '../../clients/wildduck.client'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { BadRequest } from '@feathersjs/errors'
|
||||
import config from 'config'
|
||||
|
||||
interface Alias {
|
||||
success: boolean,
|
||||
id: string,
|
||||
address: string,
|
||||
main: boolean,
|
||||
user: string,
|
||||
tags: string[],
|
||||
created: string,
|
||||
}
|
||||
|
||||
interface GetAddressInfoResponse {
|
||||
success: boolean,
|
||||
results: Alias[]
|
||||
}
|
||||
|
||||
interface CreateAddressResponse {
|
||||
success: boolean,
|
||||
id: string,
|
||||
}
|
||||
|
||||
type AliasesData = any
|
||||
type AliasesPatch = any
|
||||
type AliasesQuery = any
|
||||
|
||||
export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery }
|
||||
|
||||
export interface AliasesServiceOptions {
|
||||
app: Application
|
||||
}
|
||||
|
||||
export interface AliasesParams extends Params<AliasesQuery> {
|
||||
session?: any
|
||||
}
|
||||
|
||||
export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
|
||||
implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch>
|
||||
{
|
||||
constructor(public options: AliasesServiceOptions) { }
|
||||
|
||||
async find(params: ServiceParams): Promise<Alias[]> {
|
||||
const userId = await this.getUserIdByEmailAddress(params)
|
||||
const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`)
|
||||
|
||||
return userAddressesResponse.results
|
||||
}
|
||||
|
||||
async create(data: AliasesData, params: ServiceParams): Promise<Alias>
|
||||
async create(data: AliasesData, params: ServiceParams): Promise<Alias | Alias[]> {
|
||||
const userId = await this.getUserIdByEmailAddress(params)
|
||||
const aliasFirstPart = faker.animal.crocodilia()
|
||||
.replace(/\D/, '')
|
||||
.replace(/\s/, '')
|
||||
.slice(10);
|
||||
|
||||
const aliasSecondPart = faker.git.commitSha({ length: 5 });
|
||||
const alias = `${aliasFirstPart}-${aliasSecondPart}@${config.get('wildDuck.domain')}`;
|
||||
// const alias = `${faker.animal.crocodilia().replace(/\s/, '').slice(10)}-${faker.git.commitSha({ length: 5 })}`;
|
||||
|
||||
const createResult = await wildDuckClient.post<CreateAddressResponse>(`/users/${userId}/addresses`, {
|
||||
address: alias
|
||||
})
|
||||
|
||||
if (!createResult.data.success) {
|
||||
throw new BadRequest('Failed to create alias')
|
||||
}
|
||||
|
||||
const { data: userAddressesResponse } = await wildDuckClient.get<GetAddressInfoResponse>(`/users/${userId}/addresses`)
|
||||
|
||||
return userAddressesResponse.results
|
||||
|
||||
}
|
||||
|
||||
private async getUserIdByEmailAddress(params: ServiceParams): Promise<string> {
|
||||
const emails = params.session?.user?.emails;
|
||||
|
||||
const addressInfoResponse = await Promise.any(emails.map((email: string) => wildDuckClient.get<Alias>(`addresses/resolve/${email}`)))
|
||||
|
||||
return addressInfoResponse.data.user
|
||||
}
|
||||
}
|
||||
|
||||
export const getOptions = (app: Application) => {
|
||||
return { app }
|
||||
}
|
41
src/services/aliases/aliases.ts
Normal file
41
src/services/aliases/aliases.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Application } from '../../declarations'
|
||||
import { validateAuth } from '../../hooks/validate-auth'
|
||||
import { AliasesService, getOptions } from './aliases.class'
|
||||
|
||||
export const aliasesPath = 'aliases'
|
||||
export const aliasesMethods = ['find', 'create'] as const
|
||||
|
||||
export * from './aliases.class'
|
||||
|
||||
export const aliases = (app: Application) => {
|
||||
app.use(aliasesPath, new AliasesService(getOptions(app)), {
|
||||
methods: aliasesMethods,
|
||||
events: []
|
||||
})
|
||||
|
||||
app.service(aliasesPath).hooks({
|
||||
around: {
|
||||
all: []
|
||||
},
|
||||
before: {
|
||||
all: [
|
||||
validateAuth
|
||||
],
|
||||
find: [],
|
||||
create: [],
|
||||
},
|
||||
after: {
|
||||
all: []
|
||||
},
|
||||
error: {
|
||||
all: []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Add this service to the service type index
|
||||
declare module '../../declarations' {
|
||||
interface ServiceTypes {
|
||||
[aliasesPath]: AliasesService
|
||||
}
|
||||
}
|
53
src/services/auth-oidc/auth-oidc.class.ts
Normal file
53
src/services/auth-oidc/auth-oidc.class.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||
|
||||
import type { Application } from '../../declarations'
|
||||
|
||||
import { Issuer, generators } from 'openid-client'
|
||||
import config from 'config';
|
||||
|
||||
|
||||
type AuthOidcResponse = string
|
||||
type AuthOidcQuery = any
|
||||
|
||||
export type { AuthOidcResponse as AuthOidc, AuthOidcQuery }
|
||||
|
||||
export interface AuthOidcServiceOptions {
|
||||
app: Application
|
||||
}
|
||||
|
||||
export interface AuthOidcParams extends Params<AuthOidcQuery> {
|
||||
session?: any
|
||||
}
|
||||
|
||||
export class AuthOidcService<ServiceParams extends AuthOidcParams = AuthOidcParams>
|
||||
implements ServiceInterface<AuthOidcResponse, ServiceParams>
|
||||
{
|
||||
constructor(public options: AuthOidcServiceOptions) { }
|
||||
|
||||
async find(params: ServiceParams): Promise<AuthOidcResponse> {
|
||||
const issuer = await Issuer.discover(config.get('oidc.gatewayUri'));
|
||||
const client = new issuer.Client({
|
||||
client_id: config.get('oidc.clientId'),
|
||||
client_secret: config.get('oidc.clientSecret'),
|
||||
redirect_uris: [config.get('oidc.redirectUris')],
|
||||
response_types: ['code'],
|
||||
})
|
||||
const codeVerifier = generators.codeVerifier();
|
||||
const codeChallenge = generators.codeChallenge(codeVerifier);
|
||||
|
||||
const url = client.authorizationUrl({
|
||||
redirect_uri: config.get('clientUrl') + '/auth-oidc/callback',
|
||||
scope: 'openid profile offline_access',
|
||||
response_type: 'code',
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
});
|
||||
|
||||
params.session.codeVerifier = codeVerifier;
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export const getOptions = (app: Application) => {
|
||||
return { app }
|
||||
}
|
41
src/services/auth-oidc/auth-oidc.ts
Normal file
41
src/services/auth-oidc/auth-oidc.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Application } from '../../declarations'
|
||||
import { AuthOidcService, getOptions } from './auth-oidc.class'
|
||||
|
||||
export const authOidcPath = 'auth-oidc'
|
||||
export const authOidcMethods = ['find'] as const
|
||||
|
||||
export * from './auth-oidc.class'
|
||||
|
||||
export const authOidc = (app: Application) => {
|
||||
// TODO: fix this to use the correct type
|
||||
// @ts-ignore
|
||||
app.use(authOidcPath, new AuthOidcService(getOptions(app)), {
|
||||
methods: authOidcMethods,
|
||||
events: []
|
||||
}, (req: any, res: any) => {
|
||||
|
||||
return res.redirect(res.data);
|
||||
})
|
||||
|
||||
app.service(authOidcPath).hooks({
|
||||
around: {
|
||||
all: []
|
||||
},
|
||||
before: {
|
||||
all: [],
|
||||
find: [],
|
||||
},
|
||||
after: {
|
||||
all: []
|
||||
},
|
||||
error: {
|
||||
all: []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
declare module '../../declarations' {
|
||||
interface ServiceTypes {
|
||||
[authOidcPath]: AuthOidcService
|
||||
}
|
||||
}
|
52
src/services/auth-oidc/callback/auth-oidc-callback.class.ts
Normal file
52
src/services/auth-oidc/callback/auth-oidc-callback.class.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Params, ServiceInterface } from '@feathersjs/feathers'
|
||||
import type { Application } from '../../../declarations'
|
||||
import { Issuer } from 'openid-client'
|
||||
|
||||
import config from 'config'
|
||||
|
||||
type AuthOidcCallback = string
|
||||
type AuthOidcCallbackData = any
|
||||
type AuthOidcCallbackPatch = any
|
||||
type AuthOidcCallbackQuery = any
|
||||
|
||||
export type { AuthOidcCallback, AuthOidcCallbackData, AuthOidcCallbackPatch, AuthOidcCallbackQuery }
|
||||
|
||||
export interface AuthOidcCallbackServiceOptions {
|
||||
app: Application
|
||||
}
|
||||
|
||||
export interface AuthOidcCallbackParams extends Params<AuthOidcCallbackQuery> {
|
||||
session?: any
|
||||
query: {
|
||||
iss: string,
|
||||
code: string,
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthOidcCallbackService<ServiceParams extends AuthOidcCallbackParams = AuthOidcCallbackParams>
|
||||
implements ServiceInterface<AuthOidcCallback, AuthOidcCallbackData, ServiceParams, AuthOidcCallbackPatch>
|
||||
{
|
||||
constructor(public options: AuthOidcCallbackServiceOptions) { }
|
||||
|
||||
async find(params: ServiceParams): Promise<AuthOidcCallback> {
|
||||
const issuer = await Issuer.discover(config.get('oidc.gatewayUri'));
|
||||
const client = new issuer.Client({
|
||||
client_id: config.get('oidc.clientId'),
|
||||
client_secret: config.get('oidc.clientSecret'),
|
||||
redirect_uris: [config.get('oidc.redirectUris')],
|
||||
response_types: ['code'],
|
||||
})
|
||||
|
||||
const codeVerifier = params.session.codeVerifier;
|
||||
const tokenSet = await client.callback(config.get('clientUrl') + '/auth-oidc/callback', { code: params.query.code, iss: params.query.iss }, { code_verifier: codeVerifier });
|
||||
const userinfo = await client.userinfo(tokenSet.access_token as string);
|
||||
|
||||
params.session.user = userinfo;
|
||||
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
|
||||
export const getOptions = (app: Application) => {
|
||||
return { app }
|
||||
}
|
42
src/services/auth-oidc/callback/auth-oidc-callback.ts
Normal file
42
src/services/auth-oidc/callback/auth-oidc-callback.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { http } from '@feathersjs/transport-commons'
|
||||
import type { Application } from '../../../declarations'
|
||||
import { AuthOidcCallbackService, getOptions } from './auth-oidc-callback.class'
|
||||
|
||||
export const authOidcCallbackPath = 'auth-oidc/callback'
|
||||
export const authOidcCallbackMethods = ['find'] as const
|
||||
|
||||
export * from './auth-oidc-callback.class'
|
||||
|
||||
export const authOidcCallback = (app: Application) => {
|
||||
// TODO: fix this to use the correct type
|
||||
// @ts-ignore
|
||||
app.use(authOidcCallbackPath, new AuthOidcCallbackService(getOptions(app)), {
|
||||
methods: authOidcCallbackMethods,
|
||||
events: []
|
||||
}, (req: any, res: any) => {
|
||||
|
||||
return res.redirect(res.data);
|
||||
})
|
||||
|
||||
app.service(authOidcCallbackPath).hooks({
|
||||
around: {
|
||||
all: []
|
||||
},
|
||||
before: {
|
||||
all: [],
|
||||
find: [],
|
||||
},
|
||||
after: {
|
||||
all: []
|
||||
},
|
||||
error: {
|
||||
all: []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
declare module '../../../declarations' {
|
||||
interface ServiceTypes {
|
||||
[authOidcCallbackPath]: AuthOidcCallbackService
|
||||
}
|
||||
}
|
10
src/services/index.ts
Normal file
10
src/services/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { authOidcCallback } from './auth-oidc/callback/auth-oidc-callback'
|
||||
import { authOidc } from './auth-oidc/auth-oidc'
|
||||
import { aliases } from './aliases/aliases'
|
||||
import type { Application } from '../declarations'
|
||||
|
||||
export const services = (app: Application) => {
|
||||
app.configure(authOidcCallback)
|
||||
app.configure(authOidc)
|
||||
app.configure(aliases)
|
||||
}
|
29
src/validators.ts
Normal file
29
src/validators.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html
|
||||
import { Ajv, addFormats } from '@feathersjs/schema'
|
||||
import type { FormatsPluginOptions } from '@feathersjs/schema'
|
||||
|
||||
const formats: FormatsPluginOptions = [
|
||||
'date-time',
|
||||
'time',
|
||||
'date',
|
||||
'email',
|
||||
'hostname',
|
||||
'ipv4',
|
||||
'ipv6',
|
||||
'uri',
|
||||
'uri-reference',
|
||||
'uuid',
|
||||
'uri-template',
|
||||
'json-pointer',
|
||||
'relative-json-pointer',
|
||||
'regex'
|
||||
]
|
||||
|
||||
export const dataValidator: Ajv = addFormats(new Ajv({}), formats)
|
||||
|
||||
export const queryValidator: Ajv = addFormats(
|
||||
new Ajv({
|
||||
coerceTypes: true
|
||||
}),
|
||||
formats
|
||||
)
|
Reference in New Issue
Block a user