Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c9f5e39
commits
christianhelp Oct 5, 2025
712a37b
updates db and adds email + password sign up
christianhelp Oct 19, 2025
4d1c40f
Adds bugs to todos
christianhelp Oct 19, 2025
7c585f5
ensures protection of sign in and sign up routes
christianhelp Oct 19, 2025
717a3f9
fix providers setup
christianhelp Oct 21, 2025
5969492
fixes up stuff
christianhelp Oct 24, 2025
b68a3a6
Adds sidebar
christianhelp Oct 24, 2025
1f5fe76
whole bunch of other commits
christianhelp Oct 25, 2025
dae49d6
formatter
christianhelp Oct 25, 2025
e8ace99
changes idk
christianhelp Oct 25, 2025
7a038f5
Merge remote-tracking branch 'origin/main' into other-fixes-n-stuff
christianhelp Oct 25, 2025
0a7ec04
whole lotta stuff
christianhelp Nov 2, 2025
05dd665
formatter
christianhelp Nov 2, 2025
0b79b23
modifies user button
christianhelp Nov 9, 2025
41a5921
adds a bunch of other stuff like theme switcher I think
christianhelp Nov 25, 2025
ee5abcf
I suck at commit messages
christianhelp Dec 17, 2025
23aca39
More commits and stuff
christianhelp Jan 18, 2026
c3ab36d
formatter + stuff
christianhelp Jan 20, 2026
e70e7bb
More commits for formatter and fixes mutations
christianhelp Jan 20, 2026
3c7bc60
updates and fixes imports
christianhelp Jan 25, 2026
a0ee072
formatter
christianhelp Jan 25, 2026
b1998f0
Fixes based on Code Rabbit suggestions (I should pay more attention)
christianhelp Feb 14, 2026
0c2c18c
formatter
christianhelp Feb 14, 2026
0c1aa2f
Updates for more feedback. The bot is cooking ngl.
christianhelp Feb 15, 2026
3bf9077
More feedback
christianhelp Feb 15, 2026
b40414d
fixes
christianhelp Feb 15, 2026
47befbd
more fixes
christianhelp Feb 16, 2026
a849513
Updates to ignore Shad cn components and route tree
christianhelp Feb 16, 2026
f1eaf42
Quick update of feedback for join
christianhelp Feb 16, 2026
25c5458
add todo for extra variables logged that can be nulled
christianhelp Feb 16, 2026
4c5ad4d
Removes undeeded TODOs and adds bugs to a few
christianhelp Feb 16, 2026
d39f303
Renames constants to be screaming snake case
christianhelp Feb 16, 2026
4beff4f
Renames all of the errors
christianhelp Feb 16, 2026
9f47800
Updates all of the routes to have the correct response format
christianhelp Feb 16, 2026
bb02ad9
Quick updates
christianhelp Feb 16, 2026
961a24e
Merge commit '48ca5a82cf10a9393219cdfdd12668dadf95377d' into rework-a…
christianhelp Feb 17, 2026
388c1f6
formatter
christianhelp Feb 17, 2026
e8a7fb8
Small updates
christianhelp Feb 17, 2026
fa08521
more updates or things I missed
christianhelp Feb 17, 2026
a8fcb0c
More updates
christianhelp Feb 17, 2026
051bdef
More fixes
christianhelp Feb 17, 2026
af0fd28
Fixes return for user on admin
christianhelp Feb 17, 2026
2d96af2
whoops
christianhelp Feb 17, 2026
ee9df94
fix type thing
christianhelp Feb 17, 2026
eadef63
Removes old comment
christianhelp Feb 17, 2026
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
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ pnpm-lock.yaml
pnpm-workspace.yaml
**/package.json
*.json
web/src/components/ui
apps/web/src/components/ui/**
apps/web/src/routeTree.gen.ts
4 changes: 3 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
"@t3-oss/env-core": "^0.13.8",
"@types/node": "20.14.11",
"better-auth": "^1.3.4",
"date-fns": "^4.1.0",
"db": "workspace:*",
"dotenv": "^17.2.1",
"drizzle-zod": "^0.8.2",
"hono": "^4.8.3",
"nanoid": "^5.1.5",
"shared": "workspace:*",
"zod": "^3.25.67"
},
Expand All @@ -24,4 +27,3 @@
"wrangler": "^4.4.0"
}
}

24 changes: 19 additions & 5 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,53 @@ import {
} from "./routes";
import { generalCorsPolicy, betterAuthCorsPolicy } from "./lib/functions/cors";
import { HonoBetterAuth } from "./lib/functions";
import { logError } from "./lib/functions/database";
import {
setUserSessionContextMiddleware,
authenticatedMiddleware,
afterRouteLogicMiddleware,
} from "./lib/functions/middleware";

interface Env {}

// api stuff
// TODO(https://github.com/acmutsa/Fallback/issues/26): Find a way to run logic after the response has been sent. Something like an After function that NextJS has.
export const api = HonoBetterAuth()
.use(
"*",
generalCorsPolicy, // see if we can get rid of this one maybe later?
betterAuthCorsPolicy,
async (c, next) => setUserSessionContextMiddleware(c, next),
async (c, next) => afterRouteLogicMiddleware(c, next),
async (c, next) => authenticatedMiddleware(c, next),
)
.route("/health", healthHandler)
.route("/log", logHandler)
.route("/backup", backupHandler)
.route("/user", userhandler)
.route("/api/auth/*", authHandler)
.route("/team", teamHandler);
.route("/team", teamHandler)
.onError(async (err, c) => {
// Log errors that are not caught by the route handlers
await logError(err.message, c);

///
return c.json(
{
error: "Internal Server Error",
},
500,
);
});
Comment thread
christianhelp marked this conversation as resolved.

// cron stuff
/**
* The basic logic for running a cron job in Cloudflare Workers. Will be updated to be more specific later.
*/
// TODO(https://github.com/acmutsa/Fallback/issues/31): These will be used later once we have the cron job functionality implemented
const cron = async (
controller: ScheduledController,
_: Env,
ctx: ExecutionContext,
_controller: ScheduledController,
_env: Env,
_ctx: ExecutionContext,
) => {
// NOTE: controller.cron is what we will use to check what jobs need to be running
// ctx.waitUntil(doBackup());
Expand Down
25 changes: 6 additions & 19 deletions apps/api/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "db"; // your drizzle instance
import { APP_NAME, AUTH_CONFIG } from "shared/constants";
import { env } from "../env";
import type { FieldAttribute, FieldType } from "better-auth/db";

export const auth = betterAuth({
database: drizzleAdapter(db, {
Expand All @@ -15,7 +16,6 @@ export const auth = betterAuth({
create: {
// used in order to break up the first and last name into separate fields
before: async (user) => {
console.log("Creating user. Raw inputs are: ", user);
// split the name into first and last name (name object is mapped to the first name by the config)
const [firstName, ...rest] = user.name.split(" ");
const lastName = rest.join(" ");
Expand All @@ -33,24 +33,11 @@ export const auth = betterAuth({
},
// this declares the extra fields that are not in the default user schema that better auth creates, but are in the database
additionalFields: {
firstName: {
type: "string",
defaultValue: "",
},
lastName: {
type: "string",
defaultValue: "",
},
lastSeen: {
type: "date",
required: false,
input: false,
},
siteRole: {
type: "string",
defaultValue: "USER",
input: false,
},
...(AUTH_CONFIG.additionalFields as
| {
[key: string]: FieldAttribute<FieldType>;
}
| undefined),
},
},
advanced: {
Expand Down
124 changes: 120 additions & 4 deletions apps/api/src/lib/functions/database.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { env } from "../../env";
import { userToTeam, db, and, eq, log } from "db";
import type { UserType, SiteRoleType } from "db/types";
import type { LoggingOptions, LoggingType } from "../types";
import { type Context } from "hono";
import { isInDevMode } from ".";

/**
*
* Fetches a database dump from a Turso database instance.
* @param databseName The name of the database.
* @param organizationSlug The organization slug associated with the database.
*/

export async function getDatabaseDumpTurso(
databseName: string,
organizationSlug: string,
Expand All @@ -30,6 +35,117 @@ export async function getDatabaseDumpTurso(
/**
* Checks the database connection by pinging it and querying for it's table count.
* Function will take in an database information and the type to make the appropriate query.
*
*/
export async function pingDatabase() {}

export function isSiteAdminUser(
permissionEnum: NonNullable<UserType>["siteRole"],
): boolean {
return ["ADMIN", "SUPER_ADMIN"].some((role) => role === permissionEnum);
}

export async function leaveTeam(userId: string, teamId: string) {
return db
.delete(userToTeam)
.where(
and(eq(userToTeam.userId, userId), eq(userToTeam.teamId, teamId)),
)
.returning({ teamId: userToTeam.teamId });
}

export async function getAdminUserForTeam(userId: string, teamId: string) {
return db.query.userToTeam.findFirst({
where: and(
eq(userToTeam.userId, userId),
eq(userToTeam.teamId, teamId),
eq(userToTeam.role, "ADMIN"),
),
});
}
// TODO: This function is lowkey pivotal so we should ensure it is WAI.
export async function isUserSiteAdminOrQueryHasPermissions<T = unknown>(
userSiteRole: SiteRoleType,
// Accept either a Promise (already invoked query) or a function that returns a Promise
query: Promise<T> | (() => Promise<T>),
): Promise<boolean> {
if (isSiteAdminUser(userSiteRole)) {
return true;
}

const result = typeof query === "function" ? await query() : await query;
return !!result;
}

export async function logError(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("ERROR", message, options);
}

export async function logInfo(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("INFO", message, options);
}

export async function logWarning(message: string, c?: Context) {
const options = getAllContextValues(c);
await logToDb("WARNING", message, options);
}

export async function logToDb(
loggingType: LoggingType,
message: string,
options?: LoggingOptions,
) {
if (isInDevMode()) {
console.log(`[${loggingType}] - ${message} - Options: `, options);
return;
}
try {
await db.insert(log).values({
...options,
logType: loggingType,
message,
});
} catch (e) {
// Silently fail if logging to the db fails.
console.error("Failed to log to database: ", e);
}
}

function getAllContextValues(c?: Context): LoggingOptions | undefined {
if (!c) {
return undefined;
}
const user = c.get("user") as UserType;
return {
route: c.req.path,
userId: user?.id || null,
teamId: c.get("teamId"),
requestId: c.get("requestId"),
};
}

/**
* Safely extract an error code string from an unknown thrown value from a db error.
* Returns the code as a string when present, otherwise null.
*
* This function can handle it being passed as either a number or string and will convert if need be
*/
export function maybeGetDbErrorCode(e: unknown): string | null {
if (e == null) return null;
if (typeof e === "object") {
const anyE = e as Record<string, unknown>;

const errorCauseKey = anyE["cause"];
if (errorCauseKey && typeof errorCauseKey === "object") {
const codeKey = (errorCauseKey as Record<string, unknown>)["code"];
if (typeof codeKey === "string") {
return codeKey;
} else if (typeof codeKey === "number") {
return codeKey.toString();
}
}
}

return null;
}
12 changes: 7 additions & 5 deletions apps/api/src/lib/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { HonoOptions } from "hono/hono-base";
import { Hono } from "hono";
import type { BlankEnv } from "hono/types";
import type { UserType, SessionType } from "../types";
import type { ApiContextVariables } from "../types";

/**
* @description Wrapper for the Hono constructor that includes the BetterAuth types
* @param options Hono options
*/
export function HonoBetterAuth(options?: HonoOptions<BlankEnv> | undefined) {
return new Hono<{
Variables: {
user: UserType;
session: SessionType;
};
Variables: ApiContextVariables;
}>({
...options,
});
}

// TODO(https://github.com/acmutsa/Fallback/issues/38): Come back and find out what proper value needs to be here
export function isInDevMode() {
return process.env.NODE_ENV === "development";
}
35 changes: 30 additions & 5 deletions apps/api/src/lib/functions/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import type { Context, Next } from "hono";
import { auth } from "../auth";
import { logInfo } from "./database";
import { nanoid } from "nanoid";
import type { ApiContext } from "../types";
import { API_ERROR_MESSAGES } from "shared";

export const MIDDLEWARE_PUBLIC_ROUTES = ["/health", "/api/auth"];
//TODO(https://github.com/acmutsa/Fallback/issues/16): Make these function's context types safe

export async function setUserSessionContextMiddleware(c: Context, next: Next) {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
const userString = session
? `Authenticated user (id: ${session?.user.id})`
: "Unauthenticated User";

const requestId = nanoid();
c.set("requestId", requestId);

await logInfo(
`Middleware for request path ${c.req.path} for ${userString}`,
c,
);
Comment thread
christianhelp marked this conversation as resolved.
Outdated

if (!session) {
c.set("user", null);
c.set("session", null);
c.set("teamId", null);
return next();
}

c.set("user", session.user);
c.set("session", session.session);
return next();
await next();
}

// TODO: Make this type safe
export async function authenticatedMiddleware(c: Context, next: Next) {
export async function authenticatedMiddleware(c: ApiContext, next: Next) {
// First check if it is a public route and if so we will return (make sure this works)
const isPublicRoute = MIDDLEWARE_PUBLIC_ROUTES.some((route) =>
c.req.path.startsWith(route),
Expand All @@ -30,7 +44,18 @@ export async function authenticatedMiddleware(c: Context, next: Next) {
const user = c.get("user");
const session = c.get("session");
if (!(user && session)) {
return c.json({ error: "Unauthorized" }, 401);
await logInfo(`Unauthorized access attempt to ${c.req.path}`, c);
return c.json({ error: API_ERROR_MESSAGES.NOT_AUTHORIZED }, 401);
Comment thread
christianhelp marked this conversation as resolved.
Outdated
}
return next();
}

/*
* Middleware to handle logging the request and results of request afterwards. Context object is apparently stateful
*/
export async function afterRouteLogicMiddleware(c: ApiContext, next: Next) {
// TODO(https://github.com/acmutsa/Fallback/issues/26): Come back and finish logging function
console.log("context before is: ", c.get("teamId"));
await next();
console.log("context after is: ", c.get("teamId"));
}
23 changes: 20 additions & 3 deletions apps/api/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import { auth } from "../lib/auth";
import { log } from "db";
import type { SessionType, UserType } from "db/types";
import type { Context } from "hono";

export type UserType = typeof auth.$Infer.Session.user | null;
export type SessionType = typeof auth.$Infer.Session.session | null;
// Match the Variables shape declared in HonoBetterAuth
export type ApiContextVariables = {
user: UserType;
session: SessionType;
teamId: string | null;
requestId: string | null;
};
export type ApiContext = Context<{
Variables: ApiContextVariables;
}>;

export type LoggingOptions = Omit<
typeof log.$inferInsert,
"id" | "occurredAt" | "logType" | "message"
>;
// Single type representing the logType value (e.g. "INFO" | "WARNING" | "ERROR")
export type LoggingType = (typeof log.$inferSelect)["logType"];
7 changes: 7 additions & 0 deletions apps/api/src/lib/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createInsertSchema } from "drizzle-zod";
import { log } from "db";

export const logSchema = createInsertSchema(log).omit({
id: true,
occurredAt: true,
});
Loading