Skip to content

Commit

Permalink
feat: Create the auth_provider module
Browse files Browse the repository at this point in the history
  • Loading branch information
Blckbrry-Pi authored and NathanFlurry committed Aug 24, 2024
1 parent 7bc0a69 commit 171fdbe
Show file tree
Hide file tree
Showing 17 changed files with 708 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- 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 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"
37 changes: 37 additions & 0 deletions modules/identities/db/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Do not modify this `datasource` block
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model UserIdentities {
userId String @db.Uuid
// Overarching identity type: email, sms, oauth, etc.
identityType String
// Specific identity type
// email:
// - passwordless
// - password
// - etc.
// oauth:
// - google
// - facebook
// - etc.
identityId String
// The data that is unique to this identity.
// In the case of username, this would be the username.
// In the case of email, this would be the email address.
// In the case of oauth, this would be the oauth identity's "sub" field.
uniqueData Json
// Additional data that is stored with the identity.
// This can be used to store things like oauth tokens, last login time, etc.
// Data here only needs to be handled by the specific identity provider.
additionalData Json
@@id([userId, identityType, identityId])
@@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": "Identities",
"description": "Manage identities and identity data for users. Intended for internal use by modules exposing auth providers.",
"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 identities."
}
},
"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"
}
}
}
38 changes: 38 additions & 0 deletions modules/identities/scripts/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ScriptContext } from "../module.gen.ts";
import { getData } from "../utils/db.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> {
// 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 getData(ctx.db, userId, req.info.identityType, req.info.identityId);
if (!identity) return { data: null };

// Ensure data is of correct type
const { uniqueData, additionalData } = identity;
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 } };
}
43 changes: 43 additions & 0 deletions modules/identities/scripts/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { RuntimeError, ScriptContext } from "../module.gen.ts";
import { getData, listIdentities } from "../utils/db.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> {

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

return await ctx.db.$transaction(async tx => {
// Error if this identity provider is ALREADY associated with the user
if (await getData(tx, userId, req.info.identityType, req.info.identityId)) {
throw new RuntimeError("identity_provider_already_added");
}

// Associate the identity provider data with the user
await tx.userIdentities.create({
data: {
userId,
identityType: req.info.identityType,
identityId: req.info.identityId,
uniqueData: req.uniqueData,
additionalData: req.additionalData,
},
});

return { identityProviders: await listIdentities(tx, userId) };
});
}
23 changes: 23 additions & 0 deletions modules/identities/scripts/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ScriptContext } from "../module.gen.ts";
import { listIdentities } from "../utils/db.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 } );

return { identityProviders: await listIdentities(ctx.db, userId) };
}
44 changes: 44 additions & 0 deletions modules/identities/scripts/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ScriptContext, Empty, RuntimeError } from "../module.gen.ts";
import { getData } from "../utils/db.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> {
// Ensure the user token is valid and get the user ID
const { userId } = await ctx.modules.users.authenticateToken({ userToken: req.userToken } );

await ctx.db.$transaction(async tx => {
// Ensure the identity provider is associated with the user
if (!await getData(tx, userId, req.info.identityType, req.info.identityId)) {
throw new RuntimeError("identity_provider_not_found");
}

// Update the associated data
await tx.userIdentities.update({
where: {
userId_identityType_identityId: {
userId,
identityType: req.info.identityType,
identityId: req.info.identityId,
}
},
data: {
uniqueData: req.uniqueData,
additionalData: req.additionalData,
},
});
});

return {};
}
42 changes: 42 additions & 0 deletions modules/identities/scripts/sign_in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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;
userId: string;
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
// Get user 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,
userId: identity.userId,
};
}
56 changes: 56 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,56 @@
import { ScriptContext } from "../module.gen.ts";
import { getUserId } from "../utils/db.ts";
import { IdentityDataInput, IdentityProviderInfo } from "../utils/types.ts";

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

username?: string;
}

export interface Response {
userToken: string;
userId: string;
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
return await ctx.db.$transaction(async tx => {
const userId = await getUserId(tx, req.info.identityType, req.info.identityId, req.uniqueData);

// If the identity provider is associated with a user, sign in
if (userId) {
// Generate a user token
const { token: { token } } = await ctx.modules.users.createToken({ userId });
return {
userToken: token,
userId,
};
} else {
// Otherwise, create a new user
const { user } = await ctx.modules.users.create({ username: req.username });

// Insert the identity data with the newly-created user
await tx.userIdentities.create({
data: {
userId: user.id,
identityType: req.info.identityType,
identityId: req.info.identityId,
uniqueData: req.uniqueData,
additionalData: req.additionalData,
},
});

// Generate a user token and return it
const { token: { token } } = await ctx.modules.users.createToken({ userId: user.id });
return {
userToken: token,
userId: user.id,
};
}
});
}
Loading

0 comments on commit 171fdbe

Please sign in to comment.