Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.

Commit ce34e98

Browse files
committed
feat: Create the auth_provider module
1 parent ad062ed commit ce34e98

File tree

16 files changed

+677
-0
lines changed

16 files changed

+677
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- CreateTable
2+
CREATE TABLE "UserIdentities" (
3+
"userId" UUID NOT NULL,
4+
"identityType" TEXT NOT NULL,
5+
"identityId" TEXT NOT NULL,
6+
"uniqueData" JSONB NOT NULL,
7+
"additionalData" JSONB NOT NULL,
8+
9+
CONSTRAINT "UserIdentities_pkey" PRIMARY KEY ("userId","identityType","identityId")
10+
);
11+
12+
-- CreateIndex
13+
CREATE INDEX "UserIdentities_userId_idx" ON "UserIdentities"("userId");
14+
15+
-- CreateIndex
16+
CREATE INDEX "UserIdentities_identityType_identityId_idx" ON "UserIdentities"("identityType", "identityId");
17+
18+
-- CreateIndex
19+
CREATE INDEX "UserIdentities_identityType_identityId_uniqueData_idx" ON "UserIdentities"("identityType", "identityId", "uniqueData");
20+
21+
-- CreateIndex
22+
CREATE UNIQUE INDEX "UserIdentities_identityType_identityId_uniqueData_key" ON "UserIdentities"("identityType", "identityId", "uniqueData");
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Please do not edit this file manually
2+
# It should be added in your version-control system (i.e. Git)
3+
provider = "postgresql"

modules/identities/db/schema.prisma

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Do not modify this `datasource` block
2+
datasource db {
3+
provider = "postgresql"
4+
url = env("DATABASE_URL")
5+
}
6+
7+
model UserIdentities {
8+
userId String @db.Uuid
9+
10+
// Used to identify the user from the identity
11+
identityType String
12+
identityId String
13+
uniqueData Json
14+
15+
additionalData Json
16+
17+
// Additional indexes for speed
18+
@@index([userId])
19+
@@index([identityType, identityId])
20+
21+
// Each user should only have one identity per (type, id) pair (for now)
22+
@@id([userId, identityType, identityId])
23+
24+
// Each identity should only be linked to one user
25+
@@index([identityType, identityId, uniqueData])
26+
@@unique([identityType, identityId, uniqueData])
27+
}

modules/identities/module.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "Identites",
3+
"description": "Manage identities and identity data for users.",
4+
"icon": "key",
5+
"tags": [
6+
"core",
7+
"user",
8+
"auth"
9+
],
10+
"authors": [
11+
"rivet-gg",
12+
"Blckbrry-Pi"
13+
],
14+
"status": "beta",
15+
"dependencies": {
16+
"rate_limit": {},
17+
"users": {},
18+
"tokens": {}
19+
},
20+
"scripts": {
21+
"list": {
22+
"name": "List Identities",
23+
"description": "List all identities the user is associated with.",
24+
"public": true
25+
},
26+
"get": {
27+
"name": "Get Identity Data",
28+
"description": "Get the data associated with a specific identity for a user."
29+
},
30+
"set": {
31+
"name": "Set Identity Data",
32+
"description": "Set the data associated with a specific identity for a user."
33+
},
34+
35+
"sign_in": {
36+
"name": "Sign In With Identity",
37+
"description": "Sign in to a user with an identity."
38+
},
39+
"sign_up": {
40+
"name": "Sign Up With Identity",
41+
"description": "Sign up with an identity. Creates a new user."
42+
},
43+
"sign_in_or_sign_up": {
44+
"name": "Sign In or Sign Up With Identity",
45+
"description": "Sign in to a user with an identity, creating a new user if it fails."
46+
},
47+
"link": {
48+
"name": "Link Identity To User",
49+
"description": "Link a new identity and its associated data to a user. This is used for login and non-login identites."
50+
}
51+
},
52+
"routes": {},
53+
"errors": {
54+
"identity_provider_not_found": {
55+
"name": "Identity Provider Not Found"
56+
},
57+
"identity_provider_already_added": {
58+
"name": "Identity Provider Already Added To User"
59+
},
60+
"identity_provider_already_used": {
61+
"name": "Identity Provider Already Used By Other User"
62+
}
63+
}
64+
}

