Skip to content

Commit 939ac8c

Browse files
authored
Merge pull request #55 from ITSEC-Research/yoko-dev
refactor auth/sidebar and add feed management feature
2 parents f13aa25 + 0e7d06c commit 939ac8c

File tree

29 files changed

+1562
-22
lines changed

29 files changed

+1562
-22
lines changed

app/api/auth/login/route.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ export async function POST(request: NextRequest) {
117117

118118
response.cookies.set("auth", token, getSecureCookieOptions(request))
119119

120+
// UI hint cookie: allows sidebar to render correct menu items immediately
121+
// without waiting for async /api/auth/get-user call.
122+
// Not httpOnly so client JS can read it. Not used for authorization.
123+
response.cookies.set("user_role", userRole, {
124+
httpOnly: false,
125+
secure: isRequestSecure(request),
126+
sameSite: 'strict',
127+
maxAge: 24 * 60 * 60,
128+
path: '/',
129+
})
130+
120131
return response
121132
} catch (err) {
122133
console.error("Login error:", err)

app/api/auth/logout/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextRequest, NextResponse } from "next/server";
2-
import { getSecureCookieOptions } from "@/lib/auth";
2+
import { getSecureCookieOptions, isRequestSecure } from "@/lib/auth";
33

44
export async function POST(request: NextRequest) {
55
const response = NextResponse.json({ success: true });
@@ -10,5 +10,14 @@ export async function POST(request: NextRequest) {
1010
maxAge: 0,
1111
});
1212

13+
// Clear the UI hint cookie
14+
response.cookies.set("user_role", "", {
15+
httpOnly: false,
16+
secure: isRequestSecure(request),
17+
sameSite: 'strict',
18+
maxAge: 0,
19+
path: '/',
20+
});
21+
1322
return response;
1423
}

