Skip to content
Open

chat #430

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
10bfec0
feat: build Next.js app with Supabase tables and routes
Feb 15, 2026
0f91c59
fix: unique SQL policy names in script
Feb 15, 2026
00f94ed
fix: configure Vercel for correct build directory
Apr 7, 2026
e60762c
Fix: Root tsconfig.json includes backend/ and frontend/ directories, …
vercel[bot] Apr 7, 2026
6c704c0
Merge pull request #33 from cipollas/v0/diegogenerali2-6931-18c8906a
vercel[bot] Apr 8, 2026
670381c
Merge branch 'main' into publish-to-sandbox
cipollas Apr 8, 2026
1e09a74
Merge pull request #34 from cipollas/publish-to-sandbox
cipollas Apr 8, 2026
e53b3b8
feat: rename "Chat Pionieri" to "App Pionieri" and add app_id for dat…
Apr 8, 2026
47321e7
feat: update SQL migration for app_source column
Apr 8, 2026
226940d
Merge pull request #35 from cipollas/app-pionieri-updates
vercel[bot] Apr 8, 2026
c180432
refactor: update all occurrences of "Chat Pionieri" to "App Pionieri"
Apr 8, 2026
b7083df
Merge pull request #36 from cipollas/v0/diegogenerali2-6931-43487e21
vercel[bot] Apr 8, 2026
982f61d
fix: force rebuild for privacy page to fix 404 error
Apr 8, 2026
bfa6565
Merge pull request #37 from cipollas/v0/diegogenerali2-6931-97609bd7
vercel[bot] Apr 8, 2026
8a659a9
feat: update APIs to use APP_SOURCE and fix table references
Apr 8, 2026
58f3902
Merge pull request #38 from cipollas/v0/diegogenerali2-6931-3a9732f5
vercel[bot] Apr 8, 2026
160a875
feat: add next.config for cross-origin and image optimization
Apr 11, 2026
f82ebc2
Merge pull request #39 from cipollas/v0/diegogenerali2-6931-9a4775a9
vercel[bot] Apr 11, 2026
f7009e0
fix: correct admin panel and ban API issues
Apr 16, 2026
4298314
Merge pull request #40 from cipollas/v0/diegogenerali2-6931-76d186ee
vercel[bot] Apr 16, 2026
b542405
feat: centralize constants and expose environment variables
Apr 24, 2026
aa39b44
Merge pull request #41 from cipollas/v0/diegogenerali2-6931-d7cd66a5
vercel[bot] Apr 24, 2026
f5e3557
fix: multiple code and SQL script fixes
Apr 26, 2026
9a9d559
Merge pull request #42 from cipollas/login-flow-review
vercel[bot] Apr 26, 2026
e33dc6a
fix: multiple bug fixes in user and access logs
Apr 26, 2026
bf47f1a
Merge pull request #43 from cipollas/v0/diegogenerali2-6931-159a277d
vercel[bot] Apr 26, 2026
a4b0b2a
feat: filter Supabase realtime for app_pionieri
Apr 26, 2026
5c7f094
Merge pull request #44 from cipollas/v0/diegogenerali2-6931-618396b0
vercel[bot] Apr 26, 2026
3f7a97e
fix: resolve admin access logs issues
Apr 30, 2026
266f67e
Merge pull request #46 from cipollas/v0/diegogenerali2-6931-ffd1e704
vercel[bot] Apr 30, 2026
b2f2a2e
fix: adapt database column check for existing table schema
May 3, 2026
5a238e7
Merge pull request #47 from cipollas/v0/diegogenerali2-6931-b47e7921
vercel[bot] May 3, 2026
1c70f5a
fix: resolve Supabase type inference error with dynamic select
May 3, 2026
3e3cb0f
Merge pull request #48 from cipollas/v0/diegogenerali2-6931-20fd2c1d
vercel[bot] May 3, 2026
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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ DOMAIN_VALIDATION_KEY=todo_developer_portal
# Pi Platform API Key:
PI_API_KEY=todo_developer_portal

# Supabase connection:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key

# App separazione database - NON MODIFICARE (identifica questa app nel DB condiviso)
# Le 4 app nel database: app_pionieri | chat_pionieri | marketplace | chat_bot_develop
NEXT_PUBLIC_APP_SOURCE=app_pionieri
NEXT_PUBLIC_ADMIN_USERNAME=cipollas

# Generate a random string, or roll your face on the keyboard to fill this value:
SESSION_SECRET=abcd1324_TODO

Expand Down
101 changes: 101 additions & 0 deletions app/api/admin/access-logs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { NextResponse } from "next/server"
import { getAdmin, APP_SOURCE, ADMIN_USERNAME } from "@/lib/supabase/admin"

export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url)
const adminUsername = searchParams.get("adminUsername")
const date = searchParams.get("date") // Format: YYYY-MM-DD

if (adminUsername !== ADMIN_USERNAME) {
return NextResponse.json({ error: "Non autorizzato" }, { status: 403 })
}

const supabase = getAdmin()

