This commit is contained in:
2026-05-28 20:22:30 -04:00
parent d48bbc0fc3
commit 30b2d03c68
17 changed files with 174 additions and 290 deletions

View File

@@ -5,7 +5,7 @@ import LikeIcon from "~/components/icons/LikeIcon";
export interface PostLike {
id: number;
user_id: string;
post_id: string;
post_id: number;
}
export interface AuthenticatedLikeProps {
@@ -36,15 +36,15 @@ export default function AuthenticatedLike(props: AuthenticatedLikeProps) {
if (initialHasLiked) {
const result = await api.database.removePostLike.mutate({
user_id: props.currentUserID,
post_id: props.projectID.toString()
post_id: props.projectID
});
setLikes(result.newLikes as PostLike[]);
setLikes(result.newLikes as unknown as PostLike[]);
} else {
const result = await api.database.addPostLike.mutate({
user_id: props.currentUserID,
post_id: props.projectID.toString()
post_id: props.projectID
});
setLikes(result.newLikes as PostLike[]);
setLikes(result.newLikes as unknown as PostLike[]);
}
setInstantOffset(0);
} catch (error) {

View File

@@ -6,7 +6,8 @@ import type {
UserPublicData,
ReactionType,
ModificationType,
SortingMode
SortingMode,
CommentSectionProps
} from "~/types/comment";
import CommentInputBlock from "./CommentInputBlock";
import CommentSortingSelect from "./CommentSortingSelect";

View File

@@ -20,6 +20,7 @@ export default function DeletePostButton(props: DeletePostButtonProps) {
await api.database.deletePost.mutate({ id: props.postID });
window.location.reload();
} catch (error) {
console.error("Failed to delete post:", error);
alert("Failed to delete post");
setLoading(false);
}

View File

@@ -271,11 +271,12 @@ export default function PostBodyClient(props: PostBodyClientProps) {
const headings = contentRef.querySelectorAll<HTMLElement>("h2");
let referencesSection: HTMLElement | null = null;
headings.forEach((heading) => {
for (const heading of headings) {
if (heading.textContent?.trim() === referencesHeadingText) {
referencesSection = heading;
break;
}
});
}
if (referencesSection) {
referencesSection.className = "text-2xl font-bold mb-4 text-text";
@@ -285,7 +286,7 @@ export default function PostBodyClient(props: PostBodyClientProps) {
parentDiv.classList.add("references-heading");
}
let currentElement = referencesSection.nextElementSibling;
let currentElement: Element | null = referencesSection.nextElementSibling;
while (currentElement) {
if (currentElement.tagName === "P") {

View File

@@ -22,7 +22,7 @@ interface PostFormProps {
published: boolean;
tags: string[];
};
userID: number;
userID: string;
}
export default function PostForm(props: PostFormProps) {
@@ -89,7 +89,7 @@ export default function PostForm(props: PostFormProps) {
tags: null,
author_id: props.userID
});
const newId = result.data as number;
const newId = Number(result.data);
setCreatedPostId(newId);
setHasSaved(true);
return newId;

View File

@@ -0,0 +1,79 @@
import type { APIEvent } from "@solidjs/start/server";
import { createServerCaller } from "~/server/api/root";
/**
* Result from an auth callback tRPC procedure
*/
interface AuthCallbackResult {
success: boolean;
redirectTo?: string;
}
/**
* Handle a successful auth callback result by redirecting
*/
export function redirectSuccess(result: AuthCallbackResult) {
return new Response(null, {
status: 302,
headers: { Location: result.redirectTo || "/account" }
});
}
/**
* Redirect to login with an error parameter
*/
export function redirectError(error: string) {
return new Response(null, {
status: 302,
headers: { Location: `/login?error=${encodeURIComponent(error)}` }
});
}
/**
* Handle tRPC CONFLICT error (email already in use)
*/
export function isConflictError(error: unknown): boolean {
return (
error != null &&
typeof error === "object" &&
"code" in error &&
(error as { code: string }).code === "CONFLICT"
);
}
/**
* Create an auth callback handler that calls a tRPC procedure and redirects
*/
export function createAuthCallbackHandler<Params extends object>(
procedureName: string,
callProcedure: (
caller: ReturnType<typeof createServerCaller> extends Promise<infer T>
? T
: never,
params: Params
) => Promise<AuthCallbackResult>,
handleError?: (error: unknown) => Response
) {
return async (event: APIEvent, params: Params) => {
try {
const caller = await createServerCaller(event);
const result = await callProcedure(caller, params);
if (result.success) {
return redirectSuccess(result);
}
return redirectError("auth_failed");
} catch (error) {
if (handleError) {
return handleError(error);
}
if (isConflictError(error)) {
return redirectError("email_in_use");
}
return redirectError("server_error");
}
};
}

View File

@@ -8,7 +8,7 @@ import { env } from "~/env/server";
*
* URL: https://freno.me/api/Gaze/appcast.xml
*/
export async function GET(event: APIEvent) {
export async function GET(_event: APIEvent) {
const bucket = env.VITE_DOWNLOAD_BUCKET_STRING;
const key = "api/Gaze/appcast.xml";
@@ -47,7 +47,7 @@ export async function GET(event: APIEvent) {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "public, max-age=300", // Cache for 5 minutes
"Access-Control-Allow-Origin": "*" // Allow CORS for appcast
"Access-Control-Allow-Origin": "*" // Allow CORS for Sparkle appcast
}
});
} catch (error) {

View File

@@ -8,7 +8,7 @@ import { env } from "~/env/server";
*
* URL: https://freno.me/api/InputHalo/appcast.xml
*/
export async function GET(event: APIEvent) {
export async function GET(_event: APIEvent) {
const bucket = env.VITE_DOWNLOAD_BUCKET_STRING;
const key = "api/InputHalo/appcast.xml";
@@ -47,7 +47,7 @@ export async function GET(event: APIEvent) {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "public, max-age=300", // Cache for 5 minutes
"Access-Control-Allow-Origin": "*" // Allow CORS for appcast
"Access-Control-Allow-Origin": "*" // Allow CORS for Sparkle appcast
}
});
} catch (error) {

View File

@@ -1,86 +1,26 @@
import type { APIEvent } from "@solidjs/start/server";
import { createServerCaller } from "~/server/api/root";
import {
createAuthCallbackHandler,
redirectError
} from "~/lib/auth-callback-utils";
export async function GET(event: APIEvent) {
const url = new URL(event.request.url);
const code = url.searchParams.get("code");
const error = url.searchParams.get("error");
console.log("[GitHub OAuth Callback] Request received:", {
hasCode: !!code,
codeLength: code?.length,
error
});
if (error) {
console.error("[GitHub OAuth Callback] OAuth error from provider:", error);
return new Response(null, {
status: 302,
headers: { Location: `/login?error=${encodeURIComponent(error)}` }
});
return redirectError(error);
}
if (!code) {
console.error("[GitHub OAuth Callback] Missing authorization code");
return new Response(null, {
status: 302,
headers: { Location: "/login?error=missing_code" }
});
return redirectError("missing_code");
}
try {
console.log("[GitHub OAuth Callback] Creating tRPC caller...");
const caller = await createServerCaller(event);
const handler = createAuthCallbackHandler<{ code: string }>(
"githubCallback",
(caller, params) => caller.auth.githubCallback(params)
);
console.log("[GitHub OAuth Callback] Calling githubCallback procedure...");
const result = await caller.auth.githubCallback({ code });
console.log("[GitHub OAuth Callback] Result:", result);
if (result.success) {
console.log(
"[GitHub OAuth Callback] Login successful, redirecting to:",
result.redirectTo
);
// Auth handler already set cookie headers
// Just redirect - the cookies are already in the response
const redirectUrl = result.redirectTo || "/account";
return new Response(null, {
status: 302,
headers: { Location: redirectUrl }
});
} else {
console.error(
"[GitHub OAuth Callback] Login failed (result.success=false)"
);
return new Response(null, {
status: 302,
headers: { Location: "/login?error=auth_failed" }
});
}
} catch (error) {
console.error("[GitHub OAuth Callback] Error caught:", error);
if (error && typeof error === "object" && "code" in error) {
const trpcError = error as { code: string; message?: string };
console.error("[GitHub OAuth Callback] tRPC error:", {
code: trpcError.code,
message: trpcError.message
});
if (trpcError.code === "CONFLICT") {
return new Response(null, {
status: 302,
headers: { Location: "/login?error=email_in_use" }
});
}
}
return new Response(null, {
status: 302,
headers: { Location: "/login?error=server_error" }
});
}
return handler(event, { code });
}

View File

@@ -1,86 +1,26 @@
import type { APIEvent } from "@solidjs/start/server";
import { createServerCaller } from "~/server/api/root";
import {
createAuthCallbackHandler,
redirectError
} from "~/lib/auth-callback-utils";
export async function GET(event: APIEvent) {
const url = new URL(event.request.url);
const code = url.searchParams.get("code");
const error = url.searchParams.get("error");
console.log("[Google OAuth Callback] Request received:", {
hasCode: !!code,
codeLength: code?.length,
error
});
if (error) {
console.error("[Google OAuth Callback] OAuth error from provider:", error);
return new Response(null, {
status: 302,
headers: { Location: `/login?error=${encodeURIComponent(error)}` }
});
return redirectError(error);
}
if (!code) {
console.error("[Google OAuth Callback] Missing authorization code");
return new Response(null, {
status: 302,
headers: { Location: "/login?error=missing_code" }
});
return redirectError("missing_code");
}
try {
console.log("[Google OAuth Callback] Creating tRPC caller...");
const caller = await createServerCaller(event);
const handler = createAuthCallbackHandler<{ code: string }>(
"googleCallback",
(caller, params) => caller.auth.googleCallback(params)
);
console.log("[Google OAuth Callback] Calling googleCallback procedure...");
const result = await caller.auth.googleCallback({ code });
console.log("[Google OAuth Callback] Result:", result);
if (result.success) {
console.log(
"[Google OAuth Callback] Login successful, redirecting to:",
result.redirectTo
);
// Auth handler already set cookie headers
// Just redirect - the cookies are already in the response
const redirectUrl = result.redirectTo || "/account";
return new Response(null, {
status: 302,
headers: { Location: redirectUrl }
});
} else {
console.error(
"[Google OAuth Callback] Login failed (result.success=false)"
);
return new Response(null, {
status: 302,
headers: { Location: "/login?error=auth_failed" }
});
}
} catch (error) {
console.error("[Google OAuth Callback] Error caught:", error);
if (error && typeof error === "object" && "code" in error) {
const trpcError = error as { code: string; message?: string };
console.error("[Google OAuth Callback] tRPC error:", {
code: trpcError.code,
message: trpcError.message
});
if (trpcError.code === "CONFLICT") {
return new Response(null, {
status: 302,
headers: { Location: "/login?error=email_in_use" }
});
}
}
return new Response(null, {
status: 302,
headers: { Location: "/login?error=server_error" }
});
}
return handler(event, { code });
}

View File

@@ -1,86 +1,32 @@
import type { APIEvent } from "@solidjs/start/server";
import { createServerCaller } from "~/server/api/root";
import {
createAuthCallbackHandler,
redirectError
} from "~/lib/auth-callback-utils";
export async function GET(event: APIEvent) {
const url = new URL(event.request.url);
const email = url.searchParams.get("email");
const token = url.searchParams.get("token");
console.log("[Email Login Callback] Request received:", {
email,
hasToken: !!token,
tokenLength: token?.length
});
if (!email || !token) {
console.error("[Email Login Callback] Missing required parameters:", {
hasEmail: !!email,
hasToken: !!token
});
return new Response(null, {
status: 302,
headers: { Location: "/login?error=missing_params" }
});
return redirectError("missing_params");
}
try {
console.log("[Email Login Callback] Creating tRPC caller...");
// Create tRPC caller to invoke the emailLogin procedure
const caller = await createServerCaller(event);
console.log("[Email Login Callback] Calling emailLogin procedure...");
// Call the email login handler - rememberMe will be read from JWT payload
const result = await caller.auth.emailLogin({
email,
token
});
console.log("[Email Login Callback] Login result:", result);
if (result.success) {
console.log(
"[Email Login Callback] Login successful, redirecting to:",
result.redirectTo
);
// Auth handler already set cookie headers
// Just redirect - the cookies are already in the response
const redirectUrl = result.redirectTo || "/account";
return new Response(null, {
status: 302,
headers: { Location: redirectUrl }
});
} else {
console.error(
"[Email Login Callback] Login failed (result.success=false)"
);
return new Response(null, {
status: 302,
headers: { Location: "/login?error=auth_failed" }
});
const handler = createAuthCallbackHandler<{
email: string;
token: string;
}>(
"emailLogin",
(caller, params) => caller.auth.emailLogin(params),
(error) => {
// Check for token expiration
const message = error instanceof Error ? error.message : "";
const isTokenError =
message.includes("expired") || message.includes("invalid");
return redirectError(isTokenError ? "link_expired" : "server_error");
}
} catch (error) {
console.error("[Email Login Callback] Error caught:", error);
);
// Check if it's a token expiration error
const errorMessage =
error instanceof Error ? error.message : "server_error";
const isTokenError =
errorMessage.includes("expired") || errorMessage.includes("invalid");
console.error("[Email Login Callback] Error details:", {
errorMessage,
isTokenError,
errorType: error instanceof Error ? error.constructor.name : typeof error
});
return new Response(null, {
status: 302,
headers: {
Location: isTokenError
? "/login?error=link_expired"
: "/login?error=server_error"
}
});
}
return handler(event, { email, token });
}

View File

@@ -155,7 +155,9 @@ export const reactionTypeSchema = z.enum([
"moneyEye",
"sick",
"upsideDown",
"worried"
"worried",
"upVote",
"downVote"
]);
/**

View File

@@ -1,10 +1,10 @@
import type { H3Event } from "vinxi/http";
import { getCookie, setCookie } from "vinxi/http";
import { getCookie, setCookie, getHeader } from "vinxi/http";
import { OAuth2Client } from "google-auth-library";
import type { Row } from "@libsql/client/web";
import { SignJWT, jwtVerify } from "jose";
import { env } from "~/env/server";
import { ConnectionFactory } from "./database";
import { ConnectionFactory } from "./db-connections";
import { AUTH_CONFIG, expiryToSeconds, getAccessTokenExpiry } from "~/config";
export const authCookieName = "auth_token";
@@ -30,7 +30,7 @@ function getAuthCookieOptions(rememberMe: boolean) {
}
function getAuthHeaderToken(event: H3Event): string | null {
const requestHeader = event.request?.headers?.get?.("authorization") || null;
const requestHeader = getHeader(event, "authorization") || null;
const eventHeader = event.headers
? typeof (event.headers as any).get === "function"
? (event.headers as any).get("authorization")
@@ -199,6 +199,7 @@ export async function validateLineageRequest({
return false;
}
} catch (err) {
console.error("Failed to verify email auth token:", err);
return false;
}
} else if (provider == "apple") {

View File

@@ -11,43 +11,13 @@ import {
TimeoutError,
APIError
} from "~/server/fetch-utils";
let mainDBConnection: ReturnType<typeof createClient> | null = null;
let lineageDBConnection: ReturnType<typeof createClient> | null = null;
let nessaDBConnection: ReturnType<typeof createClient> | null = null;
export function ConnectionFactory() {
if (!mainDBConnection) {
const config = {
url: env.TURSO_DB_URL,
authToken: env.TURSO_DB_TOKEN
};
mainDBConnection = createClient(config);
}
return mainDBConnection;
}
export function LineageConnectionFactory() {
if (!lineageDBConnection) {
const config = {
url: env.TURSO_LINEAGE_URL,
authToken: env.TURSO_LINEAGE_TOKEN
};
lineageDBConnection = createClient(config);
}
return lineageDBConnection;
}
export function NessaConnectionFactory() {
if (!nessaDBConnection) {
const config = {
url: env.NESSA_DB_URL,
authToken: env.NESSA_DB_TOKEN
};
nessaDBConnection = createClient(config);
}
return nessaDBConnection;
}
import {
ConnectionFactory,
LineageConnectionFactory,
NessaConnectionFactory
} from "~/server/db-connections";
// Re-export connection factories to avoid circular import with auth.ts
export { ConnectionFactory, LineageConnectionFactory, NessaConnectionFactory };
export async function LineageDBInit() {
const turso = createAPIClient({
@@ -209,7 +179,7 @@ export async function getUserBasicInfo(event: H3Event): Promise<{
return { email: null, isAuthenticated: false };
}
const user = res.rows[0] as { email: string | null };
const user = res.rows[0] as unknown as { email: string | null };
return {
email: user.email,
isAuthenticated: true

View File

@@ -1,19 +1,15 @@
import { defineMiddleware } from "vinxi/http";
import { defineMiddleware, setHeaders } from "vinxi/http";
// Security headers middleware — sets CSP and hardening headers on all responses
export default defineMiddleware((_event, next) => {
return next().then((response) => {
response.headers.set(
"Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
);
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=()"
);
return response;
});
export default defineMiddleware({
onRequest: (event) => {
setHeaders(event, {
"Content-Security-Policy":
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()"
});
}
});

View File

@@ -51,7 +51,12 @@ function getCookieValue(event: H3Event, name: string): string | undefined {
try {
const value = getCookie(event, name);
if (value) return value;
} catch (e) {}
} catch (e) {
console.warn(
"[security] getCookie failed, falling back to header parse:",
e
);
}
try {
const cookieHeader =

View File

@@ -26,7 +26,9 @@ export type ReactionType =
| "moneyEye"
| "sick"
| "upsideDown"
| "worried";
| "worried"
| "upVote"
| "downVote";
export interface UserPublicData {
email?: string;