sanitize aliases response, hide delete button for non-prefereable domain aliases
This commit is contained in:
		| @@ -103,13 +103,3 @@ spec: | ||||
|           envFrom: | ||||
|             - secretRef: | ||||
|                 name: oidc-client-walias-owner-secrets | ||||
|  | ||||
| --- | ||||
| apiVersion: codemowers.cloud/v1beta1 | ||||
| kind: RedisClaim | ||||
| metadata: | ||||
|   name: walias-cache | ||||
|   namespace: msergo-bwybg | ||||
| spec: | ||||
|   capacity: 100Mi | ||||
|   class: cache | ||||
|   | ||||
| @@ -31,29 +31,17 @@ function renderAliases(aliases) { | ||||
|             const tdCreated = document.createElement('td') | ||||
|             tdCreated.innerText = alias.created | ||||
|             const tdActions = document.createElement('td') | ||||
|             const aDelete = document.createElement('a') | ||||
|  | ||||
|             aDelete.addEventListener('click', async () => { | ||||
|                 const res = await fetch(`/aliases/${alias.id}`, { | ||||
|                     method: 'DELETE' | ||||
|                 }) | ||||
|             const deleteButton = createDeleteButton(alias) | ||||
|  | ||||
|                 if (res.ok) { | ||||
|                     const data = await res.json() | ||||
|                     renderAliases(data) | ||||
|                     return | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             aDelete.classList.add('btn', 'btn-danger') | ||||
|             aDelete.innerText = 'Delete' | ||||
|             tdActions.appendChild(aDelete) | ||||
|             tdActions.appendChild(deleteButton) | ||||
|  | ||||
|             tr.appendChild(tdAddress) | ||||
|             tr.appendChild(tdCreated) | ||||
|             tr.appendChild(tdActions) | ||||
|             tbody.appendChild(tr) | ||||
|         }) | ||||
|  | ||||
|     table.appendChild(tbody) | ||||
|  | ||||
|     dataContainer.innerHTML = '' | ||||
| @@ -62,6 +50,32 @@ function renderAliases(aliases) { | ||||
|     renderCreateButton(); | ||||
| } | ||||
|  | ||||
| function createDeleteButton(alias) { | ||||
|     const deleteButton = document.createElement('a') | ||||
|     deleteButton.classList.add('btn', 'btn-danger') | ||||
|  | ||||
|     if (!alias.id) { | ||||
|         deleteButton.classList.add('disabled') | ||||
|     } else { | ||||
|  | ||||
|         deleteButton.addEventListener('click', async () => { | ||||
|             const res = await fetch(`/aliases/${alias.id}`, { | ||||
|                 method: 'DELETE' | ||||
|             }) | ||||
|  | ||||
|             if (res.ok) { | ||||
|                 const data = await res.json() | ||||
|                 renderAliases(data) | ||||
|                 return | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     deleteButton.innerText = 'Delete' | ||||
|  | ||||
|     return deleteButton | ||||
| } | ||||
|  | ||||
| function renderCreateButton() { | ||||
|     const dataContainer = document.getElementById('container') | ||||
|     const button = document.createElement('a') | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/app.ts
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/app.ts
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| import { randomUUID } from "crypto"; | ||||
| import { feathers } from "@feathersjs/feathers"; | ||||
| import express, { | ||||
|   rest, | ||||
| @@ -16,12 +17,11 @@ 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 { randomUUID } from "crypto"; | ||||
| import { Env, getEnv } from "./helpers/get-env"; | ||||
|  | ||||
| const app: Application = express(feathers()); | ||||
|  | ||||
| @@ -35,14 +35,20 @@ app.use( | ||||
| ); | ||||
|  | ||||
| app.use(cookieParser()); | ||||
|  | ||||
| const sessionStore = | ||||
|   getEnv() === Env.prod | ||||
|     ? new RedisStore({ | ||||
|         prefix: "walias:", | ||||
|         client: createClient({ | ||||
|           url: config.get("redis.url"), | ||||
|         }), | ||||
|       }) | ||||
|     : undefined; | ||||
|  | ||||
| app.use( | ||||
|   session({ | ||||
|     store: new RedisStore({ | ||||
|       prefix: "walias:", | ||||
|       client: createClient({ | ||||
|         url: config.get("redis.url"), | ||||
|       }), | ||||
|     }), | ||||
|     store: sessionStore, | ||||
|     secret: randomUUID(), | ||||
|     resave: false, | ||||
|     saveUninitialized: false, | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/helpers/get-env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/helpers/get-env.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| export enum Env { | ||||
|   dev = "dev", | ||||
|   prod = "prod", | ||||
|   test = "test", | ||||
| } | ||||
|  | ||||
| export const getEnv = (): Env => { | ||||
|   const env = process.env.NODE_ENV; | ||||
|   if (env === "prod") { | ||||
|     return Env.prod; | ||||
|   } else if (env === "test") { | ||||
|     return Env.test; | ||||
|   } else { | ||||
|     return Env.dev; | ||||
|   } | ||||
| }; | ||||
| @@ -6,11 +6,11 @@ import type { | ||||
|  | ||||
| import type { Application } from "../../declarations"; | ||||
| import wildDuckClient from "../../clients/wildduck.client"; | ||||
| import { faker } from "@faker-js/faker"; | ||||
| import { faker, th } from "@faker-js/faker"; | ||||
| import { BadRequest } from "@feathersjs/errors"; | ||||
| import config from "config"; | ||||
|  | ||||
| interface Alias { | ||||
| interface WildDuckAddress { | ||||
|   success: boolean; | ||||
|   id: string; | ||||
|   address: string; | ||||
| @@ -20,12 +20,19 @@ interface Alias { | ||||
|   created: string; | ||||
| } | ||||
|  | ||||
| interface GetAddressInfoResponse { | ||||
| interface GetWildDuckAddressInfoResponse { | ||||
|   success: boolean; | ||||
|   results: Alias[]; | ||||
|   results: WildDuckAddress[]; | ||||
| } | ||||
|  | ||||
| interface CreateAddressResponse { | ||||
| interface AliasApiResponse { | ||||
|   id: string | null; | ||||
|   address: string; | ||||
|   tags: string[]; | ||||
|   created: string; | ||||
| } | ||||
|  | ||||
| interface CreateWildDuckAddressResponse { | ||||
|   success: boolean; | ||||
|   id: string; | ||||
| } | ||||
| @@ -34,7 +41,12 @@ type AliasesData = any; | ||||
| type AliasesPatch = any; | ||||
| type AliasesQuery = any; | ||||
|  | ||||
| export type { Alias as Aliases, AliasesData, AliasesPatch, AliasesQuery }; | ||||
| export type { | ||||
|   WildDuckAddress as Aliases, | ||||
|   AliasesData, | ||||
|   AliasesPatch, | ||||
|   AliasesQuery, | ||||
| }; | ||||
|  | ||||
| export interface AliasesServiceOptions { | ||||
|   app: Application; | ||||
| @@ -45,21 +57,30 @@ export interface AliasesParams extends Params<AliasesQuery> { | ||||
| } | ||||
|  | ||||
| export class AliasesService<ServiceParams extends AliasesParams = AliasesParams> | ||||
|   implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch> | ||||
|   implements | ||||
|     ServiceInterface< | ||||
|       AliasApiResponse, | ||||
|       AliasesData, | ||||
|       ServiceParams, | ||||
|       AliasesPatch | ||||
|     > | ||||
| { | ||||
|   constructor(public options: AliasesServiceOptions) {} | ||||
|  | ||||
|   async find(params: ServiceParams): Promise<Alias[]> { | ||||
|   async find(params: ServiceParams): Promise<AliasApiResponse[]> { | ||||
|     const userId = await this.getUserIdByEmailAddress(params); | ||||
|  | ||||
|     return this.getUserAddresses(userId); | ||||
|   } | ||||
|  | ||||
|   async create(data: AliasesData, params: ServiceParams): Promise<Alias>; | ||||
|   async create( | ||||
|     data: AliasesData, | ||||
|     params: ServiceParams, | ||||
|   ): Promise<Alias | Alias[]> { | ||||
|   ): Promise<AliasApiResponse>; | ||||
|   async create( | ||||
|     data: AliasesData, | ||||
|     params: ServiceParams, | ||||
|   ): Promise<AliasApiResponse | AliasApiResponse[]> { | ||||
|     const userId = await this.getUserIdByEmailAddress(params); | ||||
|  | ||||
|     const randomString = faker.git.commitSha({ length: 4 }); | ||||
| @@ -73,12 +94,13 @@ export class AliasesService<ServiceParams extends AliasesParams = AliasesParams> | ||||
|  | ||||
|     const emailDomain = config.get("wildDuck.domain"); | ||||
|  | ||||
|     const createResult = await wildDuckClient.post<CreateAddressResponse>( | ||||
|       `/users/${userId}/addresses`, | ||||
|       { | ||||
|         address: `${alias}@${emailDomain}`, | ||||
|       }, | ||||
|     ); | ||||
|     const createResult = | ||||
|       await wildDuckClient.post<CreateWildDuckAddressResponse>( | ||||
|         `/users/${userId}/addresses`, | ||||
|         { | ||||
|           address: `${alias}@${emailDomain}`, | ||||
|         }, | ||||
|       ); | ||||
|  | ||||
|     if (!createResult.data.success) { | ||||
|       throw new BadRequest("Failed to create alias"); | ||||
| @@ -92,32 +114,40 @@ export class AliasesService<ServiceParams extends AliasesParams = AliasesParams> | ||||
|   ): Promise<string> { | ||||
|     const emails = params.session?.user?.emails; | ||||
|  | ||||
|     const preferredDomain = config.get("wildDuck.preferredDomain"); | ||||
|  | ||||
|     if (!emails.length || !preferredDomain) { | ||||
|       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<Alias>(`addresses/resolve/${email}`), | ||||
|           wildDuckClient.get<WildDuckAddress>(`addresses/resolve/${email}`), | ||||
|         ), | ||||
|     ); | ||||
|  | ||||
|     return addressInfoResponse.data.user; | ||||
|   } | ||||
|  | ||||
|   private async getUserAddresses(userId: string): Promise<Alias[]> { | ||||
|   private async getUserAddresses(userId: string): Promise<AliasApiResponse[]> { | ||||
|     const { data: userAddressesResponse } = | ||||
|       await wildDuckClient.get<GetAddressInfoResponse>( | ||||
|       await wildDuckClient.get<GetWildDuckAddressInfoResponse>( | ||||
|         `/users/${userId}/addresses`, | ||||
|       ); | ||||
|  | ||||
|     return userAddressesResponse.results; | ||||
|     return userAddressesResponse.results.map(this.sanitizeAliasResponse); | ||||
|   } | ||||
|  | ||||
|   async remove(id: NullableId, params: ServiceParams): Promise<Alias[]> { | ||||
|     const { data: addressInfoResponse } = await wildDuckClient.get<Alias>( | ||||
|       `addresses/resolve/${id}`, | ||||
|     ); | ||||
|   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 | ||||
| @@ -129,10 +159,26 @@ export class AliasesService<ServiceParams extends AliasesParams = AliasesParams> | ||||
|     } | ||||
|     const userId = await this.getUserIdByEmailAddress(params); | ||||
|  | ||||
|     await wildDuckClient.delete<Alias>(`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")); | ||||
|  | ||||
|     return { | ||||
|       id: isRemovable ? null : alias.id, | ||||
|       address: alias.address, | ||||
|       tags: alias.tags, | ||||
|       created: alias.created, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const getOptions = (app: Application) => { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user