// Costruisce range giornaliero come stringhe pure YYYY-MM-DD
// per evitare problemi di timezone server vs client.
// Supabase confronta TIMESTAMPTZ con stringhe ISO — usiamo T00:00:00 e T23:59:59
// con offset +00:00 (UTC) cosi' copriamo l'intera giornata indipendentemente dal fuso.
const targetDateStr = date || new Date().toISOString().split("T")[0]
const startOfDay = `${targetDateStr}T00:00:00+00:00`
const endOfDay = `${targetDateStr}T23:59:59.999+00:00`

// Strategia di query doppia:
// Prima tenta con "logged_at" (script 006), poi fallback su "created_at" (script 002).
// In questo modo funziona sia prima che dopo l'esecuzione dello script 006.
// Il DB puo' avere la colonna chiamata "pi_uid" (script 002) oppure "user_id" (versione precedente).
// Proviamo tutte le combinazioni possibili di colonna ID e colonna timestamp
// finche' non troviamo quella che funziona nel DB reale.
type LogRow = {
id: string
uid: string // valore normalizzato (pi_uid o user_id che sia)
username: string
timestamp: string // valore normalizzato (logged_at o created_at che sia)
app_source: string
}

let logs: LogRow[] = []
let resolved = false

// Combinazioni da tentare in ordine: [colonna_uid, colonna_timestamp]
const combos = [
["pi_uid", "logged_at"],
["pi_uid", "created_at"],
["user_id", "logged_at"],
["user_id", "created_at"],
]

for (const [uidCol, tsCol] of combos) {
if (resolved) break

// Usiamo select("*") per evitare il ParserError di TypeScript con select dinamico.
// Filtriamo poi i campi necessari nel mapping.
const { data: rows, error } = await supabase
.from("access_logs")
.select("*")
.eq("app_source", APP_SOURCE)
.gte(tsCol, startOfDay)
.lte(tsCol, endOfDay)
.order(tsCol, { ascending: false })

// Verifica che la riga abbia effettivamente la colonna attesa (non undefined)
if (!error && rows && rows.length >= 0 && (rows.length === 0 || rows[0][uidCol] !== undefined)) {
logs = (rows as Record<string, unknown>[]).map((r) => ({
id: String(r.id ?? ""),
uid: String(r[uidCol] ?? ""),
username: String(r.username ?? ""),
timestamp: String(r[tsCol] ?? ""),
app_source: String(r.app_source ?? APP_SOURCE),
}))
resolved = true
}
}

if (!resolved) {
return NextResponse.json({ error: "Struttura tabella access_logs non riconosciuta. Eseguire gli script di migrazione." }, { status: 500 })
}

// Normalizza per il frontend: espone sia user_id che pi_uid e logged_at
const normalizedLogs = logs.map((log) => ({
id: log.id,
pi_uid: log.uid,
user_id: log.uid,
username: log.username,
logged_at: log.timestamp,
app_source: log.app_source,
}))

const uniqueUsers = new Set(logs.map((log) => log.uid))

return NextResponse.json({
logs: normalizedLogs,
totalAccesses: normalizedLogs.length,
uniqueUsers: uniqueUsers.size,
date: targetDateStr,
app: APP_SOURCE,
})
} catch {
return NextResponse.json({ error: "Errore del server" }, { status: 500 })
}
}
93 changes: 93 additions & 0 deletions app/api/admin/ban/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { NextResponse } from "next/server"
import { getAdmin, APP_SOURCE, ADMIN_USERNAME } from "@/lib/supabase/admin"

export async function POST(req: Request) {
try {
const { userId, adminUsername, reason } = await req.json()
if (adminUsername !== ADMIN_USERNAME) {
return NextResponse.json({ error: "Non autorizzato" }, { status: 403 })
}

const supabase = getAdmin()

// Get profile for this app
const { data: profile } = await supabase
.from("profiles")
.select("display_name")
.eq("id", userId)
.eq("app_source", APP_SOURCE)
.single()

if (!profile) return NextResponse.json({ error: "Utente non trovato" }, { status: 404 })

// Get pi_user for this app
const { data: piUser } = await supabase
.from("pi_users")
.select("pi_uid")
.eq("pi_username", profile.display_name)
.eq("app_source", APP_SOURCE)
.maybeSingle()

if (!piUser) return NextResponse.json({ error: "Pi user non trovato" }, { status: 404 })

// Ban user for this specific app only - check if already banned first
const { data: alreadyBanned } = await supabase
.from("banned_users")
.select("id")
.eq("pi_uid", piUser.pi_uid)
.eq("app_source", APP_SOURCE)
.maybeSingle()

if (!alreadyBanned) {
await supabase.from("banned_users").insert({
pi_uid: piUser.pi_uid,
username: profile.display_name,
reason: reason || "Violazione regole chat",
app_source: APP_SOURCE,
})
}

// Delete all messages from banned user for this app
await supabase
.from("messages")
.delete()
.eq("user_id", userId)
.eq("app_source", APP_SOURCE)

return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: "Errore del server" }, { status: 500 })
}
}

