Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/user/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"typecheck": "tsc --noEmit -p tsconfig.json --composite false"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"better-auth": "1.2.7",
"humps": "2.0.1",
"kysely": "^0.27.6",
Expand All @@ -47,6 +48,7 @@
"@prefabs.tech/fastify-s3": "0.93.5",
"@prefabs.tech/fastify-slonik": "0.93.5",
"@prefabs.tech/tsconfig": "0.5.0",
"@types/bcrypt": "^6.0.0",
"@types/humps": "2.0.6",
"@types/node": "24.10.13",
"@types/pg": "^8.20.0",
Expand Down
47 changes: 43 additions & 4 deletions packages/user/src/auth/better-auth/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import bcrypt from "bcryptjs";
import { betterAuth } from "better-auth";
import { admin } from "better-auth/plugins/admin";
import { bearer } from "better-auth/plugins/bearer";
Expand All @@ -23,6 +24,26 @@ import type { BetterAuthConfig } from "../../types/config";
* - admin plugin required for revokeUserSessions(userId)
* - passkey not in better-auth v1.x — tracked in findings.md
*/
// Custom password hashing using bcrypt to support SuperTokens legacy passwords
async function bcryptHash(password: string): Promise<string> {
return bcrypt.hashSync(password, 10); // 10 rounds - synchronous for simplicity
}

async function bcryptVerify({
password,
hash,
}: {
password: string;
hash: string;
}): Promise<boolean> {
try {
return bcrypt.compareSync(password, hash);
} catch {
// If comparison fails due to invalid hash format, return false
return false;
}
}

export function createAuth(config: BetterAuthConfig, connectionString: string) {
// Create a Kysely instance with a pg Pool that better-auth will use.
// This satisfies better-auth's expectation for a Kysely adapter.
Expand All @@ -31,10 +52,19 @@ export function createAuth(config: BetterAuthConfig, connectionString: string) {
dialect: new PostgresDialect({ pool }),
});

// Determine the users table name from config, default to "users"
// Note: TABLE_USERS constant is "users" but config can override
// However, config.tables?.users?.name is not directly accessible here
// because BetterAuthConfig doesn't expose it. We'd need to pass it through.
// For now, we'll keep it simple and assume "users".
const usersTableName = "users"; // Could be made configurable in a future iteration

return betterAuth({
database: {
db: db,
type: "postgres",
// Use default casing (preserve field names as defined in schema)
// The account table uses camelCase column names: accountId, providerId, userId, createdAt, updatedAt
},

basePath: config.routePrefix ?? "/auth",
Expand All @@ -43,6 +73,14 @@ export function createAuth(config: BetterAuthConfig, connectionString: string) {

trustedOrigins: config.trustedOrigins ?? [],

// Configure BetterAuth to use our existing 'users' table
user: {
// Use our existing table name instead of default "user"
modelName: usersTableName,
// Disable automatic table creation - we manage the table ourselves
disableMigrations: true,
},

emailAndPassword: {
enabled: true,
sendResetPassword: async ({ user, url }) => {
Expand All @@ -52,14 +90,17 @@ export function createAuth(config: BetterAuthConfig, connectionString: string) {
);
},
sendEmailVerificationOnSignUp: false,
// Use bcrypt for password hashing and verification to support SuperTokens legacy passwords
password: {
hash: bcryptHash,
verify: bcryptVerify,
},
},

plugins: [
// POC assumption #4 — phone OTP capability
bearer(),
phoneNumber({
sendOTP: async ({ phoneNumber: phone, code }) => {
// POC: log to console. Phase 3: use SMS provider via fastify
console.log(`[BetterAuth POC] OTP for ${phone}: ${code}`);
},
expiresIn: 300,
Expand All @@ -68,8 +109,6 @@ export function createAuth(config: BetterAuthConfig, connectionString: string) {
getTempName: (phone) => phone,
},
}),

// Required for revokeAllSessions(userId)
admin(),
],
});
Expand Down
7 changes: 4 additions & 3 deletions packages/user/src/auth/better-auth/betterAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,10 @@ export class BetterAuthProvider implements AuthProvider {

async getUser(id: string): Promise<AuthUser | undefined> {
return this.db.connect(async (connection) => {
// BetterAuth is configured to use 'users' table, so we query that
const row = await connection.maybeOne(sql.unsafe`
SELECT id, email, ${sql.identifier(["emailVerified"])}
FROM "user"
SELECT id, email, email_verified
FROM "users"
WHERE id = ${id}
`);

Expand All @@ -399,7 +400,7 @@ export class BetterAuthProvider implements AuthProvider {
return {
id: row.id as string,
email: row.email as string,
emailVerified: row.emailVerified as boolean,
emailVerified: row.email_verified as boolean,
roles,
};
});
Expand Down
Loading
Loading