Skip to content

Commit 19e8290

Browse files
committed
setup the scaffolding for linkry routes
1 parent 3ccbccd commit 19e8290

File tree

8 files changed

+167
-3
lines changed

8 files changed

+167
-3
lines changed

cloudformation/iam.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ Resources:
7777
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-userroles/*
7878
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles
7979
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles/*
80-
80+
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-linkry
81+
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-linkry/*
8182
PolicyName: lambda-dynamo
8283
Outputs:
8384
MainFunctionRoleArn:
8485
Description: Main API IAM role ARN
8586
Value:
8687
Fn::GetAtt:
8788
- ApiLambdaIAMRole
88-
- Arn
89+
- Arn

cloudformation/main.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,34 @@ Resources:
203203
Projection:
204204
ProjectionType: ALL
205205

206+
LinkryRecordsTable:
207+
Type: "AWS::DynamoDB::Table"
208+
Properties:
209+
BillingMode: "PAY_PER_REQUEST"
210+
TableName: "infra-core-api-linkry"
211+
DeletionProtectionEnabled: true
212+
PointInTimeRecoverySpecification:
213+
PointInTimeRecoveryEnabled: !If [IsProd, true, false]
214+
AttributeDefinitions:
215+
- AttributeName: "slug"
216+
AttributeType: "S"
217+
- AttributeName: "access"
218+
AttributeType: "S"
219+
KeySchema:
220+
- AttributeName: "slug"
221+
KeyType: "HASH"
222+
- AttributeName: "access"
223+
KeyType: "RANGE"
224+
GlobalSecondaryIndexes:
225+
- IndexName: "AccessIndex"
226+
KeySchema:
227+
- AttributeName: "access"
228+
KeyType: "HASH"
229+
- AttributeName: "slug"
230+
KeyType: "RANGE"
231+
Projection:
232+
ProjectionType: "ALL"
233+
206234
CacheRecordsTable:
207235
Type: 'AWS::DynamoDB::Table'
208236
DeletionPolicy: "Retain"

src/api/plugins/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from "../../common/errors/index.js";
1616
import { genericConfig, SecretConfig } from "../../common/config.js";
1717

18-
function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
18+
export function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
1919
const _intersection = new Set<T>();
2020
for (const elem of setB) {
2121
if (setA.has(elem)) {

src/api/routes/linkry.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { FastifyPluginAsync } from "fastify";
2+
import { z } from "zod";
3+
import { AppRoles } from "../../common/roles.js";
4+
import { NotImplementedError } from "../../common/errors/index.js";
5+
import { intersection } from "../plugins/auth.js";
6+
import { NoDataRequest } from "../types.js";
7+
8+
type LinkrySlugOnlyRequest = {
9+
Params: { id: string };
10+
Querystring: undefined;
11+
Body: undefined;
12+
};
13+
14+
const rawRequest = {
15+
slug: z.string().min(1),
16+
full: z.string().url().min(1),
17+
groups: z.optional(z.array(z.string()).min(1)),
18+
};
19+
20+
const createRequest = z.object(rawRequest);
21+
const patchRequest = z.object({ ...rawRequest, slug: z.undefined() });
22+
23+
type LinkyCreateRequest = {
24+
Params: undefined;
25+
Querystring: undefined;
26+
Body: z.infer<typeof createRequest>;
27+
};
28+
29+
type LinkryPatchRequest = {
30+
Params: { id: string };
31+
Querystring: undefined;
32+
Body: z.infer<typeof patchRequest>;
33+
};
34+
35+
const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => {
36+
fastify.get<LinkrySlugOnlyRequest>("/redir/:id", async (request, reply) => {
37+
throw new NotImplementedError({});
38+
});
39+
fastify.post<LinkyCreateRequest>(
40+
"/redir",
41+
{
42+
preValidation: async (request, reply) => {
43+
await fastify.zodValidateBody(request, reply, createRequest);
44+
},
45+
onRequest: async (request, reply) => {
46+
await fastify.authorize(request, reply, [
47+
AppRoles.LINKS_MANAGER,
48+
AppRoles.LINKS_ADMIN,
49+
]);
50+
},
51+
},
52+
async (request, reply) => {
53+
throw new NotImplementedError({});
54+
},
55+
);
56+
fastify.patch<LinkryPatchRequest>(
57+
"/redir/:id",
58+
{
59+
preValidation: async (request, reply) => {
60+
await fastify.zodValidateBody(request, reply, patchRequest);
61+
},
62+
onRequest: async (request, reply) => {
63+
await fastify.authorize(request, reply, [
64+
AppRoles.LINKS_MANAGER,
65+
AppRoles.LINKS_ADMIN,
66+
]);
67+
},
68+
},
69+
async (request, reply) => {
70+
// make sure that a user can manage this link, either via owning or being in a group that has access to it, or is a LINKS_ADMIN.
71+
throw new NotImplementedError({});
72+
},
73+
);
74+
fastify.delete<LinkrySlugOnlyRequest>(
75+
"/redir/:id",
76+
{
77+
preValidation: async (request, reply) => {
78+
await fastify.zodValidateBody(request, reply, createRequest);
79+
},
80+
onRequest: async (request, reply) => {
81+
await fastify.authorize(request, reply, [
82+
AppRoles.LINKS_MANAGER,
83+
AppRoles.LINKS_ADMIN,
84+
]);
85+
},
86+
},
87+
async (request, reply) => {
88+
// make sure that a user can manage this link, either via owning or being in a group that has access to it, or is a LINKS_ADMIN.
89+
throw new NotImplementedError({});
90+
},
91+
);
92+
fastify.get<NoDataRequest>(
93+
"/redir",
94+
{
95+
onRequest: async (request, reply) => {
96+
await fastify.authorize(request, reply, [
97+
AppRoles.LINKS_MANAGER,
98+
AppRoles.LINKS_ADMIN,
99+
]);
100+
},
101+
},
102+
async (request, reply) => {
103+
// if an admin, show all links
104+
// if a links manager, show all my links + links I can manage
105+
throw new NotImplementedError({});
106+
},
107+
);
108+
};
109+
110+
export default linkryRoutes;

src/api/types.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,9 @@ declare module "fastify" {
2727
tokenPayload?: AadToken;
2828
}
2929
}
30+
31+
export type NoDataRequest = {
32+
Params: undefined;
33+
Querystring: undefined;
34+
Body: undefined;
35+
};

src/common/errors/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ export class NotSupportedError extends BaseError<"NotSupportedError"> {
180180
}
181181
}
182182

183+
export class DatabaseDeleteError extends BaseError<"DatabaseDeleteError"> {
184+
constructor({ message }: { message: string }) {
185+
super({
186+
name: "DatabaseDeleteError",
187+
id: 111,
188+
message,
189+
httpStatusCode: 500,
190+
});
191+
}
192+
}
193+
183194
export class EntraGroupError extends BaseError<"EntraGroupError"> {
184195
group: string;
185196
constructor({
@@ -201,3 +212,4 @@ export class EntraGroupError extends BaseError<"EntraGroupError"> {
201212
this.group = group;
202213
}
203214
}
215+

src/common/roles.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export enum AppRoles {
77
TICKETS_MANAGER = "manage:tickets",
88
IAM_ADMIN = "admin:iam",
99
IAM_INVITE_ONLY = "invite:iam",
10+
LINKS_MANAGER = "manage:links",
11+
LINKS_ADMIN = "admin:links",
1012
}
1113
export const allAppRoles = Object.values(AppRoles).filter(
1214
(value) => typeof value === "string",

src/common/types/linkry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type ShortLinkEntry = {
2+
slug: string;
3+
access: string;
4+
redir?: string;
5+
}

0 commit comments

Comments
 (0)