modules/identities/scripts/get.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { IdentityData, IdentityProviderInfo } from "../utils/types.ts";
3+
4+
export interface Request {
5+
userToken: string;
6+
info: IdentityProviderInfo;
7+
}
8+
9+
export interface Response {
10+
data: {
11+
uniqueData: IdentityData;
12+
additionalData: IdentityData;
13+
} | null;
14+
}
15+
16+
export async function run(
17+
ctx: ScriptContext,
18+
req: Request,
19+
): Promise<Response> {
20+
await ctx.modules.rateLimit.throttle({
21+
key: req.userToken,
22+
period: 10,
23+
requests: 10,
24+
type: "user",
25+
});
26+
27+
// Ensure the user token is valid and get the user ID
28+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );
29+
30+
// Get identity data
31+
const identity = await ctx.db.userIdentities.findFirst({
32+
where: {
33+
userId,
34+
identityType: req.info.identityType,
35+
identityId: req.info.identityId,
36+
},
37+
select: {
38+
uniqueData: true,
39+
additionalData: true
40+
}
41+
});
42+
43+
// Type checking to make typescript happy
44+
const data = identity ?? null;
45+
if (!data) {
46+
return { data: null };
47+
}
48+
49+
const { uniqueData, additionalData } = data;
50+
if (typeof uniqueData !== 'object' || Array.isArray(uniqueData) || uniqueData === null) {
51+
return { data: null };
52+
}
53+
if (typeof additionalData !== 'object' || Array.isArray(additionalData) || additionalData === null) {
54+
return { data: null };
55+
}
56+
57+
return { data: { uniqueData, additionalData } };
58+
}

modules/identities/scripts/link.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { RuntimeError, ScriptContext } from "../module.gen.ts";
2+
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";
3+
4+
export interface Request {
5+
userToken: string;
6+
info: IdentityProviderInfo;
7+
uniqueData: IdentityDataInput;
8+
additionalData: IdentityDataInput;
9+
}
10+
11+
export interface Response {
12+
identityProviders: IdentityProviderInfo[];
13+
}
14+
15+
export async function run(
16+
ctx: ScriptContext,
17+
req: Request,
18+
): Promise<Response> {
19+
await ctx.modules.rateLimit.throttle({
20+
key: req.userToken,
21+
period: 10,
22+
requests: 10,
23+
type: "user",
24+
});
25+
26+
// Ensure the user token is valid and get the user ID
27+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );
28+
29+
// Error if this identity provider is ALREADY associated with the user
30+
const { data: prevData } = await ctx.modules.identities.get({ userToken: req.userToken, info: req.info });
31+
if (prevData) throw new RuntimeError("identity_provider_already_added");
32+
33+
// Add a new entry to the table with the associated data
34+
await ctx.db.userIdentities.create({
35+
data: {
36+
userId,
37+
identityType: req.info.identityType,
38+
identityId: req.info.identityId,
39+
uniqueData: req.uniqueData,
40+
additionalData: req.additionalData,
41+
},
42+
});
43+
44+
return await ctx.modules.identities.list({ userToken: req.userToken });
45+
}

modules/identities/scripts/list.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { IdentityProviderInfo } from "../utils/types.ts";
3+
4+
export interface Request {
5+
userToken: string;
6+
}
7+
8+
export interface Response {
9+
identityProviders: IdentityProviderInfo[];
10+
}
11+
12+
export async function run(
13+
ctx: ScriptContext,
14+
req: Request,
15+
): Promise<Response> {
16+
await ctx.modules.rateLimit.throttlePublic({});
17+
18+
// Ensure the user token is valid and get the user ID
19+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );
20+
21+
// Select identityType and identityId entries that match the userId
22+
const identityProviders = await ctx.db.userIdentities.findMany({
23+
where: {
24+
userId,
25+
},
26+
select: {
27+
identityType: true,
28+
identityId: true,
29+
}
30+
});
31+
32+
return { identityProviders };
33+
}

