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

Commit 7a38516

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

File tree

16 files changed

+642
-0
lines changed

16 files changed

+642
-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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
@@index([userId])
18+
@@id([userId, identityType, identityId])
19+
@@unique([identityType, identityId, uniqueData])
20+
}

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": "Identities",
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 identities."
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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
// Ensure the user token is valid and get the user ID
21+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );
22+
23+
// Get identity data
24+
const identity = await ctx.db.userIdentities.findFirst({
25+
where: {
26+
userId,
27+
identityType: req.info.identityType,
28+
identityId: req.info.identityId,
29+
},
30+
select: {
31+
uniqueData: true,
32+
additionalData: true
33+
}
34+
});
35+
36+
// Type checking to make typescript happy
37+
const data = identity ?? null;
38+
if (!data) {
39+
return { data: null };
40+
}
41+
42+
const { uniqueData, additionalData } = data;
43+
if (typeof uniqueData !== 'object' || Array.isArray(uniqueData) || uniqueData === null) {
44+
return { data: null };
45+
}
46+
if (typeof additionalData !== 'object' || Array.isArray(additionalData) || additionalData === null) {
47+
return { data: null };
48+
}
49+
50+
return { data: { uniqueData, additionalData } };
51+
}

modules/identities/scripts/link.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
20+
// Ensure the user token is valid and get the user ID
21+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );
22+
23+
// Error if this identity provider is ALREADY associated with the user
24+
const { data: prevData } = await ctx.modules.identities.get({ userToken: req.userToken, info: req.info });
25+
if (prevData) throw new RuntimeError("identity_provider_already_added");
26+
27+
// Add a new entry to the table with the associated data
28+
await ctx.db.userIdentities.create({
29+
data: {
30+
userId,
31+
identityType: req.info.identityType,
32+
identityId: req.info.identityId,
33+
uniqueData: req.uniqueData,
34+
additionalData: req.additionalData,
35+
},
36+
});
37+
38+
return await ctx.modules.identities.list({ userToken: req.userToken });
39+
}

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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
// Ensure the user token is valid and get the user ID
18+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );
19+
20+
// Error if this identity provider is not associated with the user
21+
const { data: prevData } = await ctx.modules.identities.get({ userToken: req.userToken, info: req.info });
22+
if (!prevData) throw new RuntimeError("identity_provider_not_found");
23+
24+
25+
// Update the identity data where userId, identityType, and identityId match
26+
await ctx.db.userIdentities.update({
27+
where: {
28+
userId_identityType_identityId: {
29+
userId,
30+
identityType: req.info.identityType,
31+
identityId: req.info.identityId,
32+
}
33+
},
34+
data: {
35+
uniqueData: req.uniqueData,
36+
additionalData: req.additionalData,
37+
},
38+
});
39+
40+
return {};
41+
}

modules/identities/scripts/sign_in.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
userId: string;
12+
}
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
18+
// Get user the provider is associated with
19+
const identity = await ctx.db.userIdentities.findFirst({
20+
where: {
21+
identityType: req.info.identityType,
22+
identityId: req.info.identityId,
23+
uniqueData: { equals: req.uniqueData },
24+
},
25+
select: {
26+
userId: true,
27+
},
28+
});
29+
30+
// If the provider info/uniqueData combo is not associated with a user,
31+
// throw provider_not_found error.
32+
if (!identity) {
33+
throw new Error("identity_not_found");
34+
}
35+
36+
// Generate a user token
37+
const { token: { token } } = await ctx.modules.users.createToken({ userId: identity.userId });
38+
return {
39+
userToken: token,
40+
userId: identity.userId,
41+
};
42+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
userId: string;
15+
}
16+
17+
export async function run(
18+
ctx: ScriptContext,
19+
req: Request,
20+
): Promise<Response> {
21+
try {
22+
return await ctx.modules.identities.signIn({
23+
info: req.info,
24+
uniqueData: req.uniqueData,
25+
});
26+
} catch (e) {
27+
if (e instanceof RuntimeError) {
28+
if (e.code === "identity_not_found") {
29+
return await ctx.modules.identities.signUp({
30+
info: req.info,
31+
uniqueData: req.uniqueData,
32+
additionalData: req.additionalData,
33+
username: req.username,
34+
});
35+
}
36+
}
37+
throw e;
38+
}
39+
}

0 commit comments

Comments
 (0)