🚀 Initial commit - PDIMaker v1.0.0
Sistema completo de gestão de PDI com: - Autenticação com email/senha e Google OAuth - Workspaces privados isolados - Sistema de convites com código único - Interface profissional com Next.js 14 - Backend NestJS com PostgreSQL - Docker com Nginx e SSL Desenvolvido por Sergio Correa
This commit is contained in:
55
frontend/Dockerfile
Normal file
55
frontend/Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
FROM node:20-slim AS base
|
||||
|
||||
# Instalar OpenSSL e dependências do Prisma
|
||||
RUN apt-get update && apt-get install -y openssl libssl3 ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instalar dependências apenas quando necessário
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
# Rebuild o código fonte apenas quando necessário
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Gerar Prisma Client
|
||||
RUN npx prisma generate
|
||||
|
||||
# Build do Next.js (skip validation pra não precisar conectar no DB durante build)
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV SKIP_ENV_VALIDATION=1
|
||||
RUN npm run build
|
||||
|
||||
# Imagem de produção
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copiar arquivos públicos
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Copiar arquivos standalone
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
# Copiar Prisma e node_modules necessários
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/bcryptjs ./node_modules/bcryptjs
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
8
frontend/app/api/auth/[...nextauth]/route.ts
Normal file
8
frontend/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// app/api/auth/[...nextauth]/route.ts
|
||||
import NextAuth from "next-auth"
|
||||
import { authOptions } from "@/lib/auth/config"
|
||||
|
||||
const handler = NextAuth(authOptions)
|
||||
|
||||
export { handler as GET, handler as POST }
|
||||
|
||||
15
frontend/app/api/auth/logout/route.ts
Normal file
15
frontend/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// app/api/auth/logout/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const response = NextResponse.redirect(new URL("/login", request.url))
|
||||
|
||||
// Limpar todos os cookies de autenticação
|
||||
response.cookies.delete("next-auth.session-token")
|
||||
response.cookies.delete("__Secure-next-auth.session-token")
|
||||
response.cookies.delete("next-auth.csrf-token")
|
||||
response.cookies.delete("__Host-next-auth.csrf-token")
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
51
frontend/app/api/auth/register/route.ts
Normal file
51
frontend/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// app/api/auth/register/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { createUser } from "@/lib/auth/credentials"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, password, name } = await request.json()
|
||||
|
||||
// Validações
|
||||
if (!email || !password || !name) {
|
||||
return NextResponse.json(
|
||||
{ error: "Todos os campos são obrigatórios" },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ error: "A senha deve ter pelo menos 6 caracteres" },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Criar usuário
|
||||
const user = await createUser(email, password, name)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao registrar:", error)
|
||||
|
||||
if (error.message === "Email já cadastrado") {
|
||||
return NextResponse.json(
|
||||
{ error: "Este email já está cadastrado" },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Erro ao criar conta" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
84
frontend/app/api/invites/accept/route.ts
Normal file
84
frontend/app/api/invites/accept/route.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// app/api/invites/accept/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth/config"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Não autenticado" }, { status: 401 })
|
||||
}
|
||||
|
||||
const { inviteCode } = await request.json()
|
||||
|
||||
if (!inviteCode) {
|
||||
return NextResponse.json({ error: "Código de convite inválido" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Buscar convite
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: { token: inviteCode }
|
||||
})
|
||||
|
||||
if (!invite) {
|
||||
return NextResponse.json({ error: "Convite não encontrado" }, { status: 404 })
|
||||
}
|
||||
|
||||
// Buscar workspace
|
||||
const workspace = invite.workspaceId
|
||||
? await prisma.workspace.findUnique({ where: { id: invite.workspaceId } })
|
||||
: null
|
||||
|
||||
if (!workspace) {
|
||||
return NextResponse.json({ error: "Workspace não encontrado" }, { status: 404 })
|
||||
}
|
||||
|
||||
// Verificar se está expirado
|
||||
if (invite.expiresAt < new Date()) {
|
||||
return NextResponse.json({ error: "Convite expirado" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verificar se já foi aceito
|
||||
if (invite.status !== "PENDING") {
|
||||
return NextResponse.json({ error: "Convite já foi utilizado" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verificar se o email confere
|
||||
if (invite.email !== session.user.email) {
|
||||
return NextResponse.json({ error: "Este convite não é para você" }, { status: 403 })
|
||||
}
|
||||
|
||||
// Atualizar convite
|
||||
await prisma.invite.update({
|
||||
where: { id: invite.id },
|
||||
data: {
|
||||
status: "ACCEPTED",
|
||||
acceptedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
// Ativar workspace
|
||||
await prisma.workspace.update({
|
||||
where: { id: invite.workspaceId! },
|
||||
data: { status: "ACTIVE" }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
workspace: {
|
||||
id: workspace.id,
|
||||
slug: workspace.slug
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao aceitar convite:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Erro ao aceitar convite" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
97
frontend/app/api/workspaces/create/route.ts
Normal file
97
frontend/app/api/workspaces/create/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// app/api/workspaces/create/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth/config"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { generateInviteCode, generateWorkspaceSlug } from "@/lib/utils/invite-code"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session || !session.user) {
|
||||
return NextResponse.json({ error: "Não autenticado" }, { status: 401 })
|
||||
}
|
||||
|
||||
const { email, inviteRole } = await request.json()
|
||||
|
||||
console.log("Criando workspace:", { email, inviteRole, userId: session.user.id })
|
||||
|
||||
// Validar email
|
||||
if (!email || !email.includes("@")) {
|
||||
return NextResponse.json({ error: "Email inválido" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Buscar ou criar o usuário convidado
|
||||
let invitedUser = await prisma.user.findUnique({ where: { email } })
|
||||
|
||||
if (!invitedUser) {
|
||||
// Criar registro temporário do usuário convidado
|
||||
invitedUser = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
name: email.split("@")[0],
|
||||
role: inviteRole === "MANAGER" ? "MANAGER" : "EMPLOYEE"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Determinar quem é employee e quem é manager
|
||||
const employeeId = inviteRole === "MANAGER" ? session.user.id : invitedUser.id
|
||||
const managerId = inviteRole === "MANAGER" ? invitedUser.id : session.user.id
|
||||
|
||||
// Buscar nomes para o slug
|
||||
const employee = await prisma.user.findUnique({ where: { id: employeeId } })
|
||||
const manager = await prisma.user.findUnique({ where: { id: managerId } })
|
||||
|
||||
if (!employee || !manager) {
|
||||
return NextResponse.json({ error: "Erro ao buscar usuários" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Gerar slug único
|
||||
const slug = generateWorkspaceSlug(employee.name, manager.name)
|
||||
|
||||
// Criar workspace
|
||||
const workspace = await prisma.workspace.create({
|
||||
data: {
|
||||
slug,
|
||||
employeeId,
|
||||
managerId,
|
||||
status: "PENDING_INVITE"
|
||||
}
|
||||
})
|
||||
|
||||
// Criar convite
|
||||
const inviteCode = generateInviteCode()
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 7) // Expira em 7 dias
|
||||
|
||||
await prisma.invite.create({
|
||||
data: {
|
||||
email: invitedUser.email,
|
||||
role: inviteRole === "MANAGER" ? "MANAGER" : "EMPLOYEE",
|
||||
token: inviteCode,
|
||||
invitedBy: session.user.id,
|
||||
workspaceId: workspace.id,
|
||||
expiresAt
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: Enviar email com o código de convite
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
workspace: { id: workspace.id, slug: workspace.slug },
|
||||
inviteCode
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao criar workspace:", error)
|
||||
console.error("Stack:", error.stack)
|
||||
console.error("Message:", error.message)
|
||||
return NextResponse.json(
|
||||
{ error: error.message || "Erro ao criar workspace" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
51
frontend/app/dashboard/page.tsx
Normal file
51
frontend/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
// app/dashboard/page.tsx
|
||||
import { redirect } from "next/navigation"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth/config"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session) {
|
||||
redirect("/login")
|
||||
}
|
||||
|
||||
// Buscar workspaces do usuário
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: {
|
||||
workspacesAsEmployee: {
|
||||
where: { status: "ACTIVE" },
|
||||
include: {
|
||||
manager: { select: { name: true, avatar: true } }
|
||||
}
|
||||
},
|
||||
workspacesAsManager: {
|
||||
where: { status: "ACTIVE" },
|
||||
include: {
|
||||
employee: { select: { name: true, avatar: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Se tiver apenas 1 workspace, redireciona diretamente
|
||||
const allWorkspaces = [
|
||||
...(user?.workspacesAsEmployee || []),
|
||||
...(user?.workspacesAsManager || [])
|
||||
]
|
||||
|
||||
if (allWorkspaces.length === 1) {
|
||||
redirect(`/workspace/${allWorkspaces[0].slug}`)
|
||||
}
|
||||
|
||||
// Se não tiver workspaces, redireciona para criar/aceitar convite
|
||||
if (allWorkspaces.length === 0) {
|
||||
redirect("/onboarding")
|
||||
}
|
||||
|
||||
// Lista todos os workspaces
|
||||
redirect("/workspaces")
|
||||
}
|
||||
|
||||
23
frontend/app/layout.tsx
Normal file
23
frontend/app/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// app/layout.tsx
|
||||
import { SessionProvider } from "@/components/SessionProvider"
|
||||
|
||||
export const metadata = {
|
||||
title: 'PDIMaker',
|
||||
description: 'Plataforma de Desenvolvimento Individual',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="pt-BR">
|
||||
<body>
|
||||
<SessionProvider>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
244
frontend/app/login/page.tsx
Normal file
244
frontend/app/login/page.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
// app/login/page.tsx
|
||||
"use client"
|
||||
|
||||
import { signIn } from "next-auth/react"
|
||||
import { useSearchParams, useRouter } from "next/navigation"
|
||||
import { Suspense, useState } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
function LoginForm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard"
|
||||
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const handleCredentialsLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
try {
|
||||
const result = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false
|
||||
})
|
||||
|
||||
if (result?.error) {
|
||||
setError("Email ou senha incorretos")
|
||||
} else {
|
||||
router.push(callbackUrl)
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Erro ao fazer login")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
signIn("google", { callbackUrl })
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundImage: "url('/pdimaker-background.jpg')",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
position: "relative"
|
||||
}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "rgba(0, 0, 0, 0.5)"
|
||||
}} />
|
||||
|
||||
<div style={{
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
background: "rgba(255, 255, 255, 0.95)",
|
||||
padding: "3rem",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
||||
maxWidth: "450px",
|
||||
width: "100%"
|
||||
}}>
|
||||
<h1 style={{ fontSize: "2rem", marginBottom: "0.5rem", color: "#333", textAlign: "center" }}>
|
||||
🚀 PDIMaker
|
||||
</h1>
|
||||
<p style={{ color: "#666", marginBottom: "2rem", textAlign: "center" }}>
|
||||
Plataforma de Desenvolvimento Individual
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleCredentialsLogin}>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Senha
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: "1rem",
|
||||
background: "#fee2e2",
|
||||
border: "1px solid #fecaca",
|
||||
borderRadius: "0.375rem",
|
||||
color: "#dc2626",
|
||||
marginBottom: "1.5rem",
|
||||
fontSize: "0.875rem"
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
background: loading ? "#cbd5e0" : "#667eea",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "600",
|
||||
cursor: loading ? "not-allowed" : "pointer",
|
||||
marginBottom: "1rem"
|
||||
}}
|
||||
>
|
||||
{loading ? "Entrando..." : "Entrar"}
|
||||
</button>
|
||||
|
||||
<div style={{ textAlign: "center", marginBottom: "1.5rem" }}>
|
||||
<span style={{ color: "#666", fontSize: "0.875rem" }}>
|
||||
Não tem uma conta?{" "}
|
||||
</span>
|
||||
<Link href="/register" style={{ color: "#667eea", fontWeight: "500", fontSize: "0.875rem" }}>
|
||||
Criar conta
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: "relative",
|
||||
textAlign: "center",
|
||||
margin: "1.5rem 0"
|
||||
}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "1px",
|
||||
background: "#e2e8f0"
|
||||
}} />
|
||||
<span style={{
|
||||
position: "relative",
|
||||
background: "rgba(255, 255, 255, 0.95)",
|
||||
padding: "0 1rem",
|
||||
color: "#999",
|
||||
fontSize: "0.875rem"
|
||||
}}>
|
||||
ou
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleLogin}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
background: "white",
|
||||
color: "#333",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "0.5rem",
|
||||
fontWeight: "500"
|
||||
}}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/>
|
||||
<path d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" fill="#34A853"/>
|
||||
<path d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" fill="#FBBC05"/>
|
||||
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z" fill="#EA4335"/>
|
||||
</svg>
|
||||
Continuar com Google
|
||||
</button>
|
||||
|
||||
<div style={{ textAlign: "center", marginTop: "2rem", paddingTop: "1.5rem", borderTop: "1px solid #e2e8f0" }}>
|
||||
<p style={{ color: "#999", fontSize: "0.75rem", margin: 0 }}>
|
||||
Powered By{" "}
|
||||
<a
|
||||
href="https://sergiocorrea.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#667eea", textDecoration: "none", fontWeight: "500" }}
|
||||
>
|
||||
Sergio Correa
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense fallback={<div>Carregando...</div>}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
197
frontend/app/onboarding/page.tsx
Normal file
197
frontend/app/onboarding/page.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
// app/onboarding/page.tsx
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const router = useRouter()
|
||||
const { data: session } = useSession()
|
||||
const [activeTab, setActiveTab] = useState<"accept" | "create">("accept")
|
||||
const [inviteCode, setInviteCode] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const handleAcceptInvite = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/invites/accept", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ inviteCode: inviteCode.trim() })
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Erro ao aceitar convite")
|
||||
}
|
||||
|
||||
router.push(`/workspace/${data.workspace.slug}`)
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: "100vh",
|
||||
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "2rem"
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: "600px",
|
||||
width: "100%",
|
||||
background: "white",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||
<h1 style={{ fontSize: "2rem", marginBottom: "0.5rem" }}>
|
||||
🚀 Bem-vindo ao PDIMaker
|
||||
</h1>
|
||||
<p style={{ color: "#666" }}>
|
||||
Escolha como deseja começar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: "flex", borderBottom: "1px solid #e2e8f0" }}>
|
||||
<button
|
||||
onClick={() => setActiveTab("accept")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "1rem",
|
||||
background: activeTab === "accept" ? "white" : "#f7fafc",
|
||||
border: "none",
|
||||
borderBottom: activeTab === "accept" ? "3px solid #667eea" : "none",
|
||||
fontWeight: activeTab === "accept" ? "600" : "normal",
|
||||
cursor: "pointer",
|
||||
color: activeTab === "accept" ? "#667eea" : "#666"
|
||||
}}
|
||||
>
|
||||
Tenho um código
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("create")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "1rem",
|
||||
background: activeTab === "create" ? "white" : "#f7fafc",
|
||||
border: "none",
|
||||
borderBottom: activeTab === "create" ? "3px solid #667eea" : "none",
|
||||
fontWeight: activeTab === "create" ? "600" : "normal",
|
||||
cursor: "pointer",
|
||||
color: activeTab === "create" ? "#667eea" : "#666"
|
||||
}}
|
||||
>
|
||||
Criar workspace
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: "2rem" }}>
|
||||
{activeTab === "accept" ? (
|
||||
<div>
|
||||
<h2 style={{ fontSize: "1.5rem", marginBottom: "1rem" }}>
|
||||
Aceitar Convite
|
||||
</h2>
|
||||
<p style={{ color: "#666", marginBottom: "1.5rem" }}>
|
||||
Cole o código de convite que você recebeu do seu mentor ou mentorado:
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleAcceptInvite}>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteCode}
|
||||
onChange={(e) => setInviteCode(e.target.value)}
|
||||
placeholder="cmhrtjzk6001jox4z9dzb5al"
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "1rem",
|
||||
border: "2px solid #e2e8f0",
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
fontFamily: "monospace",
|
||||
marginBottom: "1rem"
|
||||
}}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: "1rem",
|
||||
background: "#fee2e2",
|
||||
border: "1px solid #fecaca",
|
||||
borderRadius: "0.375rem",
|
||||
color: "#dc2626",
|
||||
marginBottom: "1rem",
|
||||
fontSize: "0.875rem"
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "1rem",
|
||||
background: loading ? "#cbd5e0" : "#667eea",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "600",
|
||||
cursor: loading ? "not-allowed" : "pointer"
|
||||
}}
|
||||
>
|
||||
{loading ? "Aceitando..." : "Aceitar Convite"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h2 style={{ fontSize: "1.5rem", marginBottom: "1rem" }}>
|
||||
Criar Novo Workspace
|
||||
</h2>
|
||||
<p style={{ color: "#666", marginBottom: "1.5rem" }}>
|
||||
Crie um workspace e convide seu mentor ou mentorado para colaborar.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => router.push("/workspace/create")}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "1rem",
|
||||
background: "#667eea",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "600",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
Criar Workspace
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
15
frontend/app/page.tsx
Normal file
15
frontend/app/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { redirect } from "next/navigation"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth/config"
|
||||
|
||||
export default async function Home() {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
// Se estiver autenticado, redireciona para dashboard
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
// Se não estiver autenticado, redireciona para login
|
||||
redirect("/login")
|
||||
}
|
||||
251
frontend/app/register/page.tsx
Normal file
251
frontend/app/register/page.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
// app/register/page.tsx
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { signIn } from "next-auth/react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter()
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
// Validações
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError("As senhas não coincidem")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.password.length < 6) {
|
||||
setError("A senha deve ter pelo menos 6 caracteres")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Registrar usuário
|
||||
const response = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
password: formData.password
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Erro ao criar conta")
|
||||
}
|
||||
|
||||
// Fazer login automático
|
||||
const result = await signIn("credentials", {
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
redirect: false
|
||||
})
|
||||
|
||||
if (result?.error) {
|
||||
setError("Conta criada! Faça login para continuar")
|
||||
setTimeout(() => router.push("/login"), 2000)
|
||||
} else {
|
||||
router.push("/dashboard")
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: "100vh",
|
||||
backgroundImage: "url('/pdimaker-background.jpg')",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "2rem",
|
||||
position: "relative"
|
||||
}}>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "rgba(0, 0, 0, 0.5)"
|
||||
}} />
|
||||
|
||||
<div style={{
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
background: "rgba(255, 255, 255, 0.95)",
|
||||
padding: "3rem",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
||||
maxWidth: "450px",
|
||||
width: "100%"
|
||||
}}>
|
||||
<h1 style={{ fontSize: "2rem", marginBottom: "0.5rem", textAlign: "center" }}>
|
||||
🚀 PDIMaker
|
||||
</h1>
|
||||
<p style={{ color: "#666", marginBottom: "2rem", textAlign: "center" }}>
|
||||
Criar nova conta
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Nome completo
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Senha
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
minLength={6}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Confirmar senha
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
required
|
||||
minLength={6}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: "1rem",
|
||||
background: "#fee2e2",
|
||||
border: "1px solid #fecaca",
|
||||
borderRadius: "0.375rem",
|
||||
color: "#dc2626",
|
||||
marginBottom: "1.5rem",
|
||||
fontSize: "0.875rem"
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
background: loading ? "#cbd5e0" : "#667eea",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "600",
|
||||
cursor: loading ? "not-allowed" : "pointer",
|
||||
marginBottom: "1rem"
|
||||
}}
|
||||
>
|
||||
{loading ? "Criando conta..." : "Criar conta"}
|
||||
</button>
|
||||
|
||||
<div style={{ textAlign: "center", color: "#666", fontSize: "0.875rem" }}>
|
||||
Já tem uma conta?{" "}
|
||||
<Link href="/login" style={{ color: "#667eea", fontWeight: "500" }}>
|
||||
Fazer login
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "center", marginTop: "2rem", paddingTop: "1.5rem", borderTop: "1px solid #e2e8f0" }}>
|
||||
<p style={{ color: "#999", fontSize: "0.75rem", margin: 0 }}>
|
||||
Powered By{" "}
|
||||
<a
|
||||
href="https://sergiocorrea.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#667eea", textDecoration: "none", fontWeight: "500" }}
|
||||
>
|
||||
Sergio Correa
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
41
frontend/app/unauthorized/page.tsx
Normal file
41
frontend/app/unauthorized/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
// app/unauthorized/page.tsx
|
||||
import Link from "next/link"
|
||||
|
||||
export default function UnauthorizedPage() {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#f7fafc"
|
||||
}}>
|
||||
<div style={{
|
||||
textAlign: "center",
|
||||
padding: "2rem"
|
||||
}}>
|
||||
<h1 style={{ fontSize: "4rem", marginBottom: "1rem" }}>🚫</h1>
|
||||
<h2 style={{ fontSize: "2rem", marginBottom: "1rem", color: "#333" }}>
|
||||
Acesso Negado
|
||||
</h2>
|
||||
<p style={{ color: "#666", marginBottom: "2rem" }}>
|
||||
Você não tem permissão para acessar este workspace.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
style={{
|
||||
padding: "0.75rem 2rem",
|
||||
background: "#667eea",
|
||||
color: "white",
|
||||
textDecoration: "none",
|
||||
borderRadius: "0.5rem",
|
||||
display: "inline-block"
|
||||
}}
|
||||
>
|
||||
Voltar para Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
263
frontend/app/workspace/[slug]/page.tsx
Normal file
263
frontend/app/workspace/[slug]/page.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
// app/workspace/[slug]/page.tsx
|
||||
import { redirect } from "next/navigation"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth/config"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import Link from "next/link"
|
||||
import { CopyButton } from "@/components/CopyButton"
|
||||
|
||||
interface WorkspacePageProps {
|
||||
params: { slug: string }
|
||||
}
|
||||
|
||||
export default async function WorkspacePage({ params }: WorkspacePageProps) {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session) {
|
||||
redirect("/login")
|
||||
}
|
||||
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: { slug: params.slug },
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, email: true, avatar: true } },
|
||||
manager: { select: { id: true, name: true, email: true, avatar: true } },
|
||||
hr: { select: { id: true, name: true, email: true, avatar: true } }
|
||||
}
|
||||
})
|
||||
|
||||
if (!workspace) {
|
||||
redirect("/workspaces")
|
||||
}
|
||||
|
||||
// Verificar se o usuário tem acesso
|
||||
const hasAccess =
|
||||
workspace.employeeId === session.user.id ||
|
||||
workspace.managerId === session.user.id ||
|
||||
workspace.hrId === session.user.id
|
||||
|
||||
if (!hasAccess) {
|
||||
redirect("/unauthorized")
|
||||
}
|
||||
|
||||
// Determinar o papel do usuário
|
||||
const userRole =
|
||||
workspace.employeeId === session.user.id ? "employee" :
|
||||
workspace.managerId === session.user.id ? "manager" : "hr"
|
||||
|
||||
// Buscar código de convite ativo (se for gestor)
|
||||
let inviteCode = null
|
||||
if (userRole === "manager") {
|
||||
const activeInvite = await prisma.invite.findFirst({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
status: "PENDING",
|
||||
expiresAt: { gt: new Date() }
|
||||
},
|
||||
select: { token: true }
|
||||
})
|
||||
inviteCode = activeInvite?.token
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100vh", background: "#f7fafc" }}>
|
||||
{/* Header */}
|
||||
<div style={{ background: "#667eea", color: "white", padding: "1.5rem" }}>
|
||||
<div style={{ maxWidth: "1200px", margin: "0 auto", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: "1.5rem", fontWeight: "bold" }}>Workspace</h1>
|
||||
<p style={{ opacity: 0.9, fontSize: "0.875rem" }}>
|
||||
Gerencie sua área de colaboração
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/workspaces"
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
background: "rgba(255,255,255,0.2)",
|
||||
borderRadius: "0.375rem",
|
||||
textDecoration: "none",
|
||||
color: "white"
|
||||
}}
|
||||
>
|
||||
Voltar
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ maxWidth: "1200px", margin: "0 auto", padding: "2rem" }}>
|
||||
{/* Código de Convite (apenas para gestores) */}
|
||||
{userRole === "manager" && inviteCode && (
|
||||
<div style={{ background: "white", padding: "2rem", borderRadius: "0.5rem", boxShadow: "0 1px 3px rgba(0,0,0,0.1)", marginBottom: "2rem" }}>
|
||||
<h2 style={{ fontSize: "1.5rem", fontWeight: "bold", marginBottom: "1rem" }}>
|
||||
Código de Convite
|
||||
</h2>
|
||||
<p style={{ color: "#666", marginBottom: "1rem" }}>
|
||||
Compartilhe este código com seu mentor para que ele possa se juntar ao workspace:
|
||||
</p>
|
||||
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
||||
<code style={{
|
||||
flex: 1,
|
||||
padding: "1rem",
|
||||
background: "#f7fafc",
|
||||
border: "2px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1.25rem",
|
||||
fontFamily: "monospace",
|
||||
color: "#667eea"
|
||||
}}>
|
||||
{inviteCode}
|
||||
</code>
|
||||
<CopyButton text={inviteCode} />
|
||||
</div>
|
||||
<div style={{
|
||||
marginTop: "1rem",
|
||||
padding: "0.75rem",
|
||||
background: "#d1fae5",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "0.875rem",
|
||||
color: "#047857"
|
||||
}}>
|
||||
✓ Mentor conectado! Seu workspace está ativo e pronto para colaboração
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Membros do Workspace */}
|
||||
<div style={{ background: "white", padding: "2rem", borderRadius: "0.5rem", boxShadow: "0 1px 3px rgba(0,0,0,0.1)" }}>
|
||||
<h2 style={{ fontSize: "1.5rem", fontWeight: "bold", marginBottom: "1.5rem" }}>
|
||||
Membros do Workspace
|
||||
</h2>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||
{/* Funcionário */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "1rem",
|
||||
background: "#f7fafc",
|
||||
borderRadius: "0.375rem"
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||
<div style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
borderRadius: "50%",
|
||||
background: "#667eea",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "white",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "bold"
|
||||
}}>
|
||||
{workspace.employee.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: "600" }}>{workspace.employee.name}</div>
|
||||
<div style={{ fontSize: "0.875rem", color: "#666" }}>{workspace.employee.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: "0.375rem 0.75rem",
|
||||
background: "#e0e7ff",
|
||||
borderRadius: "9999px",
|
||||
fontSize: "0.875rem",
|
||||
color: "#4c51bf",
|
||||
fontWeight: "500"
|
||||
}}>
|
||||
👨💼 Mentorado
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gestor */}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "1rem",
|
||||
background: "#f7fafc",
|
||||
borderRadius: "0.375rem"
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||
<div style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
borderRadius: "50%",
|
||||
background: "#10b981",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "white",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "bold"
|
||||
}}>
|
||||
{workspace.manager.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: "600" }}>{workspace.manager.name}</div>
|
||||
<div style={{ fontSize: "0.875rem", color: "#666" }}>{workspace.manager.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: "0.375rem 0.75rem",
|
||||
background: "#d1fae5",
|
||||
borderRadius: "9999px",
|
||||
fontSize: "0.875rem",
|
||||
color: "#047857",
|
||||
fontWeight: "500"
|
||||
}}>
|
||||
🎓 Mentor
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RH (se existir) */}
|
||||
{workspace.hr && (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "1rem",
|
||||
background: "#f7fafc",
|
||||
borderRadius: "0.375rem"
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||
<div style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
borderRadius: "50%",
|
||||
background: "#f59e0b",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "white",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "bold"
|
||||
}}>
|
||||
{workspace.hr.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: "600" }}>{workspace.hr.name}</div>
|
||||
<div style={{ fontSize: "0.875rem", color: "#666" }}>{workspace.hr.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: "0.375rem 0.75rem",
|
||||
background: "#fef3c7",
|
||||
borderRadius: "9999px",
|
||||
fontSize: "0.875rem",
|
||||
color: "#b45309",
|
||||
fontWeight: "500"
|
||||
}}>
|
||||
👔 RH
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
127
frontend/app/workspace/create/page.tsx
Normal file
127
frontend/app/workspace/create/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
// app/workspace/create/page.tsx
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
export default function CreateWorkspacePage() {
|
||||
const router = useRouter()
|
||||
const { data: session } = useSession()
|
||||
const [email, setEmail] = useState("")
|
||||
const [role, setRole] = useState<"EMPLOYEE" | "MANAGER">("EMPLOYEE")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/workspaces/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, inviteRole: role })
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Erro ao criar workspace")
|
||||
}
|
||||
|
||||
router.push(`/workspace/${data.workspace.slug}`)
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100vh", background: "#f7fafc", padding: "2rem" }}>
|
||||
<div style={{ maxWidth: "600px", margin: "0 auto" }}>
|
||||
<h1 style={{ fontSize: "2rem", fontWeight: "bold", marginBottom: "2rem" }}>
|
||||
Criar Novo Workspace
|
||||
</h1>
|
||||
|
||||
<div style={{ background: "white", padding: "2rem", borderRadius: "0.5rem", boxShadow: "0 1px 3px rgba(0,0,0,0.1)" }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Você será o:
|
||||
</label>
|
||||
<select
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value as any)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
>
|
||||
<option value="MANAGER">Gestor (Mentor)</option>
|
||||
<option value="EMPLOYEE">Funcionário (Mentorado)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem", fontWeight: "500" }}>
|
||||
Email do {role === "MANAGER" ? "Funcionário" : "Gestor"}:
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="email@exemplo.com"
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: "1rem",
|
||||
background: "#fee2e2",
|
||||
border: "1px solid #fecaca",
|
||||
borderRadius: "0.375rem",
|
||||
color: "#dc2626",
|
||||
marginBottom: "1.5rem"
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.75rem",
|
||||
background: loading ? "#cbd5e0" : "#667eea",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "500",
|
||||
cursor: loading ? "not-allowed" : "pointer"
|
||||
}}
|
||||
>
|
||||
{loading ? "Criando..." : "Criar Workspace e Enviar Convite"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
172
frontend/app/workspaces/page.tsx
Normal file
172
frontend/app/workspaces/page.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
// app/workspaces/page.tsx
|
||||
import { redirect } from "next/navigation"
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/lib/auth/config"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import Link from "next/link"
|
||||
|
||||
export default async function WorkspacesPage() {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session) {
|
||||
redirect("/login")
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: {
|
||||
workspacesAsEmployee: {
|
||||
where: { status: "ACTIVE" },
|
||||
include: {
|
||||
manager: { select: { name: true, avatar: true, email: true } }
|
||||
}
|
||||
},
|
||||
workspacesAsManager: {
|
||||
where: { status: "ACTIVE" },
|
||||
include: {
|
||||
employee: { select: { name: true, avatar: true, email: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100vh", background: "#f7fafc", padding: "2rem" }}>
|
||||
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
|
||||
<div style={{ marginBottom: "2rem" }}>
|
||||
<h1 style={{ fontSize: "2rem", fontWeight: "bold", marginBottom: "0.5rem" }}>
|
||||
Meus Workspaces
|
||||
</h1>
|
||||
<p style={{ color: "#666" }}>
|
||||
Selecione um workspace para acessar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: "1.5rem" }}>
|
||||
{/* Workspaces como Funcionário */}
|
||||
{user?.workspacesAsEmployee.map((workspace) => (
|
||||
<Link
|
||||
key={workspace.id}
|
||||
href={`/workspace/${workspace.slug}`}
|
||||
style={{
|
||||
background: "white",
|
||||
padding: "1.5rem",
|
||||
borderRadius: "0.5rem",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
transition: "all 0.2s",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem", marginBottom: "1rem" }}>
|
||||
<div style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
borderRadius: "50%",
|
||||
background: "#667eea",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "white",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "bold"
|
||||
}}>
|
||||
{workspace.manager.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: "600", fontSize: "1.1rem" }}>
|
||||
{workspace.manager.name}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.875rem", color: "#666" }}>
|
||||
Seu Gestor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: "0.5rem",
|
||||
background: "#e0e7ff",
|
||||
borderRadius: "0.25rem",
|
||||
fontSize: "0.875rem",
|
||||
color: "#4c51bf"
|
||||
}}>
|
||||
👤 Você é o Funcionário
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Workspaces como Gestor */}
|
||||
{user?.workspacesAsManager.map((workspace) => (
|
||||
<Link
|
||||
key={workspace.id}
|
||||
href={`/workspace/${workspace.slug}`}
|
||||
style={{
|
||||
background: "white",
|
||||
padding: "1.5rem",
|
||||
borderRadius: "0.5rem",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
transition: "all 0.2s",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem", marginBottom: "1rem" }}>
|
||||
<div style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
borderRadius: "50%",
|
||||
background: "#10b981",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "white",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "bold"
|
||||
}}>
|
||||
{workspace.employee.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: "600", fontSize: "1.1rem" }}>
|
||||
{workspace.employee.name}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.875rem", color: "#666" }}>
|
||||
Seu Mentorado
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: "0.5rem",
|
||||
background: "#d1fae5",
|
||||
borderRadius: "0.25rem",
|
||||
fontSize: "0.875rem",
|
||||
color: "#047857"
|
||||
}}>
|
||||
🎓 Você é o Gestor
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Botão para criar novo workspace */}
|
||||
<div style={{ marginTop: "2rem", textAlign: "center" }}>
|
||||
<Link
|
||||
href="/workspace/create"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "1rem 2rem",
|
||||
background: "#667eea",
|
||||
color: "white",
|
||||
borderRadius: "0.5rem",
|
||||
textDecoration: "none",
|
||||
fontWeight: "500"
|
||||
}}
|
||||
>
|
||||
+ Criar Novo Workspace
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
31
frontend/components/CopyButton.tsx
Normal file
31
frontend/components/CopyButton.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
// components/CopyButton.tsx
|
||||
"use client"
|
||||
|
||||
interface CopyButtonProps {
|
||||
text: string
|
||||
}
|
||||
|
||||
export function CopyButton({ text }: CopyButtonProps) {
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(text)
|
||||
alert("Código copiado!")
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
style={{
|
||||
padding: "1rem 2rem",
|
||||
background: "#667eea",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.375rem",
|
||||
cursor: "pointer",
|
||||
fontWeight: "500"
|
||||
}}
|
||||
>
|
||||
Copiar
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
13
frontend/components/SessionProvider.tsx
Normal file
13
frontend/components/SessionProvider.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
// components/SessionProvider.tsx
|
||||
"use client"
|
||||
|
||||
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"
|
||||
|
||||
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextAuthSessionProvider>
|
||||
{children}
|
||||
</NextAuthSessionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
136
frontend/lib/auth/config.ts
Normal file
136
frontend/lib/auth/config.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// lib/auth/config.ts
|
||||
import { NextAuthOptions } from "next-auth"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { authenticateUser } from "./credentials"
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
adapter: PrismaAdapter(prisma) as any,
|
||||
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Senha", type: "password" }
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await authenticateUser(credentials.email, credentials.password)
|
||||
return user
|
||||
} catch (error) {
|
||||
console.error("Erro ao autenticar:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID || "",
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
|
||||
}),
|
||||
],
|
||||
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
error: "/login",
|
||||
newUser: "/onboarding",
|
||||
},
|
||||
|
||||
callbacks: {
|
||||
async signIn({ user, account, profile }) {
|
||||
// Verificar se o usuário já existe
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: user.email! }
|
||||
})
|
||||
|
||||
// Se não existir, criar com dados do Google
|
||||
if (!existingUser) {
|
||||
await prisma.user.upsert({
|
||||
where: { email: user.email! },
|
||||
create: {
|
||||
email: user.email!,
|
||||
name: user.name || user.email!.split("@")[0],
|
||||
avatar: user.image,
|
||||
googleId: account?.providerAccountId,
|
||||
role: "EMPLOYEE"
|
||||
},
|
||||
update: {
|
||||
googleId: account?.providerAccountId,
|
||||
avatar: user.image,
|
||||
lastLoginAt: new Date()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Atualizar último login
|
||||
await prisma.user.update({
|
||||
where: { id: existingUser.id },
|
||||
data: { lastLoginAt: new Date() }
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { email: user.email! }
|
||||
})
|
||||
|
||||
if (dbUser) {
|
||||
token.id = dbUser.id
|
||||
token.role = dbUser.role
|
||||
}
|
||||
}
|
||||
return token
|
||||
},
|
||||
|
||||
async session({ session, token }) {
|
||||
if (session.user && token.id) {
|
||||
const userId = String(token.id);
|
||||
(session.user as any).id = userId;
|
||||
(session.user as any).role = token.role;
|
||||
|
||||
// Buscar workspaces do usuário
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
workspacesAsEmployee: {
|
||||
where: { status: "ACTIVE" },
|
||||
select: { id: true, slug: true, managerId: true },
|
||||
},
|
||||
workspacesAsManager: {
|
||||
where: { status: "ACTIVE" },
|
||||
select: { id: true, slug: true, employeeId: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (user) {
|
||||
(session.user as any).workspaces = {
|
||||
asEmployee: user.workspacesAsEmployee,
|
||||
asManager: user.workspacesAsManager,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar workspaces:", error)
|
||||
}
|
||||
}
|
||||
return session
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export { authOptions as auth }
|
||||
|
||||
64
frontend/lib/auth/credentials.ts
Normal file
64
frontend/lib/auth/credentials.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// lib/auth/credentials.ts
|
||||
import bcrypt from "bcryptjs"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, 10)
|
||||
}
|
||||
|
||||
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hashedPassword)
|
||||
}
|
||||
|
||||
export async function createUser(email: string, password: string, name: string) {
|
||||
// Verificar se usuário já existe
|
||||
const existing = await prisma.user.findUnique({ where: { email } })
|
||||
if (existing) {
|
||||
throw new Error("Email já cadastrado")
|
||||
}
|
||||
|
||||
// Hash da senha
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
// Criar usuário
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
name,
|
||||
role: "EMPLOYEE"
|
||||
}
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
export async function authenticateUser(email: string, password: string) {
|
||||
// Buscar usuário
|
||||
const user = await prisma.user.findUnique({ where: { email } })
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Verificar senha
|
||||
const isValid = await verifyPassword(password, user.password)
|
||||
|
||||
if (!isValid) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Atualizar último login
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() }
|
||||
})
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
}
|
||||
}
|
||||
|
||||
13
frontend/lib/prisma.ts
Normal file
13
frontend/lib/prisma.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// lib/prisma.ts
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient }
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ||
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
|
||||
56
frontend/lib/types/next-auth.d.ts
vendored
Normal file
56
frontend/lib/types/next-auth.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// lib/types/next-auth.d.ts
|
||||
import { Role } from "@prisma/client"
|
||||
import "next-auth"
|
||||
import "next-auth/jwt"
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
role: Role
|
||||
avatar?: string
|
||||
workspaces: {
|
||||
asEmployee: Array<{
|
||||
id: string
|
||||
slug: string
|
||||
managerId: string
|
||||
}>
|
||||
asManager: Array<{
|
||||
id: string
|
||||
slug: string
|
||||
employeeId: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
role: Role
|
||||
avatar?: string
|
||||
}
|
||||
}
|
||||
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
id: string
|
||||
role: Role
|
||||
workspaces?: {
|
||||
asEmployee: Array<{
|
||||
id: string
|
||||
slug: string
|
||||
managerId: string
|
||||
}>
|
||||
asManager: Array<{
|
||||
id: string
|
||||
slug: string
|
||||
employeeId: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
frontend/lib/utils/invite-code.ts
Normal file
41
frontend/lib/utils/invite-code.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// lib/utils/invite-code.ts
|
||||
import crypto from "crypto"
|
||||
|
||||
/**
|
||||
* Gera um código de convite único
|
||||
* Formato: cmhrtjzk6001jox4z9dzb5al
|
||||
*/
|
||||
export function generateInviteCode(): string {
|
||||
return crypto
|
||||
.randomBytes(16)
|
||||
.toString("base64")
|
||||
.replace(/[^a-z0-9]/gi, "")
|
||||
.toLowerCase()
|
||||
.substring(0, 24)
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida formato do código de convite
|
||||
*/
|
||||
export function isValidInviteCode(code: string): boolean {
|
||||
return /^[a-z0-9]{24}$/.test(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera slug único para workspace
|
||||
* Formato: employee-name-manager-name-xyz123
|
||||
*/
|
||||
export function generateWorkspaceSlug(employeeName: string, managerName: string): string {
|
||||
const slugify = (text: string) =>
|
||||
text
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.substring(0, 20)
|
||||
|
||||
const random = crypto.randomBytes(3).toString("hex")
|
||||
return `${slugify(employeeName)}-${slugify(managerName)}-${random}`
|
||||
}
|
||||
|
||||
57
frontend/middleware.ts
Normal file
57
frontend/middleware.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// middleware.ts
|
||||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import { getToken } from "next-auth/jwt"
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const { pathname } = req.nextUrl
|
||||
|
||||
// Rotas públicas
|
||||
const publicRoutes = ["/", "/login", "/about", "/api/auth", "/test.html"]
|
||||
const isPublicRoute = publicRoutes.some(route => pathname.startsWith(route))
|
||||
|
||||
if (isPublicRoute) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// Verificar autenticação
|
||||
const token = await getToken({
|
||||
req,
|
||||
secret: process.env.NEXTAUTH_SECRET
|
||||
})
|
||||
|
||||
// Requer autenticação
|
||||
if (!token) {
|
||||
const loginUrl = new URL("/login", req.url)
|
||||
loginUrl.searchParams.set("callbackUrl", pathname)
|
||||
return NextResponse.redirect(loginUrl)
|
||||
}
|
||||
|
||||
// Proteção de workspace
|
||||
if (pathname.startsWith("/workspace/")) {
|
||||
const slug = pathname.split("/")[2]
|
||||
|
||||
if (slug && token.workspaces) {
|
||||
const workspaces = token.workspaces as any
|
||||
const allWorkspaces = [
|
||||
...(workspaces.asEmployee || []),
|
||||
...(workspaces.asManager || [])
|
||||
]
|
||||
|
||||
const hasAccess = allWorkspaces.some((w: any) => w.slug === slug)
|
||||
|
||||
if (!hasAccess) {
|
||||
return NextResponse.redirect(new URL("/unauthorized", req.url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!api/auth|_next/static|_next/image|favicon.ico|public).*)"
|
||||
]
|
||||
}
|
||||
|
||||
13
frontend/next.config.js
Normal file
13
frontend/next.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
webpack: (config, { isServer }) => {
|
||||
if (isServer) {
|
||||
config.externals.push('_http_common')
|
||||
}
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
882
frontend/package-lock.json
generated
Normal file
882
frontend/package-lock.json
generated
Normal file
@@ -0,0 +1,882 @@
|
||||
{
|
||||
"name": "pdimaker-web",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pdimaker-web",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^1.4.0",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"crypto": "^1.0.1",
|
||||
"next": "14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"prisma": "^5.8.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.3.tgz",
|
||||
"integrity": "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.1.1",
|
||||
"@types/cookie": "0.6.0",
|
||||
"cookie": "0.6.0",
|
||||
"jose": "^5.1.3",
|
||||
"oauth4webapi": "^2.10.4",
|
||||
"preact": "10.11.3",
|
||||
"preact-render-to-string": "5.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.2",
|
||||
"nodemailer": "^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@simplewebauthn/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@simplewebauthn/server": {
|
||||
"optional": true
|
||||
},
|
||||
"nodemailer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/prisma-adapter": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.6.0.tgz",
|
||||
"integrity": "sha512-PQU8/Oi5gfjzb0MkhMGVX0Dg877phPzsQdK54+C7ubukCeZPjyvuSAx1vVtWEYVWp2oQvjgG/C6QiDoeC7S10A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@auth/core": "0.29.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/prisma-adapter/node_modules/@auth/core": {
|
||||
"version": "0.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.29.0.tgz",
|
||||
"integrity": "sha512-MdfEjU6WRjUnPG1+XeBWrTIlAsLZU6V0imCIqVDDDPxLI6UZWldXVqAA2EsDazGofV78jqiCLHaN85mJITDqdg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.1.1",
|
||||
"@types/cookie": "0.6.0",
|
||||
"cookie": "0.6.0",
|
||||
"jose": "^5.1.3",
|
||||
"oauth4webapi": "^2.4.0",
|
||||
"preact": "10.11.3",
|
||||
"preact-render-to-string": "5.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.2",
|
||||
"nodemailer": "^6.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@simplewebauthn/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@simplewebauthn/server": {
|
||||
"optional": true
|
||||
},
|
||||
"nodemailer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
|
||||
"integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz",
|
||||
"integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz",
|
||||
"integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz",
|
||||
"integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz",
|
||||
"integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz",
|
||||
"integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz",
|
||||
"integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz",
|
||||
"integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz",
|
||||
"integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz",
|
||||
"integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@panva/hkdf": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
||||
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
|
||||
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
|
||||
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/fetch-engine": "5.22.0",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
||||
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
||||
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
||||
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
||||
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001755",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz",
|
||||
"integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
||||
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz",
|
||||
"integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "14.1.0",
|
||||
"@swc/helpers": "0.5.2",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"next": "dist/bin/next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "14.1.0",
|
||||
"@next/swc-darwin-x64": "14.1.0",
|
||||
"@next/swc-linux-arm64-gnu": "14.1.0",
|
||||
"@next/swc-linux-arm64-musl": "14.1.0",
|
||||
"@next/swc-linux-x64-gnu": "14.1.0",
|
||||
"@next/swc-linux-x64-musl": "14.1.0",
|
||||
"@next/swc-win32-arm64-msvc": "14.1.0",
|
||||
"@next/swc-win32-ia32-msvc": "14.1.0",
|
||||
"@next/swc-win32-x64-msvc": "14.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth": {
|
||||
"version": "4.24.13",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz",
|
||||
"integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@panva/hkdf": "^1.0.2",
|
||||
"cookie": "^0.7.0",
|
||||
"jose": "^4.15.5",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.4.0",
|
||||
"preact": "^10.6.3",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@auth/core": "0.34.3",
|
||||
"next": "^12.2.5 || ^13 || ^14 || ^15 || ^16",
|
||||
"nodemailer": "^7.0.7",
|
||||
"react": "^17.0.2 || ^18 || ^19",
|
||||
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@auth/core": {
|
||||
"optional": true
|
||||
},
|
||||
"nodemailer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth/node_modules/jose": {
|
||||
"version": "4.15.9",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth": {
|
||||
"version": "0.9.15",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oauth4webapi": {
|
||||
"version": "2.17.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz",
|
||||
"integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
|
||||
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^10.13.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jose": "^4.15.9",
|
||||
"lru-cache": "^6.0.0",
|
||||
"object-hash": "^2.2.0",
|
||||
"oidc-token-hash": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/jose": {
|
||||
"version": "4.15.9",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.11.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/preact-render-to-string": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
|
||||
"integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pretty-format": "^3.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.22.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
|
||||
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"client-only": "0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
30
frontend/package.json
Normal file
30
frontend/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "pdimaker-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "prisma generate && next build",
|
||||
"start": "next start",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:push": "prisma db push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^1.4.0",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"crypto": "^1.0.1",
|
||||
"next": "14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"prisma": "^5.8.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
381
frontend/prisma/schema.prisma
Normal file
381
frontend/prisma/schema.prisma
Normal file
@@ -0,0 +1,381 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// USUÁRIOS E AUTENTICAÇÃO
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
enum Role {
|
||||
EMPLOYEE
|
||||
MANAGER
|
||||
HR_ADMIN
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
avatar String?
|
||||
role Role @default(EMPLOYEE)
|
||||
googleId String? @unique
|
||||
password String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lastLoginAt DateTime?
|
||||
|
||||
workspacesAsEmployee Workspace[] @relation("EmployeeWorkspaces")
|
||||
workspacesAsManager Workspace[] @relation("ManagerWorkspaces")
|
||||
workspacesAsHR Workspace[] @relation("HRWorkspaces")
|
||||
|
||||
journalEntries JournalEntry[]
|
||||
comments Comment[]
|
||||
reactions Reaction[]
|
||||
goals Goal[]
|
||||
assessmentResults AssessmentResult[]
|
||||
oneOnOnesAsEmployee OneOnOne[] @relation("EmployeeOneOnOnes")
|
||||
oneOnOnesAsManager OneOnOne[] @relation("ManagerOneOnOnes")
|
||||
|
||||
@@index([email])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// WORKSPACES (Salas 1:1)
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
enum WorkspaceStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
PENDING_INVITE
|
||||
}
|
||||
|
||||
model Workspace {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
|
||||
employeeId String
|
||||
employee User @relation("EmployeeWorkspaces", fields: [employeeId], references: [id])
|
||||
|
||||
managerId String
|
||||
manager User @relation("ManagerWorkspaces", fields: [managerId], references: [id])
|
||||
|
||||
hrId String?
|
||||
hr User? @relation("HRWorkspaces", fields: [hrId], references: [id])
|
||||
|
||||
status WorkspaceStatus @default(ACTIVE)
|
||||
config Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
journalEntries JournalEntry[]
|
||||
goals Goal[]
|
||||
oneOnOnes OneOnOne[]
|
||||
assessmentResults AssessmentResult[]
|
||||
|
||||
@@unique([employeeId, managerId])
|
||||
@@index([slug])
|
||||
@@index([employeeId])
|
||||
@@index([managerId])
|
||||
@@map("workspaces")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// DIÁRIO DE ATIVIDADES
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
enum JournalVisibility {
|
||||
PUBLIC
|
||||
PRIVATE
|
||||
SUMMARY
|
||||
}
|
||||
|
||||
model JournalEntry {
|
||||
id String @id @default(cuid())
|
||||
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
authorId String
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
|
||||
date DateTime @default(now())
|
||||
whatIDid String @db.Text
|
||||
whatILearned String? @db.Text
|
||||
difficulties String? @db.Text
|
||||
|
||||
tags String[]
|
||||
attachments Json[]
|
||||
|
||||
needsFeedback Boolean @default(false)
|
||||
markedFor1on1 Boolean @default(false)
|
||||
|
||||
visibility JournalVisibility @default(PUBLIC)
|
||||
moodScore Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
comments Comment[]
|
||||
reactions Reaction[]
|
||||
linkedGoals Goal[] @relation("JournalGoalLinks")
|
||||
|
||||
@@index([workspaceId, date])
|
||||
@@index([authorId])
|
||||
@@map("journal_entries")
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(cuid())
|
||||
|
||||
journalEntryId String
|
||||
journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id], onDelete: Cascade)
|
||||
|
||||
authorId String
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
|
||||
content String @db.Text
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([journalEntryId])
|
||||
@@map("comments")
|
||||
}
|
||||
|
||||
model Reaction {
|
||||
id String @id @default(cuid())
|
||||
|
||||
journalEntryId String
|
||||
journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id], onDelete: Cascade)
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
emoji String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([journalEntryId, userId, emoji])
|
||||
@@index([journalEntryId])
|
||||
@@map("reactions")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// PDI - PLANO DE DESENVOLVIMENTO
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
enum GoalType {
|
||||
TECHNICAL
|
||||
SOFT_SKILL
|
||||
LEADERSHIP
|
||||
CAREER
|
||||
}
|
||||
|
||||
enum GoalStatus {
|
||||
NOT_STARTED
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model Goal {
|
||||
id String @id @default(cuid())
|
||||
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
ownerId String
|
||||
owner User @relation(fields: [ownerId], references: [id])
|
||||
|
||||
title String
|
||||
description String @db.Text
|
||||
type GoalType
|
||||
why String? @db.Text
|
||||
|
||||
status GoalStatus @default(NOT_STARTED)
|
||||
progress Int @default(0)
|
||||
|
||||
startDate DateTime?
|
||||
targetDate DateTime?
|
||||
completedDate DateTime?
|
||||
|
||||
successMetrics String? @db.Text
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
actions Action[]
|
||||
linkedJournals JournalEntry[] @relation("JournalGoalLinks")
|
||||
|
||||
@@index([workspaceId])
|
||||
@@index([ownerId])
|
||||
@@map("goals")
|
||||
}
|
||||
|
||||
enum ActionStatus {
|
||||
PENDING
|
||||
IN_PROGRESS
|
||||
DONE
|
||||
BLOCKED
|
||||
}
|
||||
|
||||
model Action {
|
||||
id String @id @default(cuid())
|
||||
|
||||
goalId String
|
||||
goal Goal @relation(fields: [goalId], references: [id], onDelete: Cascade)
|
||||
|
||||
title String
|
||||
description String? @db.Text
|
||||
|
||||
status ActionStatus @default(PENDING)
|
||||
dueDate DateTime?
|
||||
completedAt DateTime?
|
||||
|
||||
assignedTo String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([goalId])
|
||||
@@map("actions")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// TESTES E PERFIS
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
enum AssessmentType {
|
||||
PERSONALITY
|
||||
BEHAVIORAL
|
||||
MOTIVATIONAL
|
||||
VOCATIONAL
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
model Assessment {
|
||||
id String @id @default(cuid())
|
||||
|
||||
title String
|
||||
description String @db.Text
|
||||
type AssessmentType
|
||||
|
||||
questions Json
|
||||
scoringLogic Json
|
||||
|
||||
isActive Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
results AssessmentResult[]
|
||||
|
||||
@@map("assessments")
|
||||
}
|
||||
|
||||
model AssessmentResult {
|
||||
id String @id @default(cuid())
|
||||
|
||||
assessmentId String
|
||||
assessment Assessment @relation(fields: [assessmentId], references: [id])
|
||||
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
answers Json
|
||||
result Json
|
||||
|
||||
primaryType String?
|
||||
scores Json?
|
||||
|
||||
completedAt DateTime @default(now())
|
||||
managerNotes String? @db.Text
|
||||
|
||||
@@index([workspaceId])
|
||||
@@index([userId])
|
||||
@@map("assessment_results")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// REUNIÕES 1:1
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
enum OneOnOneStatus {
|
||||
SCHEDULED
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model OneOnOne {
|
||||
id String @id @default(cuid())
|
||||
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
employeeId String
|
||||
employee User @relation("EmployeeOneOnOnes", fields: [employeeId], references: [id])
|
||||
|
||||
managerId String
|
||||
manager User @relation("ManagerOneOnOnes", fields: [managerId], references: [id])
|
||||
|
||||
scheduledFor DateTime
|
||||
status OneOnOneStatus @default(SCHEDULED)
|
||||
|
||||
agenda Json[]
|
||||
notes String? @db.Text
|
||||
decisions Json[]
|
||||
nextSteps Json[]
|
||||
|
||||
completedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([workspaceId])
|
||||
@@index([scheduledFor])
|
||||
@@map("one_on_ones")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// CONVITES
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
enum InviteStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
EXPIRED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model Invite {
|
||||
id String @id @default(cuid())
|
||||
|
||||
email String
|
||||
role Role
|
||||
|
||||
token String @unique
|
||||
invitedBy String
|
||||
workspaceId String?
|
||||
|
||||
status InviteStatus @default(PENDING)
|
||||
|
||||
expiresAt DateTime
|
||||
acceptedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([email])
|
||||
@@index([token])
|
||||
@@map("invites")
|
||||
}
|
||||
BIN
frontend/public/pdimaker-background.jpg
Normal file
BIN
frontend/public/pdimaker-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
30
frontend/public/test.html
Normal file
30
frontend/public/test.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Teste de Imagem</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; }
|
||||
img { max-width: 100%; border: 2px solid red; }
|
||||
.bg-test {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-image: url('/pdimaker-background.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border: 2px solid blue;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Teste de Carregamento da Imagem</h1>
|
||||
<h2>Método 1: Tag IMG</h2>
|
||||
<img src="/pdimaker-background.jpg" alt="Background" />
|
||||
|
||||
<h2>Método 2: Background CSS</h2>
|
||||
<div class="bg-test"></div>
|
||||
|
||||
<h2>Informações:</h2>
|
||||
<p>URL da imagem: <a href="/pdimaker-background.jpg" target="_blank">/pdimaker-background.jpg</a></p>
|
||||
</body>
|
||||
</html>
|
||||
168
frontend/server.js
Normal file
168
frontend/server.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// Servir arquivos estáticos da pasta public
|
||||
if (req.url.startsWith('/') && req.url.includes('.')) {
|
||||
const filePath = path.join(__dirname, 'public', req.url);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentTypes = {
|
||||
'.html': 'text/html',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.webp': 'image/webp',
|
||||
'.ico': 'image/x-icon'
|
||||
};
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const contentType = contentTypes[ext] || 'application/octet-stream';
|
||||
res.writeHead(200, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PDIMaker - Plataforma de Desenvolvimento Individual</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background-image: url('/pdimaker-background.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 1.5rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.status {
|
||||
margin-top: 3rem;
|
||||
padding: 2rem;
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(15px);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||
}
|
||||
.status h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.status p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.feature {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.feature:hover {
|
||||
transform: translateY(-5px);
|
||||
background: rgba(255,255,255,0.25);
|
||||
}
|
||||
.feature h3 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 PDIMaker</h1>
|
||||
<p class="subtitle">Plataforma de Desenvolvimento Individual</p>
|
||||
|
||||
<div class="status">
|
||||
<h2>✨ Sistema em Construção</h2>
|
||||
<p>Estamos preparando uma experiência incrível para seu desenvolvimento profissional!</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<h3>📝</h3>
|
||||
<p>Diário de Atividades</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🎯</h3>
|
||||
<p>Metas & PDI</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🧪</h3>
|
||||
<p>Testes Vocacionais</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>👥</h3>
|
||||
<p>Reuniões 1:1</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 2rem; opacity: 0.6;">
|
||||
<small>Versão: 1.0.0-alpha | ${new Date().getFullYear()}</small>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
const PORT = 3000;
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`✅ PDIMaker Frontend running on port ${PORT}`);
|
||||
});
|
||||
27
frontend/tsconfig.json
Normal file
27
frontend/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user