Skip to content
Closed
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.43",
"@touch4it/ical-timezones": "^1.9.0",
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
Expand Down
11 changes: 11 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ export class InternalServerError extends BaseError<"InternalServerError"> {
}
}

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

export class NotFoundError extends BaseError<"NotFoundError"> {
constructor({ endpointName }: { endpointName: string }) {
super({
Expand Down
50 changes: 50 additions & 0 deletions src/functions/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Sequelize } from "@sequelize/core";
import { PostgresDialect } from "@sequelize/postgres";
import { InternalServerError } from "../errors/index.js";
import { ShortLinkModel } from "../models/linkry.model.js";
import { FastifyInstance } from "fastify";

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(
fastify: FastifyInstance,
): Promise<Sequelize> {
const postgresUrl =
process.env.DATABASE_URL || fastify.secretValue?.postgres_url || "";

const sequelize = new Sequelize({
dialect: PostgresDialect,
url: postgresUrl as string,
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;
}
20 changes: 15 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
import { randomUUID } from "crypto";
import fastify, { FastifyInstance } from "fastify";
import FastifyAuthProvider from "@fastify/auth";
import fastifyAuthPlugin from "./plugins/auth.js";
import fastifyAuthPlugin, { getSecretValue } from "./plugins/auth.js";
import protectedRoute from "./routes/protected.js";
import errorHandlerPlugin from "./plugins/errorHandler.js";
import { RunEnvironment, runEnvironments } from "./roles.js";
import { InternalServerError } from "./errors/index.js";
import eventsPlugin from "./routes/events.js";
import cors from "@fastify/cors";
import fastifyZodValidationPlugin from "./plugins/validate.js";
import { environmentConfig } from "./config.js";
import { environmentConfig, genericConfig } from "./config.js";
import organizationsPlugin from "./routes/organizations.js";
import icalPlugin from "./routes/ics.js";
import vendingPlugin from "./routes/vending.js";
import linkryPlugin from "./routes/linkry.js";
import * as dotenv from "dotenv";
import { getSequelizeInstance } from "./functions/database.js";
dotenv.config();

const now = () => Date.now();
Expand Down Expand Up @@ -47,12 +49,19 @@ async function init() {
}
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
app.environmentConfig = environmentConfig[app.runEnvironment];
app.addHook("onRequest", (req, _, done) => {
app.secretValue = null;
app.sequelizeInstance = null;
app.addHook("onRequest", async (req, _) => {
if (!app.secretValue) {
app.secretValue =
(await getSecretValue(genericConfig.ConfigSecretName)) || {};
}
if (!app.sequelizeInstance) {
app.sequelizeInstance = await getSequelizeInstance(app);
}
req.startTime = now();
req.log.info({ url: req.raw.url }, "received request");
done();
});

app.addHook("onResponse", (req, reply, done) => {
req.log.info(
{
Expand All @@ -71,6 +80,7 @@ async function init() {
api.register(eventsPlugin, { prefix: "/events" });
api.register(organizationsPlugin, { prefix: "/organizations" });
api.register(icalPlugin, { prefix: "/ical" });
api.register(linkryPlugin, { 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>;
}
2 changes: 1 addition & 1 deletion src/plugins/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "../errors/index.js";
import { genericConfig } 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
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