feat: migrate authentication to Appwrite and remove Better-Auth references

This commit is contained in:
2025-09-03 20:03:13 -03:00
parent 7b0526ebee
commit 2dc472c60e
15 changed files with 380 additions and 317 deletions

View File

@@ -5,7 +5,6 @@ import { trpcServer } from "@hono/trpc-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger as honoLogger } from "hono/logger";
import { auth } from "./lib/auth";
import { createContext } from "./lib/context";
import { logger } from "./lib/logger";
import { appRouter } from "./routers/index";
@@ -35,8 +34,6 @@ app.use(
})
);
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
app.use(
"/trpc/*",
trpcServer({

View File

@@ -1,23 +0,0 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db";
import * as schema from "../db/schema/auth";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema,
}),
trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: {
enabled: true,
},
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
httpOnly: true,
},
},
});

View File

@@ -1,17 +1,77 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
import { Account, Client } from "node-appwrite";
// Hoisted regex for performance and linting
const BEARER_REGEX = /^Bearer\s+(.+)$/i;
// Minimal user shape to avoid leaking node-appwrite model types
export type AuthUser = {
$id: string;
name?: string | null;
email?: string | null;
} | null;
export type CreateContextOptions = {
context: HonoContext;
};
export async function createContext({ context }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: context.req.raw.headers,
});
return {
session,
};
const endpoint = process.env.APPWRITE_ENDPOINT;
const projectId = process.env.APPWRITE_PROJECT_ID;
if (!(endpoint && projectId)) {
// Appwrite not configured; treat as unauthenticated
return { user: null as AuthUser };
}
// Initialize client per request
const client = new Client().setEndpoint(endpoint).setProject(projectId);
// Prefer JWT from Authorization header if provided
const authHeader = context.req.header("Authorization");
const bearer = authHeader?.match(BEARER_REGEX)?.[1];
if (bearer) {
client.setJWT(bearer);
} else {
// Fallback: Appwrite session cookie from browser
// Cookie name format: a_session_<PROJECT_ID>
const cookieHeader =
context.req.header("Cookie") || context.req.header("cookie");
if (cookieHeader) {
const cookieName = `a_session_${projectId}`;
const cookies = Object.fromEntries(
cookieHeader.split("; ").map((c) => {
const idx = c.indexOf("=");
return idx === -1
? [c, ""]
: [c.substring(0, idx), decodeURIComponent(c.substring(idx + 1))];
})
);
const session = cookies[cookieName];
if (session) {
client.setSession(session);
}
}
}
const account = new Account(client);
try {
const user = (await account.get()) as unknown as {
$id: string;
name?: string | null;
email?: string | null;
};
const minimal: AuthUser = {
$id: user.$id,
name: user.name ?? null,
email: user.email ?? null,
};
return { user: minimal };
} catch {
return { user: null as AuthUser };
}
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -8,17 +8,17 @@ export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
if (!ctx.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
cause: "No user",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
user: ctx.user,
},
});
});

View File

@@ -7,7 +7,7 @@ export const appRouter = router({
privateData: protectedProcedure.query(({ ctx }) => {
return {
message: "This is private",
user: ctx.session.user,
user: ctx.user,
};
}),
});