store express sessions in redis, delete only aliases from preferred domain #3
@ -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
|
|
||||||
|
@ -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')
|
||||||
|
22
src/app.ts
22
src/app.ts
@ -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
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 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) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user