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

Commit

Permalink
feat: Create the auth_provider module
Browse files Browse the repository at this point in the history
  • Loading branch information
Blckbrry-Pi committed Jul 5, 2024
1 parent 4411d94 commit efc852f
Show file tree
Hide file tree
Showing 16 changed files with 677 additions and 0 deletions.
22 changes: 22 additions & 0 deletions modules/identities/db/migrations/20240703025745_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "UserIdentities" (
"userId" UUID NOT NULL,
"identityType" TEXT NOT NULL,
"identityId" TEXT NOT NULL,
"uniqueData" JSONB NOT NULL,
"additionalData" JSONB NOT NULL,

CONSTRAINT "UserIdentities_pkey" PRIMARY KEY ("userId","identityType","identityId")
);

-- CreateIndex
CREATE INDEX "UserIdentities_userId_idx" ON "UserIdentities"("userId");

-- CreateIndex
CREATE INDEX "UserIdentities_identityType_identityId_idx" ON "UserIdentities"("identityType", "identityId");

-- CreateIndex
CREATE INDEX "UserIdentities_identityType_identityId_uniqueData_idx" ON "UserIdentities"("identityType", "identityId", "uniqueData");

-- CreateIndex
CREATE UNIQUE INDEX "UserIdentities_identityType_identityId_uniqueData_key" ON "UserIdentities"("identityType", "identityId", "uniqueData");
3 changes: 3 additions & 0 deletions modules/identities/db/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
27 changes: 27 additions & 0 deletions modules/identities/db/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Do not modify this `datasource` block
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model UserIdentities {
userId String @db.Uuid
// Used to identify the user from the identity
identityType String
identityId String
uniqueData Json
additionalData Json
// Additional indexes for speed
@@index([userId])
@@index([identityType, identityId])
// Each user should only have one identity per (type, id) pair (for now)
@@id([userId, identityType, identityId])
// Each identity should only be linked to one user
@@index([identityType, identityId, uniqueData])
@@unique([identityType, identityId, uniqueData])
}
64 changes: 64 additions & 0 deletions modules/identities/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "Identites",
"description": "Manage identities and identity data for users.",
"icon": "key",
"tags": [
"core",
"user",
"auth"
],
"authors": [
"rivet-gg",
"Blckbrry-Pi"
],
"status": "beta",
"dependencies": {
"rate_limit": {},
"users": {},
"tokens": {}
},
"scripts": {
"list": {
"name": "List Identities",
"description": "List all identities the user is associated with.",
"public": true
},
"get": {
"name": "Get Identity Data",
"description": "Get the data associated with a specific identity for a user."
},
"set": {
"name": "Set Identity Data",
"description": "Set the data associated with a specific identity for a user."
},

"sign_in": {
"name": "Sign In With Identity",
"description": "Sign in to a user with an identity."
},
"sign_up": {
"name": "Sign Up With Identity",
"description": "Sign up with an identity. Creates a new user."
},
"sign_in_or_sign_up": {
"name": "Sign In or Sign Up With Identity",
"description": "Sign in to a user with an identity, creating a new user if it fails."
},
"link": {
"name": "Link Identity To User",
"description": "Link a new identity and its associated data to a user. This is used for login and non-login identites."
}
},
"routes": {},
"errors": {
"identity_provider_not_found": {
"name": "Identity Provider Not Found"
},
"identity_provider_already_added": {
"name": "Identity Provider Already Added To User"
},
"identity_provider_already_used": {
"name": "Identity Provider Already Used By Other User"
}
}
}
58 changes: 58 additions & 0 deletions modules/identities/scripts/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ScriptContext } from "../module.gen.ts";
import { IdentityData, IdentityProviderInfo } from "../utils/types.ts";

export interface Request {
userToken: string;
info: IdentityProviderInfo;
}

export interface Response {
data: {
uniqueData: IdentityData;
additionalData: IdentityData;
} | null;
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttle({
key: req.userToken,
period: 10,
requests: 10,
type: "user",
});

// Ensure the user token is valid and get the user ID
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );

// Get identity data
const identity = await ctx.db.userIdentities.findFirst({
where: {
userId,
identityType: req.info.identityType,
identityId: req.info.identityId,
},
select: {
uniqueData: true,
additionalData: true
}
});

// Type checking to make typescript happy
const data = identity ?? null;
if (!data) {
return { data: null };
}

const { uniqueData, additionalData } = data;
if (typeof uniqueData !== 'object' || Array.isArray(uniqueData) || uniqueData === null) {
return { data: null };
}
if (typeof additionalData !== 'object' || Array.isArray(additionalData) || additionalData === null) {
return { data: null };
}

