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

Commit faed173

Browse files
Blckbrry-PiNathanFlurry
authored andcommitted
feat: Create the auth_provider module
1 parent 1caae6f commit faed173

File tree

17 files changed

+708
-0
lines changed

17 files changed

+708
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
// Overarching identity type: email, sms, oauth, etc.
11+
identityType String
12+
13+
// Specific identity type
14+
// email:
15+
// - passwordless
16+
// - password
17+
// - etc.
18+
// oauth:
19+
// - google
20+
// - facebook
21+
// - etc.
22+
identityId String
23+
24+
// The data that is unique to this identity.
25+
// In the case of username, this would be the username.
26+
// In the case of email, this would be the email address.
27+
// In the case of oauth, this would be the oauth identity's "sub" field.
28+
uniqueData Json
29+
30+
// Additional data that is stored with the identity.
31+
// This can be used to store things like oauth tokens, last login time, etc.
32+
// Data here only needs to be handled by the specific identity provider.
33+
additionalData Json
34+
35+
@@id([userId, identityType, identityId])
36+
@@unique([identityType, identityId, uniqueData])
37+
}

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. Intended for internal use by modules exposing auth providers.",
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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { getData } from "../utils/db.ts";
3+
import { IdentityData, IdentityProviderInfo } from "../utils/types.ts";
4+
5+
export interface Request {
6+
userToken: string;
7+
info: IdentityProviderInfo;
8+
}
9+
10+
export interface Response {
11+
data: {
12+
uniqueData: IdentityData;
13+
additionalData: IdentityData;
14+
} | null;
15+
}
16+
17+
export async function run(
18+
ctx: ScriptContext,
19+
req: Request,
20+
): Promise<Response> {
21+
// Ensure the user token is valid and get the user ID
22+
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken });
23+
24+
// Get identity data
25+
const identity = await getData(ctx.db, userId, req.info.identityType, req.info.identityId);
26+
if (!identity) return { data: null };
27+
28+
// Ensure data is of correct type
29+
const { uniqueData, additionalData } = identity;
30+
if (typeof uniqueData !== 'object' || Array.isArray(uniqueData) || uniqueData === null) {
31+
return { data: null };
32+
}
33+
if (typeof additionalData !== 'object' || Array.isArray(additionalData) || additionalData === null) {
34+
return { data: null };
35+
}
36+
37+
return { data: { uniqueData, additionalData } };
38+
}

modules/identities/scripts/link.ts

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

modules/identities/scripts/list.ts

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

modules/identities/scripts/set.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ScriptContext, Empty, RuntimeError } from "../module.gen.ts";
2+
import { getData } from "../utils/db.ts";
3+
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";
4+
5+
export interface Request {
6+
userToken: string;
7+
info: IdentityProviderInfo;
8+
uniqueData?: IdentityDataInput;
9+
additionalData: IdentityDataInput;
10+
}
11+
12+
export type Response = Empty;
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
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+
await ctx.db.$transaction(async tx => {
22+
// Ensure the identity provider is associated with the user
23+
if (!await getData(tx, userId, req.info.identityType, req.info.identityId)) {
24+
throw new RuntimeError("identity_provider_not_found");
25+
}
26+
27+
// Update the associated data
28+
await tx.userIdentities.update({
29+
where: {
30+
userId_identityType_identityId: {
31+
userId,
32+
identityType: req.info.identityType,
33+
identityId: req.info.identityId,
34+
}
35+
},
36+
data: {
37+
uniqueData: req.uniqueData,
38+
additionalData: req.additionalData,
39+
},
40+
});
41+
});
42+
43+
return {};
44+
}

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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { getUserId } from "../utils/db.ts";
3+
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";
4+
5+
export interface Request {
6+
info: IdentityProviderInfo;
7+
uniqueData: IdentityDataInput;
8+
additionalData: IdentityDataInput;
9+
10+
username?: string;
11+
}
12+
13+
export interface Response {
14+
userToken: string;
15+
userId: string;
16+
}
17+
18+
export async function run(
19+
ctx: ScriptContext,
20+
req: Request,
21+
): Promise<Response> {
22+
return await ctx.db.$transaction(async tx => {
23+
const userId = await getUserId(tx, req.info.identityType, req.info.identityId, req.uniqueData);
24+
25+
// If the identity provider is associated with a user, sign in
26+
if (userId) {
27+
// Generate a user token
28+
const { token: { token } } = await ctx.modules.users.createToken({ userId });
29+
return {
30+
userToken: token,
31+
userId,
32+
};
33+
} else {
34+
// Otherwise, create a new user
35+
const { user } = await ctx.modules.users.create({ username: req.username });
36+
37+
// Insert the identity data with the newly-created user
38+
await tx.userIdentities.create({
39+
data: {
40+
userId: user.id,
41+
identityType: req.info.identityType,
42+
identityId: req.info.identityId,
43+
uniqueData: req.uniqueData,
44+
additionalData: req.additionalData,
45+
},
46+
});
47+
48+
// Generate a user token and return it
49+
const { token: { token } } = await ctx.modules.users.createToken({ userId: user.id });
50+
return {
51+
userToken: token,
52+
userId: user.id,
53+
};
54+
}
55+
});
56+
}

0 commit comments

Comments
 (0)