store express sessions in redis, delete only aliases from preferred domain #3

Merged
lauri merged 5 commits from dev into master 2023-08-12 08:14:49 +00:00
5 changed files with 130 additions and 58 deletions
Showing only changes of commit cc453e2337 - Show all commits

View File

@ -103,13 +103,3 @@ spec:
envFrom: envFrom:
- secretRef: - secretRef:
name: oidc-client-walias-owner-secrets name: oidc-client-walias-owner-secrets
---
apiVersion: codemowers.cloud/v1beta1
kind: RedisClaim
metadata:
name: walias-cache
namespace: msergo-bwybg
spec:
capacity: 100Mi
class: cache

View File

@ -31,29 +31,17 @@ function renderAliases(aliases) {
const tdCreated = document.createElement('td') const tdCreated = document.createElement('td')
tdCreated.innerText = alias.created tdCreated.innerText = alias.created
const tdActions = document.createElement('td') const tdActions = document.createElement('td')
const aDelete = document.createElement('a')
aDelete.addEventListener('click', async () => { const deleteButton = createDeleteButton(alias)
const res = await fetch(`/aliases/${alias.id}`, {
method: 'DELETE'
})
if (res.ok) { tdActions.appendChild(deleteButton)
const data = await res.json()
renderAliases(data)
return
}
})
aDelete.classList.add('btn', 'btn-danger')
aDelete.innerText = 'Delete'
tdActions.appendChild(aDelete)
tr.appendChild(tdAddress) tr.appendChild(tdAddress)
tr.appendChild(tdCreated) tr.appendChild(tdCreated)
tr.appendChild(tdActions) tr.appendChild(tdActions)
tbody.appendChild(tr) tbody.appendChild(tr)
}) })
table.appendChild(tbody) table.appendChild(tbody)
dataContainer.innerHTML = '' dataContainer.innerHTML = ''
@ -62,6 +50,32 @@ function renderAliases(aliases) {
renderCreateButton(); 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() { function renderCreateButton() {
const dataContainer = document.getElementById('container') const dataContainer = document.getElementById('container')
const button = document.createElement('a') const button = document.createElement('a')

View File

@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
import { feathers } from "@feathersjs/feathers"; import { feathers } from "@feathersjs/feathers";
import express, { import express, {
rest, rest,
@ -16,12 +17,11 @@ import RedisStore from "connect-redis";
import { createClient } from "redis"; import { createClient } from "redis";
import config from "config"; import config from "config";
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 { Env, getEnv } from "./helpers/get-env";
const app: Application = express(feathers()); const app: Application = express(feathers());
@ -35,14 +35,20 @@ app.use(
); );
app.use(cookieParser()); app.use(cookieParser());
const sessionStore =
getEnv() === Env.prod
? new RedisStore({
prefix: "walias:",
client: createClient({
url: config.get("redis.url"),
}),
})
: undefined;
app.use( app.use(
session({ session({
store: new RedisStore({ store: sessionStore,
prefix: "walias:",
client: createClient({
url: config.get("redis.url"),
}),
}),
secret: randomUUID(), secret: randomUUID(),
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,

16
src/helpers/get-env.ts Normal file
View 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;
}
};

View File

@ -6,11 +6,11 @@ import type {
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, th } 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 WildDuckAddress {
success: boolean; success: boolean;
id: string; id: string;
address: string; address: string;
@ -20,12 +20,19 @@ interface Alias {
created: string; created: string;
} }
interface GetAddressInfoResponse { interface GetWildDuckAddressInfoResponse {
success: boolean; success: boolean;
results: Alias[]; results: WildDuckAddress[];
} }
interface CreateAddressResponse { interface AliasApiResponse {
id: string | null;
address: string;
tags: string[];
created: string;
}
interface CreateWildDuckAddressResponse {
success: boolean; success: boolean;
id: string; id: string;
} }
@ -34,7 +41,12 @@ 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 {
WildDuckAddress as Aliases,
AliasesData,
AliasesPatch,
AliasesQuery,
};
export interface AliasesServiceOptions { export interface AliasesServiceOptions {
app: Application; app: Application;
@ -45,21 +57,30 @@ export interface AliasesParams extends Params<AliasesQuery> {
} }
export class AliasesService<ServiceParams extends AliasesParams = AliasesParams> export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
implements ServiceInterface<Alias, AliasesData, ServiceParams, AliasesPatch> implements
ServiceInterface<
AliasApiResponse,
AliasesData,
ServiceParams,
AliasesPatch
>
{ {
constructor(public options: AliasesServiceOptions) {} constructor(public options: AliasesServiceOptions) {}
async find(params: ServiceParams): Promise<Alias[]> { async find(params: ServiceParams): Promise<AliasApiResponse[]> {
const userId = await this.getUserIdByEmailAddress(params); const userId = await this.getUserIdByEmailAddress(params);
return this.getUserAddresses(userId); return this.getUserAddresses(userId);
} }
async create(data: AliasesData, params: ServiceParams): Promise<Alias>;
async create( async create(
data: AliasesData, data: AliasesData,
params: ServiceParams, params: ServiceParams,
): Promise<Alias | Alias[]> { ): Promise<AliasApiResponse>;
async create(
data: AliasesData,
params: ServiceParams,
): Promise<AliasApiResponse | AliasApiResponse[]> {
const userId = await this.getUserIdByEmailAddress(params); const userId = await this.getUserIdByEmailAddress(params);
const randomString = faker.git.commitSha({ length: 4 }); 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 emailDomain = config.get("wildDuck.domain");
const createResult = await wildDuckClient.post<CreateAddressResponse>( const createResult =
`/users/${userId}/addresses`, await wildDuckClient.post<CreateWildDuckAddressResponse>(
{ `/users/${userId}/addresses`,
address: `${alias}@${emailDomain}`, {
}, address: `${alias}@${emailDomain}`,
); },
);
if (!createResult.data.success) { if (!createResult.data.success) {
throw new BadRequest("Failed to create alias"); throw new BadRequest("Failed to create alias");
@ -92,32 +114,40 @@ export class AliasesService<ServiceParams extends AliasesParams = AliasesParams>
): Promise<string> { ): Promise<string> {
const emails = params.session?.user?.emails; 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( const addressInfoResponse = await Promise.any(
emails emails
.filter((email: string) => .filter((email: string) =>
email.endsWith(config.get("wildDuck.preferredDomain")), email.endsWith(config.get("wildDuck.preferredDomain")),
) )
.map((email: string) => .map((email: string) =>
wildDuckClient.get<Alias>(`addresses/resolve/${email}`), wildDuckClient.get<WildDuckAddress>(`addresses/resolve/${email}`),
), ),
); );
return addressInfoResponse.data.user; return addressInfoResponse.data.user;
} }
private async getUserAddresses(userId: string): Promise<Alias[]> { private async getUserAddresses(userId: string): Promise<AliasApiResponse[]> {
const { data: userAddressesResponse } = const { data: userAddressesResponse } =
await wildDuckClient.get<GetAddressInfoResponse>( await wildDuckClient.get<GetWildDuckAddressInfoResponse>(
`/users/${userId}/addresses`, `/users/${userId}/addresses`,
); );
return userAddressesResponse.results; return userAddressesResponse.results.map(this.sanitizeAliasResponse);
} }
async remove(id: NullableId, params: ServiceParams): Promise<Alias[]> { async remove(
const { data: addressInfoResponse } = await wildDuckClient.get<Alias>( id: NullableId,
`addresses/resolve/${id}`, params: ServiceParams,
); ): Promise<AliasApiResponse[]> {
const { data: addressInfoResponse } =
await wildDuckClient.get<WildDuckAddress>(`addresses/resolve/${id}`);
const allowedDomain: string = config.get("wildDuck.domain"); const allowedDomain: string = config.get("wildDuck.domain");
// If address does not match the allowed domain, throw an error // 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); 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); 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) => { export const getOptions = (app: Application) => {