app/api/auth/verify-totp/route.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ export async function POST(request: NextRequest) {
128128

129129
response.cookies.set("auth", token, getSecureCookieOptions(request))
130130

131+
// UI hint cookie: allows sidebar to render correct menu items immediately
132+
// without waiting for async /api/auth/get-user call.
133+
// Not httpOnly so client JS can read it. Not used for authorization.
134+
response.cookies.set("user_role", userRole, {
135+
httpOnly: false,
136+
secure: isRequestSecure(request),
137+
sameSite: 'strict',
138+
maxAge: 24 * 60 * 60,
139+
path: '/',
140+
})
141+
131142
// SECURITY: Clear the pending_2fa cookie after successful verification
132143
response.cookies.set("pending_2fa", "", {
133144
httpOnly: true,

app/api/db-sync/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ async function checkSchema(): Promise<SchemaCheckResult> {
355355
const warnings = differences.filter(d => d.severity === 'warning').length
356356

357357
return {
358-
isValid: criticalIssues === 0 && missingTables.length === 0,
358+
isValid: criticalIssues === 0 && missingTables.length === 0 && warnings === 0,
359359
schemaVersion: SCHEMA_VERSION,
360360
differences,
361361
missingTables,

app/api/feeds/articles/route.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
export const dynamic = 'force-dynamic'
2+
import { NextRequest, NextResponse } from "next/server"
3+
import { validateRequest } from "@/lib/auth"
4+
import { executeQuery } from "@/lib/mysql"
5+
import { triggerBackgroundSyncIfNeeded } from "@/lib/feed-sync"
6+
7+
export async function GET(request: NextRequest) {
8+
const user = await validateRequest(request)
9+
if (!user) {
10+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
11+
}
12+
13+
try {
14+
// 1. SWR: Perform a non-blocking check to see if feeds need a background sync
15+
// This function will spawn a background promise and return immediately
16+
triggerBackgroundSyncIfNeeded()
17+
18+
const { searchParams } = new URL(request.url)
19+
const category_slug = searchParams.get('category_slug')
20+
const source_id = searchParams.get('source_id')
21+
const search = searchParams.get('q')
22+
const startDate = searchParams.get('startDate')
23+
const endDate = searchParams.get('endDate')
24+
const page = parseInt(searchParams.get('page') || '1')
25+
const limit = parseInt(searchParams.get('limit') || '20')
26+
const offset = (page - 1) * limit
27+
28+
let query = `
29+
SELECT a.*, s.name as source_name, c.name as category_name
30+
FROM feed_articles a
31+
JOIN feed_sources s ON a.source_id = s.id
32+
JOIN feed_categories c ON s.category_id = c.id
33+
WHERE 1=1
34+
`
35+
const params: any[] = []
36+
37+
if (category_slug && category_slug !== 'all') {
38+
query += ` AND c.slug = ?`
39+
params.push(category_slug)
40+
}
41+
42+
if (source_id) {
43+
query += ` AND s.id = ?`
44+
params.push(source_id)
45+
}
46+
47+
if (search) {
48+
query += ` AND (a.title LIKE ? OR a.description LIKE ?)`
49+
params.push(`%${search}%`, `%${search}%`)
50+
}
51+
52+
if (startDate) {
53+
query += ` AND DATE(COALESCE(a.pub_date, a.created_at)) >= ?`
54+
params.push(startDate)
55+
}
56+
57+
if (endDate) {
58+
query += ` AND DATE(COALESCE(a.pub_date, a.created_at)) <= ?`
59+
params.push(endDate)
60+
}
61+
62+
// Get total count
63+
const countQuery = query.replace('SELECT a.*, s.name as source_name, c.name as category_name', 'SELECT COUNT(*) as total')
64+
const [countResult]: any = await executeQuery(countQuery, params)
65+
const total = countResult?.total || 0
66+
67+
// Get paginated data
68+
query += ` ORDER BY COALESCE(a.pub_date, a.created_at) DESC LIMIT ${Number(limit)} OFFSET ${Number(offset)}`
69+
70+
const articles = await executeQuery(query, params)
71+
72+
return NextResponse.json({
73+
success: true,
74+
articles,
75+
pagination: {
76+
page,
77+
limit,
78+
total,
79+
totalPages: Math.ceil(total / limit)
80+
}
81+
})
82+
} catch (error) {
83+
console.error("[NewsFeed] Error getting articles:", error)
84+
return NextResponse.json({ success: false, error: "Failed to fetch articles" }, { status: 500 })
85+
}
86+
}

app/api/feeds/categories/route.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { validateRequest, requireAdminRole } from "@/lib/auth"
3+
import { executeQuery } from "@/lib/mysql"
4+
5+
export async function GET(request: NextRequest) {
6+
const user = await validateRequest(request)
7+
if (!user) {
8+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
9+
}
10+
11+
try {
12+
const categories = await executeQuery(`SELECT * FROM feed_categories ORDER BY display_order ASC, name ASC`)
13+
return NextResponse.json({ success: true, categories })
14+
} catch (error) {
15+
console.error("[NewsFeed] Error getting categories:", error)
16+
return NextResponse.json({ success: false, error: "Failed to get categories" }, { status: 500 })
17+
}
18+
}
19+
20+
export async function POST(request: NextRequest) {
21+
const user = await validateRequest(request)
22+
if (!user) {
23+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
24+
}
25+
26+
const roleError = requireAdminRole(user)
27+
if (roleError) return roleError
28+
29+
try {
30+
const body = await request.json()
31+
const { name, slug, display_order = 0 } = body
32+
33+
if (!name || !slug) {
34+
return NextResponse.json({ success: false, error: "Name and slug are required" }, { status: 400 })
35+
}
36+
37+
const result = await executeQuery(
38+
`INSERT INTO feed_categories (name, slug, display_order) VALUES (?, ?, ?)`,
39+
[name, slug, display_order]
40+
)
41+
42+
return NextResponse.json({
43+
success: true,
44+
message: "Category created",
45+
id: (result as any).insertId
46+
})
47+
} catch (error: any) {
48+
console.error("[NewsFeed] Error creating category:", error)
49+
if (error.code === 'ER_DUP_ENTRY') {
50+
return NextResponse.json({ success: false, error: "A category with this slug already exists" }, { status: 400 })
51+
}
52+
return NextResponse.json({ success: false, error: "Failed to create category" }, { status: 500 })
53+
}
54+
}
55+
56+
export async function PUT(request: NextRequest) {
57+
const user = await validateRequest(request)
58+
if (!user) {
59+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
60+
}
61+
62+
const roleError = requireAdminRole(user)
63+
if (roleError) return roleError
64+
65+
try {
66+
const body = await request.json()
67+
const { id, name, slug, display_order } = body
68+
69+
if (!id || (!name && !slug && display_order === undefined)) {
70+
return NextResponse.json({ success: false, error: "ID and at least one field to update are required" }, { status: 400 })
71+
}
72+
73+
// Build dynamic update
74+
const updates: string[] = []
75+
const values: any[] = []
76+
77+
if (name) { updates.push('name = ?'); values.push(name) }
78+
if (slug) { updates.push('slug = ?'); values.push(slug) }
79+
if (display_order !== undefined) { updates.push('display_order = ?'); values.push(display_order) }
80+
81+
values.push(id)
82+
83+
await executeQuery(
84+
`UPDATE feed_categories SET ${updates.join(', ')} WHERE id = ?`,
85+
values
86+
)
87+
88+
return NextResponse.json({ success: true, message: "Category updated" })
89+
} catch (error: any) {
90+
console.error("[NewsFeed] Error updating category:", error)
91+
if (error.code === 'ER_DUP_ENTRY') {
92+
return NextResponse.json({ success: false, error: "A category with this slug already exists" }, { status: 400 })
93+
}
94+
return NextResponse.json({ success: false, error: "Failed to update category" }, { status: 500 })
95+
}
96+
}
97+
98+
export async function DELETE(request: NextRequest) {
99+
const user = await validateRequest(request)
100+
if (!user) {
101+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
102+
}
103+
104+
const roleError = requireAdminRole(user)
105+
if (roleError) return roleError
106+
107+
try {
108+
const { searchParams } = new URL(request.url)
109+
const id = searchParams.get('id')
110+
111+
if (!id) {
112+
return NextResponse.json({ success: false, error: "ID is required" }, { status: 400 })
113+
}
114+
115+
await executeQuery(`DELETE FROM feed_categories WHERE id = ?`, [id])
116+
117+
return NextResponse.json({ success: true, message: "Category deleted" })
118+
} catch (error) {
119+
console.error("[NewsFeed] Error deleting category:", error)
120+
return NextResponse.json({ success: false, error: "Failed to delete category" }, { status: 500 })
121+
}
122+
}

app/api/feeds/debug-fetch/route.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const dynamic = 'force-dynamic'
2+
import { NextResponse } from "next/server"
3+
import { executeQuery } from "@/lib/mysql"
4+
5+
export async function GET() {
6+
try {
7+
const rawJoin = await executeQuery(`
8+
SELECT a.*, s.name as source_name, c.name as category_name
9+
FROM feed_articles a
10+
JOIN feed_sources s ON a.source_id = s.id
11+
JOIN feed_categories c ON s.category_id = c.id
12+
LIMIT 5
13+
`)
14+
15+
// Explicitly test JSON serialization
16+
const testJson = JSON.stringify(rawJoin)
17+
18+
return NextResponse.json({
19+
success: true,
20+
data: JSON.parse(testJson), // If it serialized properly, this works
21+
count: (rawJoin as any[]).length
22+
})
23+
} catch (error: any) {
24+
return NextResponse.json({
25+
success: false,
26+
error_message: error.message,
27+
error_name: error.name,
28+
stack: error.stack
29+
}, { status: 500 })
30+
}
31+
}

app/api/feeds/debug/route.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const dynamic = 'force-dynamic'
2+
import { NextResponse } from "next/server"
3+
import { executeQuery } from "@/lib/mysql"
4+
5+
export async function GET() {
6+
try {
7+
const cats = await executeQuery("SELECT * FROM feed_categories")
8+
const sources = await executeQuery("SELECT * FROM feed_sources")
9+
const articlesQuery = "SELECT COUNT(*) as c FROM feed_articles"
10+
const [articles]: any = await executeQuery(articlesQuery)
11+
const joinRaw = await executeQuery(`
12+
SELECT a.id, a.title, c.slug
13+
FROM feed_articles a
14+
JOIN feed_sources s ON a.source_id = s.id
15+
JOIN feed_categories c ON s.category_id = c.id
16+
LIMIT 5
17+
`)
18+
return NextResponse.json({
19+
cats,
20+
sources,
21+
article_count: articles?.c || 0,
22+
joinRaw,
23+
})
24+
} catch (error: any) {
25+
return NextResponse.json({ error: error.message }, { status: 500 })
26+
}
27+
}

0 commit comments

Comments
 (0)