Skip to content

Commit 5731dd4

Browse files
committed
setup the scaffolding for linkry routes
1 parent 54a5804 commit 5731dd4

File tree

8 files changed

+175
-11
lines changed

8 files changed

+175
-11
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: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Resources:
9393
Environment:
9494
Variables:
9595
RunEnvironment: !Ref RunEnvironment
96-
VpcConfig:
96+
VpcConfig:
9797
Ipv6AllowedForDualStack: !If [ShouldAttachVpc, True, !Ref AWS::NoValue]
9898
SecurityGroupIds: !If [ShouldAttachVpc, !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SecurityGroupIds], !Ref AWS::NoValue]
9999
SubnetIds: !If [ShouldAttachVpc, !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SubnetIds], !Ref AWS::NoValue]
@@ -107,7 +107,7 @@ Resources:
107107

108108
IamGroupRolesTable:
109109
Type: 'AWS::DynamoDB::Table'
110-
DeletionPolicy: "Retain"
110+
DeletionPolicy: "Retain"
111111
Properties:
112112
BillingMode: 'PAY_PER_REQUEST'
113113
TableName: infra-core-api-iam-grouproles
@@ -123,7 +123,7 @@ Resources:
123123

124124
IamUserRolesTable:
125125
Type: 'AWS::DynamoDB::Table'
126-
DeletionPolicy: "Retain"
126+
DeletionPolicy: "Retain"
127127
Properties:
128128
BillingMode: 'PAY_PER_REQUEST'
129129
TableName: infra-core-api-iam-userroles
@@ -139,7 +139,7 @@ Resources:
139139

140140
EventRecordsTable:
141141
Type: 'AWS::DynamoDB::Table'
142-
DeletionPolicy: "Retain"
142+
DeletionPolicy: "Retain"
143143
Properties:
144144
BillingMode: 'PAY_PER_REQUEST'
145145
TableName: infra-core-api-events
@@ -162,9 +162,37 @@ Resources:
162162
Projection:
163163
ProjectionType: ALL
164164

165+
LinkryRecordsTable:
166+
Type: "AWS::DynamoDB::Table"
167+
Properties:
168+
BillingMode: "PAY_PER_REQUEST"
169+
TableName: "infra-core-api-linkry"
170+
DeletionProtectionEnabled: true
171+
PointInTimeRecoverySpecification:
172+
PointInTimeRecoveryEnabled: !If [IsProd, true, false]
173+
AttributeDefinitions:
174+
- AttributeName: "slug"
175+
AttributeType: "S"
176+
- AttributeName: "access"
177+
AttributeType: "S"
178+
KeySchema:
179+
- AttributeName: "slug"
180+
KeyType: "HASH"
181+
- AttributeName: "access"
182+
KeyType: "RANGE"
183+
GlobalSecondaryIndexes:
184+
- IndexName: "AccessIndex"
185+
KeySchema:
186+
- AttributeName: "access"
187+
KeyType: "HASH"
188+
- AttributeName: "slug"
189+
KeyType: "RANGE"
190+
Projection:
191+
ProjectionType: "ALL"
192+
165193
CacheRecordsTable:
166194
Type: 'AWS::DynamoDB::Table'
167-
DeletionPolicy: "Retain"
195+
DeletionPolicy: "Retain"
168196
Properties:
169197
BillingMode: 'PAY_PER_REQUEST'
170198
TableName: infra-core-api-cache
@@ -183,7 +211,7 @@ Resources:
183211

184212
AppApiGateway:
185213
Type: AWS::Serverless::Api
186-
DependsOn:
214+
DependsOn:
187215
- AppApiLambdaFunction
188216
Properties:
189217
Name: !Sub ${ApplicationPrefix}-gateway
@@ -194,7 +222,7 @@ Resources:
194222
Name: AWS::Include
195223
Parameters:
196224
Location: ./phony-swagger.yml
197-
Domain:
225+
Domain:
198226
DomainName: !Sub
199227
- "${ApplicationPrefix}.${BaseDomainName}"
200228
- BaseDomainName: !FindInMap
@@ -296,4 +324,4 @@ Resources:
296324
- !Ref AWS::AccountId
297325
- ":"
298326
- !Ref AppApiGateway
299-
- "/*/*/*"
327+
- "/*/*/*"

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)