Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Link Shortening into Core API #14

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cloudformation/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,35 @@ Resources:
Path: /{proxy+}
Method: ANY

AppApiLambdaFunctionVpc:
Type: AWS::Serverless::Function
DependsOn:
- AppLogGroups
Properties:
CodeUri: ../dist/src/
AutoPublishAlias: live
Runtime: nodejs20.x
Description: !Sub "${ApplicationFriendlyName} API Lambda - VPC attached"
FunctionName: !Sub ${ApplicationPrefix}-lambda-vpc
Handler: lambda.handler
MemorySize: 512
Role: !GetAtt AppSecurityRoles.Outputs.MainFunctionRoleArn
Timeout: 60
Environment:
Variables:
RunEnvironment: !Ref RunEnvironment
VpcConfig:
Ipv6AllowedForDualStack: True
SecurityGroupIds: !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SecurityGroupIds]
SubnetIds: !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SubnetIds]
Events:
LinkryEvent:
Type: Api
Properties:
RestApiId: !Ref AppApiGateway
Path: /api/v1/linkry/{proxy+}
Method: ANY

EventRecordsTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: "Retain"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@fastify/auth": "^4.6.1",
"@fastify/aws-lambda": "^4.1.0",
"@fastify/cors": "^9.0.1",
"@sequelize/postgres": "^7.0.0-alpha.41",
"@touch4it/ical-timezones": "^1.9.0",
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
Expand Down
22 changes: 20 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ type ArrayOfValueOrArray<T> = Array<ValueOrArray<T>>;
type OriginType = string | boolean | RegExp;
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;

type GroupRoleMapping = Record<string, readonly AppRoles[]>;
type GroupRoleMapping = {
[K in KnownAzureGroupId]?: readonly AppRoles[];
};
type AzureRoleMapping = Record<string, readonly AppRoles[]>;

