🚀 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:
2025-11-19 02:09:04 +00:00
commit 0524656198
58 changed files with 6660 additions and 0 deletions

136
frontend/lib/auth/config.ts Normal file
View 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 }

View 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
View 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
View 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
}>
}
}
}

View 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}`
}