Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bot/src/services/status_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self, bot: discord.Client, config: StatusAPIConfig) -> None:
self._app: Optional[web.Application] = None
self._cache: Dict[str, Any] = {"payload": None, "expires": 0.0}
self._streams_total = 0
self._last_tracks: Dict[int, Dict[str, Any]] = {}
self._lock = asyncio.Lock()
self._command_events: deque[float] = deque()
self._incident_events: deque[float] = deque()
Expand Down Expand Up @@ -158,7 +159,19 @@ async def on_track_start(self, event: TrackStartEvent) -> None:
if guild_id is None:
return
track_payload = self._track_payload(getattr(event, "track", None))
previous_payload = self._last_tracks.get(guild_id)
self.record_stream_event(guild_id=guild_id, track=track_payload)
if track_payload:
metadata = {**track_payload, "artist": track_payload.get("author", "")}
if previous_payload:
metadata["previous"] = {**previous_payload, "artist": previous_payload.get("author", "")}
self._queue_event({
"type": "track_start",
"guildId": str(guild_id),
"metadata": metadata,
"ts": int(time.time()),
})
self._last_tracks[guild_id] = track_payload

# ------------------------------------------------------------------ request handlers
async def _handle_status(self, request: web.Request) -> web.Response:
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default async function AboutPage() {
icon: Shield,
title: "Reliability & Security",
description:
"End-to-end encrypted connections, no data logging, and full compliance with Discord’s terms of service.",
"End-to-end encrypted connections, no data logging, and full compliance with Discord\u2019s terms of service.",
},
{
icon: Code2,
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
SiYoutube,
} from "react-icons/si"
import { FaMicrosoft } from "react-icons/fa"
import { buildDiscordLoginUrl } from "@/lib/config"
import { buildDiscordLoginUrl, getInternalBaseUrl } from "@/lib/config"
import { RoleBadge } from "@/components/role-badge"
import { logError } from "@/lib/utils/error-handling"

Expand Down Expand Up @@ -390,7 +390,7 @@ const [subscriptionPreview, setSubscriptionPreview] = useState<AccountSubscripti
const profileShareBase =
typeof window !== "undefined"
? window.location.origin
: process.env.NEXT_PUBLIC_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000")
: getInternalBaseUrl()
const profileShareUrl = profileShareSlug
? `${profileShareBase.replace(/\/$/, "")}/profile/${profileShareSlug}`
: ""
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/api/account/notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { logError } from "@/lib/utils/error-handling"

const appUrl =
process.env.NEXT_PUBLIC_URL ||
(process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000")
(process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3050")
const normalizedAppUrl = appUrl.replace(/\/$/, "")

type RouteDeps = {
Expand Down
14 changes: 9 additions & 5 deletions frontend/app/api/account/security/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { type NextRequest, NextResponse } from "next/server"
import { getUserSecurity, updateUserSecurity } from "@/lib/db"
import { requireAuth, verifyRequestForUser } from "@/lib/auth"

export async function GET(request: NextRequest) {
const discordId = request.nextUrl.searchParams.get("discordId")
if (!discordId) {
return NextResponse.json({ error: "discordId is required" }, { status: 400 })
}
const result = await requireAuth(request)
if (!result.ok) return result.response

const security = await getUserSecurity(discordId)
const security = await getUserSecurity(result.discordId)
return NextResponse.json(security)
}

Expand All @@ -18,6 +17,11 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: "discordId is required" }, { status: 400 })
}

const auth = await verifyRequestForUser(request, discordId)
if (!auth.valid) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 })
}

