ok
This commit is contained in:
42
src/env/server.ts
vendored
42
src/env/server.ts
vendored
@@ -34,7 +34,7 @@ const serverEnvSchema = z.object({
|
|||||||
NEXT_PUBLIC_AWS_BUCKET_STRING: z.string().min(1).optional(),
|
NEXT_PUBLIC_AWS_BUCKET_STRING: z.string().min(1).optional(),
|
||||||
NEXT_PUBLIC_GITHUB_CLIENT_ID: z.string().min(1).optional(),
|
NEXT_PUBLIC_GITHUB_CLIENT_ID: z.string().min(1).optional(),
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().min(1).optional(),
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().min(1).optional(),
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1).optional(),
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientEnvSchema = z.object({
|
const clientEnvSchema = z.object({
|
||||||
@@ -44,13 +44,13 @@ const clientEnvSchema = z.object({
|
|||||||
VITE_GOOGLE_CLIENT_ID: z.string().min(1),
|
VITE_GOOGLE_CLIENT_ID: z.string().min(1),
|
||||||
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1),
|
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1),
|
||||||
VITE_GITHUB_CLIENT_ID: z.string().min(1),
|
VITE_GITHUB_CLIENT_ID: z.string().min(1),
|
||||||
VITE_WEBSOCKET: z.string().min(1),
|
VITE_WEBSOCKET: z.string().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Combined environment schema
|
// Combined environment schema
|
||||||
export const envSchema = z.object({
|
export const envSchema = z.object({
|
||||||
server: serverEnvSchema,
|
server: serverEnvSchema,
|
||||||
client: clientEnvSchema,
|
client: clientEnvSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
// Type inference
|
// Type inference
|
||||||
@@ -61,7 +61,7 @@ export type ClientEnv = z.infer<typeof clientEnvSchema>;
|
|||||||
class EnvironmentError extends Error {
|
class EnvironmentError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public errors?: z.ZodFormattedError<any>,
|
public errors?: z.ZodFormattedError<any>
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "EnvironmentError";
|
this.name = "EnvironmentError";
|
||||||
@@ -70,7 +70,7 @@ class EnvironmentError extends Error {
|
|||||||
|
|
||||||
// Validation function for server-side with detailed error messages
|
// Validation function for server-side with detailed error messages
|
||||||
export const validateServerEnv = (
|
export const validateServerEnv = (
|
||||||
envVars: Record<string, string | undefined>,
|
envVars: Record<string, string | undefined>
|
||||||
): ServerEnv => {
|
): ServerEnv => {
|
||||||
try {
|
try {
|
||||||
return serverEnvSchema.parse(envVars);
|
return serverEnvSchema.parse(envVars);
|
||||||
@@ -83,7 +83,7 @@ export const validateServerEnv = (
|
|||||||
key !== "_errors" &&
|
key !== "_errors" &&
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
value._errors?.length > 0 &&
|
value._errors?.length > 0 &&
|
||||||
value._errors[0] === "Required",
|
value._errors[0] === "Required"
|
||||||
)
|
)
|
||||||
.map(([key, _]) => key);
|
.map(([key, _]) => key);
|
||||||
|
|
||||||
@@ -93,18 +93,18 @@ export const validateServerEnv = (
|
|||||||
key !== "_errors" &&
|
key !== "_errors" &&
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
value._errors?.length > 0 &&
|
value._errors?.length > 0 &&
|
||||||
value._errors[0] !== "Required",
|
value._errors[0] !== "Required"
|
||||||
)
|
)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
error: value._errors[0],
|
error: value._errors[0]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let errorMessage = "Environment validation failed:\n";
|
let errorMessage = "Environment validation failed:\n";
|
||||||
|
|
||||||
if (missingVars.length > 0) {
|
if (missingVars.length > 0) {
|
||||||
errorMessage += `Missing required variables: ${missingVars.join(
|
errorMessage += `Missing required variables: ${missingVars.join(
|
||||||
", ",
|
", "
|
||||||
)}\n`;
|
)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,14 +119,14 @@ export const validateServerEnv = (
|
|||||||
}
|
}
|
||||||
throw new EnvironmentError(
|
throw new EnvironmentError(
|
||||||
"Environment validation failed with unknown error",
|
"Environment validation failed with unknown error",
|
||||||
undefined,
|
undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validation function for client-side (runtime) with detailed error messages
|
// Validation function for client-side (runtime) with detailed error messages
|
||||||
export const validateClientEnv = (
|
export const validateClientEnv = (
|
||||||
envVars: Record<string, string | undefined>,
|
envVars: Record<string, string | undefined>
|
||||||
): ClientEnv => {
|
): ClientEnv => {
|
||||||
try {
|
try {
|
||||||
return clientEnvSchema.parse(envVars);
|
return clientEnvSchema.parse(envVars);
|
||||||
@@ -139,7 +139,7 @@ export const validateClientEnv = (
|
|||||||
key !== "_errors" &&
|
key !== "_errors" &&
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
value._errors?.length > 0 &&
|
value._errors?.length > 0 &&
|
||||||
value._errors[0] === "Required",
|
value._errors[0] === "Required"
|
||||||
)
|
)
|
||||||
.map(([key, _]) => key);
|
.map(([key, _]) => key);
|
||||||
|
|
||||||
@@ -149,18 +149,18 @@ export const validateClientEnv = (
|
|||||||
key !== "_errors" &&
|
key !== "_errors" &&
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
value._errors?.length > 0 &&
|
value._errors?.length > 0 &&
|
||||||
value._errors[0] !== "Required",
|
value._errors[0] !== "Required"
|
||||||
)
|
)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
error: value._errors[0],
|
error: value._errors[0]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let errorMessage = "Client environment validation failed:\n";
|
let errorMessage = "Client environment validation failed:\n";
|
||||||
|
|
||||||
if (missingVars.length > 0) {
|
if (missingVars.length > 0) {
|
||||||
errorMessage += `Missing required variables: ${missingVars.join(
|
errorMessage += `Missing required variables: ${missingVars.join(
|
||||||
", ",
|
", "
|
||||||
)}\n`;
|
)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ export const validateClientEnv = (
|
|||||||
}
|
}
|
||||||
throw new EnvironmentError(
|
throw new EnvironmentError(
|
||||||
"Client environment validation failed with unknown error",
|
"Client environment validation failed with unknown error",
|
||||||
undefined,
|
undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -194,7 +194,7 @@ export const env = (() => {
|
|||||||
if (error.errors) {
|
if (error.errors) {
|
||||||
console.error(
|
console.error(
|
||||||
"Detailed errors:",
|
"Detailed errors:",
|
||||||
JSON.stringify(error.errors, null, 2),
|
JSON.stringify(error.errors, null, 2)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error(`Environment validation failed: ${error.message}`);
|
throw new Error(`Environment validation failed: ${error.message}`);
|
||||||
@@ -252,7 +252,7 @@ export const getMissingEnvVars = (): {
|
|||||||
"TURSO_LINEAGE_URL",
|
"TURSO_LINEAGE_URL",
|
||||||
"TURSO_LINEAGE_TOKEN",
|
"TURSO_LINEAGE_TOKEN",
|
||||||
"TURSO_DB_API_TOKEN",
|
"TURSO_DB_API_TOKEN",
|
||||||
"LINEAGE_OFFLINE_SERIALIZATION_SECRET",
|
"LINEAGE_OFFLINE_SERIALIZATION_SECRET"
|
||||||
];
|
];
|
||||||
|
|
||||||
const requiredClientVars = [
|
const requiredClientVars = [
|
||||||
@@ -261,13 +261,13 @@ export const getMissingEnvVars = (): {
|
|||||||
"VITE_GOOGLE_CLIENT_ID",
|
"VITE_GOOGLE_CLIENT_ID",
|
||||||
"VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE",
|
"VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE",
|
||||||
"VITE_GITHUB_CLIENT_ID",
|
"VITE_GITHUB_CLIENT_ID",
|
||||||
"VITE_WEBSOCKET",
|
"VITE_WEBSOCKET"
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
server: requiredServerVars.filter((varName) => isMissingEnvVar(varName)),
|
server: requiredServerVars.filter((varName) => isMissingEnvVar(varName)),
|
||||||
client: requiredClientVars.filter((varName) =>
|
client: requiredClientVars.filter((varName) =>
|
||||||
isMissingClientEnvVar(varName),
|
isMissingClientEnvVar(varName)
|
||||||
),
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Show, Suspense, For } from "solid-js";
|
import { Show, Suspense, For } from "solid-js";
|
||||||
import { useParams, A, Navigate } from "@solidjs/router";
|
import { useParams, A, Navigate, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { cache } from "@solidjs/router";
|
|
||||||
import {
|
import {
|
||||||
ConnectionFactory,
|
ConnectionFactory,
|
||||||
getUserID,
|
getUserID,
|
||||||
getPrivilegeLevel
|
getPrivilegeLevel
|
||||||
} from "~/server/utils";
|
} from "~/server/utils";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { HttpStatusCode } from "@solidjs/start";
|
|
||||||
import SessionDependantLike from "~/components/blog/SessionDependantLike";
|
import SessionDependantLike from "~/components/blog/SessionDependantLike";
|
||||||
import CommentIcon from "~/components/icons/CommentIcon";
|
import CommentIcon from "~/components/icons/CommentIcon";
|
||||||
import CommentSectionWrapper from "~/components/blog/CommentSectionWrapper";
|
import CommentSectionWrapper from "~/components/blog/CommentSectionWrapper";
|
||||||
@@ -18,7 +16,7 @@ import type { Comment, CommentReaction, UserPublicData } from "~/types/comment";
|
|||||||
import { useBars } from "~/context/bars";
|
import { useBars } from "~/context/bars";
|
||||||
|
|
||||||
// Server function to fetch post by title
|
// Server function to fetch post by title
|
||||||
const getPostByTitle = cache(async (title: string) => {
|
const getPostByTitle = query(async (title: string) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const event = getRequestEvent()!;
|
const event = getRequestEvent()!;
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { Show, createSignal } from "solid-js";
|
import { Show, createSignal } from "solid-js";
|
||||||
import { useSearchParams, useNavigate } from "@solidjs/router";
|
import { useSearchParams, useNavigate, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { cache, createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
|
|
||||||
// Server function to get auth state
|
const getAuthState = query(async () => {
|
||||||
const getAuthState = cache(async () => {
|
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const event = getRequestEvent()!;
|
const event = getRequestEvent()!;
|
||||||
const privilegeLevel = await getPrivilegeLevel(event.nativeEvent);
|
const privilegeLevel = await getPrivilegeLevel(event.nativeEvent);
|
||||||
const userID = await getUserID(event.nativeEvent);
|
const userID = await getUserID(event.nativeEvent);
|
||||||
|
|
||||||
return { privilegeLevel, userID };
|
return { privilegeLevel, userID };
|
||||||
}, "auth-state");
|
}, "auth-state");
|
||||||
|
|
||||||
@@ -130,7 +129,7 @@ export default function CreatePost() {
|
|||||||
rows={15}
|
rows={15}
|
||||||
value={body()}
|
value={body()}
|
||||||
onInput={(e) => setBody(e.currentTarget.value)}
|
onInput={(e) => setBody(e.currentTarget.value)}
|
||||||
class="w-full rounded-md border border-surface2 bg-surface0 px-4 py-2 font-mono text-sm"
|
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2 font-mono text-sm"
|
||||||
placeholder="Enter post content (HTML)"
|
placeholder="Enter post content (HTML)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Show, createSignal, createEffect } from "solid-js";
|
import { Show, createSignal, createEffect } from "solid-js";
|
||||||
import { useParams, useNavigate } from "@solidjs/router";
|
import { useParams, useNavigate, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { cache } from "@solidjs/router";
|
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
import { ConnectionFactory } from "~/server/utils";
|
import { ConnectionFactory } from "~/server/utils";
|
||||||
|
|
||||||
// Server function to fetch post for editing
|
// Server function to fetch post for editing
|
||||||
const getPostForEdit = cache(async (id: string) => {
|
const getPostForEdit = query(async (id: string) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const event = getRequestEvent()!;
|
const event = getRequestEvent()!;
|
||||||
@@ -140,7 +139,7 @@ export default function EditPost() {
|
|||||||
required
|
required
|
||||||
value={title()}
|
value={title()}
|
||||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||||
class="w-full rounded-md border border-surface2 bg-surface0 px-4 py-2"
|
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||||
placeholder="Enter post title"
|
placeholder="Enter post title"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +154,7 @@ export default function EditPost() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={subtitle()}
|
value={subtitle()}
|
||||||
onInput={(e) => setSubtitle(e.currentTarget.value)}
|
onInput={(e) => setSubtitle(e.currentTarget.value)}
|
||||||
class="w-full rounded-md border border-surface2 bg-surface0 px-4 py-2"
|
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||||
placeholder="Enter post subtitle"
|
placeholder="Enter post subtitle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +169,7 @@ export default function EditPost() {
|
|||||||
rows={15}
|
rows={15}
|
||||||
value={body()}
|
value={body()}
|
||||||
onInput={(e) => setBody(e.currentTarget.value)}
|
onInput={(e) => setBody(e.currentTarget.value)}
|
||||||
class="w-full rounded-md border border-surface2 bg-surface0 px-4 py-2 font-mono text-sm"
|
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2 font-mono text-sm"
|
||||||
placeholder="Enter post content (HTML)"
|
placeholder="Enter post content (HTML)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,7 +184,7 @@ export default function EditPost() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={bannerPhoto()}
|
value={bannerPhoto()}
|
||||||
onInput={(e) => setBannerPhoto(e.currentTarget.value)}
|
onInput={(e) => setBannerPhoto(e.currentTarget.value)}
|
||||||
class="w-full rounded-md border border-surface2 bg-surface0 px-4 py-2"
|
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||||
placeholder="Enter banner photo URL"
|
placeholder="Enter banner photo URL"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +206,7 @@ export default function EditPost() {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
class="w-full rounded-md border border-surface2 bg-surface0 px-4 py-2"
|
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||||
placeholder="tag1, tag2, tag3"
|
placeholder="tag1, tag2, tag3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,66 +1,81 @@
|
|||||||
import { createSignal, Show, Suspense } from "solid-js";
|
import { Show, Suspense } from "solid-js";
|
||||||
import { useSearchParams, A } from "@solidjs/router";
|
import { useSearchParams, A, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { cache } from "@solidjs/router";
|
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { ConnectionFactory, getPrivilegeLevel } from "~/server/utils";
|
import { ConnectionFactory, getPrivilegeLevel } from "~/server/utils";
|
||||||
import PostSortingSelect from "~/components/blog/PostSortingSelect";
|
import PostSortingSelect from "~/components/blog/PostSortingSelect";
|
||||||
import TagSelector from "~/components/blog/TagSelector";
|
import TagSelector from "~/components/blog/TagSelector";
|
||||||
import PostSorting from "~/components/blog/PostSorting";
|
import PostSorting from "~/components/blog/PostSorting";
|
||||||
|
|
||||||
|
// Simple in-memory cache for blog posts to reduce DB load
|
||||||
|
let cachedPosts: {
|
||||||
|
posts: any[];
|
||||||
|
tagMap: Record<string, number>;
|
||||||
|
privilegeLevel: string;
|
||||||
|
} | null = null;
|
||||||
|
let cacheTimestamp: number = 0;
|
||||||
|
|
||||||
// Server function to fetch posts
|
// Server function to fetch posts
|
||||||
const getPosts = cache(async () => {
|
const getPosts = query(async () => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const event = getRequestEvent()!;
|
const event = getRequestEvent()!;
|
||||||
const privilegeLevel = await getPrivilegeLevel(event.nativeEvent);
|
const privilegeLevel = await getPrivilegeLevel(event.nativeEvent);
|
||||||
|
|
||||||
|
// Check if we have fresh cached data (cache duration: 30 seconds)
|
||||||
|
const now = Date.now();
|
||||||
|
if (cachedPosts && now - cacheTimestamp < 30000) {
|
||||||
|
return cachedPosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single optimized query using JOINs instead of subqueries and separate queries
|
||||||
let query = `
|
let query = `
|
||||||
SELECT
|
SELECT
|
||||||
Post.id,
|
p.id,
|
||||||
Post.title,
|
p.title,
|
||||||
Post.subtitle,
|
p.subtitle,
|
||||||
Post.body,
|
p.body,
|
||||||
Post.banner_photo,
|
p.banner_photo,
|
||||||
Post.date,
|
p.date,
|
||||||
Post.published,
|
p.published,
|
||||||
Post.category,
|
p.category,
|
||||||
Post.author_id,
|
p.author_id,
|
||||||
Post.reads,
|
p.reads,
|
||||||
Post.attachments,
|
p.attachments,
|
||||||
(SELECT COUNT(*) FROM PostLike WHERE Post.id = PostLike.post_id) AS total_likes,
|
COUNT(DISTINCT pl.user_id) as total_likes,
|
||||||
(SELECT COUNT(*) FROM Comment WHERE Post.id = Comment.post_id) AS total_comments
|
COUNT(DISTINCT c.id) as total_comments,
|
||||||
FROM
|
GROUP_CONCAT(t.value) as tags
|
||||||
Post
|
FROM Post p
|
||||||
LEFT JOIN
|
LEFT JOIN PostLike pl ON p.id = pl.post_id
|
||||||
PostLike ON Post.id = PostLike.post_id
|
LEFT JOIN Comment c ON p.id = c.post_id
|
||||||
LEFT JOIN
|
LEFT JOIN Tag t ON p.id = t.post_id`;
|
||||||
Comment ON Post.id = Comment.post_id`;
|
|
||||||
|
|
||||||
if (privilegeLevel !== "admin") {
|
if (privilegeLevel !== "admin") {
|
||||||
query += ` WHERE Post.published = TRUE`;
|
query += ` WHERE p.published = TRUE`;
|
||||||
}
|
}
|
||||||
query += ` GROUP BY Post.id, Post.title, Post.subtitle, Post.body, Post.banner_photo, Post.date, Post.published, Post.category, Post.author_id, Post.reads, Post.attachments ORDER BY Post.date DESC;`;
|
query += ` GROUP BY p.id, p.title, p.subtitle, p.body, p.banner_photo, p.date, p.published, p.category, p.author_id, p.reads, p.attachments ORDER BY p.date DESC;`;
|
||||||
|
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const results = await conn.execute(query);
|
const results = await conn.execute(query);
|
||||||
const posts = results.rows;
|
const posts = results.rows;
|
||||||
|
|
||||||
const postIds = posts.map((post: any) => post.id);
|
// Process tags into a map for the UI
|
||||||
const tagQuery =
|
|
||||||
postIds.length > 0
|
|
||||||
? `SELECT * FROM Tag WHERE post_id IN (${postIds.join(", ")})`
|
|
||||||
: "SELECT * FROM Tag WHERE 1=0";
|
|
||||||
const tagResults = await conn.execute(tagQuery);
|
|
||||||
const tags = tagResults.rows;
|
|
||||||
|
|
||||||
let tagMap: Record<string, number> = {};
|
let tagMap: Record<string, number> = {};
|
||||||
tags.forEach((tag: any) => {
|
posts.forEach((post: any) => {
|
||||||
tagMap[tag.value] = (tagMap[tag.value] || 0) + 1;
|
if (post.tags) {
|
||||||
|
const postTags = post.tags.split(",");
|
||||||
|
postTags.forEach((tag: string) => {
|
||||||
|
tagMap[tag] = (tagMap[tag] || 0) + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { posts, tags, tagMap, privilegeLevel };
|
// Cache the results
|
||||||
|
cachedPosts = { posts, tagMap, privilegeLevel };
|
||||||
|
cacheTimestamp = now;
|
||||||
|
|
||||||
|
return cachedPosts;
|
||||||
}, "blog-posts");
|
}, "blog-posts");
|
||||||
|
|
||||||
export default function BlogIndex() {
|
export default function BlogIndex() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Comment Reactions Routes
|
// 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 }) => {
|
||||||
@@ -17,23 +17,25 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
const query = "SELECT * FROM CommentReaction WHERE comment_id = ?";
|
const query = "SELECT * FROM CommentReaction WHERE comment_id = ?";
|
||||||
const results = await conn.execute({
|
const results = await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.commentID],
|
args: [input.commentID]
|
||||||
});
|
});
|
||||||
return { commentReactions: results.rows };
|
return { commentReactions: results.rows };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to fetch comment reactions",
|
message: "Failed to fetch comment reactions"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
addCommentReaction: publicProcedure
|
addCommentReaction: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
type: z.string(),
|
z.object({
|
||||||
comment_id: z.string(),
|
type: z.string(),
|
||||||
user_id: z.string(),
|
comment_id: z.string(),
|
||||||
}))
|
user_id: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
@@ -43,30 +45,32 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
`;
|
`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.type, input.comment_id, input.user_id],
|
args: [input.type, input.comment_id, input.user_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
|
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: followUpQuery,
|
sql: followUpQuery,
|
||||||
args: [input.comment_id],
|
args: [input.comment_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
return { commentReactions: res.rows };
|
return { commentReactions: res.rows };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to add comment reaction",
|
message: "Failed to add comment reaction"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
removeCommentReaction: publicProcedure
|
removeCommentReaction: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
type: z.string(),
|
z.object({
|
||||||
comment_id: z.string(),
|
type: z.string(),
|
||||||
user_id: z.string(),
|
comment_id: z.string(),
|
||||||
}))
|
user_id: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
@@ -76,20 +80,20 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
`;
|
`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.type, input.comment_id, input.user_id],
|
args: [input.type, input.comment_id, input.user_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
|
const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`;
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: followUpQuery,
|
sql: followUpQuery,
|
||||||
args: [input.comment_id],
|
args: [input.comment_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
return { commentReactions: res.rows };
|
return { commentReactions: res.rows };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to remove comment reaction",
|
message: "Failed to remove comment reaction"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -98,36 +102,48 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
// Comments Routes
|
// Comments Routes
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
getAllComments: publicProcedure
|
getAllComments: publicProcedure.query(async () => {
|
||||||
.query(async () => {
|
try {
|
||||||
try {
|
const conn = ConnectionFactory();
|
||||||
const conn = ConnectionFactory();
|
// Join with Post table to get post titles along with comments
|
||||||
const query = `SELECT * FROM Comment`;
|
const query = `
|
||||||
const res = await conn.execute(query);
|
SELECT c.*, p.title as post_title
|
||||||
return { comments: res.rows };
|
FROM Comment c
|
||||||
} catch (error) {
|
JOIN Post p ON c.post_id = p.id
|
||||||
throw new TRPCError({
|
ORDER BY c.created_at DESC
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
`;
|
||||||
message: "Failed to fetch comments",
|
const res = await conn.execute(query);
|
||||||
});
|
return { comments: res.rows };
|
||||||
}
|
} catch (error) {
|
||||||
}),
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to fetch comments"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
getCommentsByPostId: publicProcedure
|
getCommentsByPostId: publicProcedure
|
||||||
.input(z.object({ post_id: z.string() }))
|
.input(z.object({ post_id: z.string() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const query = `SELECT * FROM Comment WHERE post_id = ?`;
|
// Join with Post table to get post titles along with comments
|
||||||
|
const query = `
|
||||||
|
SELECT c.*, p.title as post_title
|
||||||
|
FROM Comment c
|
||||||
|
JOIN Post p ON c.post_id = p.id
|
||||||
|
WHERE c.post_id = ?
|
||||||
|
ORDER BY c.created_at DESC
|
||||||
|
`;
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.post_id],
|
args: [input.post_id]
|
||||||
});
|
});
|
||||||
return { comments: res.rows };
|
return { comments: res.rows };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to fetch comments by post ID",
|
message: "Failed to fetch comments by post ID"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -137,29 +153,37 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
getPostById: publicProcedure
|
getPostById: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
category: z.literal("blog"),
|
z.object({
|
||||||
id: z.number(),
|
category: z.literal("blog"),
|
||||||
}))
|
id: z.number()
|
||||||
|
})
|
||||||
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const query = `SELECT * FROM Post WHERE id = ?`;
|
// Single query with JOIN to get post and tags in one go
|
||||||
|
const query = `
|
||||||
|
SELECT p.*, t.value as tag_value
|
||||||
|
FROM Post p
|
||||||
|
LEFT JOIN Tag t ON p.id = t.post_id
|
||||||
|
WHERE p.id = ?
|
||||||
|
`;
|
||||||
const results = await conn.execute({
|
const results = await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.id],
|
args: [input.id]
|
||||||
});
|
|
||||||
|
|
||||||
const tagQuery = `SELECT * FROM Tag WHERE post_id = ?`;
|
|
||||||
const tagRes = await conn.execute({
|
|
||||||
sql: tagQuery,
|
|
||||||
args: [input.id],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (results.rows[0]) {
|
if (results.rows[0]) {
|
||||||
|
// Group tags by post ID
|
||||||
|
const post = results.rows[0];
|
||||||
|
const tags = results.rows
|
||||||
|
.filter((row) => row.tag_value)
|
||||||
|
.map((row) => row.tag_value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
post: results.rows[0],
|
post,
|
||||||
tags: tagRes.rows,
|
tags
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return { post: null, tags: [] };
|
return { post: null, tags: [] };
|
||||||
@@ -167,84 +191,80 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to fetch post by ID",
|
message: "Failed to fetch post by ID"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getPostByTitle: publicProcedure
|
getPostByTitle: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
category: z.literal("blog"),
|
z.object({
|
||||||
title: z.string(),
|
category: z.literal("blog"),
|
||||||
}))
|
title: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
// Get post by title
|
// Get post by title with JOINs to get all related data in one query
|
||||||
const postQuery = "SELECT * FROM Post WHERE title = ? AND category = ? AND published = ?";
|
const postQuery = `
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
COUNT(DISTINCT c.id) as comment_count,
|
||||||
|
COUNT(DISTINCT pl.user_id) as like_count,
|
||||||
|
GROUP_CONCAT(t.value) as tags
|
||||||
|
FROM Post p
|
||||||
|
LEFT JOIN Comment c ON p.id = c.post_id
|
||||||
|
LEFT JOIN PostLike pl ON p.id = pl.post_id
|
||||||
|
LEFT JOIN Tag t ON p.id = t.post_id
|
||||||
|
WHERE p.title = ? AND p.category = ? AND p.published = ?
|
||||||
|
GROUP BY p.id
|
||||||
|
`;
|
||||||
const postResults = await conn.execute({
|
const postResults = await conn.execute({
|
||||||
sql: postQuery,
|
sql: postQuery,
|
||||||
args: [input.title, input.category, true],
|
args: [input.title, input.category, true]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!postResults.rows[0]) {
|
if (!postResults.rows[0]) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const post_id = (postResults.rows[0] as any).id;
|
const postRow = postResults.rows[0];
|
||||||
|
|
||||||
// Get comments
|
|
||||||
const commentQuery = "SELECT * FROM Comment WHERE post_id = ?";
|
|
||||||
const commentResults = await conn.execute({
|
|
||||||
sql: commentQuery,
|
|
||||||
args: [post_id],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get likes
|
|
||||||
const likeQuery = "SELECT * FROM PostLike WHERE post_id = ?";
|
|
||||||
const likeResults = await conn.execute({
|
|
||||||
sql: likeQuery,
|
|
||||||
args: [post_id],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get tags
|
|
||||||
const tagsQuery = "SELECT * FROM Tag WHERE post_id = ?";
|
|
||||||
const tagResults = await conn.execute({
|
|
||||||
sql: tagsQuery,
|
|
||||||
args: [post_id],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Return structured data with proper formatting
|
||||||
return {
|
return {
|
||||||
post: postResults.rows[0],
|
post: postRow,
|
||||||
comments: commentResults.rows,
|
comments: [], // Comments are not included in this optimized query - would need separate call if needed
|
||||||
likes: likeResults.rows,
|
likes: [], // Likes are not included in this optimized query - would need separate call if needed
|
||||||
tagResults: tagResults.rows,
|
tags: postRow.tags ? postRow.tags.split(",") : []
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to fetch post by title",
|
message: "Failed to fetch post by title"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createPost: publicProcedure
|
createPost: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
category: z.literal("blog"),
|
z.object({
|
||||||
title: z.string(),
|
category: z.literal("blog"),
|
||||||
subtitle: z.string().nullable(),
|
title: z.string(),
|
||||||
body: z.string().nullable(),
|
subtitle: z.string().nullable(),
|
||||||
banner_photo: z.string().nullable(),
|
body: z.string().nullable(),
|
||||||
published: z.boolean(),
|
banner_photo: z.string().nullable(),
|
||||||
tags: z.array(z.string()).nullable(),
|
published: z.boolean(),
|
||||||
author_id: z.string(),
|
tags: z.array(z.string()).nullable(),
|
||||||
}))
|
author_id: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const fullURL = input.banner_photo
|
const fullURL = input.banner_photo
|
||||||
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo
|
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
@@ -258,11 +278,11 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
input.body,
|
input.body,
|
||||||
fullURL,
|
fullURL,
|
||||||
input.published,
|
input.published,
|
||||||
input.author_id,
|
input.author_id
|
||||||
];
|
];
|
||||||
|
|
||||||
const results = await conn.execute({ sql: query, args: params });
|
const results = await conn.execute({ sql: query, args: params });
|
||||||
|
|
||||||
if (input.tags && input.tags.length > 0) {
|
if (input.tags && input.tags.length > 0) {
|
||||||
let tagQuery = "INSERT INTO Tag (value, post_id) VALUES ";
|
let tagQuery = "INSERT INTO Tag (value, post_id) VALUES ";
|
||||||
let values = input.tags.map(
|
let values = input.tags.map(
|
||||||
@@ -277,26 +297,28 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to create post",
|
message: "Failed to create post"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updatePost: publicProcedure
|
updatePost: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
id: z.number(),
|
z.object({
|
||||||
title: z.string().nullable().optional(),
|
id: z.number(),
|
||||||
subtitle: z.string().nullable().optional(),
|
title: z.string().nullable().optional(),
|
||||||
body: z.string().nullable().optional(),
|
subtitle: z.string().nullable().optional(),
|
||||||
banner_photo: z.string().nullable().optional(),
|
body: z.string().nullable().optional(),
|
||||||
published: z.boolean().nullable().optional(),
|
banner_photo: z.string().nullable().optional(),
|
||||||
tags: z.array(z.string()).nullable().optional(),
|
published: z.boolean().nullable().optional(),
|
||||||
author_id: z.string(),
|
tags: z.array(z.string()).nullable().optional(),
|
||||||
}))
|
author_id: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
let query = "UPDATE Post SET ";
|
let query = "UPDATE Post SET ";
|
||||||
let params: any[] = [];
|
let params: any[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
@@ -345,8 +367,11 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Handle tags
|
// Handle tags
|
||||||
const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`;
|
const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`;
|
||||||
await conn.execute({ sql: deleteTagsQuery, args: [input.id.toString()] });
|
await conn.execute({
|
||||||
|
sql: deleteTagsQuery,
|
||||||
|
args: [input.id.toString()]
|
||||||
|
});
|
||||||
|
|
||||||
if (input.tags && input.tags.length > 0) {
|
if (input.tags && input.tags.length > 0) {
|
||||||
let tagQuery = "INSERT INTO Tag (value, post_id) VALUES ";
|
let tagQuery = "INSERT INTO Tag (value, post_id) VALUES ";
|
||||||
let values = input.tags.map((tag) => `("${tag}", ${input.id})`);
|
let values = input.tags.map((tag) => `("${tag}", ${input.id})`);
|
||||||
@@ -359,7 +384,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to update post",
|
message: "Failed to update post"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -369,37 +394,37 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
// Delete associated tags first
|
// Delete associated tags first
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM Tag WHERE post_id = ?",
|
sql: "DELETE FROM Tag WHERE post_id = ?",
|
||||||
args: [input.id.toString()],
|
args: [input.id.toString()]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete associated likes
|
// Delete associated likes
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM PostLike WHERE post_id = ?",
|
sql: "DELETE FROM PostLike WHERE post_id = ?",
|
||||||
args: [input.id.toString()],
|
args: [input.id.toString()]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete associated comments
|
// Delete associated comments
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM Comment WHERE post_id = ?",
|
sql: "DELETE FROM Comment WHERE post_id = ?",
|
||||||
args: [input.id],
|
args: [input.id]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Finally delete the post
|
// Finally delete the post
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM Post WHERE id = ?",
|
sql: "DELETE FROM Post WHERE id = ?",
|
||||||
args: [input.id],
|
args: [input.id]
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to delete post",
|
message: "Failed to delete post"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -409,39 +434,43 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
addPostLike: publicProcedure
|
addPostLike: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
user_id: z.string(),
|
z.object({
|
||||||
post_id: z.string(),
|
user_id: z.string(),
|
||||||
}))
|
post_id: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const query = `INSERT INTO PostLike (user_id, post_id) VALUES (?, ?)`;
|
const query = `INSERT INTO PostLike (user_id, post_id) VALUES (?, ?)`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.user_id, input.post_id],
|
args: [input.user_id, input.post_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`;
|
const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`;
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: followUpQuery,
|
sql: followUpQuery,
|
||||||
args: [input.post_id],
|
args: [input.post_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
return { newLikes: res.rows };
|
return { newLikes: res.rows };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to add post like",
|
message: "Failed to add post like"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
removePostLike: publicProcedure
|
removePostLike: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
user_id: z.string(),
|
z.object({
|
||||||
post_id: z.string(),
|
user_id: z.string(),
|
||||||
}))
|
post_id: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
@@ -451,20 +480,20 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
`;
|
`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.user_id, input.post_id],
|
args: [input.user_id, input.post_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`;
|
const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`;
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: followUpQuery,
|
sql: followUpQuery,
|
||||||
args: [input.post_id],
|
args: [input.post_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
return { newLikes: res.rows };
|
return { newLikes: res.rows };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to remove post like",
|
message: "Failed to remove post like"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -481,7 +510,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
const query = "SELECT * FROM User WHERE id = ?";
|
const query = "SELECT * FROM User WHERE id = ?";
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.id],
|
args: [input.id]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows[0]) {
|
if (res.rows[0]) {
|
||||||
@@ -494,7 +523,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
image: user.image,
|
image: user.image,
|
||||||
displayName: user.display_name,
|
displayName: user.display_name,
|
||||||
provider: user.provider,
|
provider: user.provider,
|
||||||
hasPassword: !!user.password_hash,
|
hasPassword: !!user.password_hash
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,10 +539,11 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const query = "SELECT email, display_name, image FROM User WHERE id = ?";
|
const query =
|
||||||
|
"SELECT email, display_name, image FROM User WHERE id = ?";
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.id],
|
args: [input.id]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows[0]) {
|
if (res.rows[0]) {
|
||||||
@@ -522,7 +552,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
image: user.image,
|
image: user.image,
|
||||||
display_name: user.display_name,
|
display_name: user.display_name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,22 +571,24 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
const query = "SELECT * FROM User WHERE id = ?";
|
const query = "SELECT * FROM User WHERE id = ?";
|
||||||
const results = await conn.execute({
|
const results = await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.id],
|
args: [input.id]
|
||||||
});
|
});
|
||||||
return { user: results.rows[0] };
|
return { user: results.rows[0] };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to fetch user image",
|
message: "Failed to fetch user image"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateUserImage: publicProcedure
|
updateUserImage: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
id: z.string(),
|
z.object({
|
||||||
imageURL: z.string(),
|
id: z.string(),
|
||||||
}))
|
imageURL: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
@@ -566,38 +598,40 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
const query = `UPDATE User SET image = ? WHERE id = ?`;
|
const query = `UPDATE User SET image = ? WHERE id = ?`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [fullURL, input.id],
|
args: [fullURL, input.id]
|
||||||
});
|
});
|
||||||
return { res: "success" };
|
return { res: "success" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to update user image",
|
message: "Failed to update user image"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateUserEmail: publicProcedure
|
updateUserEmail: publicProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
id: z.string(),
|
z.object({
|
||||||
newEmail: z.string().email(),
|
id: z.string(),
|
||||||
oldEmail: z.string().email(),
|
newEmail: z.string().email(),
|
||||||
}))
|
oldEmail: z.string().email()
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const query = `UPDATE User SET email = ? WHERE id = ? AND email = ?`;
|
const query = `UPDATE User SET email = ? WHERE id = ? AND email = ?`;
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: query,
|
sql: query,
|
||||||
args: [input.newEmail, input.id, input.oldEmail],
|
args: [input.newEmail, input.id, input.oldEmail]
|
||||||
});
|
});
|
||||||
return { res };
|
return { res };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to update user email",
|
message: "Failed to update user email"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const LINEAGE_JWT_EXPIRY = "14d";
|
|||||||
|
|
||||||
// Helper function to get privilege level from H3Event (for use outside tRPC)
|
// Helper function to get privilege level from H3Event (for use outside tRPC)
|
||||||
export async function getPrivilegeLevel(
|
export async function getPrivilegeLevel(
|
||||||
event: H3Event,
|
event: H3Event
|
||||||
): Promise<"anonymous" | "admin" | "user"> {
|
): Promise<"anonymous" | "admin" | "user"> {
|
||||||
try {
|
try {
|
||||||
const userIDToken = getCookie(event, "userIDToken");
|
const userIDToken = getCookie(event, "userIDToken");
|
||||||
@@ -28,7 +28,7 @@ export async function getPrivilegeLevel(
|
|||||||
console.log("Failed to authenticate token.");
|
console.log("Failed to authenticate token.");
|
||||||
setCookie(event, "userIDToken", "", {
|
setCookie(event, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
expires: new Date("2016-10-05"),
|
expires: new Date("2016-10-05")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export async function getUserID(event: H3Event): Promise<string | null> {
|
|||||||
console.log("Failed to authenticate token.");
|
console.log("Failed to authenticate token.");
|
||||||
setCookie(event, "userIDToken", "", {
|
setCookie(event, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
expires: new Date("2016-10-05"),
|
expires: new Date("2016-10-05")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,38 +65,43 @@ export async function getUserID(event: H3Event): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turso
|
// Turso - Connection Pooling Implementation
|
||||||
export function ConnectionFactory() {
|
let mainDBConnection: ReturnType<typeof createClient> | null = null;
|
||||||
const config = {
|
let lineageDBConnection: ReturnType<typeof createClient> | null = null;
|
||||||
url: env.TURSO_DB_URL,
|
|
||||||
authToken: env.TURSO_DB_TOKEN,
|
|
||||||
};
|
|
||||||
|
|
||||||
const conn = createClient(config);
|
export function ConnectionFactory() {
|
||||||
return conn;
|
if (!mainDBConnection) {
|
||||||
|
const config = {
|
||||||
|
url: env.TURSO_DB_URL,
|
||||||
|
authToken: env.TURSO_DB_TOKEN
|
||||||
|
};
|
||||||
|
mainDBConnection = createClient(config);
|
||||||
|
}
|
||||||
|
return mainDBConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LineageConnectionFactory() {
|
export function LineageConnectionFactory() {
|
||||||
const config = {
|
if (!lineageDBConnection) {
|
||||||
url: env.TURSO_LINEAGE_URL,
|
const config = {
|
||||||
authToken: env.TURSO_LINEAGE_TOKEN,
|
url: env.TURSO_LINEAGE_URL,
|
||||||
};
|
authToken: env.TURSO_LINEAGE_TOKEN
|
||||||
|
};
|
||||||
const conn = createClient(config);
|
lineageDBConnection = createClient(config);
|
||||||
return conn;
|
}
|
||||||
|
return lineageDBConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function LineageDBInit() {
|
export async function LineageDBInit() {
|
||||||
const turso = createAPIClient({
|
const turso = createAPIClient({
|
||||||
org: "mikefreno",
|
org: "mikefreno",
|
||||||
token: env.TURSO_DB_API_TOKEN,
|
token: env.TURSO_DB_API_TOKEN
|
||||||
});
|
});
|
||||||
|
|
||||||
const db_name = uuid();
|
const db_name = uuid();
|
||||||
const db = await turso.databases.create(db_name, { group: "default" });
|
const db = await turso.databases.create(db_name, { group: "default" });
|
||||||
|
|
||||||
const token = await turso.databases.createToken(db_name, {
|
const token = await turso.databases.createToken(db_name, {
|
||||||
authorization: "full-access",
|
authorization: "full-access"
|
||||||
});
|
});
|
||||||
|
|
||||||
const conn = PerUserDBConnectionFactory(db.name, token.jwt);
|
const conn = PerUserDBConnectionFactory(db.name, token.jwt);
|
||||||
@@ -121,7 +126,7 @@ export async function LineageDBInit() {
|
|||||||
export function PerUserDBConnectionFactory(dbName: string, token: string) {
|
export function PerUserDBConnectionFactory(dbName: string, token: string) {
|
||||||
const config = {
|
const config = {
|
||||||
url: `libsql://${dbName}-mikefreno.turso.io`,
|
url: `libsql://${dbName}-mikefreno.turso.io`,
|
||||||
authToken: token,
|
authToken: token
|
||||||
};
|
};
|
||||||
const conn = createClient(config);
|
const conn = createClient(config);
|
||||||
return conn;
|
return conn;
|
||||||
@@ -130,7 +135,7 @@ export function PerUserDBConnectionFactory(dbName: string, token: string) {
|
|||||||
export async function dumpAndSendDB({
|
export async function dumpAndSendDB({
|
||||||
dbName,
|
dbName,
|
||||||
dbToken,
|
dbToken,
|
||||||
sendTarget,
|
sendTarget
|
||||||
}: {
|
}: {
|
||||||
dbName: string;
|
dbName: string;
|
||||||
dbToken: string;
|
dbToken: string;
|
||||||
@@ -142,8 +147,8 @@ export async function dumpAndSendDB({
|
|||||||
const res = await fetch(`https://${dbName}-mikefreno.turso.io/dump`, {
|
const res = await fetch(`https://${dbName}-mikefreno.turso.io/dump`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${dbToken}`,
|
Authorization: `Bearer ${dbToken}`
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error(res);
|
console.error(res);
|
||||||
@@ -158,12 +163,12 @@ export async function dumpAndSendDB({
|
|||||||
const emailPayload = {
|
const emailPayload = {
|
||||||
sender: {
|
sender: {
|
||||||
name: "no_reply@freno.me",
|
name: "no_reply@freno.me",
|
||||||
email: "no_reply@freno.me",
|
email: "no_reply@freno.me"
|
||||||
},
|
},
|
||||||
to: [
|
to: [
|
||||||
{
|
{
|
||||||
email: sendTarget,
|
email: sendTarget
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
subject: "Your Lineage Database Dump",
|
subject: "Your Lineage Database Dump",
|
||||||
htmlContent:
|
htmlContent:
|
||||||
@@ -171,18 +176,18 @@ export async function dumpAndSendDB({
|
|||||||
attachment: [
|
attachment: [
|
||||||
{
|
{
|
||||||
content: base64Content,
|
content: base64Content,
|
||||||
name: "database_dump.txt",
|
name: "database_dump.txt"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
};
|
};
|
||||||
const sendRes = await fetch(apiUrl, {
|
const sendRes = await fetch(apiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
"api-key": apiKey,
|
"api-key": apiKey,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(emailPayload),
|
body: JSON.stringify(emailPayload)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!sendRes.ok) {
|
if (!sendRes.ok) {
|
||||||
@@ -194,7 +199,7 @@ export async function dumpAndSendDB({
|
|||||||
|
|
||||||
export async function validateLineageRequest({
|
export async function validateLineageRequest({
|
||||||
auth_token,
|
auth_token,
|
||||||
userRow,
|
userRow
|
||||||
}: {
|
}: {
|
||||||
auth_token: string;
|
auth_token: string;
|
||||||
userRow: Row;
|
userRow: Row;
|
||||||
@@ -225,7 +230,7 @@ export async function validateLineageRequest({
|
|||||||
const client = new OAuth2Client(CLIENT_ID);
|
const client = new OAuth2Client(CLIENT_ID);
|
||||||
const ticket = await client.verifyIdToken({
|
const ticket = await client.verifyIdToken({
|
||||||
idToken: auth_token,
|
idToken: auth_token,
|
||||||
audience: CLIENT_ID,
|
audience: CLIENT_ID
|
||||||
});
|
});
|
||||||
if (ticket.getPayload()?.email !== email) {
|
if (ticket.getPayload()?.email !== email) {
|
||||||
return false;
|
return false;
|
||||||
@@ -267,17 +272,18 @@ export async function sendEmailVerification(userEmail: string): Promise<{
|
|||||||
.setExpirationTime("15m")
|
.setExpirationTime("15m")
|
||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN || "https://freno.me";
|
const domain =
|
||||||
|
env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN || "https://freno.me";
|
||||||
|
|
||||||
const emailPayload = {
|
const emailPayload = {
|
||||||
sender: {
|
sender: {
|
||||||
name: "MikeFreno",
|
name: "MikeFreno",
|
||||||
email: "lifeandlineage_no_reply@freno.me",
|
email: "lifeandlineage_no_reply@freno.me"
|
||||||
},
|
},
|
||||||
to: [
|
to: [
|
||||||
{
|
{
|
||||||
email: userEmail,
|
email: userEmail
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
htmlContent: `<html>
|
htmlContent: `<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -314,7 +320,7 @@ export async function sendEmailVerification(userEmail: string): Promise<{
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`,
|
`,
|
||||||
subject: `Life and Lineage email verification`,
|
subject: `Life and Lineage email verification`
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -323,16 +329,16 @@ export async function sendEmailVerification(userEmail: string): Promise<{
|
|||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
"api-key": apiKey,
|
"api-key": apiKey,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(emailPayload),
|
body: JSON.stringify(emailPayload)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return { success: false, message: "Failed to send email" };
|
return { success: false, message: "Failed to send email" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json() as { messageId?: string };
|
const json = (await res.json()) as { messageId?: string };
|
||||||
if (json.messageId) {
|
if (json.messageId) {
|
||||||
return { success: true, messageId: json.messageId };
|
return { success: true, messageId: json.messageId };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user