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,9 +31,34 @@ 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)
tdActions.appendChild(deleteButton)
tr.appendChild(tdAddress)
tr.appendChild(tdCreated)
tr.appendChild(tdActions)
tbody.appendChild(tr)
})
table.appendChild(tbody)
dataContainer.innerHTML = ''
dataContainer.appendChild(table)
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}`, { const res = await fetch(`/aliases/${alias.id}`, {
method: 'DELETE' method: 'DELETE'
}) })
@ -44,22 +69,11 @@ function renderAliases(aliases) {
return return
} }
}) })
}
aDelete.classList.add('btn', 'btn-danger') deleteButton.innerText = 'Delete'
aDelete.innerText = 'Delete'
tdActions.appendChild(aDelete)
tr.appendChild(tdAddress) return deleteButton
tr.appendChild(tdCreated)
tr.appendChild(tdActions)
tbody.appendChild(tr)
})
table.appendChild(tbody)
dataContainer.innerHTML = ''
dataContainer.appendChild(table)
renderCreateButton();
} }
function renderCreateButton() { function renderCreateButton() {

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());
app.use(
session({ const sessionStore =
store: new RedisStore({ getEnv() === Env.prod
? new RedisStore({
prefix: "walias:", prefix: "walias:",
client: createClient({ client: createClient({
url: config.get("redis.url"), url: config.get("redis.url"),
}), }),
}), })
: undefined;
app.use(
session({
store: sessionStore,
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,7 +94,8 @@ 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 =
await wildDuckClient.post<CreateWildDuckAddressResponse>(
`/users/${userId}/addresses`, `/users/${userId}/addresses`,
{ {
address: `${alias}@${emailDomain}`, address: `${alias}@${emailDomain}`,
@ -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) => {