export type ConfigType = {
Expand All @@ -27,6 +29,18 @@ type EnvironmentConfigType = {
[env in RunEnvironment]: ConfigType;
};

export const GroupNameMapping = {
"48591dbc-cdcb-4544-9f63-e6b92b067e33": "Infra Chairs",
"940e4f9e-6891-4e28-9e29-148798495cdb": "Infra Team",
"f8dfc4cf-456b-4da3-9053-f7fdeda5d5d6": "Infra Leads",
"ff49e948-4587-416b-8224-65147540d5fc": "Officers",
"ad81254b-4eeb-4c96-8191-3acdce9194b1": "ACM Exec",
"0": "Testing Admin",
"1": "Testing Public",
};

export type KnownAzureGroupId = keyof typeof GroupNameMapping;

const genericConfig: GenericConfigType = {
DynamoTableName: "infra-core-api-events",
ConfigSecretName: "infra-core-api-config",
Expand Down Expand Up @@ -58,7 +72,10 @@ const environmentConfig: EnvironmentConfigType = {
GroupRoleMapping: {
"48591dbc-cdcb-4544-9f63-e6b92b067e33": allAppRoles, // Infra Chairs
"ff49e948-4587-416b-8224-65147540d5fc": allAppRoles, // Officers
"ad81254b-4eeb-4c96-8191-3acdce9194b1": [AppRoles.EVENTS_MANAGER], // Exec
"ad81254b-4eeb-4c96-8191-3acdce9194b1": [
AppRoles.EVENTS_MANAGER,
AppRoles.LINKS_MANAGER,
], // Exec
},
AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] },
ValidCorsOrigins: [
Expand All @@ -75,6 +92,7 @@ export type SecretConfig = {
jwt_key?: string;
discord_guild_id: string;
discord_bot_token: string;
postgres_url: string;
};

export { genericConfig, environmentConfig };
15 changes: 13 additions & 2 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class NotFoundError extends BaseError<"NotFoundError"> {
super({
name: "NotFoundError",
id: 103,
message: `${endpointName} is not a valid URL.`,
message: `${endpointName} is not a valid resource.`,
httpStatusCode: 404,
});
}
Expand Down Expand Up @@ -112,11 +112,22 @@ export class DatabaseFetchError extends BaseError<"DatabaseFetchError"> {
}
}

export class DatabaseDeleteError extends BaseError<"DatabaseDeleteError"> {
constructor({ message }: { message: string }) {
super({
name: "DatabaseDeleteError",
id: 107,
message,
httpStatusCode: 500,
});
}
}

export class DiscordEventError extends BaseError<"DiscordEventError"> {
constructor({ message }: { message?: string }) {
super({
name: "DiscordEventError",
id: 107,
id: 108,
message: message || "Could not create Discord event.",
httpStatusCode: 500,
});
Expand Down
56 changes: 56 additions & 0 deletions src/functions/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Sequelize } from "@sequelize/core";
import { PostgresDialect } from "@sequelize/postgres";
import { getSecretValue } from "../plugins/auth.js";
import { genericConfig } from "../config.js";
import { InternalServerError } from "../errors/index.js";
import { ShortLinkModel } from "../models/linkry.model.js";

let logDebug: CallableFunction = console.log;
let logFatal: CallableFunction = console.log;

// Function to set the current logger for each invocation
export function setSequelizeLogger(
debugLogger: CallableFunction,
fatalLogger: CallableFunction,
) {
logDebug = (msg: string) => debugLogger(msg);
logFatal = (msg: string) => fatalLogger(msg);
}

export async function getSequelizeInstance(): Promise<Sequelize> {
let secret = null;
if (!process.env.DATABASE_URL) {
secret = await getSecretValue(genericConfig.ConfigSecretName);
if (!secret) {
throw new InternalServerError({
message: "Invalid secret configuration",
});
}
}

const sequelize = new Sequelize({
dialect: PostgresDialect,
url: process.env.DATABASE_URL || secret?.postgres_url,
ssl: {
rejectUnauthorized: false,
},
models: [ShortLinkModel],
logging: logDebug as (sql: string, timing?: number) => void,
pool: {
max: 2,
min: 0,
idle: 0,
acquire: 3000,
evict: 30, // lambda function timeout in seconds
},
});
try {
await sequelize.sync();
} catch (e: unknown) {
logFatal(`Could not authenticate to DB! ${e}`);
throw new InternalServerError({
message: "Could not establish database connection.",
});
}
return sequelize;
}
3 changes: 1 addition & 2 deletions src/functions/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ export const updateDiscord = async (
isDelete: boolean = false,
logger: FastifyBaseLogger,
): Promise<null | GuildScheduledEventCreateOptions> => {
const secretApiConfig =
(await getSecretValue(genericConfig.ConfigSecretName)) || {};
const secretApiConfig = await getSecretValue(genericConfig.ConfigSecretName);
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
let payload: GuildScheduledEventCreateOptions | null = null;

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import organizationsPlugin from "./routes/organizations.js";
import icalPlugin from "./routes/ics.js";
import vendingPlugin from "./routes/vending.js";
import * as dotenv from "dotenv";
import linkryRoutes from "./routes/linkry.js";
dotenv.config();

const now = () => Date.now();
Expand Down Expand Up @@ -71,6 +72,7 @@ async function init() {
api.register(eventsPlugin, { prefix: "/events" });
api.register(organizationsPlugin, { prefix: "/organizations" });
api.register(icalPlugin, { prefix: "/ical" });
api.register(linkryRoutes, { prefix: "/linkry" });
if (app.runEnvironment === "dev") {
api.register(vendingPlugin, { prefix: "/vending" });
}
Expand Down
45 changes: 45 additions & 0 deletions src/models/linkry.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
InferCreationAttributes,
InferAttributes,
Model,
CreationOptional,
DataTypes,
} from "@sequelize/core";
import {
AllowNull,
Attribute,
CreatedAt,
NotNull,
PrimaryKey,
Table,
UpdatedAt,
} from "@sequelize/core/decorators-legacy";

@Table({ timestamps: true, tableName: "short_links" })
export class ShortLinkModel extends Model<
InferAttributes<ShortLinkModel>,
InferCreationAttributes<ShortLinkModel>
> {
@Attribute(DataTypes.STRING)
@PrimaryKey
declare slug: string;

@Attribute(DataTypes.STRING)
@NotNull
declare full: string;

@Attribute(DataTypes.ARRAY(DataTypes.STRING))
@AllowNull
declare groups?: CreationOptional<string[]>;

@Attribute(DataTypes.STRING)
declare author: string;

@Attribute(DataTypes.DATE)
@CreatedAt
declare createdAt: CreationOptional<Date>;

@Attribute(DataTypes.DATE)
@UpdatedAt
declare updatedAt: CreationOptional<Date>;
}
36 changes: 19 additions & 17 deletions src/plugins/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
UnauthenticatedError,
UnauthorizedError,
} from "../errors/index.js";
import { genericConfig } from "../config.js";
import { genericConfig, KnownAzureGroupId, SecretConfig } from "../config.js";

function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
export function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
const _intersection = new Set<T>();
for (const elem of setB) {
if (setA.has(elem)) {
Expand Down Expand Up @@ -57,20 +57,19 @@

export const getSecretValue = async (
secretId: string,
): Promise<Record<string, string | number | boolean> | null> => {
): Promise<SecretConfig> => {
const data = await smClient.send(

Check failure on line 61 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/auth.test.ts

CredentialsProviderError: Could not load credentials from any providers ❯ node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js:136:13 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:97:33 ❯ coalesceProvider node_modules/@smithy/property-provider/dist-cjs/index.js:124:18 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:142:18 ❯ node_modules/@smithy/core/dist-cjs/index.js:82:17 ❯ node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22 ❯ Module.getSecretValue src/plugins/auth.ts:61:16 ❯ Module.getSequelizeInstance src/functions/database.ts:23:14 ❯ src/routes/linkry.ts:52:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { tryNextLink: false }

Check failure on line 61 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/discordEvent.test.ts

CredentialsProviderError: Could not load credentials from any providers ❯ node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js:136:13 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:97:33 ❯ coalesceProvider node_modules/@smithy/property-provider/dist-cjs/index.js:124:18 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:142:18 ❯ node_modules/@smithy/core/dist-cjs/index.js:82:17 ❯ node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22 ❯ Module.getSecretValue src/plugins/auth.ts:61:16 ❯ Module.getSequelizeInstance src/functions/database.ts:23:14 ❯ src/routes/linkry.ts:52:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { tryNextLink: false }

Check failure on line 61 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/eventPost.test.ts

CredentialsProviderError: Could not load credentials from any providers ❯ node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js:136:13 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:97:33 ❯ coalesceProvider node_modules/@smithy/property-provider/dist-cjs/index.js:124:18 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:142:18 ❯ node_modules/@smithy/core/dist-cjs/index.js:82:17 ❯ node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22 ❯ Module.getSecretValue src/plugins/auth.ts:61:16 ❯ Module.getSequelizeInstance src/functions/database.ts:23:14 ❯ src/routes/linkry.ts:52:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { tryNextLink: false }

Check failure on line 61 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/events.test.ts

CredentialsProviderError: Could not load credentials from any providers ❯ node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js:136:13 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:97:33 ❯ coalesceProvider node_modules/@smithy/property-provider/dist-cjs/index.js:124:18 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:142:18 ❯ node_modules/@smithy/core/dist-cjs/index.js:82:17 ❯ node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22 ❯ Module.getSecretValue src/plugins/auth.ts:61:16 ❯ Module.getSequelizeInstance src/functions/database.ts:23:14 ❯ src/routes/linkry.ts:52:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { tryNextLink: false }

Check failure on line 61 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/health.test.ts

CredentialsProviderError: Could not load credentials from any providers ❯ node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js:136:13 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:97:33 ❯ coalesceProvider node_modules/@smithy/property-provider/dist-cjs/index.js:124:18 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:142:18 ❯ node_modules/@smithy/core/dist-cjs/index.js:82:17 ❯ node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22 ❯ Module.getSecretValue src/plugins/auth.ts:61:16 ❯ Module.getSequelizeInstance src/functions/database.ts:23:14 ❯ src/routes/linkry.ts:52:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { tryNextLink: false }

Check failure on line 61 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/ical.test.ts

CredentialsProviderError: Could not load credentials from any providers ❯ node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js:136:13 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:97:33 ❯ coalesceProvider node_modules/@smithy/property-provider/dist-cjs/index.js:124:18 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:142:18 ❯ node_modules/@smithy/core/dist-cjs/index.js:82:17 ❯ node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22 ❯ Module.getSecretValue src/plugins/auth.ts:61:16 ❯ Module.getSequelizeInstance src/functions/database.ts:23:14 ❯ src/routes/linkry.ts:52:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { tryNextLink: false }

Check failure on line 61 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/vending.test.ts

CredentialsProviderError: Could not load credentials from any providers ❯ node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js:136:13 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:97:33 ❯ coalesceProvider node_modules/@smithy/property-provider/dist-cjs/index.js:124:18 ❯ node_modules/@smithy/property-provider/dist-cjs/index.js:142:18 ❯ node_modules/@smithy/core/dist-cjs/index.js:82:17 ❯ node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:34:22 ❯ Module.getSecretValue src/plugins/auth.ts:61:16 ❯ Module.getSequelizeInstance src/functions/database.ts:23:14 ❯ src/routes/linkry.ts:52:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { tryNextLink: false }
new GetSecretValueCommand({ SecretId: secretId }),
);
if (!data.SecretString) {
return null;
throw new InternalServerError({ message: "config secret is invalid." });
}
try {
return JSON.parse(data.SecretString) as Record<
string,
string | number | boolean
>;
return JSON.parse(data.SecretString) as SecretConfig;
} catch {
return null;
throw new InternalServerError({
message: "config secret cannot be parsed as JSON.",
});
}
};

Expand Down Expand Up @@ -164,10 +163,12 @@
fastify.environmentConfig.GroupRoleMapping
) {
for (const group of verifiedTokenData.groups) {
if (fastify.environmentConfig["GroupRoleMapping"][group]) {
for (const role of fastify.environmentConfig["GroupRoleMapping"][
group
]) {
const roles =
fastify.environmentConfig["GroupRoleMapping"][
group as KnownAzureGroupId
];
if (roles) {
for (const role of roles) {
userRoles.add(role);
}
}
Expand All @@ -178,10 +179,10 @@
fastify.environmentConfig.AzureRoleMapping
) {
for (const group of verifiedTokenData.roles) {
if (fastify.environmentConfig["AzureRoleMapping"][group]) {
for (const role of fastify.environmentConfig[
"AzureRoleMapping"
][group]) {
const roles =
fastify.environmentConfig["AzureRoleMapping"][group];
if (roles) {
for (const role of roles) {
userRoles.add(role);
}
}
Expand Down Expand Up @@ -216,6 +217,7 @@
message: "Invalid token.",
});
}
request.userRoles = userRoles;
return userRoles;
},
);
Expand Down
2 changes: 2 additions & 0 deletions src/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export const runEnvironments = ["dev", "prod"] as const;
export type RunEnvironment = (typeof runEnvironments)[number];
export enum AppRoles {
EVENTS_MANAGER = "manage:events",
LINKS_MANAGER = "manage:links",
LINKS_ADMIN = "admin:links",
}
export const allAppRoles = Object.values(AppRoles).filter(
(value) => typeof value === "string",
Expand Down
Loading
Loading