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:
- 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

View File

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

View File

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