return { data: { uniqueData, additionalData } };
}
45 changes: 45 additions & 0 deletions modules/identities/scripts/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { RuntimeError, ScriptContext } from "../module.gen.ts";
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";

export interface Request {
userToken: string;
info: IdentityProviderInfo;
uniqueData: IdentityDataInput;
additionalData: IdentityDataInput;
}

export interface Response {
identityProviders: IdentityProviderInfo[];
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttle({
key: req.userToken,
period: 10,
requests: 10,
type: "user",
});

// Ensure the user token is valid and get the user ID
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );

// Error if this identity provider is ALREADY associated with the user
const { data: prevData } = await ctx.modules.identities.get({ userToken: req.userToken, info: req.info });
if (prevData) throw new RuntimeError("identity_provider_already_added");

// Add a new entry to the table with the associated data
await ctx.db.userIdentities.create({
data: {
userId,
identityType: req.info.identityType,
identityId: req.info.identityId,
uniqueData: req.uniqueData,
additionalData: req.additionalData,
},
});

return await ctx.modules.identities.list({ userToken: req.userToken });
}
33 changes: 33 additions & 0 deletions modules/identities/scripts/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ScriptContext } from "../module.gen.ts";
import { IdentityProviderInfo } from "../utils/types.ts";

export interface Request {
userToken: string;
}

export interface Response {
identityProviders: IdentityProviderInfo[];
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

// Ensure the user token is valid and get the user ID
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );

// Select identityType and identityId entries that match the userId
const identityProviders = await ctx.db.userIdentities.findMany({
where: {
userId,
},
select: {
identityType: true,
identityId: true,
}
});

return { identityProviders };
}
48 changes: 48 additions & 0 deletions modules/identities/scripts/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ScriptContext, Empty, RuntimeError } from "../module.gen.ts";
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";

export interface Request {
userToken: string;
info: IdentityProviderInfo;
uniqueData?: IdentityDataInput;
additionalData: IdentityDataInput;
}

export type Response = Empty;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttle({
key: req.userToken,
period: 10,
requests: 10,
type: "user",
});

// Ensure the user token is valid and get the user ID
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );

// Error if this identity provider is not associated with the user
const { data: prevData } = await ctx.modules.identities.get({ userToken: req.userToken, info: req.info });
if (!prevData) throw new RuntimeError("identity_provider_not_found");


// Update the identity data where userId, identityType, and identityId match
await ctx.db.userIdentities.update({
where: {
userId_identityType_identityId: {
userId,
identityType: req.info.identityType,
identityId: req.info.identityId,
}
},
data: {
uniqueData: req.uniqueData,
additionalData: req.additionalData,
},
});

return {};
}
46 changes: 46 additions & 0 deletions modules/identities/scripts/sign_in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ScriptContext } from "../module.gen.ts";
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";

export interface Request {
info: IdentityProviderInfo;
uniqueData: IdentityDataInput;
}

export interface Response {
userToken: string;
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
const key = req.info.identityType + ":" + req.info.identityId + ":" + JSON.stringify(req.uniqueData);
await ctx.modules.rateLimit.throttle({
key,
period: 10,
requests: 10,
type: "user",
});

// Get users the provider is associated with
const identity = await ctx.db.userIdentities.findFirst({
where: {
identityType: req.info.identityType,
identityId: req.info.identityId,
uniqueData: { equals: req.uniqueData },
},
select: {
userId: true,
},
});

// If the provider info/uniqueData combo is not associated with a user,
// throw provider_not_found error.
if (!identity) {
throw new Error("identity_not_found");
}

// Generate a user token
const { token: { token } } = await ctx.modules.users.createToken({ userId: identity.userId });
return { userToken: token };
}
38 changes: 38 additions & 0 deletions modules/identities/scripts/sign_in_or_sign_up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RuntimeError, ScriptContext } from "../module.gen.ts";
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";

export interface Request {
info: IdentityProviderInfo;
uniqueData: IdentityDataInput;
additionalData: IdentityDataInput;

username?: string;
}

export interface Response {
userToken: string;
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
try {
return await ctx.modules.identities.signIn({
info: req.info,
uniqueData: req.uniqueData,
});
} catch (e) {
if (e instanceof RuntimeError) {
if (e.code === "identity_not_found") {
return await ctx.modules.identities.signUp({
info: req.info,
uniqueData: req.uniqueData,
additionalData: req.additionalData,
username: req.username,
});
}
}
throw e;
}
}
Loading

0 comments on commit efc852f

Please sign in to comment.