zod fix
This commit is contained in:
@@ -7,9 +7,6 @@ export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable button component with variants and loading state
|
|
||||||
*/
|
|
||||||
export default function Button(props: ButtonProps) {
|
export default function Button(props: ButtonProps) {
|
||||||
const [local, others] = splitProps(props, [
|
const [local, others] = splitProps(props, [
|
||||||
"variant",
|
"variant",
|
||||||
@@ -18,13 +15,14 @@ export default function Button(props: ButtonProps) {
|
|||||||
"fullWidth",
|
"fullWidth",
|
||||||
"class",
|
"class",
|
||||||
"children",
|
"children",
|
||||||
"disabled",
|
"disabled"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const variant = () => local.variant || "primary";
|
const variant = () => local.variant || "primary";
|
||||||
const size = () => local.size || "md";
|
const size = () => local.size || "md";
|
||||||
|
|
||||||
const baseClasses = "flex justify-center items-center rounded font-semibold transition-all duration-300 ease-out disabled:opacity-50 disabled:cursor-not-allowed";
|
const baseClasses =
|
||||||
|
"flex justify-center items-center rounded font-semibold transition-all duration-300 ease-out disabled:opacity-50 disabled:cursor-not-allowed";
|
||||||
|
|
||||||
const variantClasses = () => {
|
const variantClasses = () => {
|
||||||
switch (variant()) {
|
switch (variant()) {
|
||||||
@@ -64,7 +62,7 @@ export default function Button(props: ButtonProps) {
|
|||||||
>
|
>
|
||||||
<Show when={local.loading} fallback={local.children}>
|
<Show when={local.loading} fallback={local.children}>
|
||||||
<svg
|
<svg
|
||||||
class="animate-spin h-5 w-5 mr-2"
|
class="mr-2 h-5 w-5 animate-spin"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
|
|||||||
helperText?: string;
|
helperText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable input component with label and error handling
|
|
||||||
* Styled to match Next.js migration source (underlined input style)
|
|
||||||
*/
|
|
||||||
export default function Input(props: InputProps) {
|
export default function Input(props: InputProps) {
|
||||||
const [local, others] = splitProps(props, ["label", "error", "helperText", "class"]);
|
const [local, others] = splitProps(props, [
|
||||||
|
"label",
|
||||||
|
"error",
|
||||||
|
"helperText",
|
||||||
|
"class"
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -23,22 +24,18 @@ export default function Input(props: InputProps) {
|
|||||||
aria-describedby={local.error ? `${others.id}-error` : undefined}
|
aria-describedby={local.error ? `${others.id}-error` : undefined}
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
<span class="bar"></span>
|
||||||
{local.label && (
|
{local.label && <label class="underlinedInputLabel">{local.label}</label>}
|
||||||
<label class="underlinedInputLabel">{local.label}</label>
|
|
||||||
)}
|
|
||||||
{local.error && (
|
{local.error && (
|
||||||
<span
|
<span
|
||||||
id={`${others.id}-error`}
|
id={`${others.id}-error`}
|
||||||
class="text-xs text-red-500 mt-1 block"
|
class="mt-1 block text-xs text-red-500"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{local.error}
|
{local.error}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{local.helperText && !local.error && (
|
{local.helperText && !local.error && (
|
||||||
<span class="text-xs text-gray-500 mt-1 block">
|
<span class="mt-1 block text-xs text-gray-500">{local.helperText}</span>
|
||||||
{local.helperText}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function EditPost() {
|
|||||||
subtitle: p.subtitle || "",
|
subtitle: p.subtitle || "",
|
||||||
body: p.body || "",
|
body: p.body || "",
|
||||||
banner_photo: p.banner_photo || "",
|
banner_photo: p.banner_photo || "",
|
||||||
published: p.published || false,
|
published: Boolean(p.published),
|
||||||
tags: tagValues,
|
tags: tagValues,
|
||||||
attachments: p.attachments
|
attachments: p.attachments
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
APIError
|
APIError
|
||||||
} from "~/server/fetch-utils";
|
} from "~/server/fetch-utils";
|
||||||
|
|
||||||
// Helper to create JWT token
|
|
||||||
async function createJWT(
|
async function createJWT(
|
||||||
userId: string,
|
userId: string,
|
||||||
expiresIn: string = "14d"
|
expiresIn: string = "14d"
|
||||||
@@ -29,7 +28,6 @@ async function createJWT(
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to send email via Brevo/SendInBlue with retry logic
|
|
||||||
async function sendEmail(to: string, subject: string, htmlContent: string) {
|
async function sendEmail(to: string, subject: string, htmlContent: string) {
|
||||||
const apiKey = env.SENDINBLUE_KEY;
|
const apiKey = env.SENDINBLUE_KEY;
|
||||||
const apiUrl = "https://api.sendinblue.com/v3/smtp/email";
|
const apiUrl = "https://api.sendinblue.com/v3/smtp/email";
|
||||||
@@ -68,14 +66,12 @@ async function sendEmail(to: string, subject: string, htmlContent: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const authRouter = createTRPCRouter({
|
export const authRouter = createTRPCRouter({
|
||||||
// GitHub callback route
|
|
||||||
githubCallback: publicProcedure
|
githubCallback: publicProcedure
|
||||||
.input(z.object({ code: z.string() }))
|
.input(z.object({ code: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { code } = input;
|
const { code } = input;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Exchange code for access token with timeout
|
|
||||||
const tokenResponse = await fetchWithTimeout(
|
const tokenResponse = await fetchWithTimeout(
|
||||||
"https://github.com/login/oauth/access_token",
|
"https://github.com/login/oauth/access_token",
|
||||||
{
|
{
|
||||||
@@ -103,7 +99,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch user info from GitHub with timeout
|
|
||||||
const userResponse = await fetchWithTimeout(
|
const userResponse = await fetchWithTimeout(
|
||||||
"https://api.github.com/user",
|
"https://api.github.com/user",
|
||||||
{
|
{
|
||||||
@@ -119,7 +114,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
const login = user.login;
|
const login = user.login;
|
||||||
const icon = user.avatar_url;
|
const icon = user.avatar_url;
|
||||||
|
|
||||||
// Fetch primary email from GitHub emails endpoint
|
|
||||||
const emailsResponse = await fetchWithTimeout(
|
const emailsResponse = await fetchWithTimeout(
|
||||||
"https://api.github.com/user/emails",
|
"https://api.github.com/user/emails",
|
||||||
{
|
{
|
||||||
@@ -133,7 +127,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
await checkResponse(emailsResponse);
|
await checkResponse(emailsResponse);
|
||||||
const emails = await emailsResponse.json();
|
const emails = await emailsResponse.json();
|
||||||
|
|
||||||
// Find primary verified email
|
|
||||||
const primaryEmail = emails.find(
|
const primaryEmail = emails.find(
|
||||||
(e: { primary: boolean; verified: boolean; email: string }) =>
|
(e: { primary: boolean; verified: boolean; email: string }) =>
|
||||||
e.primary && e.verified
|
e.primary && e.verified
|
||||||
@@ -143,7 +136,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
// Check if user exists
|
|
||||||
const query = `SELECT * FROM User WHERE provider = ? AND display_name = ?`;
|
const query = `SELECT * FROM User WHERE provider = ? AND display_name = ?`;
|
||||||
const params = ["github", login];
|
const params = ["github", login];
|
||||||
const res = await conn.execute({ sql: query, args: params });
|
const res = await conn.execute({ sql: query, args: params });
|
||||||
@@ -151,7 +143,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
let userId: string;
|
let userId: string;
|
||||||
|
|
||||||
if (res.rows[0]) {
|
if (res.rows[0]) {
|
||||||
// User exists - update email and image if changed
|
|
||||||
userId = (res.rows[0] as unknown as User).id;
|
userId = (res.rows[0] as unknown as User).id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -160,7 +151,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
args: [email, emailVerified ? 1 : 0, icon, userId]
|
args: [email, emailVerified ? 1 : 0, icon, userId]
|
||||||
});
|
});
|
||||||
} catch (updateError: any) {
|
} catch (updateError: any) {
|
||||||
// Handle unique constraint error on email
|
|
||||||
if (
|
if (
|
||||||
updateError.code === "SQLITE_CONSTRAINT" &&
|
updateError.code === "SQLITE_CONSTRAINT" &&
|
||||||
updateError.message?.includes("User.email")
|
updateError.message?.includes("User.email")
|
||||||
@@ -174,7 +164,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
throw updateError;
|
throw updateError;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new user
|
|
||||||
userId = uuidV4();
|
userId = uuidV4();
|
||||||
|
|
||||||
const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`;
|
const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
@@ -190,7 +179,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
await conn.execute({ sql: insertQuery, args: insertParams });
|
await conn.execute({ sql: insertQuery, args: insertParams });
|
||||||
} catch (insertError: any) {
|
} catch (insertError: any) {
|
||||||
// Handle unique constraint error on email
|
|
||||||
if (
|
if (
|
||||||
insertError.code === "SQLITE_CONSTRAINT" &&
|
insertError.code === "SQLITE_CONSTRAINT" &&
|
||||||
insertError.message?.includes("User.email")
|
insertError.message?.includes("User.email")
|
||||||
@@ -205,10 +193,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JWT token
|
|
||||||
const token = await createJWT(userId);
|
const token = await createJWT(userId);
|
||||||
|
|
||||||
// Set cookie
|
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
|
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
|
||||||
maxAge: 60 * 60 * 24 * 14, // 14 days
|
maxAge: 60 * 60 * 24 * 14, // 14 days
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -226,7 +212,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide specific error messages for different failure types
|
|
||||||
if (error instanceof TimeoutError) {
|
if (error instanceof TimeoutError) {
|
||||||
console.error("GitHub API timeout:", error.message);
|
console.error("GitHub API timeout:", error.message);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -255,14 +240,12 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Google callback route
|
|
||||||
googleCallback: publicProcedure
|
googleCallback: publicProcedure
|
||||||
.input(z.object({ code: z.string() }))
|
.input(z.object({ code: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { code } = input;
|
const { code } = input;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Exchange code for access token with timeout
|
|
||||||
const tokenResponse = await fetchWithTimeout(
|
const tokenResponse = await fetchWithTimeout(
|
||||||
"https://oauth2.googleapis.com/token",
|
"https://oauth2.googleapis.com/token",
|
||||||
{
|
{
|
||||||
@@ -291,7 +274,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch user info from Google with timeout
|
|
||||||
const userResponse = await fetchWithTimeout(
|
const userResponse = await fetchWithTimeout(
|
||||||
"https://www.googleapis.com/oauth2/v3/userinfo",
|
"https://www.googleapis.com/oauth2/v3/userinfo",
|
||||||
{
|
{
|
||||||
@@ -311,7 +293,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
// Check if user exists
|
|
||||||
const query = `SELECT * FROM User WHERE provider = ? AND email = ?`;
|
const query = `SELECT * FROM User WHERE provider = ? AND email = ?`;
|
||||||
const params = ["google", email];
|
const params = ["google", email];
|
||||||
const res = await conn.execute({ sql: query, args: params });
|
const res = await conn.execute({ sql: query, args: params });
|
||||||
@@ -319,16 +300,13 @@ export const authRouter = createTRPCRouter({
|
|||||||
let userId: string;
|
let userId: string;
|
||||||
|
|
||||||
if (res.rows[0]) {
|
if (res.rows[0]) {
|
||||||
// User exists - update email, email_verified, display_name, and image if changed
|
|
||||||
userId = (res.rows[0] as unknown as User).id;
|
userId = (res.rows[0] as unknown as User).id;
|
||||||
|
|
||||||
// No need to catch constraint error here since we're updating the same user's record
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: `UPDATE User SET email = ?, email_verified = ?, display_name = ?, image = ? WHERE id = ?`,
|
sql: `UPDATE User SET email = ?, email_verified = ?, display_name = ?, image = ? WHERE id = ?`,
|
||||||
args: [email, email_verified ? 1 : 0, name, image, userId]
|
args: [email, email_verified ? 1 : 0, name, image, userId]
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Create new user
|
|
||||||
userId = uuidV4();
|
userId = uuidV4();
|
||||||
|
|
||||||
const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`;
|
const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
@@ -347,7 +325,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
args: insertParams
|
args: insertParams
|
||||||
});
|
});
|
||||||
} catch (insertError: any) {
|
} catch (insertError: any) {
|
||||||
// Handle unique constraint error on email
|
|
||||||
if (
|
if (
|
||||||
insertError.code === "SQLITE_CONSTRAINT" &&
|
insertError.code === "SQLITE_CONSTRAINT" &&
|
||||||
insertError.message?.includes("User.email")
|
insertError.message?.includes("User.email")
|
||||||
@@ -362,10 +339,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JWT token
|
|
||||||
const token = await createJWT(userId);
|
const token = await createJWT(userId);
|
||||||
|
|
||||||
// Set cookie
|
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
|
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
|
||||||
maxAge: 60 * 60 * 24 * 14, // 14 days
|
maxAge: 60 * 60 * 24 * 14, // 14 days
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -383,7 +358,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide specific error messages for different failure types
|
|
||||||
if (error instanceof TimeoutError) {
|
if (error instanceof TimeoutError) {
|
||||||
console.error("Google API timeout:", error.message);
|
console.error("Google API timeout:", error.message);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -412,7 +386,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Email login route
|
|
||||||
emailLogin: publicProcedure
|
emailLogin: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -425,11 +398,9 @@ export const authRouter = createTRPCRouter({
|
|||||||
const { email, token, rememberMe } = input;
|
const { email, token, rememberMe } = input;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify JWT token
|
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const { payload } = await jwtVerify(token, secret);
|
const { payload } = await jwtVerify(token, secret);
|
||||||
|
|
||||||
// Check if email matches
|
|
||||||
if (payload.email !== email) {
|
if (payload.email !== email) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -451,10 +422,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const userId = (res.rows[0] as unknown as User).id;
|
const userId = (res.rows[0] as unknown as User).id;
|
||||||
|
|
||||||
// Create JWT token
|
|
||||||
const userToken = await createJWT(userId);
|
const userToken = await createJWT(userId);
|
||||||
|
|
||||||
// Set cookie based on rememberMe flag
|
|
||||||
const cookieOptions: any = {
|
const cookieOptions: any = {
|
||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
@@ -490,7 +459,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Email verification route
|
|
||||||
emailVerification: publicProcedure
|
emailVerification: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -502,11 +470,9 @@ export const authRouter = createTRPCRouter({
|
|||||||
const { email, token } = input;
|
const { email, token } = input;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify JWT token
|
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const { payload } = await jwtVerify(token, secret);
|
const { payload } = await jwtVerify(token, secret);
|
||||||
|
|
||||||
// Check if email matches
|
|
||||||
if (payload.email !== email) {
|
if (payload.email !== email) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -535,7 +501,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Email/password registration
|
|
||||||
emailRegistration: publicProcedure
|
emailRegistration: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -564,10 +529,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
args: [userId, email, passwordHash, "email"]
|
args: [userId, email, passwordHash, "email"]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create JWT token
|
|
||||||
const token = await createJWT(userId);
|
const token = await createJWT(userId);
|
||||||
|
|
||||||
// Set cookie
|
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
|
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
|
||||||
maxAge: 60 * 60 * 24 * 14, // 14 days
|
maxAge: 60 * 60 * 24 * 14, // 14 days
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -586,7 +549,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Email/password login
|
|
||||||
emailPasswordLogin: publicProcedure
|
emailPasswordLogin: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -640,11 +602,9 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JWT token with appropriate expiry
|
|
||||||
const expiresIn = rememberMe ? "14d" : "12h";
|
const expiresIn = rememberMe ? "14d" : "12h";
|
||||||
const token = await createJWT(user.id, expiresIn);
|
const token = await createJWT(user.id, expiresIn);
|
||||||
|
|
||||||
// Set cookie
|
|
||||||
const cookieOptions: any = {
|
const cookieOptions: any = {
|
||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
@@ -661,7 +621,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
return { success: true, message: "success" };
|
return { success: true, message: "success" };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Request email login link
|
|
||||||
requestEmailLinkLogin: publicProcedure
|
requestEmailLinkLogin: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -673,7 +632,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
const { email, rememberMe } = input;
|
const { email, rememberMe } = input;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check rate limiting
|
|
||||||
const requested = getCookie(
|
const requested = getCookie(
|
||||||
ctx.event.nativeEvent,
|
ctx.event.nativeEvent,
|
||||||
"emailLoginLinkRequested"
|
"emailLoginLinkRequested"
|
||||||
@@ -702,7 +660,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JWT token for email link (15min expiry)
|
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const token = await new SignJWT({
|
const token = await new SignJWT({
|
||||||
email,
|
email,
|
||||||
@@ -754,7 +711,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await sendEmail(email, "freno.me login link", htmlContent);
|
await sendEmail(email, "freno.me login link", htmlContent);
|
||||||
|
|
||||||
// Set rate limit cookie (2 minutes)
|
|
||||||
const exp = new Date(Date.now() + 2 * 60 * 1000);
|
const exp = new Date(Date.now() + 2 * 60 * 1000);
|
||||||
setCookie(
|
setCookie(
|
||||||
ctx.event.nativeEvent,
|
ctx.event.nativeEvent,
|
||||||
@@ -772,7 +728,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle email sending failures gracefully
|
|
||||||
if (
|
if (
|
||||||
error instanceof TimeoutError ||
|
error instanceof TimeoutError ||
|
||||||
error instanceof NetworkError ||
|
error instanceof NetworkError ||
|
||||||
@@ -793,7 +748,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Request password reset
|
|
||||||
requestPasswordReset: publicProcedure
|
requestPasswordReset: publicProcedure
|
||||||
.input(z.object({ email: z.string().email() }))
|
.input(z.object({ email: z.string().email() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -896,7 +850,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle email sending failures gracefully
|
|
||||||
if (
|
if (
|
||||||
error instanceof TimeoutError ||
|
error instanceof TimeoutError ||
|
||||||
error instanceof NetworkError ||
|
error instanceof NetworkError ||
|
||||||
@@ -1108,7 +1061,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle email sending failures gracefully
|
|
||||||
if (
|
if (
|
||||||
error instanceof TimeoutError ||
|
error instanceof TimeoutError ||
|
||||||
error instanceof NetworkError ||
|
error instanceof NetworkError ||
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ import { cache, withCacheAndStale } from "~/server/cache";
|
|||||||
const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
export const databaseRouter = createTRPCRouter({
|
export const databaseRouter = createTRPCRouter({
|
||||||
// ============================================================
|
|
||||||
// Comment Reactions Routes
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
getCommentReactions: publicProcedure
|
getCommentReactions: publicProcedure
|
||||||
.input(z.object({ commentID: z.string() }))
|
.input(z.object({ commentID: z.string() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
@@ -105,14 +101,9 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Comments Routes
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
getAllComments: publicProcedure.query(async () => {
|
getAllComments: publicProcedure.query(async () => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
// Join with Post table to get post titles along with comments
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT c.*, p.title as post_title
|
SELECT c.*, p.title as post_title
|
||||||
FROM Comment c
|
FROM Comment c
|
||||||
@@ -148,7 +139,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
privilegeLevel: ctx.privilegeLevel
|
privilegeLevel: ctx.privilegeLevel
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the comment to check ownership
|
|
||||||
const commentQuery = await conn.execute({
|
const commentQuery = await conn.execute({
|
||||||
sql: "SELECT * FROM Comment WHERE id = ?",
|
sql: "SELECT * FROM Comment WHERE id = ?",
|
||||||
args: [input.commentID]
|
args: [input.commentID]
|
||||||
@@ -162,7 +152,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization checks
|
|
||||||
const isOwner = comment.commenter_id === ctx.userId;
|
const isOwner = comment.commenter_id === ctx.userId;
|
||||||
const isAdmin = ctx.privilegeLevel === "admin";
|
const isAdmin = ctx.privilegeLevel === "admin";
|
||||||
|
|
||||||
|
|||||||
@@ -19,17 +19,11 @@ class SimpleCache {
|
|||||||
return entry.data as T;
|
return entry.data as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cached data even if expired (for stale-while-revalidate)
|
|
||||||
*/
|
|
||||||
getStale<T>(key: string): T | null {
|
getStale<T>(key: string): T | null {
|
||||||
const entry = this.cache.get(key);
|
const entry = this.cache.get(key);
|
||||||
return entry ? (entry.data as T) : null;
|
return entry ? (entry.data as T) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if cache entry exists (regardless of expiration)
|
|
||||||
*/
|
|
||||||
has(key: string): boolean {
|
has(key: string): boolean {
|
||||||
return this.cache.has(key);
|
return this.cache.has(key);
|
||||||
}
|
}
|
||||||
@@ -49,9 +43,6 @@ class SimpleCache {
|
|||||||
this.cache.delete(key);
|
this.cache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all keys starting with a prefix
|
|
||||||
*/
|
|
||||||
deleteByPrefix(prefix: string): void {
|
deleteByPrefix(prefix: string): void {
|
||||||
for (const key of this.cache.keys()) {
|
for (const key of this.cache.keys()) {
|
||||||
if (key.startsWith(prefix)) {
|
if (key.startsWith(prefix)) {
|
||||||
@@ -62,8 +53,6 @@ class SimpleCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const cache = new SimpleCache();
|
export const cache = new SimpleCache();
|
||||||
|
|
||||||
// Helper function to wrap async operations with caching
|
|
||||||
export async function withCache<T>(
|
export async function withCache<T>(
|
||||||
key: string,
|
key: string,
|
||||||
ttlMs: number,
|
ttlMs: number,
|
||||||
@@ -80,7 +69,6 @@ export async function withCache<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache wrapper with stale-while-revalidate support
|
|
||||||
* Returns stale data if fetch fails, with optional stale time limit
|
* Returns stale data if fetch fails, with optional stale time limit
|
||||||
*/
|
*/
|
||||||
export async function withCacheAndStale<T>(
|
export async function withCacheAndStale<T>(
|
||||||
@@ -88,19 +76,17 @@ export async function withCacheAndStale<T>(
|
|||||||
ttlMs: number,
|
ttlMs: number,
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
options: {
|
options: {
|
||||||
maxStaleMs?: number; // Maximum age of stale data to return (default: 7 days)
|
maxStaleMs?: number;
|
||||||
logErrors?: boolean; // Whether to log errors (default: true)
|
logErrors?: boolean;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const { maxStaleMs = 7 * 24 * 60 * 60 * 1000, logErrors = true } = options;
|
const { maxStaleMs = 7 * 24 * 60 * 60 * 1000, logErrors = true } = options;
|
||||||
|
|
||||||
// Try to get fresh cached data
|
|
||||||
const cached = cache.get<T>(key, ttlMs);
|
const cached = cache.get<T>(key, ttlMs);
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to fetch new data
|
|
||||||
try {
|
try {
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
cache.set(key, result);
|
cache.set(key, result);
|
||||||
@@ -110,10 +96,8 @@ export async function withCacheAndStale<T>(
|
|||||||
console.error(`Error fetching data for cache key "${key}":`, error);
|
console.error(`Error fetching data for cache key "${key}":`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If fetch fails, try to serve stale data
|
|
||||||
const stale = cache.getStale<T>(key);
|
const stale = cache.getStale<T>(key);
|
||||||
if (stale !== null) {
|
if (stale !== null) {
|
||||||
// Check if stale data is within acceptable age
|
|
||||||
const entry = (cache as any).cache.get(key);
|
const entry = (cache as any).cache.get(key);
|
||||||
const age = Date.now() - entry.timestamp;
|
const age = Date.now() - entry.timestamp;
|
||||||
|
|
||||||
@@ -127,7 +111,6 @@ export async function withCacheAndStale<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No stale data available or too old, re-throw the error
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
/**
|
|
||||||
* Server-side conditional parser for blog content
|
|
||||||
* Evaluates conditional blocks and returns processed HTML
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get safe environment variables for conditional evaluation
|
|
||||||
* Only exposes non-sensitive variables that are safe to use in content conditionals
|
|
||||||
*/
|
|
||||||
export function getSafeEnvVariables(): Record<string, string | undefined> {
|
export function getSafeEnvVariables(): Record<string, string | undefined> {
|
||||||
return {
|
return {
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
VERCEL_ENV: process.env.VERCEL_ENV
|
VERCEL_ENV: process.env.VERCEL_ENV
|
||||||
// Add other safe, non-sensitive env vars here as needed
|
|
||||||
// DO NOT expose API keys, secrets, database URLs, etc.
|
// DO NOT expose API keys, secrets, database URLs, etc.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -33,12 +23,6 @@ interface ConditionalBlock {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse HTML and evaluate conditional blocks (both block and inline)
|
|
||||||
* @param html - Raw HTML from database
|
|
||||||
* @param context - Evaluation context (user, date, features)
|
|
||||||
* @returns Processed HTML with conditionals evaluated
|
|
||||||
*/
|
|
||||||
export function parseConditionals(
|
export function parseConditionals(
|
||||||
html: string,
|
html: string,
|
||||||
context: ConditionalContext
|
context: ConditionalContext
|
||||||
@@ -47,40 +31,29 @@ export function parseConditionals(
|
|||||||
|
|
||||||
let processedHtml = html;
|
let processedHtml = html;
|
||||||
|
|
||||||
// First, process block-level conditionals (div elements)
|
|
||||||
processedHtml = processBlockConditionals(processedHtml, context);
|
processedHtml = processBlockConditionals(processedHtml, context);
|
||||||
|
|
||||||
// Then, process inline conditionals (span elements)
|
|
||||||
processedHtml = processInlineConditionals(processedHtml, context);
|
processedHtml = processInlineConditionals(processedHtml, context);
|
||||||
|
|
||||||
return processedHtml;
|
return processedHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process block-level conditional divs
|
|
||||||
*/
|
|
||||||
function processBlockConditionals(
|
function processBlockConditionals(
|
||||||
html: string,
|
html: string,
|
||||||
context: ConditionalContext
|
context: ConditionalContext
|
||||||
): string {
|
): string {
|
||||||
// More flexible regex that handles attributes in any order
|
|
||||||
// Match div with class="conditional-block" and capture the full tag
|
|
||||||
const divRegex =
|
const divRegex =
|
||||||
/<div\s+([^>]*class="[^"]*conditional-block[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi;
|
/<div\s+([^>]*class="[^"]*conditional-block[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi;
|
||||||
|
|
||||||
let processedHtml = html;
|
let processedHtml = html;
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
// Reset regex lastIndex
|
|
||||||
divRegex.lastIndex = 0;
|
divRegex.lastIndex = 0;
|
||||||
|
|
||||||
// Collect all matches first to avoid regex state issues
|
|
||||||
const matches: ConditionalBlock[] = [];
|
const matches: ConditionalBlock[] = [];
|
||||||
while ((match = divRegex.exec(html)) !== null) {
|
while ((match = divRegex.exec(html)) !== null) {
|
||||||
const attributes = match[1];
|
const attributes = match[1];
|
||||||
const content = match[2];
|
const content = match[2];
|
||||||
|
|
||||||
// Extract individual attributes
|
|
||||||
const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes);
|
const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes);
|
||||||
const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes);
|
const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes);
|
||||||
const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes);
|
const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes);
|
||||||
@@ -96,7 +69,6 @@ function processBlockConditionals(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each conditional block
|
|
||||||
for (const block of matches) {
|
for (const block of matches) {
|
||||||
const shouldShow = evaluateCondition(
|
const shouldShow = evaluateCondition(
|
||||||
block.conditionType,
|
block.conditionType,
|
||||||
@@ -106,8 +78,6 @@ function processBlockConditionals(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
// Keep content, but remove conditional wrapper
|
|
||||||
// Extract content from inner <div class="conditional-content">
|
|
||||||
const innerContentRegex =
|
const innerContentRegex =
|
||||||
/<div\s+class="conditional-content">([\s\S]*?)<\/div>/i;
|
/<div\s+class="conditional-content">([\s\S]*?)<\/div>/i;
|
||||||
const innerMatch = block.fullMatch.match(innerContentRegex);
|
const innerMatch = block.fullMatch.match(innerContentRegex);
|
||||||
@@ -115,7 +85,6 @@ function processBlockConditionals(
|
|||||||
|
|
||||||
processedHtml = processedHtml.replace(block.fullMatch, innerContent);
|
processedHtml = processedHtml.replace(block.fullMatch, innerContent);
|
||||||
} else {
|
} else {
|
||||||
// Remove entire block
|
|
||||||
processedHtml = processedHtml.replace(block.fullMatch, "");
|
processedHtml = processedHtml.replace(block.fullMatch, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,31 +92,23 @@ function processBlockConditionals(
|
|||||||
return processedHtml;
|
return processedHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process inline conditional spans
|
|
||||||
*/
|
|
||||||
function processInlineConditionals(
|
function processInlineConditionals(
|
||||||
html: string,
|
html: string,
|
||||||
context: ConditionalContext
|
context: ConditionalContext
|
||||||
): string {
|
): string {
|
||||||
// More flexible regex that handles attributes in any order
|
|
||||||
// Match span with class="conditional-inline" and capture the full tag
|
|
||||||
const spanRegex =
|
const spanRegex =
|
||||||
/<span\s+([^>]*class="[^"]*conditional-inline[^"]*"[^>]*)>([\s\S]*?)<\/span>/gi;
|
/<span\s+([^>]*class="[^"]*conditional-inline[^"]*"[^>]*)>([\s\S]*?)<\/span>/gi;
|
||||||
|
|
||||||
let processedHtml = html;
|
let processedHtml = html;
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
// Reset regex lastIndex
|
|
||||||
spanRegex.lastIndex = 0;
|
spanRegex.lastIndex = 0;
|
||||||
|
|
||||||
// Collect all matches first
|
|
||||||
const matches: ConditionalBlock[] = [];
|
const matches: ConditionalBlock[] = [];
|
||||||
while ((match = spanRegex.exec(html)) !== null) {
|
while ((match = spanRegex.exec(html)) !== null) {
|
||||||
const attributes = match[1];
|
const attributes = match[1];
|
||||||
const content = match[2];
|
const content = match[2];
|
||||||
|
|
||||||
// Extract individual attributes
|
|
||||||
const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes);
|
const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes);
|
||||||
const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes);
|
const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes);
|
||||||
const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes);
|
const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes);
|
||||||
@@ -163,7 +124,6 @@ function processInlineConditionals(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each inline conditional
|
|
||||||
for (const inline of matches) {
|
for (const inline of matches) {
|
||||||
const shouldShow = evaluateCondition(
|
const shouldShow = evaluateCondition(
|
||||||
inline.conditionType,
|
inline.conditionType,
|
||||||
@@ -173,10 +133,8 @@ function processInlineConditionals(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
// Keep content, remove span wrapper
|
|
||||||
processedHtml = processedHtml.replace(inline.fullMatch, inline.content);
|
processedHtml = processedHtml.replace(inline.fullMatch, inline.content);
|
||||||
} else {
|
} else {
|
||||||
// Remove entire inline span
|
|
||||||
processedHtml = processedHtml.replace(inline.fullMatch, "");
|
processedHtml = processedHtml.replace(inline.fullMatch, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,9 +142,6 @@ function processInlineConditionals(
|
|||||||
return processedHtml;
|
return processedHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate a single condition
|
|
||||||
*/
|
|
||||||
function evaluateCondition(
|
function evaluateCondition(
|
||||||
conditionType: string,
|
conditionType: string,
|
||||||
conditionValue: string,
|
conditionValue: string,
|
||||||
@@ -212,18 +167,12 @@ function evaluateCondition(
|
|||||||
conditionMet = evaluateEnvCondition(conditionValue, context);
|
conditionMet = evaluateEnvCondition(conditionValue, context);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unknown condition type - default to hiding content for safety
|
|
||||||
conditionMet = false;
|
conditionMet = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply showWhen logic: if showWhen is true, show when condition is met
|
|
||||||
// If showWhen is false, show when condition is NOT met
|
|
||||||
return showWhen ? conditionMet : !conditionMet;
|
return showWhen ? conditionMet : !conditionMet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate authentication condition
|
|
||||||
*/
|
|
||||||
function evaluateAuthCondition(
|
function evaluateAuthCondition(
|
||||||
value: string,
|
value: string,
|
||||||
context: ConditionalContext
|
context: ConditionalContext
|
||||||
@@ -238,9 +187,6 @@ function evaluateAuthCondition(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate privilege level condition
|
|
||||||
*/
|
|
||||||
function evaluatePrivilegeCondition(
|
function evaluatePrivilegeCondition(
|
||||||
value: string,
|
value: string,
|
||||||
context: ConditionalContext
|
context: ConditionalContext
|
||||||
@@ -249,7 +195,6 @@ function evaluatePrivilegeCondition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate date-based condition
|
|
||||||
* Supports: "before:YYYY-MM-DD", "after:YYYY-MM-DD", "between:YYYY-MM-DD,YYYY-MM-DD"
|
* Supports: "before:YYYY-MM-DD", "after:YYYY-MM-DD", "between:YYYY-MM-DD,YYYY-MM-DD"
|
||||||
*/
|
*/
|
||||||
function evaluateDateCondition(
|
function evaluateDateCondition(
|
||||||
@@ -287,9 +232,6 @@ function evaluateDateCondition(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate feature flag condition
|
|
||||||
*/
|
|
||||||
function evaluateFeatureCondition(
|
function evaluateFeatureCondition(
|
||||||
value: string,
|
value: string,
|
||||||
context: ConditionalContext
|
context: ConditionalContext
|
||||||
@@ -298,7 +240,6 @@ function evaluateFeatureCondition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate environment variable condition
|
|
||||||
* Format: "ENV_VAR_NAME:expected_value" or "ENV_VAR_NAME:*" for any truthy value
|
* Format: "ENV_VAR_NAME:expected_value" or "ENV_VAR_NAME:*" for any truthy value
|
||||||
*/
|
*/
|
||||||
function evaluateEnvCondition(
|
function evaluateEnvCondition(
|
||||||
@@ -306,7 +247,6 @@ function evaluateEnvCondition(
|
|||||||
context: ConditionalContext
|
context: ConditionalContext
|
||||||
): boolean {
|
): boolean {
|
||||||
try {
|
try {
|
||||||
// Parse format: "VAR_NAME:expected_value"
|
|
||||||
const colonIndex = value.indexOf(":");
|
const colonIndex = value.indexOf(":");
|
||||||
if (colonIndex === -1) return false;
|
if (colonIndex === -1) return false;
|
||||||
|
|
||||||
@@ -315,12 +255,10 @@ function evaluateEnvCondition(
|
|||||||
|
|
||||||
const actualValue = context.env[varName];
|
const actualValue = context.env[varName];
|
||||||
|
|
||||||
// If expected value is "*", check if variable exists and is truthy
|
|
||||||
if (expectedValue === "*") {
|
if (expectedValue === "*") {
|
||||||
return !!actualValue;
|
return !!actualValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, check for exact match
|
|
||||||
return actualValue === expectedValue;
|
return actualValue === expectedValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing env condition:", error);
|
console.error("Error parsing env condition:", error);
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ export async function dumpAndSendDB({
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
// Fetch database dump with timeout
|
|
||||||
const res = await fetchWithTimeout(
|
const res = await fetchWithTimeout(
|
||||||
`https://${dbName}-mikefreno.turso.io/dump`,
|
`https://${dbName}-mikefreno.turso.io/dump`,
|
||||||
{
|
{
|
||||||
@@ -100,7 +99,7 @@ export async function dumpAndSendDB({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${dbToken}`
|
Authorization: `Bearer ${dbToken}`
|
||||||
},
|
},
|
||||||
timeout: 30000 // 30s for database dump
|
timeout: 30000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -132,7 +131,6 @@ export async function dumpAndSendDB({
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send email with retry logic
|
|
||||||
await fetchWithRetry(
|
await fetchWithRetry(
|
||||||
async () => {
|
async () => {
|
||||||
const sendRes = await fetchWithTimeout(apiUrl, {
|
const sendRes = await fetchWithTimeout(apiUrl, {
|
||||||
@@ -143,7 +141,7 @@ export async function dumpAndSendDB({
|
|||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(emailPayload),
|
body: JSON.stringify(emailPayload),
|
||||||
timeout: 20000 // 20s for email with attachment
|
timeout: 20000
|
||||||
});
|
});
|
||||||
|
|
||||||
await checkResponse(sendRes);
|
await checkResponse(sendRes);
|
||||||
@@ -157,7 +155,6 @@ export async function dumpAndSendDB({
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log specific error types for debugging
|
|
||||||
if (error instanceof TimeoutError) {
|
if (error instanceof TimeoutError) {
|
||||||
console.error("Database dump timeout:", error.message);
|
console.error("Database dump timeout:", error.message);
|
||||||
return { success: false, reason: "Database dump timed out" };
|
return { success: false, reason: "Database dump timed out" };
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* Feature flag system for conditional content
|
|
||||||
* Centralized configuration for feature toggles
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface FeatureFlags {
|
export interface FeatureFlags {
|
||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Error types for better error classification
|
|
||||||
export class NetworkError extends Error {
|
export class NetworkError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
@@ -34,9 +33,6 @@ interface FetchWithTimeoutOptions extends RequestInit {
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch wrapper with timeout support and proper error classification
|
|
||||||
*/
|
|
||||||
export async function fetchWithTimeout(
|
export async function fetchWithTimeout(
|
||||||
url: string,
|
url: string,
|
||||||
options: FetchWithTimeoutOptions = {}
|
options: FetchWithTimeoutOptions = {}
|
||||||
@@ -57,9 +53,7 @@ export async function fetchWithTimeout(
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
// Classify the error for better handling
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
// Check for abort/timeout
|
|
||||||
if (error.name === "AbortError") {
|
if (error.name === "AbortError") {
|
||||||
throw new TimeoutError(
|
throw new TimeoutError(
|
||||||
`Request to ${url} timed out after ${timeout}ms`,
|
`Request to ${url} timed out after ${timeout}ms`,
|
||||||
@@ -67,7 +61,6 @@ export async function fetchWithTimeout(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for connection errors (various runtime-specific errors)
|
|
||||||
if (
|
if (
|
||||||
error.message.includes("fetch failed") ||
|
error.message.includes("fetch failed") ||
|
||||||
error.message.includes("ECONNREFUSED") ||
|
error.message.includes("ECONNREFUSED") ||
|
||||||
@@ -84,14 +77,10 @@ export async function fetchWithTimeout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-throw unknown errors
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to check response status and throw APIError if not ok
|
|
||||||
*/
|
|
||||||
export async function checkResponse(response: Response): Promise<Response> {
|
export async function checkResponse(response: Response): Promise<Response> {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new APIError(
|
throw new APIError(
|
||||||
@@ -103,9 +92,6 @@ export async function checkResponse(response: Response): Promise<Response> {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Safe JSON parse that handles errors gracefully
|
|
||||||
*/
|
|
||||||
export async function safeJsonParse<T>(response: Response): Promise<T | null> {
|
export async function safeJsonParse<T>(response: Response): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
@@ -115,9 +101,6 @@ export async function safeJsonParse<T>(response: Response): Promise<T | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry logic with exponential backoff
|
|
||||||
*/
|
|
||||||
export async function fetchWithRetry<T>(
|
export async function fetchWithRetry<T>(
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
options: {
|
options: {
|
||||||
@@ -141,12 +124,10 @@ export async function fetchWithRetry<T>(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
|
||||||
// Don't retry if it's the last attempt or error is not retryable
|
|
||||||
if (attempt === maxRetries || !retryableErrors(error)) {
|
if (attempt === maxRetries || !retryableErrors(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exponential backoff
|
|
||||||
const delay = retryDelay * Math.pow(2, attempt);
|
const delay = retryDelay * Math.pow(2, attempt);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Comment System Type Definitions
|
* Comment System Type Definitions
|
||||||
*
|
*
|
||||||
* Types for the blog comment system including:
|
|
||||||
* - Comment and CommentReaction models
|
* - Comment and CommentReaction models
|
||||||
* - WebSocket message types
|
* - WebSocket message types
|
||||||
* - User data structures
|
* - User data structures
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
/**
|
|
||||||
* User type definitions matching database schema
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
email_verified: number; // SQLite boolean (0 or 1)
|
email_verified: number;
|
||||||
password_hash: string | null;
|
password_hash: string | null;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
provider: "email" | "google" | "github" | null;
|
provider: "email" | "google" | "github" | null;
|
||||||
@@ -19,9 +15,6 @@ export interface User {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-safe user data (excludes sensitive fields)
|
|
||||||
*/
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -32,9 +25,6 @@ export interface UserProfile {
|
|||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert database User to client-safe UserProfile
|
|
||||||
*/
|
|
||||||
export function toUserProfile(user: User): UserProfile {
|
export function toUserProfile(user: User): UserProfile {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -47,24 +37,15 @@ export function toUserProfile(user: User): UserProfile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT payload for session tokens
|
|
||||||
*/
|
|
||||||
export interface SessionPayload {
|
export interface SessionPayload {
|
||||||
id: string; // user ID
|
id: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT payload for email verification
|
|
||||||
*/
|
|
||||||
export interface EmailVerificationPayload {
|
export interface EmailVerificationPayload {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT payload for password reset
|
|
||||||
*/
|
|
||||||
export interface PasswordResetPayload {
|
export interface PasswordResetPayload {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user