Skip to content

Commit dda1c85

Browse files
committed
fix: Add users with Better Auth deployment
this PR fixes an issue that prevented adding users to orgs when using Better Auth for authentication
1 parent e9f33c8 commit dda1c85

File tree

15 files changed

+537
-83
lines changed

15 files changed

+537
-83
lines changed

bifrost/lib/clients/jawnTypes/private.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,14 @@ Json: JsonObject;
757757
UpdateOrganizationParams: components["schemas"]["Pick_NewOrganizationParams.name-or-color-or-icon-or-org_provider_key-or-limits-or-organization_type-or-onboarding_status_"] & {
758758
variant?: string;
759759
};
760+
"ResultSuccess__temporaryPassword_63_-string_-or-null_": {
761+
data: {
762+
temporaryPassword?: string;
763+
} | null;
764+
/** @enum {number|null} */
765+
error: null;
766+
};
767+
"Result__temporaryPassword_63_-string_-or-null.string_": components["schemas"]["ResultSuccess__temporaryPassword_63_-string_-or-null_"] | components["schemas"]["ResultError_string_"];
760768
UIFilterRowTree: components["schemas"]["UIFilterRowNode"] | components["schemas"]["FilterRow"];
761769
UIFilterRowNode: {
762770
/** @enum {string} */
@@ -16115,7 +16123,7 @@ export interface operations {
1611516123
/** @description Ok */
1611616124
200: {
1611716125
content: {
16118-
"application/json": components["schemas"]["Result_null.string_"];
16126+
"application/json": components["schemas"]["Result__temporaryPassword_63_-string_-or-null.string_"];
1611916127
};
1612016128
};
1612116129
};

valhalla/jawn/src/controllers/private/organizationController.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export class OrganizationController extends Controller {
224224
requestBody: { email: string },
225225
@Path() organizationId: string,
226226
@Request() request: JawnAuthenticatedRequest
227-
): Promise<Result<null, string>> {
227+
): Promise<Result<{ temporaryPassword?: string } | null, string>> {
228228
const organizationManager = new OrganizationManager(request.authParams);
229229
const org = await organizationManager.getOrg();
230230
if (org.error || !org.data) {
@@ -240,7 +240,7 @@ export class OrganizationController extends Controller {
240240
}
241241

242242
const isExistingMember = members.data?.some(
243-
(member) => member.email.toLowerCase() === requestBody.email.toLowerCase()
243+
(member) => member.email?.toLowerCase() === requestBody.email.toLowerCase()
244244
);
245245

246246
if (isExistingMember) {
@@ -300,7 +300,7 @@ export class OrganizationController extends Controller {
300300
return err(result.error ?? "Error adding member to organization");
301301
} else {
302302
this.setStatus(201);
303-
return ok(null);
303+
return ok(result.data);
304304
}
305305
}
306306

valhalla/jawn/src/lib/stores/OrganizationStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ export class OrganizationStore extends BaseStore {
202202
const result = await dbExecute<{ member: string }>(
203203
`INSERT INTO organization_member (organization, member)
204204
VALUES ($1, $2)
205-
ON CONFLICT (organization, member) DO NOTHING
206205
RETURNING member`,
207206
[organizationId, userId]
208207
);
@@ -332,8 +331,9 @@ export class OrganizationStore extends BaseStore {
332331
organizationId: string
333332
): Promise<Result<OrganizationMember[], string>> {
334333
const query = `
335-
select email, member, org_role from organization_member om
334+
select pu.email, member, org_role from organization_member om
336335
left join auth.users u on u.id = om.member
336+
left join public.user pu on pu.auth_user_id = u.id
337337
where om.organization = $1
338338
`;
339339

valhalla/jawn/src/managers/organization/OrganizationManager.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { dbExecute } from "../../lib/shared/db/dbExecute";
66
import { err, ok, Result } from "../../packages/common/result";
77
import { OrganizationStore } from "../../lib/stores/OrganizationStore";
88
import { BaseManager } from "../BaseManager";
9+
import crypto from "crypto";
910

1011
export type NewOrganizationParams =
1112
Database["public"]["Tables"]["organization"]["Insert"];
@@ -175,19 +176,23 @@ export class OrganizationManager extends BaseManager {
175176
async addMember(
176177
organizationId: string,
177178
email: string
178-
): Promise<Result<string, string>> {
179+
): Promise<Result<{ userId: string, temporaryPassword?: string }, string>> {
179180
if (!this.authParams.userId) return err("Unauthorized");
180181
let { data: userId, error: userIdError } =
181182
await this.organizationStore.getUserByEmail(email);
182183

183184
if (userIdError) {
184185
return err(userIdError);
185186
}
187+
let temporaryPassword: string | undefined;
186188
if (!userId || userId.length === 0) {
187189
try {
188190
// We still need to use the auth API for this specific function
189191
const authClient = getHeliconeAuthClient();
190-
const userResult = await authClient.createUser({ email, otp: true });
192+
if (process.env.NEXT_PUBLIC_BETTER_AUTH === "true") {
193+
temporaryPassword = crypto.randomBytes(18).toString("base64url");
194+
}
195+
const userResult = await authClient.createUser({ email, password: temporaryPassword, otp: true });
191196
if (userResult.error) {
192197
return err(userResult.error);
193198
}
@@ -196,7 +201,8 @@ export class OrganizationManager extends BaseManager {
196201
return err("Failed to create user");
197202
}
198203

199-
userId = userResult.data?.id;
204+
// we refetch the id since createUser can return a better auth format id (not a UUID)
205+
userId = userResult.data?.id ?? "";
200206
userIdError = null;
201207
} catch (error) {
202208
return err(`Failed to send OTP: ${error}`);
@@ -225,7 +231,7 @@ export class OrganizationManager extends BaseManager {
225231
return err(insertError);
226232
}
227233

228-
return ok(userId!);
234+
return ok({ userId: userId!, temporaryPassword });
229235
}
230236

231237
async updateMember(

valhalla/jawn/src/packages/common/toImplement/server/BetterAuthWrapper.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { betterAuth } from "better-auth";
2+
import { customSession } from "better-auth/plugins";
23
import { fromNodeHeaders } from "better-auth/node";
34
import { Pool } from "pg";
45
import { Database } from "../../../../lib/db/database.types";
56
import { dbExecute } from "../../../../lib/shared/db/dbExecute";
7+
import crypto from "crypto";
68
import {
79
GenericHeaders,
810
HeliconeAuthClient,
@@ -23,12 +25,38 @@ export const betterAuthClient = betterAuth({
2325
database: new Pool({
2426
connectionString: process.env.SUPABASE_DATABASE_URL,
2527
}),
28+
emailAndPassword: {
29+
enabled: true,
30+
autoSignIn: false,
31+
},
2632
logger: {
2733
log: (message: string) => {
2834
console.log(message);
2935
},
3036
},
37+
trustedOrigins: [process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3008"],
38+
plugins: [
39+
customSession(async ({ user, session }) => {
40+
const dbUser = await getUserByBetterAuthId(user.id);
41+
if (dbUser.error || !dbUser.data) {
42+
console.warn("could not fetch authUserId from db");
43+
return {
44+
user,
45+
session,
46+
};
47+
}
48+
49+
return {
50+
user: {
51+
authUserId: dbUser.data.id,
52+
...user,
53+
},
54+
session,
55+
};
56+
}),
57+
],
3158
});
59+
3260
export class BetterAuthWrapper implements HeliconeAuthClient {
3361
constructor() {}
3462

@@ -70,8 +98,28 @@ export class BetterAuthWrapper implements HeliconeAuthClient {
7098
async getUserById(userId: string): HeliconeUserResult {
7199
throw new Error("Not implemented");
72100
}
101+
73102
async getUserByEmail(email: string): HeliconeUserResult {
74-
throw new Error("Not implemented");
103+
const user = await dbExecute<{
104+
user_id: string;
105+
email: string;
106+
}>(
107+
`SELECT
108+
public.user.auth_user_id as user_id,
109+
public.user.email
110+
FROM public.user
111+
LEFT JOIN auth.users on public.user.auth_user_id = auth.users.id
112+
WHERE public.user.email = $1`,
113+
[email],
114+
);
115+
if (!user || !user.data?.[0]) {
116+
return err("User not found");
117+
}
118+
119+
return ok({
120+
id: user.data?.[0]?.user_id,
121+
email: user.data?.[0]?.email,
122+
});
75123
}
76124

77125
async authenticate(
@@ -160,6 +208,50 @@ limit 1
160208
password?: string;
161209
otp?: boolean;
162210
}): HeliconeUserResult {
163-
throw new Error("Not implemented");
211+
try {
212+
const result = await betterAuthClient.api.signUpEmail({
213+
body: {
214+
email: email,
215+
password: password ?? "",
216+
name: "",
217+
},
218+
});
219+
if (result.user) {
220+
// the ID returned from signUpEmail is not a UUID, so we need to get the user by email
221+
const getUserResult = await this.getUserByEmail(email);
222+
return ok({
223+
id: getUserResult.data?.id ?? "",
224+
email: getUserResult.data?.email ?? "",
225+
});
226+
}
227+
228+
return err("Signup process outcome unclear. Check verification steps.");
229+
} catch (error: any) {
230+
console.error(error.message || "Better Auth sign up error");
231+
return err(error.message || "Sign up failed");
232+
}
164233
}
165234
}
235+
236+
async function getUserByBetterAuthId(userId: string): HeliconeUserResult {
237+
const user = await dbExecute<{
238+
user_id: string;
239+
email: string;
240+
}>(
241+
`SELECT
242+
public.user.auth_user_id as user_id,
243+
public.user.email
244+
FROM public.user
245+
LEFT JOIN auth.users on public.user.auth_user_id = auth.users.id
246+
WHERE public.user.id = $1`,
247+
[userId],
248+
);
249+
if (!user || !user.data?.[0]) {
250+
return err("User not found");
251+
}
252+
253+
return ok({
254+
id: user.data?.[0]?.user_id,
255+
email: user.data?.[0]?.email,
256+
});
257+
}

valhalla/jawn/src/tsoa-build/private/routes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,20 @@ const models: TsoaRoute.Models = {
279279
"type": {"dataType":"intersection","subSchemas":[{"ref":"Pick_NewOrganizationParams.name-or-color-or-icon-or-org_provider_key-or-limits-or-organization_type-or-onboarding_status_"},{"dataType":"nestedObjectLiteral","nestedProperties":{"variant":{"dataType":"string"}}}],"validators":{}},
280280
},
281281
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
282+
"ResultSuccess__temporaryPassword_63_-string_-or-null_": {
283+
"dataType": "refObject",
284+
"properties": {
285+
"data": {"dataType":"union","subSchemas":[{"dataType":"nestedObjectLiteral","nestedProperties":{"temporaryPassword":{"dataType":"string"}}},{"dataType":"enum","enums":[null]}],"required":true},
286+
"error": {"dataType":"enum","enums":[null],"required":true},
287+
},
288+
"additionalProperties": false,
289+
},
290+
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
291+
"Result__temporaryPassword_63_-string_-or-null.string_": {
292+
"dataType": "refAlias",
293+
"type": {"dataType":"union","subSchemas":[{"ref":"ResultSuccess__temporaryPassword_63_-string_-or-null_"},{"ref":"ResultError_string_"}],"validators":{}},
294+
},
295+
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
282296
"UIFilterRowTree": {
283297
"dataType": "refAlias",
284298
"type": {"dataType":"union","subSchemas":[{"ref":"UIFilterRowNode"},{"ref":"FilterRow"}],"validators":{}},

valhalla/jawn/src/tsoa-build/private/swagger.json

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,42 @@
11111111
}
11121112
]
11131113
},
1114+
"ResultSuccess__temporaryPassword_63_-string_-or-null_": {
1115+
"properties": {
1116+
"data": {
1117+
"properties": {
1118+
"temporaryPassword": {
1119+
"type": "string"
1120+
}
1121+
},
1122+
"type": "object",
1123+
"nullable": true
1124+
},
1125+
"error": {
1126+
"type": "number",
1127+
"enum": [
1128+
null
1129+
],
1130+
"nullable": true
1131+
}
1132+
},
1133+
"required": [
1134+
"data",
1135+
"error"
1136+
],
1137+
"type": "object",
1138+
"additionalProperties": false
1139+
},
1140+
"Result__temporaryPassword_63_-string_-or-null.string_": {
1141+
"anyOf": [
1142+
{
1143+
"$ref": "#/components/schemas/ResultSuccess__temporaryPassword_63_-string_-or-null_"
1144+
},
1145+
{
1146+
"$ref": "#/components/schemas/ResultError_string_"
1147+
}
1148+
]
1149+
},
11141150
"UIFilterRowTree": {
11151151
"anyOf": [
11161152
{
@@ -47777,7 +47813,7 @@
4777747813
"content": {
4777847814
"application/json": {
4777947815
"schema": {
47780-
"$ref": "#/components/schemas/Result_null.string_"
47816+
"$ref": "#/components/schemas/Result__temporaryPassword_63_-string_-or-null.string_"
4778147817
}
4778247818
}
4778347819
}

0 commit comments

Comments
 (0)