const allowedKeys = ["twoFactorEnabled", "loginAlerts", "twoFactorSecret", "lastPasswordChange"]
const sanitized: Record<string, any> = {}
for (const key of allowedKeys) {
Expand Down
36 changes: 31 additions & 5 deletions frontend/app/api/admin/auto-queue/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { type NextRequest, NextResponse } from "next/server"
import { getAutoQueueStats, listLearningLogs } from "@/lib/auto-queue"

// Note: In a real app, we'd use middleware or a helper to check for admin session.
// For now, we'll assume the request is authorized if it reaches this admin-scoped route.
import { checkAdmin } from "@/lib/auth"
import {
getAutoQueueStats,
listLearningLogs,
analyzeTrends,
getImprovementSuggestions,
getLearningSummary,
} from "@/lib/auto-queue"

export async function GET(request: NextRequest) {
const isAdmin = await checkAdmin(request)
if (!isAdmin) {
return NextResponse.json({ error: "forbidden" }, { status: 403 })
}

try {
const searchParams = request.nextUrl.searchParams
const action = searchParams.get("action")
Expand All @@ -20,7 +29,24 @@
return NextResponse.json(logs)
}

// Default: Return both
if (action === "trends") {
const guildId = searchParams.get("guildId")
const days = parseInt(searchParams.get("days") || "30", 10)

Check warning on line 34 in frontend/app/api/admin/auto-queue/route.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseInt` over `parseInt`.

See more on https://sonarcloud.io/project/issues?id=VectoDE_VectoBeat&issues=AZ3kmNvmOywZVpvvLOMY&open=AZ3kmNvmOywZVpvvLOMY&pullRequest=145
const trends = await analyzeTrends(guildId, days)
return NextResponse.json(trends)
}

if (action === "suggestions") {
const suggestions = await getImprovementSuggestions()
return NextResponse.json(suggestions)
}

if (action === "summary") {
const guildId = searchParams.get("guildId")
const summary = await getLearningSummary(guildId)
return NextResponse.json(summary)
}

const [stats, logs] = await Promise.all([
getAutoQueueStats(),
listLearningLogs(20)
Expand Down
5 changes: 4 additions & 1 deletion frontend/app/api/admin/bot-control/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "forbidden" }, { status: 403 })
}

const mapped = ACTIONS[action] || action
const mapped = ACTIONS[action]
if (!mapped) {
return NextResponse.json({ error: "invalid_action" }, { status: 400 })
}
const ok = await sendBotControlAction(mapped)
if (!ok) {
return NextResponse.json({ error: "control_failed" }, { status: 500 })
Expand Down
22 changes: 2 additions & 20 deletions frontend/app/api/admin/compliance/export-requests/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { NextResponse, type NextRequest } from "next/server"
import { cookies } from "next/headers"
import { verifyRequestForUser } from "@/lib/auth"
import { getPrismaClient, getUserRole } from "@/lib/db"

const checkAdmin = async (req: NextRequest) => {
const cookieStore = await cookies()
const discordId =
cookieStore.get("discord_user_id")?.value ||
cookieStore.get("discord_id")?.value ||
cookieStore.get("discordId")?.value ||
req.nextUrl.searchParams.get("discordId")

if (!discordId) return false

const verification = await verifyRequestForUser(req, discordId)
if (!verification.valid) return false

const role = await getUserRole(discordId)
return role === "admin" || role === "operator"
}
import { checkAdmin } from "@/lib/auth"
import { getPrismaClient } from "@/lib/db"

export async function GET(req: NextRequest) {
if (!await checkAdmin(req)) {
Expand Down
22 changes: 2 additions & 20 deletions frontend/app/api/admin/enterprise/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { NextResponse, type NextRequest } from "next/server"
import { cookies } from "next/headers"
import { verifyRequestForUser } from "@/lib/auth"
import { getPrismaClient, getUserRole } from "@/lib/db"

const checkAdmin = async (req: NextRequest) => {
const cookieStore = await cookies()
const discordId =
cookieStore.get("discord_user_id")?.value ||
cookieStore.get("discord_id")?.value ||
cookieStore.get("discordId")?.value ||
req.nextUrl.searchParams.get("discordId")

if (!discordId) return false

const verification = await verifyRequestForUser(req, discordId)
if (!verification.valid) return false

const role = await getUserRole(discordId)
return role === "admin" || role === "operator"
}
import { checkAdmin } from "@/lib/auth"
import { getPrismaClient } from "@/lib/db"

export async function GET(req: NextRequest) {
if (!await checkAdmin(req)) {
Expand Down
35 changes: 19 additions & 16 deletions frontend/app/api/admin/env/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"
import { verifyRequestForUser } from "@/lib/auth"
import { requireAuth, verifyRequestForUser } from "@/lib/auth"
import { getUserRole } from "@/lib/db"
import fs from "fs/promises"
import path from "path"
Expand Down Expand Up @@ -27,32 +27,35 @@ const writeEnvFile = async (target: "frontend" | "bot", entries: Record<string,
}

export async function GET(request: NextRequest) {
const discordId = request.nextUrl.searchParams.get("discordId")
const result = await requireAuth(request)
if (!result.ok) return result.response
const { discordId } = result
const targetParam = request.nextUrl.searchParams.get("target")
const target = targetParam === "bot" ? "bot" : "frontend"
if (!discordId) {
return NextResponse.json({ error: "discordId is required" }, { status: 400 })
}

const auth = await verifyRequestForUser(request, discordId)
if (!auth.valid) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 })
}

const role = await getUserRole(discordId)
if (!["admin", "operator"].includes(role)) {
return NextResponse.json({ error: "forbidden" }, { status: 403 })
}

const fileEnv = await readEnvFile(target)
const merged: Record<string, string> = { ...fileEnv }
for (const [key, value] of Object.entries(process.env)) {
if (typeof value === "string") {
merged[key] = value
}

const SENSITIVE_PATTERNS = [
/SECRET/i, /TOKEN/i, /PASSWORD/i, /KEY/i, /CREDENTIAL/i,
/DATABASE_URL/i, /REDIS_URL/i, /SMTP_PASS/i, /PRIVATE/i,
]
const isSensitive = (key: string) => SENSITIVE_PATTERNS.some((p) => p.test(key))
const redact = (key: string, value: string) => {
if (!isSensitive(key)) return value
if (!value) return ""
return value.length > 4 ? `${"*".repeat(value.length - 4)}${value.slice(-4)}` : "****"
}

const entries = Object.entries(merged).map(([key, value]) => ({ key, value }))
const entries = Object.entries(fileEnv).map(([key, value]) => ({
key,
value: redact(key, value),
configured: Boolean(value),
}))
return NextResponse.json({ entries, source: { file: getEnvPath(target), target } })
}

Expand Down
22 changes: 2 additions & 20 deletions frontend/app/api/admin/federation/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { NextResponse, type NextRequest } from "next/server"
import { cookies } from "next/headers"
import { verifyRequestForUser } from "@/lib/auth"
import { getPrismaClient, getUserRole } from "@/lib/db"

const checkAdmin = async (req: NextRequest) => {
const cookieStore = await cookies()
const discordId =
cookieStore.get("discord_user_id")?.value ||
cookieStore.get("discord_id")?.value ||
cookieStore.get("discordId")?.value ||
req.nextUrl.searchParams.get("discordId")

if (!discordId) return false

const verification = await verifyRequestForUser(req, discordId)
if (!verification.valid) return false

const role = await getUserRole(discordId)
return role === "admin" || role === "operator"
}
import { checkAdmin } from "@/lib/auth"
import { getPrismaClient } from "@/lib/db"

export async function GET(req: NextRequest) {
if (!await checkAdmin(req)) {
Expand Down
22 changes: 2 additions & 20 deletions frontend/app/api/admin/metrics/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { NextResponse, type NextRequest } from "next/server"
import { cookies } from "next/headers"
import { verifyRequestForUser } from "@/lib/auth"
import { getPrismaClient, getUserRole } from "@/lib/db"

const checkAdmin = async (req: NextRequest) => {
const cookieStore = await cookies()
const discordId =
cookieStore.get("discord_user_id")?.value ||
cookieStore.get("discord_id")?.value ||
cookieStore.get("discordId")?.value ||
req.nextUrl.searchParams.get("discordId")

if (!discordId) return false

const verification = await verifyRequestForUser(req, discordId)
if (!verification.valid) return false

const role = await getUserRole(discordId)
return role === "admin" || role === "operator"
}
import { checkAdmin } from "@/lib/auth"
import { getPrismaClient } from "@/lib/db"

export async function GET(req: NextRequest) {
if (!await checkAdmin(req)) {
Expand Down
22 changes: 2 additions & 20 deletions frontend/app/api/admin/plugins/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { NextResponse, type NextRequest } from "next/server"
import { cookies } from "next/headers"
import { verifyRequestForUser } from "@/lib/auth"
import { getPrismaClient, getUserRole } from "@/lib/db"

const checkAdmin = async (req: NextRequest) => {
const cookieStore = await cookies()
const discordId =
cookieStore.get("discord_user_id")?.value ||
cookieStore.get("discord_id")?.value ||
cookieStore.get("discordId")?.value ||
req.nextUrl.searchParams.get("discordId")

if (!discordId) return false

const verification = await verifyRequestForUser(req, discordId)
if (!verification.valid) return false

const role = await getUserRole(discordId)
return role === "admin" || role === "operator"
}
import { checkAdmin } from "@/lib/auth"
import { getPrismaClient } from "@/lib/db"

export async function GET(req: NextRequest) {
if (!await checkAdmin(req)) {
Expand Down
Loading
Loading