diff --git a/app/(config-wrapper)/settings/account/page.tsx b/app/(config-wrapper)/settings/account/page.tsx index 7149f5cb..c8444d92 100644 --- a/app/(config-wrapper)/settings/account/page.tsx +++ b/app/(config-wrapper)/settings/account/page.tsx @@ -24,7 +24,7 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert" -import { ChangePasswordError, ChangePasswordRequest, ChangePasswordSuccess } from "@/app/api/v1/auth/change-password/route" +import { ChangePasswordRequest } from "@/packages/sdk/types/auth/change-password" import { useRouter } from "next/navigation" import { postAuthChangePassword } from "@/lib/apiClient"; import { DialogDescription } from "@radix-ui/react-dialog" diff --git a/app/api/v1/auth/change-password/route.ts b/app/api/v1/auth/change-password/route.ts index 02165d06..38c4b3ac 100644 --- a/app/api/v1/auth/change-password/route.ts +++ b/app/api/v1/auth/change-password/route.ts @@ -1,6 +1,8 @@ import { getServerPB } from "@/lib/pb"; +import { ChangePasswordRequest, ChangePasswordResponse } from "@/packages/sdk/types/auth/change-password"; import { NextResponse } from "next/server"; +/** Changes the authenticated user's password and reissues a token when re-auth succeeds. */ export async function POST(request: Request): Promise> { try { const body = (await request.json().catch(() => ({}))) as ChangePasswordRequest; @@ -81,21 +83,3 @@ export async function POST(request: Request): Promise { - let json: any; + const serverPb = getServerPB(); + const authHeader = req.headers.get("authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const token = authHeader.split(" ")[1]; + serverPb.authStore.save(token, null); + + let authData; try { - json = await req.json(); + authData = await serverPb.collection("users").authRefresh(); } catch { - throw new Response("Invalid or empty JSON body", { status: 400 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - if (!json || typeof json !== "object") { - throw new Response("Invalid JSON body", { status: 400 }); + const userId = authData?.record?.id; + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - if (!json.oldCategory || typeof json.oldCategory !== "string") { - throw new Response("'oldCategory' is required", { status: 400 }); + const superPb = await getSuperuserPB(); + const filter = `userId="${escapeFilter(userId)}"`; + + let record: any = null; + try { + record = await superPb.collection("newsFeeds").getFirstListItem(filter); + } catch (e: any) { + if (e?.status === 404) { + return NextResponse.json({ message: "No feeds found" }, { status: 404 }); + } + throw e; } - if (!json.newCategory || typeof json.newCategory !== "string") { - throw new Response("'newCategory' is required", { status: 400 }); + const oldCat = body.oldCategory; + const newCat = body.newCategory; + + let current = Array.isArray(record.subscriptions) + ? record.subscriptions.map((x: any) => (typeof x === "string" ? { feedUrl: x } : x)) + : []; + + let changed = false; + current = current.map((s: any) => { + const cat = (s.category ?? "").toString(); + if (cat === oldCat) { + changed = true; + return { ...s, category: newCat }; + } + return s; + }); + + if (changed) { + await superPb.collection("newsFeeds").update(record.id, { + subscriptions: current, + }); } - return { - oldCategory: json.oldCategory, - newCategory: json.newCategory, - }; + return NextResponse.json({ message: "Category rename applied", changed }, { status: 200 }); + } catch (err: any) { + console.error("Error in POST /api/v1/news/feed-category-rename:", err); + return NextResponse.json( + { error: "Internal Server Error", details: String(err?.message ?? err) }, + { status: 500 } + ); + } } -export async function POST(req: NextRequest) { - try { - const body = await validateBody(req); - - const serverPb = getServerPB(); - const authHeader = req.headers.get("authorization"); - - if (!authHeader?.startsWith("Bearer ")) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const token = authHeader.split(" ")[1]; - serverPb.authStore.save(token, null); - - let authData; - try { - authData = await serverPb.collection("users").authRefresh(); - } catch { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = authData?.record?.id; - if (!userId) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const superPb = await getSuperuserPB(); - - const filter = `userId="${escapeFilter(userId)}"`; - - let record: any = null; - try { - record = await superPb.collection("newsFeeds").getFirstListItem(filter); - } catch (e: any) { - if (e?.status === 404) { - return NextResponse.json({ message: "No feeds found" }, { status: 404 }); - } - throw e; - } - - const oldCat = body.oldCategory; - const newCat = body.newCategory; - - let current = Array.isArray(record.subscriptions) - ? record.subscriptions.map((x: any) => (typeof x === "string" ? { feedUrl: x } : x)) - : []; - - let changed = false; - current = current.map((s: any) => { - const cat = (s.category ?? "").toString(); - if (cat === oldCat) { - changed = true; - return { ...s, category: newCat }; - } - return s; - }); - - if (changed) { - await superPb.collection("newsFeeds").update(record.id, { - subscriptions: current, - }); - } - - return NextResponse.json({ message: "Category rename applied", changed }, { status: 200 }); - } catch (err: any) { - console.error("Error in POST /api/v1/news/feed-category-rename:", err); - return NextResponse.json( - { error: "Internal Server Error", details: String(err?.message ?? err) }, - { status: 500 } - ); - } +function escapeFilter(str: string) { + return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); +} + +async function validateBody(req: NextRequest): Promise { + let json: any; + + try { + json = await req.json(); + } catch { + throw new Response("Invalid or empty JSON body", { status: 400 }); + } + + if (!json || typeof json !== "object") { + throw new Response("Invalid JSON body", { status: 400 }); + } + + if (!json.oldCategory || typeof json.oldCategory !== "string") { + throw new Response("'oldCategory' is required", { status: 400 }); + } + + if (!json.newCategory || typeof json.newCategory !== "string") { + throw new Response("'newCategory' is required", { status: 400 }); + } + + return { + oldCategory: json.oldCategory, + newCategory: json.newCategory, + }; } diff --git a/app/api/v1/news/feed-update/route.ts b/app/api/v1/news/feed-update/route.ts index e40b9573..525169c3 100644 --- a/app/api/v1/news/feed-update/route.ts +++ b/app/api/v1/news/feed-update/route.ts @@ -1,120 +1,122 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerPB, getSuperuserPB } from "@/lib/pb"; +import { getServerPB } from "@/lib/pb"; +import { getSuperuserPB } from "@/lib/pb"; +import { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; interface UpdateRequestBody { - oldFeedUrl: string; - feedUrl: string; - name: string; - icon: string; - category: string; + oldFeedUrl: string; + feedUrl: string; + name: string; + icon: string; + category: string; } -function escapeFilter(str: string) { - return str.replace(/"/g, '\\"'); -} +/** Updates one existing subscription entry for the authenticated user. */ +export async function POST(req: NextRequest) { + try { + const body = await validateBody(req); -async function validateBody(req: NextRequest): Promise { - let json: any; + const serverPb = getServerPB(); + const authHeader = req.headers.get("authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const token = authHeader.split(" ")[1]; + serverPb.authStore.save(token, null); + + let authData; try { - json = await req.json(); + authData = await serverPb.collection("users").authRefresh(); } catch { - throw new Response("Invalid or empty JSON body", { status: 400 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - if (!json || typeof json !== "object") { - throw new Response("Invalid JSON body", { status: 400 }); + const userId = authData?.record?.id; + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - if (!json.oldFeedUrl || typeof json.oldFeedUrl !== "string") { - throw new Response("'oldFeedUrl' is required", { status: 400 }); + const superPb = await getSuperuserPB(); + const filter = `userId="${escapeFilter(userId)}"`; + + let record: any = null; + try { + record = await superPb.collection("newsFeeds").getFirstListItem(filter); + } catch (e: any) { + if (e?.status === 404) { + return NextResponse.json({ message: "No feeds found" }, { status: 404 }); + } + throw e; } - if (!json.feedUrl || typeof json.feedUrl !== "string") { - throw new Response("'feedUrl' is required", { status: 400 }); + let current = Array.isArray(record.subscriptions) + ? record.subscriptions.map((x: any) => (typeof x === "string" ? { feedUrl: x } : x)) + : []; + + let found = false; + current = current.map((s: any) => { + if (s.feedUrl === body.oldFeedUrl) { + found = true; + return { + feedUrl: body.feedUrl, + name: body.name || s.name || body.feedUrl, + icon: body.icon || s.icon || "", + category: body.category || s.category || "", + }; + } + return s; + }); + + if (!found) { + return NextResponse.json({ error: "Feed not found" }, { status: 404 }); } - return { - oldFeedUrl: json.oldFeedUrl, - feedUrl: json.feedUrl, - name: json.name || "", - icon: json.icon || "", - category: json.category || "", - }; + await superPb.collection("newsFeeds").update(record.id, { + subscriptions: current, + }); + + return NextResponse.json({ message: "Feed updated", subscriptions: current }, { status: 200 }); + } catch (err: any) { + console.error("Error in POST /api/v1/news/feed-update:", err); + return NextResponse.json( + { error: "Internal Server Error", details: String(err?.message ?? err) }, + { status: 500 } + ); + } } -export async function POST(req: NextRequest) { - try { - const body = await validateBody(req); - - const serverPb = getServerPB(); - const authHeader = req.headers.get("authorization"); - - if (!authHeader?.startsWith("Bearer ")) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const token = authHeader.split(" ")[1]; - serverPb.authStore.save(token, null); - - let authData; - try { - authData = await serverPb.collection("users").authRefresh(); - } catch { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const userId = authData?.record?.id; - if (!userId) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const superPb = await getSuperuserPB(); - - const filter = `userId="${escapeFilter(userId)}"`; - - let record: any = null; - try { - record = await superPb.collection("newsFeeds").getFirstListItem(filter); - } catch (e: any) { - if (e?.status === 404) { - return NextResponse.json({ message: "No feeds found" }, { status: 404 }); - } - throw e; - } - - let current = Array.isArray(record.subscriptions) - ? record.subscriptions.map((x: any) => (typeof x === "string" ? { feedUrl: x } : x)) - : []; - - let found = false; - current = current.map((s: any) => { - if (s.feedUrl === body.oldFeedUrl) { - found = true; - return { - feedUrl: body.feedUrl, - name: body.name || s.name || body.feedUrl, - icon: body.icon || s.icon || "", - category: body.category || s.category || "", - }; - } - return s; - }); - - if (!found) { - return NextResponse.json({ error: "Feed not found" }, { status: 404 }); - } - - await superPb.collection("newsFeeds").update(record.id, { - subscriptions: current, - }); - - return NextResponse.json({ message: "Feed updated", subscriptions: current }, { status: 200 }); - } catch (err: any) { - console.error("Error in POST /api/v1/news/feed-update:", err); - return NextResponse.json( - { error: "Internal Server Error", details: String(err?.message ?? err) }, - { status: 500 } - ); - } +function escapeFilter(str: string) { + return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); +} + +async function validateBody(req: NextRequest): Promise { + let json: any; + + try { + json = await req.json(); + } catch { + throw new Response("Invalid or empty JSON body", { status: 400 }); + } + + if (!json || typeof json !== "object") { + throw new Response("Invalid JSON body", { status: 400 }); + } + + if (!json.oldFeedUrl || typeof json.oldFeedUrl !== "string") { + throw new Response("'oldFeedUrl' is required", { status: 400 }); + } + + if (!json.feedUrl || typeof json.feedUrl !== "string") { + throw new Response("'feedUrl' is required", { status: 400 }); + } + + return { + oldFeedUrl: json.oldFeedUrl, + feedUrl: json.feedUrl, + name: json.name || "", + icon: json.icon || "", + category: json.category || "", + }; } diff --git a/app/api/v1/notifications/topics/route.ts b/app/api/v1/notifications/topics/route.ts index b8534101..06769abb 100644 --- a/app/api/v1/notifications/topics/route.ts +++ b/app/api/v1/notifications/topics/route.ts @@ -1,39 +1,6 @@ -import { NextRequest, NextResponse } from "next/server"; import { getServerPB } from "@/lib/pb"; - -export async function GET(req: NextRequest) { - try { - const pb = getServerPB(); - - // --- 1. Require Bearer auth - const authHeader = req.headers.get("authorization"); - if (!authHeader?.startsWith("Bearer ")) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const token = authHeader.split(" ")[1]; - pb.authStore.save(token, null); - - // refresh to validate token & get user ID - const authModel = await pb.collection("users").authRefresh(); - const userId = authModel?.record?.id; - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - // --- 2. Get notification topics for this user - const topics = await pb.collection("notificationTopics").getFullList({ - filter: `userId="${userId}"`, - }); - - // Return empty array if no topics found - return NextResponse.json({ items: topics.map(t => ({ id: t.id, title: t.title })) }); - } catch (err: any) { - console.error("Error in GET /notificationTopics", err); - return NextResponse.json( - { error: "Internal Server Error", details: err.message }, - { status: 500 } - ); - } -} +import { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; type NotificationTopic = { id: string; @@ -41,6 +8,38 @@ type NotificationTopic = { userId: string; }; +/** Returns notification topics for the authenticated user for selector UIs. */ +export async function GET(req: NextRequest) { + try { + const pb = getServerPB(); + const authHeader = req.headers.get("authorization"); + + if (!authHeader?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const token = authHeader.split(" ")[1]; + pb.authStore.save(token, null); + + const authModel = await pb.collection("users").authRefresh(); + const userId = authModel?.record?.id; + if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const topics = await pb.collection("notificationTopics").getFullList({ + filter: `userId="${userId}"`, + }); + + return NextResponse.json({ items: topics.map((t) => ({ id: t.id, title: t.title })) }); + } catch (err: any) { + console.error("Error in GET /notificationTopics", err); + return NextResponse.json( + { error: "Internal Server Error", details: err.message }, + { status: 500 } + ); + } +} + +/** Creates a topic when needed and seeds it with an initial system notification. */ export async function POST(req: NextRequest) { try { const body = await req.json(); @@ -51,8 +50,6 @@ export async function POST(req: NextRequest) { } const pb = getServerPB(); - - // --- Require Bearer auth const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -61,12 +58,10 @@ export async function POST(req: NextRequest) { const token = authHeader.split(" ")[1]; pb.authStore.save(token, null); - // Refresh to validate token & get user ID const authModel = await pb.collection("users").authRefresh(); const userId = authModel?.record?.id; if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - // --- Check if topic already exists let existingTopic: NotificationTopic | null = null; try { existingTopic = await pb @@ -80,14 +75,12 @@ export async function POST(req: NextRequest) { return NextResponse.json({ ok: true, topicId: existingTopic.id }); } - // --- Create new topic const created: NotificationTopic = await pb.collection("notificationTopics").create({ title, userId, priority: 1, }); - // --- Create initial "topic created" notification await pb.collection("notificationItems").create({ topicId: created.id, content: "Topic has been created", diff --git a/packages/sdk/types/auth/change-password.ts b/packages/sdk/types/auth/change-password.ts new file mode 100644 index 00000000..aa104cda --- /dev/null +++ b/packages/sdk/types/auth/change-password.ts @@ -0,0 +1,17 @@ +export interface ChangePasswordRequest { + email?: string; + oldPassword: string; + newPassword: string; + confirmPassword: string; +} + +export interface ChangePasswordSuccess { + message: string; + token?: string | null; +} + +export interface ChangePasswordError { + error: string; +} + +export type ChangePasswordResponse = ChangePasswordSuccess | ChangePasswordError;