// DELETE - rimuove il ban di un utente per questa app
export async function DELETE(req: Request) {
try {
const { piUid, adminUsername } = await req.json()

if (adminUsername !== ADMIN_USERNAME) {
return NextResponse.json({ error: "Non autorizzato" }, { status: 403 })
}

if (!piUid) {
return NextResponse.json({ error: "pi_uid mancante" }, { status: 400 })
}

const supabase = getAdmin()

// Only unban for this specific app
const { error } = await supabase
.from("banned_users")
.delete()
.eq("pi_uid", piUid)
.eq("app_source", APP_SOURCE)

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}

return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: "Errore del server" }, { status: 500 })
}
}
34 changes: 34 additions & 0 deletions app/api/admin/banned/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from "next/server"
import { getAdmin, APP_SOURCE, ADMIN_USERNAME } from "@/lib/supabase/admin"

// GET - lista tutti gli utenti bannati per questa app
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url)
const adminUsername = searchParams.get("adminUsername")

if (adminUsername !== ADMIN_USERNAME) {
return NextResponse.json({ error: "Non autorizzato" }, { status: 403 })
}

const supabase = getAdmin()

// Only get banned users for this specific app
const { data, error } = await supabase
.from("banned_users")
.select("id, pi_uid, username, reason, banned_at")
.eq("app_source", APP_SOURCE)
.order("banned_at", { ascending: false })

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}

return NextResponse.json({
bannedUsers: data || [],
app: APP_SOURCE,
})
} catch {
return NextResponse.json({ error: "Errore del server" }, { status: 500 })
}
}
119 changes: 119 additions & 0 deletions app/api/messages/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { NextResponse } from "next/server"
import { getAdmin, APP_SOURCE, ADMIN_USERNAME } from "@/lib/supabase/admin"

export async function GET() {
try {
const supabase = getAdmin()
// Only get messages for this specific app
const { data: messages, error } = await supabase
.from("messages")
.select("id, content, created_at, user_id, reply_to, image_url, audio_url, app_source")
.eq("app_source", APP_SOURCE)
.order("created_at", { ascending: true })
.limit(200)

if (error) return NextResponse.json({ error: error.message }, { status: 500 })
if (!messages || messages.length === 0) return NextResponse.json([])

const userIds = [...new Set(messages.map((m) => m.user_id))]
const { data: profiles } = await supabase
.from("profiles")
.select("id, display_name")
.in("id", userIds)
.eq("app_source", APP_SOURCE)
const profileMap = new Map(profiles?.map((p) => [p.id, p.display_name]) || [])

// Get replied-to messages
const replyIds = messages.map((m) => m.reply_to).filter(Boolean)
let replyMap = new Map()
if (replyIds.length > 0) {
const { data: replies } = await supabase
.from("messages")
.select("id, content, user_id")
.in("id", replyIds)
.eq("app_source", APP_SOURCE)
if (replies) {
for (const r of replies) {
replyMap.set(r.id, {
content: r.content,
display_name: profileMap.get(r.user_id) || "Pioniere",
})
}
}
}

const enriched = messages.map((m) => ({
...m,
display_name: profileMap.get(m.user_id) || "Pioniere",
reply_message: m.reply_to ? replyMap.get(m.reply_to) || null : null,
}))

return NextResponse.json(enriched)
} catch {
return NextResponse.json({ error: "Errore del server" }, { status: 500 })
}
}

export async function POST(req: Request) {
try {
const { content, userId, replyTo, imageUrl, audioUrl } = await req.json()
if ((!content?.trim() && !imageUrl && !audioUrl) || !userId) {
return NextResponse.json({ error: "Dati mancanti" }, { status: 400 })
}
const supabase = getAdmin()

// Check if banned for this app
const { data: piUser } = await supabase
.from("pi_users")
.select("pi_uid")
.eq("pi_username",
(await supabase.from("profiles").select("display_name").eq("id", userId).single()).data?.display_name
)
.eq("app_source", APP_SOURCE)
.maybeSingle()

if (piUser) {
const { data: banned } = await supabase
.from("banned_users")
.select("id")
.eq("pi_uid", piUser.pi_uid)
.eq("app_source", APP_SOURCE)
.maybeSingle()
if (banned) return NextResponse.json({ error: "Utente bannato" }, { status: 403 })
}

const insertData: Record<string, unknown> = {
content: content?.trim() || "",
user_id: userId,
app_source: APP_SOURCE,
}
if (replyTo) insertData.reply_to = replyTo
if (imageUrl) insertData.image_url = imageUrl
if (audioUrl) insertData.audio_url = audioUrl

const { error } = await supabase.from("messages").insert(insertData)
if (error) return NextResponse.json({ error: error.message }, { status: 500 })

return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: "Errore del server" }, { status: 500 })
}
}

export async function DELETE(req: Request) {
try {
const { messageId, adminUsername } = await req.json()
if (adminUsername !== ADMIN_USERNAME) return NextResponse.json({ error: "Non autorizzato" }, { status: 403 })

const supabase = getAdmin()
// Only delete messages from this app
await supabase
.from("messages")
.delete()
.eq("id", messageId)
.eq("app_source", APP_SOURCE)
return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: "Errore del server" }, { status: 500 })
}
}
Loading