modules/identities/scripts/set.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ScriptContext, Empty, RuntimeError } from "../module.gen.ts";
2+
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";
3+
4+
export interface Request {
5+
userToken: string;
6+
info: IdentityProviderInfo;
7+
uniqueData?: IdentityDataInput;
8+
additionalData: IdentityDataInput;
9+
}
10+
11+
export type Response = Empty;
12+
13+
export async function run(
14+
ctx: ScriptContext,
15+
req: Request,
16+
): Promise<Response> {
17+
await ctx.modules.rateLimit.throttle({
18+
key: req.userToken,
19+
period: 10,
20+
requests: 10,
21+
type: "user",
22+
});
23+
24+
// Ensure the user token is valid and get the user ID
25+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );
26+
27+
// Error if this identity provider is not associated with the user
28+
const { data: prevData } = await ctx.modules.identities.get({ userToken: req.userToken, info: req.info });
29+
if (!prevData) throw new RuntimeError("identity_provider_not_found");
30+
31+
32+
// Update the identity data where userId, identityType, and identityId match
33+
await ctx.db.userIdentities.update({
34+
where: {
35+
userId_identityType_identityId: {
36+
userId,
37+
identityType: req.info.identityType,
38+
identityId: req.info.identityId,
39+
}
40+
},
41+
data: {
42+
uniqueData: req.uniqueData,
43+
additionalData: req.additionalData,
44+
},
45+
});
46+
47+
return {};
48+
}

modules/identities/scripts/sign_in.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";
3+
4+
export interface Request {
5+
info: IdentityProviderInfo;
6+
uniqueData: IdentityDataInput;
7+
}
8+
9+
export interface Response {
10+
userToken: string;
11+
}
12+
13+
export async function run(
14+
ctx: ScriptContext,
15+
req: Request,
16+
): Promise<Response> {
17+
const key = req.info.identityType + ":" + req.info.identityId + ":" + JSON.stringify(req.uniqueData);
18+
await ctx.modules.rateLimit.throttle({
19+
key,
20+
period: 10,
21+
requests: 10,
22+
type: "user",
23+
});
24+
25+
// Get users the provider is associated with
26+
const identity = await ctx.db.userIdentities.findFirst({
27+
where: {
28+
identityType: req.info.identityType,
29+
identityId: req.info.identityId,
30+
uniqueData: { equals: req.uniqueData },
31+
},
32+
select: {
33+
userId: true,
34+
},
35+
});
36+
37+
// If the provider info/uniqueData combo is not associated with a user,
38+
// throw provider_not_found error.
39+
if (!identity) {
40+
throw new Error("identity_not_found");
41+
}
42+
43+
// Generate a user token
44+
const { token: { token } } = await ctx.modules.users.createToken({ userId: identity.userId });
45+
return { userToken: token };
46+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { RuntimeError, ScriptContext } from "../module.gen.ts";
2+
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";
3+
4+
export interface Request {
5+
info: IdentityProviderInfo;
6+
uniqueData: IdentityDataInput;
7+
additionalData: IdentityDataInput;
8+
9+
username?: string;
10+
}
11+
12+
export interface Response {
13+
userToken: string;
14+
}
15+
16+
export async function run(
17+
ctx: ScriptContext,
18+
req: Request,
19+
): Promise<Response> {
20+
try {
21+
return await ctx.modules.identities.signIn({
22+
info: req.info,
23+
uniqueData: req.uniqueData,
24+
});
25+
} catch (e) {
26+
if (e instanceof RuntimeError) {
27+
if (e.code === "identity_not_found") {
28+
return await ctx.modules.identities.signUp({
29+
info: req.info,
30+
uniqueData: req.uniqueData,
31+
additionalData: req.additionalData,
32+
username: req.username,
33+
});
34+
}
35+
}
36+
throw e;
37+
}
38+
}

0 commit comments

Comments
 (0)