🚀 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:
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}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user