This commit is contained in:
2026-06-26 19:23:02 +02:00
13 changed files with 295 additions and 93 deletions
+36 -23
View File
@@ -513,26 +513,27 @@ def is_offer(text: str) -> bool:
# Each run picks 2-4 random queries to vary behavior and reduce detection. # Each run picks 2-4 random queries to vary behavior and reduce detection.
FB_SEARCHES = [ FB_SEARCHES = [
"looking for web developer", "looking for web developer",
"need a website designed",
"need website built South Africa",
"need someone to build my website", "need someone to build my website",
"need web designer",
"looking for someone to create website",
"need ecommerce website built",
"want to hire web developer",
"need wordpress website",
"I need a website for my business",
"need a site for my business",
"looking for website designer South Africa",
"need website for my small business", "need website for my small business",
"who can build me a website", "who can build me a website",
"want to create a website for my business", "recommend a web developer",
"looking for affordable web design", "I need a website for my",
"need a web developer South Africa", "need a site for my business",
"looking for web designer",
"help me build a website", "help me build a website",
"need someone to design my website", "need someone to design my website",
"looking for web designer near me", "anyone know a web developer",
"need a website for my startup", "need a web developer for my project",
"need website for my startup",
"want a website for my company",
"need a new website for my",
"looking for web developer for my business",
"need ecommerce website for my",
"looking for affordable web design",
"need wordpress website",
"who can design a website for my",
"looking for someone to create a website",
"recommendations for a web designer",
] ]
VIEWPORTS = [ VIEWPORTS = [
@@ -1401,26 +1402,38 @@ Return a JSON array like ["yes","no","yes"] matching the order above."""
logger.warning("AI classification failed, falling back to keyword filter: %s", e) logger.warning("AI classification failed, falling back to keyword filter: %s", e)
if not ai_succeeded: if not ai_succeeded:
strict_keywords = [ web_terms = [
"website", "web design", "web develop", "web dev", "website", "web design", "web develop", "web dev",
"web designer", "web developer",
"build my website", "build a website", "create a website", "build my website", "build a website", "create a website",
"need web", "looking for web", "new website", "landing page", "wordpress", "ecommerce",
"landing page", "wordpress", "my website", "business website",
"need a website", "my website", "business website", "site for my", "site for my business",
"need a designer", "help with my website", "new website", "redesign my website",
"redesign", "update my website", "help with my website", "update my website",
]
request_terms = [
"looking for", "need a", "need an", "looking to", "looking for", "need a", "need an", "looking to",
"need someone", "hire a", "want someone", "need someone", "hire a", "want someone",
"need help with", "would like", "build me", "need help with", "would like", "build me",
"design my", "make me a", "create my", "design my", "make me a", "create my",
"looking", "need", "want", "help",
"who can", "i need",
"recommend", "anyone know", "anyone recommend",
"know a", "know any", "recommendation",
"suggest", "looking for recommendations",
"can anyone", "does anyone",
] ]
filtered = [] filtered = []
for r in results: for r in results:
t = r['title'].lower() t = r['title'].lower()
if not any(kw in t for kw in strict_keywords): has_web = any(kw in t for kw in web_terms)
has_request = any(kw in t for kw in request_terms)
if not has_web or not has_request:
continue continue
if any(kw in t for kw in ['i build website', 'i offer web', 'i am a web developer', if any(kw in t for kw in ['i build website', 'i offer web', 'i am a web developer',
'affordable web design package', 'web hosting']): 'affordable web design package', 'web hosting',
'i do web design', 'i develop websites']):
continue continue
filtered.append(r) filtered.append(r)
return filtered[:10] return filtered[:10]
+3
View File
@@ -43,6 +43,9 @@ BEGIN;
\echo '=== Running 011_calendar_events.sql (Calendar Events) ===' \echo '=== Running 011_calendar_events.sql (Calendar Events) ==='
\i 011_calendar_events.sql \i 011_calendar_events.sql
\echo '=== Running 012_invites.sql (Invites) ==='
\i 012_invites.sql
\echo '=== Running 012_sent_emails.sql (Sent Email Log) ===' \echo '=== Running 012_sent_emails.sql (Sent Email Log) ==='
\i 012_sent_emails.sql \i 012_sent_emails.sql
+1 -1
View File
@@ -920,7 +920,7 @@ const formatPreviewContent = (content: string) => {
</div> </div>
</div> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => toast.info("Voice calling coming soon")}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setIsCallModalOpen(true)}>
<Phone className="h-4 w-4" /> <Phone className="h-4 w-4" />
</Button> </Button>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => toast.info("Video calling coming soon")}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => toast.info("Video calling coming soon")}>
+29
View File
@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server"
import { chatWithAI } from "@/lib/ai"
import { getSessionUser } from "@/lib/auth"
export async function POST(request: NextRequest) {
try {
const user = await getSessionUser()
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { message } = await request.json()
if (!message || typeof message !== "string") {
return NextResponse.json({ error: "Message is required" }, { status: 400 })
}
const sessionCookie = request.cookies.get("session")
const jwtToken = sessionCookie?.value
if (!jwtToken) {
return NextResponse.json({ error: "No session token" }, { status: 401 })
}
const response = await chatWithAI(message, jwtToken)
return NextResponse.json({ response })
} catch (error: any) {
console.error("AI chat error:", error)
return NextResponse.json({ error: error.message || "AI service error" }, { status: 500 })
}
}
+2
View File
@@ -9,6 +9,7 @@ import {
resetFailedAttempts, resetFailedAttempts,
isAccountLocked, isAccountLocked,
createSession, createSession,
setSessionContext,
} from "@/lib/auth" } from "@/lib/auth"
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
@@ -106,6 +107,7 @@ export async function POST(request: NextRequest) {
) )
await createSession(dbUser.id, dbUser.role_name) await createSession(dbUser.id, dbUser.role_name)
await setSessionContext(dbUser.id, ipAddress)
const user = mapDbUserToSessionUser(dbUser) const user = mapDbUserToSessionUser(dbUser)
+18 -2
View File
@@ -46,10 +46,11 @@ export async function PATCH(request: NextRequest) {
const body = await request.json() const body = await request.json()
await query( const result = await query(
`UPDATE company_settings SET `UPDATE company_settings SET
company_name = $1, company_email = $2, company_phone = $3, company_name = $1, company_email = $2, company_phone = $3,
company_website = $4, company_address = $5, updated_by = $6, updated_at = NOW()`, company_website = $4, company_address = $5, updated_by = $6, updated_at = NOW()
WHERE id = (SELECT id FROM company_settings ORDER BY updated_at DESC LIMIT 1)`,
[ [
body.companyName || "", body.companyName || "",
body.companyEmail || "", body.companyEmail || "",
@@ -60,6 +61,21 @@ export async function PATCH(request: NextRequest) {
], ],
) )
if (result.rowCount === 0) {
await query(
`INSERT INTO company_settings (company_name, company_email, company_phone, company_website, company_address, updated_by)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
body.companyName || "",
body.companyEmail || "",
body.companyPhone || "",
body.companyWebsite || "",
body.companyAddress || "",
user.id,
],
)
}
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
} catch (error) { } catch (error) {
console.error("Company settings PATCH error:", error) console.error("Company settings PATCH error:", error)
+7 -4
View File
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { query } from "@/lib/db" import { query } from "@/lib/db"
import { hashPassword, getSessionUser } from "@/lib/auth" import { hashPassword, getSessionUser, encryptPassword, setSessionContext } from "@/lib/auth"
import { avatarSvgUrl } from "@/lib/avatar" import { avatarSvgUrl } from "@/lib/avatar"
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
@@ -68,12 +68,15 @@ export async function POST(request: NextRequest) {
const lastName = nameParts.slice(1).join(" ") || firstName const lastName = nameParts.slice(1).join(" ") || firstName
const username = email.split("@")[0] const username = email.split("@")[0]
const passwordHash = await hashPassword(password) const passwordHash = await hashPassword(password)
const passwordEncrypted = await encryptPassword(password)
await setSessionContext(sessionUser.id)
const result = await query( const result = await query(
`INSERT INTO users (username, email, password_hash, first_name, last_name, is_active, created_by) `INSERT INTO users (username, email, password_hash, password_encrypted, first_name, last_name, is_active, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id`, RETURNING id`,
[username.toLowerCase(), email.toLowerCase(), passwordHash, firstName, lastName, active ?? true, sessionUser.id] [username.toLowerCase(), email.toLowerCase(), passwordHash, passwordEncrypted, firstName, lastName, active ?? true, sessionUser.id]
) )
const roleId = ( const roleId = (
+1 -1
View File
@@ -231,7 +231,7 @@ export default function LoginPage() {
<div className="left-panel flex flex-1 flex-col p-12"> <div className="left-panel flex flex-1 flex-col p-12">
<div className="relative z-10 flex-1 flex items-center justify-center"> <div className="relative z-10 flex-1 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="bc-logo mb-8" style={{"--theme-primary":"#1BB0CE"}}> <div className="bc-logo mb-8" style={{"--theme-primary":"#1BB0CE"} as React.CSSProperties}>
Black <span className="accent">Cipher</span> Black <span className="accent">Cipher</span>
</div> </div>
<h1 className="text-[34px] font-extrabold text-[#e8e8ef] leading-tight tracking-[-0.6px]"> <h1 className="text-[34px] font-extrabold text-[#e8e8ef] leading-tight tracking-[-0.6px]">
+111 -33
View File
@@ -1,7 +1,7 @@
"use client" "use client"
import { useState, useRef, useEffect, Fragment } from "react" import { useState, useRef, useEffect, Fragment } from "react"
import { Send, Loader2, Bot, User, RefreshCw, AlertCircle } from "lucide-react" import { Send, Bot, User, RefreshCw, AlertCircle, Check, Terminal } from "lucide-react"
function linkifyText(text: string) { function linkifyText(text: string) {
const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/ const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/
@@ -24,9 +24,26 @@ export function AIChat() {
const [input, setInput] = useState("") const [input, setInput] = useState("")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
const [ollamaStatus, setOllamaStatus] = useState<boolean | null>(null) const [bootState, setBootState] = useState<"booting" | "ready" | "error">("booting")
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const checkServer = async () => {
try {
const res = await fetch("/api/ai/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "ping" }),
})
if (res.status !== 503) {
setBootState("ready")
} else {
setTimeout(checkServer, 2000)
}
} catch {
setTimeout(checkServer, 2000)
}
}
useEffect(() => { useEffect(() => {
fetch("/api/ai/jobs") fetch("/api/ai/jobs")
.then((r) => r.json()) .then((r) => r.json())
@@ -56,29 +73,13 @@ export function AIChat() {
}, },
]) ])
}) })
checkServer()
}, []) }, [])
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages]) }, [messages])
const checkOllama = async () => {
try {
const res = await fetch("/api/ai/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "__ping__" }),
})
setOllamaStatus(res.status !== 503)
} catch {
setOllamaStatus(false)
}
}
useEffect(() => { checkOllama() }, [])
const sendMessage = async () => { const sendMessage = async () => {
const msg = input.trim() const msg = input.trim()
if (!msg || loading) return if (!msg || loading) return
@@ -96,8 +97,8 @@ export function AIChat() {
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json().catch(() => ({}))
throw new Error(data.error || "Failed to get response") throw new Error(data.error || `Error ${res.status}`)
} }
const data = await res.json() const data = await res.json()
@@ -121,17 +122,86 @@ export function AIChat() {
} }
} }
const bootOverlay = (
<div className="flex-1 flex items-center justify-center">
<style>{`
@keyframes walk {
0%, 100% { transform: translateX(0) translateY(0); }
25% { transform: translateX(12px) translateY(-4px); }
50% { transform: translateX(24px) translateY(0); }
75% { transform: translateX(12px) translateY(-4px); }
}
@keyframes legLeft {
0%, 100% { transform: rotate(-10deg); }
50% { transform: rotate(10deg); }
}
@keyframes legRight {
0%, 100% { transform: rotate(10deg); }
50% { transform: rotate(-10deg); }
}
@keyframes armLeft {
0%, 100% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
}
@keyframes armRight {
0%, 100% { transform: rotate(-15deg); }
50% { transform: rotate(15deg); }
}
@keyframes blink {
0%, 45%, 55%, 100% { height: 4px; }
50% { height: 1px; }
}
.robot-walk { animation: walk 0.6s ease-in-out infinite; }
.robot-leg-l { transform-origin: top center; animation: legLeft 0.3s ease-in-out infinite; }
.robot-leg-r { transform-origin: top center; animation: legRight 0.3s ease-in-out infinite; }
.robot-arm-l { transform-origin: top center; animation: armLeft 0.3s ease-in-out infinite; }
.robot-arm-r { transform-origin: top center; animation: armRight 0.3s ease-in-out infinite; }
.robot-eye { animation: blink 2s ease-in-out infinite; }
`}</style>
<div className="flex flex-col items-center gap-4">
<p className="text-sm text-[#6a6a75]">Servers booting...</p>
<div className="h-[50px] w-[100px] bg-[#1a1a24] border border-[#2a2a35] rounded-lg flex items-center justify-center overflow-hidden">
<div className="robot-walk relative">
<svg width="40" height="36" viewBox="0 0 40 36" fill="none">
<rect x="10" y="2" width="20" height="16" rx="3" fill="#1BB0CE" opacity="0.9"/>
<rect x="6" y="6" width="6" height="2" rx="1" className="robot-arm-l" fill="#1BB0CE" opacity="0.7"/>
<rect x="28" y="6" width="6" height="2" rx="1" className="robot-arm-r" fill="#1BB0CE" opacity="0.7"/>
<rect x="14" y="5" width="4" height="4" rx="1" fill="#0d1117"/>
<rect x="22" y="5" width="4" height="4" rx="1" fill="#0d1117"/>
<circle cx="16" cy="7" r="1.5" className="robot-eye" fill="#1BB0CE"/>
<circle cx="24" cy="7" r="1.5" className="robot-eye" fill="#1BB0CE"/>
<rect x="17" y="10" width="6" height="2" rx="1" fill="#0d1117"/>
<rect x="12" y="18" width="5" height="10" rx="2" className="robot-leg-l" fill="#1BB0CE" opacity="0.8"/>
<rect x="23" y="18" width="5" height="10" rx="2" className="robot-leg-r" fill="#1BB0CE" opacity="0.8"/>
</svg>
</div>
</div>
</div>
</div>
)
const readyOverlay = (
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="h-[50px] w-[100px] bg-[#1a1a24] border border-[#2a2a35] rounded-lg flex items-center justify-center">
<Check className="h-6 w-6 text-green-400" />
</div>
<div className="text-center space-y-1">
<p className="text-xs text-[#6a6a75]">Try these commands:</p>
<div className="flex gap-2">
<code className="text-xs px-2 py-1 rounded bg-[#2a2a35] text-[#1BB0CE] flex items-center gap-1"><Terminal className="h-3 w-3" /> lists</code>
<code className="text-xs px-2 py-1 rounded bg-[#2a2a35] text-[#1BB0CE] flex items-center gap-1"><Terminal className="h-3 w-3" /> leads</code>
</div>
</div>
</div>
</div>
)
if (bootState === "booting") return bootOverlay
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{ollamaStatus === false && ( {bootState === "ready" && readyOverlay}
<div className="flex items-center gap-2 px-4 py-2 bg-amber-500/10 border-b border-amber-500/20 text-amber-400 text-xs">
<AlertCircle className="h-3.5 w-3.5 flex-none" />
<span className="flex-1">Ollama not responding. Start it with <code className="bg-amber-500/20 px-1 rounded">ollama serve</code></span>
<button type="button" onClick={checkOllama} className="hover:text-amber-300">
<RefreshCw className="h-3.5 w-3.5" />
</button>
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin"> <div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin">
{messages.map((msg, i) => ( {messages.map((msg, i) => (
@@ -163,7 +233,10 @@ export function AIChat() {
<Bot className="h-4 w-4 text-[#1BB0CE]" /> <Bot className="h-4 w-4 text-[#1BB0CE]" />
</div> </div>
<div className="max-w-[75%] rounded-lg px-4 py-2.5 bg-[#1a1a24] border border-[#2a2a35]"> <div className="max-w-[75%] rounded-lg px-4 py-2.5 bg-[#1a1a24] border border-[#2a2a35]">
<Loader2 className="h-4 w-4 animate-spin text-[#1BB0CE]" /> <svg className="h-4 w-4 animate-spin text-[#1BB0CE]" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" strokeLinecap="round"/>
</svg>
</div> </div>
</div> </div>
)} )}
@@ -192,10 +265,15 @@ export function AIChat() {
disabled={loading || !input.trim()} disabled={loading || !input.trim()}
className="h-9 w-9 rounded-lg bg-[#1BB0CE] hover:bg-[#1BB0CE]/80 disabled:opacity-40 flex items-center justify-center flex-none transition-colors" className="h-9 w-9 rounded-lg bg-[#1BB0CE] hover:bg-[#1BB0CE]/80 disabled:opacity-40 flex items-center justify-center flex-none transition-colors"
> >
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} {loading ? (
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" strokeLinecap="round"/>
</svg>
) : <Send className="h-4 w-4" />}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
) )
} }
+61 -26
View File
@@ -1,41 +1,76 @@
import http from "http"
const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001" const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001"
function parseUrl(url: string) {
const u = new URL(url)
return { hostname: u.hostname, port: parseInt(u.port) || 80, path: u.pathname + u.search }
}
export async function chatWithAI(message: string, jwtToken: string) { export async function chatWithAI(message: string, jwtToken: string) {
const res = await fetch(`${AI_SERVICE}/ai/chat`, { const body = JSON.stringify({ message })
method: "POST", const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/chat`)
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}` },
body: JSON.stringify({ message }), const text = await new Promise<string>((resolve, reject) => {
const req = http.request(
{ hostname, port, path, method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}`, "Content-Length": Buffer.byteLength(body) } },
(res) => {
let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(data)
} else {
reject(new Error(`AI error ${res.statusCode}: ${data.substring(0, 200)}`))
}
})
}
)
req.on("error", reject)
req.write(body)
req.end()
}) })
if (!res.ok) { const data = JSON.parse(text)
const text = await res.text()
throw new Error(`AI service error (${res.status}): ${text}`)
}
const data = await res.json()
return data.response || "" return data.response || ""
} }
export async function fetchJobs() { export async function checkAiServiceStatus() {
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/health`)
try { try {
const res = await fetch(`${AI_SERVICE}/ai/jobs`) await new Promise<void>((resolve, reject) => {
if (!res.ok) return [] const req = http.get({ hostname, port, path, timeout: 3000 }, (res) => {
const data = await res.json() let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => {
try { resolve(JSON.parse(data).status === "ok" ? undefined : reject(new Error("not ok"))) } catch { reject(new Error("bad response")) }
})
})
req.on("error", reject)
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) })
})
return true
} catch {
return false
}
}
export async function fetchJobs() {
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/jobs`)
try {
const text = await new Promise<string>((resolve, reject) => {
const req = http.get({ hostname, port, path, timeout: 5000 }, (res) => {
let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => resolve(data))
})
req.on("error", reject)
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) })
})
const data = JSON.parse(text)
return data.jobs || [] return data.jobs || []
} catch { } catch {
console.warn("Failed to fetch AI jobs") console.warn("Failed to fetch AI jobs")
return [] return []
} }
} }
export async function checkAiServiceStatus() {
try {
const res = await fetch(`${AI_SERVICE}/health`)
if (!res.ok) return false
const data = await res.json()
return data.status === "ok"
} catch {
console.warn("Failed to check AI service status")
return false
}
}
+24 -2
View File
@@ -17,6 +17,7 @@ export interface SessionUser {
id: string; id: string;
username: string; username: string;
email: string; email: string;
phone: string | null;
firstName: string; firstName: string;
lastName: string; lastName: string;
role: string; role: string;
@@ -85,7 +86,7 @@ export async function getUserByUsername(username: string) {
export async function getUserById(id: string) { export async function getUserById(id: string) {
const result = await query( const result = await query(
` SELECT u.id, u.username, u.email, u.first_name, u.last_name, ` SELECT u.id, u.username, u.email, u.phone, u.first_name, u.last_name,
u.is_active, u.avatar_url, u.is_active, u.avatar_url,
r.name AS role_name r.name AS role_name
FROM users u FROM users u
@@ -185,6 +186,7 @@ export function mapDbUserToSessionUser(
id: dbUser.id as string, id: dbUser.id as string,
username: dbUser.username as string, username: dbUser.username as string,
email: dbUser.email as string, email: dbUser.email as string,
phone: (dbUser.phone as string) || null,
firstName: dbUser.first_name as string, firstName: dbUser.first_name as string,
lastName: dbUser.last_name as string, lastName: dbUser.last_name as string,
role: roleMapping[roleName] || roleName.toLowerCase(), role: roleMapping[roleName] || roleName.toLowerCase(),
@@ -201,7 +203,6 @@ export async function createSession(userId: string, role: string) {
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
sameSite: "strict", sameSite: "strict",
path: "/", path: "/",
maxAge: 60 * 60 * 24, // 24 hours
}); });
return token; return token;
@@ -218,6 +219,27 @@ export async function destroySession() {
}); });
} }
export async function encryptPassword(password: string): Promise<string> {
const result = await query("SELECT encrypt_password($1) AS encrypted", [password]);
return result.rows[0]?.encrypted;
}
export async function decryptPassword(encrypted: string): Promise<string | null> {
try {
const result = await query("SELECT decrypt_password($1) AS decrypted", [encrypted]);
return result.rows[0]?.decrypted || null;
} catch {
return null;
}
}
export async function setSessionContext(userId: string, ip?: string) {
await query("SELECT set_config('app.current_user_id', $1, true)", [userId]);
if (ip) {
await query("SELECT set_config('app.current_ip', $1, true)", [ip]);
}
}
export async function getSessionUser(): Promise<SessionUser | null> { export async function getSessionUser(): Promise<SessionUser | null> {
try { try {
const cookieStore = await cookies(); const cookieStore = await cookies();
+2 -1
View File
@@ -4,12 +4,13 @@ export type LeadStatus =
| "pending" | "pending"
| "closed" | "closed"
| "ignored"; | "ignored";
export type UserRole = "super_admin" | "admin" | "sales"; export type UserRole = "super_admin" | "admin" | "sales" | "dev";
export interface User { export interface User {
id: string; id: string;
name: string; name: string;
email: string; email: string;
phone?: string;
role: UserRole; role: UserRole;
active: boolean; active: boolean;
avatar: string; avatar: string;