🚀 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:
75
.gitignore
vendored
Normal file
75
.gitignore
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Production
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
*.env
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
postgres_data/
|
||||
redis_data/
|
||||
uploads_data/
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Prisma
|
||||
migrations/
|
||||
|
||||
# Uploads
|
||||
uploads/
|
||||
backend/uploads/
|
||||
|
||||
# SSL
|
||||
ssl/*.pem
|
||||
ssl/*.key
|
||||
ssl/*.crt
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
/var/log/
|
||||
|
||||
# Backups
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
# Temporary
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
|
||||
202
CONFIGURAR_GOOGLE_OAUTH.md
Normal file
202
CONFIGURAR_GOOGLE_OAUTH.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 🔐 Como Configurar Google OAuth no PDIMaker
|
||||
|
||||
## ⚠️ Erro Atual: "invalid_client"
|
||||
|
||||
Você precisa criar as credenciais OAuth no Google Cloud Console.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Passo a Passo Completo
|
||||
|
||||
### **1. Acessar Google Cloud Console**
|
||||
|
||||
Abra: https://console.cloud.google.com/
|
||||
|
||||
### **2. Criar/Selecionar Projeto**
|
||||
|
||||
- Clique em **"Select a project"** (topo da página)
|
||||
- Clique em **"NEW PROJECT"**
|
||||
- Nome do projeto: `PDIMaker`
|
||||
- Clique em **"CREATE"**
|
||||
- Aguarde a criação e selecione o projeto
|
||||
|
||||
### **3. Configurar OAuth Consent Screen**
|
||||
|
||||
1. No menu lateral, vá em: **APIs & Services** → **OAuth consent screen**
|
||||
2. Selecione: **External** (permite qualquer email do Google)
|
||||
3. Clique em **"CREATE"**
|
||||
|
||||
4. **App information:**
|
||||
- App name: `PDIMaker`
|
||||
- User support email: `seu_email@gmail.com`
|
||||
- Application home page: `https://pdimaker.com.br`
|
||||
- Application privacy policy: `https://pdimaker.com.br/privacy` (pode deixar em branco)
|
||||
- Application terms of service: `https://pdimaker.com.br/terms` (pode deixar em branco)
|
||||
|
||||
5. **Developer contact information:**
|
||||
- Email: `seu_email@gmail.com`
|
||||
|
||||
6. Clique em **"SAVE AND CONTINUE"**
|
||||
|
||||
7. **Scopes:** Clique em **"ADD OR REMOVE SCOPES"**
|
||||
- Selecione:
|
||||
- `.../auth/userinfo.email`
|
||||
- `.../auth/userinfo.profile`
|
||||
- Clique em **"UPDATE"**
|
||||
- Clique em **"SAVE AND CONTINUE"**
|
||||
|
||||
8. **Test users:** (Opcional)
|
||||
- Clique em **"ADD USERS"**
|
||||
- Adicione seu email: `sergio09@gmail.com`
|
||||
- Clique em **"ADD"**
|
||||
- Clique em **"SAVE AND CONTINUE"**
|
||||
|
||||
9. **Summary:** Clique em **"BACK TO DASHBOARD"**
|
||||
|
||||
### **4. Criar Credenciais OAuth**
|
||||
|
||||
1. No menu lateral, vá em: **APIs & Services** → **Credentials**
|
||||
2. Clique em **"+ CREATE CREDENTIALS"** → **"OAuth client ID"**
|
||||
|
||||
3. **Application type:** Web application
|
||||
|
||||
4. **Name:** `PDIMaker Web Client`
|
||||
|
||||
5. **Authorized JavaScript origins:**
|
||||
- `https://pdimaker.com.br`
|
||||
- `https://www.pdimaker.com.br`
|
||||
- `http://localhost:3300` (para testes locais)
|
||||
|
||||
6. **Authorized redirect URIs:**
|
||||
- `https://pdimaker.com.br/api/auth/callback/google`
|
||||
- `https://www.pdimaker.com.br/api/auth/callback/google`
|
||||
- `http://localhost:3300/api/auth/callback/google` (para testes locais)
|
||||
|
||||
7. Clique em **"CREATE"**
|
||||
|
||||
8. **IMPORTANTE:** Uma janela vai abrir com:
|
||||
- **Client ID** (começando com algo como `123456789-abc...apps.googleusercontent.com`)
|
||||
- **Client Secret** (algo como `GOCSPX-abc123...`)
|
||||
|
||||
**COPIE ESSES DOIS VALORES!**
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configurar no Servidor
|
||||
|
||||
### **Opção 1: Editar .env manualmente**
|
||||
|
||||
```bash
|
||||
# Conecte no servidor
|
||||
cd /var/www/pdimaker
|
||||
|
||||
# Edite o arquivo .env
|
||||
nano .env
|
||||
|
||||
# Adicione/atualize estas linhas:
|
||||
GOOGLE_CLIENT_ID=cole_aqui_o_client_id
|
||||
GOOGLE_CLIENT_SECRET=cole_aqui_o_client_secret
|
||||
NEXTAUTH_SECRET=cole_uma_string_aleatoria_longa_aqui
|
||||
NEXTAUTH_URL=https://pdimaker.com.br
|
||||
```
|
||||
|
||||
### **Opção 2: Usar comandos (mais rápido)**
|
||||
|
||||
```bash
|
||||
cd /var/www/pdimaker
|
||||
|
||||
# Substitua os valores abaixo pelos seus
|
||||
export GOOGLE_CLIENT_ID="seu_client_id_aqui"
|
||||
export GOOGLE_CLIENT_SECRET="seu_client_secret_aqui"
|
||||
|
||||
# Adicionar ao .env
|
||||
echo "GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID" >> .env
|
||||
echo "GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET" >> .env
|
||||
|
||||
# Gerar NEXTAUTH_SECRET aleatório
|
||||
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
echo "NEXTAUTH_URL=https://pdimaker.com.br" >> .env
|
||||
```
|
||||
|
||||
### **Reiniciar o Container**
|
||||
|
||||
```bash
|
||||
cd /var/www/pdimaker
|
||||
docker restart pdimaker-web
|
||||
|
||||
# Verificar se está rodando
|
||||
docker logs pdimaker-web --tail=20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testar
|
||||
|
||||
1. Acesse: **https://pdimaker.com.br**
|
||||
2. Clique em **"Entrar com Google"**
|
||||
3. Escolha sua conta Google
|
||||
4. Aceite as permissões
|
||||
5. Você será redirecionado para o dashboard! 🎉
|
||||
|
||||
---
|
||||
|
||||
## ❓ Problemas Comuns
|
||||
|
||||
### **Erro: "invalid_client"**
|
||||
- ✅ Verifique se o `GOOGLE_CLIENT_ID` está correto
|
||||
- ✅ Verifique se o `GOOGLE_CLIENT_SECRET` está correto
|
||||
- ✅ Verifique se não há espaços ou aspas extras no `.env`
|
||||
|
||||
### **Erro: "redirect_uri_mismatch"**
|
||||
- ✅ Verifique se adicionou o callback correto no Google Console:
|
||||
- `https://pdimaker.com.br/api/auth/callback/google`
|
||||
|
||||
### **Erro: "access_denied"**
|
||||
- ✅ Verifique se adicionou seu email nos Test Users (se o app está em teste)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Exemplo de .env Completo
|
||||
|
||||
```bash
|
||||
# Banco de Dados
|
||||
DB_PASSWORD=sua_senha_db
|
||||
DATABASE_URL=postgresql://postgres:sua_senha@postgres:5432/pdimaker_prod
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=sua_senha_redis
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=seu_jwt_secret
|
||||
|
||||
# NextAuth
|
||||
NEXTAUTH_SECRET=gere_uma_string_aleatoria_longa_32_caracteres
|
||||
NEXTAUTH_URL=https://pdimaker.com.br
|
||||
|
||||
# Google OAuth (COLE AQUI!)
|
||||
GOOGLE_CLIENT_ID=123456789-abcdefghijklmnop.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=GOCSPX-abc123def456ghi789jkl
|
||||
|
||||
# URLs
|
||||
FRONTEND_URL=https://pdimaker.com.br
|
||||
NEXT_PUBLIC_API_URL=https://api.pdimaker.com.br
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist Final
|
||||
|
||||
- [ ] Projeto criado no Google Cloud Console
|
||||
- [ ] OAuth Consent Screen configurado
|
||||
- [ ] Credenciais OAuth criadas
|
||||
- [ ] Client ID copiado para o `.env`
|
||||
- [ ] Client Secret copiado para o `.env`
|
||||
- [ ] NEXTAUTH_SECRET gerado e adicionado
|
||||
- [ ] Redirect URI configurado no Google Console
|
||||
- [ ] Container reiniciado
|
||||
- [ ] Login testado com sucesso
|
||||
|
||||
---
|
||||
|
||||
**Agora sim! O login com Google vai funcionar!** 🚀
|
||||
|
||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# 🚀 PDIMaker - Plataforma de Desenvolvimento Individual
|
||||
|
||||
Plataforma completa para gestão de PDI (Plano de Desenvolvimento Individual) com workspaces privados entre funcionários e gestores.
|
||||
|
||||
## ✨ Funcionalidades
|
||||
|
||||
- 🔐 **Autenticação** - Login com email/senha + Google OAuth
|
||||
- 👥 **Workspaces Privados** - Salas isoladas (Funcionário + Gestor + RH)
|
||||
- 🎟️ **Sistema de Convites** - Códigos únicos para acesso
|
||||
- 📊 **Dashboards** - Interface profissional e responsiva
|
||||
- 🎨 **UI Moderna** - Design corporativo com glassmorphism
|
||||
|
||||
## 🏗️ Arquitetura
|
||||
|
||||
### Stack Tecnológica
|
||||
|
||||
**Frontend:**
|
||||
- Next.js 14 (App Router)
|
||||
- NextAuth.js (Autenticação)
|
||||
- Prisma ORM
|
||||
- TypeScript
|
||||
- React 18
|
||||
|
||||
**Backend:**
|
||||
- NestJS
|
||||
- PostgreSQL 16
|
||||
- Redis 7
|
||||
- Prisma
|
||||
|
||||
**Infraestrutura:**
|
||||
- Docker & Docker Compose
|
||||
- Nginx (Reverse Proxy + SSL)
|
||||
- Let's Encrypt (SSL)
|
||||
|
||||
## 🚀 Como Rodar
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
- Docker e Docker Compose instalados
|
||||
- Domínio configurado (DNS)
|
||||
- Credenciais Google OAuth (opcional)
|
||||
|
||||
### Instalação
|
||||
|
||||
```bash
|
||||
# Clonar o repositório
|
||||
git clone https://meurepositorio.com/pdimaker.git
|
||||
cd pdimaker
|
||||
|
||||
# Configurar variáveis de ambiente
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
|
||||
# Subir os containers
|
||||
docker-compose up -d
|
||||
|
||||
# Verificar status
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### Configuração
|
||||
|
||||
1. **Edite o arquivo `.env`:**
|
||||
```bash
|
||||
DB_PASSWORD=sua_senha_segura
|
||||
NEXTAUTH_SECRET=sua_chave_secreta
|
||||
GOOGLE_CLIENT_ID=seu_client_id (opcional)
|
||||
GOOGLE_CLIENT_SECRET=seu_client_secret (opcional)
|
||||
```
|
||||
|
||||
2. **Acesse a aplicação:**
|
||||
```
|
||||
https://seu-dominio.com
|
||||
```
|
||||
|
||||
## 📁 Estrutura do Projeto
|
||||
|
||||
```
|
||||
pdimaker/
|
||||
├── frontend/ # Next.js App
|
||||
│ ├── app/ # App Router
|
||||
│ ├── components/ # Componentes React
|
||||
│ ├── lib/ # Utilities e configs
|
||||
│ └── prisma/ # Schema do banco
|
||||
├── backend/ # NestJS API
|
||||
│ └── src/ # Código fonte
|
||||
├── nginx/ # Configurações Nginx
|
||||
├── ssl/ # Certificados SSL
|
||||
└── docker-compose.yml # Orquestração
|
||||
```
|
||||
|
||||
## 🔒 Segurança
|
||||
|
||||
- ✅ Senhas criptografadas com bcrypt
|
||||
- ✅ JWT tokens seguros
|
||||
- ✅ Middleware de proteção de rotas
|
||||
- ✅ HTTPS com SSL
|
||||
- ✅ CORS configurado
|
||||
- ✅ Workspaces isolados
|
||||
|
||||
## 📝 Credenciais Padrão
|
||||
|
||||
Após instalação, crie seu primeiro usuário em:
|
||||
```
|
||||
https://seu-dominio.com/register
|
||||
```
|
||||
|
||||
## 🛠️ Comandos Úteis
|
||||
|
||||
```bash
|
||||
# Ver logs
|
||||
docker-compose logs -f frontend
|
||||
docker-compose logs -f backend
|
||||
|
||||
# Reiniciar serviços
|
||||
docker-compose restart frontend
|
||||
docker-compose restart nginx
|
||||
|
||||
# Parar tudo
|
||||
docker-compose down
|
||||
|
||||
# Rebuild
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 📖 Documentação
|
||||
|
||||
- [Setup Completo](./SETUP_COMPLETO.md)
|
||||
- [Sistema de Autenticação](./SISTEMA_AUTENTICACAO.md)
|
||||
- [Configurar Google OAuth](./CONFIGURAR_GOOGLE_OAUTH.md)
|
||||
|
||||
## 🎯 Roadmap
|
||||
|
||||
- [ ] Diário de Atividades
|
||||
- [ ] Metas e PDI
|
||||
- [ ] Reuniões 1:1
|
||||
- [ ] Testes Vocacionais
|
||||
- [ ] Sistema de Feedback
|
||||
- [ ] Notificações em tempo real
|
||||
|
||||
## 👨💻 Desenvolvido por
|
||||
|
||||
**Sergio Correa**
|
||||
- Website: [sergiocorrea.link](https://sergiocorrea.link)
|
||||
- Email: scorrea69@gmail.com
|
||||
|
||||
## 📄 Licença
|
||||
|
||||
Proprietary - Todos os direitos reservados
|
||||
|
||||
---
|
||||
|
||||
**Versão:** 1.0.0
|
||||
**Data:** Novembro 2025
|
||||
|
||||
230
SETUP_COMPLETO.md
Normal file
230
SETUP_COMPLETO.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# ✅ PDIMaker - Setup Completo
|
||||
|
||||
## 🎉 Status da Aplicação
|
||||
|
||||
**TODOS OS SERVIÇOS ESTÃO RODANDO!**
|
||||
|
||||
```
|
||||
✅ PostgreSQL (pdimaker-db) - Porta 5432
|
||||
✅ Redis (pdimaker-redis) - Porta 6379
|
||||
✅ Backend API (pdimaker-api) - Porta 4000
|
||||
✅ Frontend Web (pdimaker-web) - Porta 3300
|
||||
✅ Nginx (pdimaker-nginx) - Portas 80 e 443
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Arquivos Criados
|
||||
|
||||
### 1. **Middleware de Autenticação**
|
||||
- `frontend/middleware.ts` - Proteção de rotas e autenticação
|
||||
|
||||
### 2. **Configuração de Autenticação**
|
||||
- `frontend/lib/auth/config.ts` - NextAuth com Google OAuth
|
||||
- `frontend/lib/prisma.ts` - Cliente Prisma
|
||||
- `frontend/app/api/auth/[...nextauth]/route.ts` - Rotas de autenticação
|
||||
|
||||
### 3. **Páginas**
|
||||
- `frontend/app/login/page.tsx` - Página de login com Google
|
||||
- `frontend/app/unauthorized/page.tsx` - Página de acesso negado
|
||||
- `frontend/components/SessionProvider.tsx` - Provider de sessão
|
||||
|
||||
### 4. **Dependências Instaladas**
|
||||
```json
|
||||
{
|
||||
"next-auth": "^4.24.5",
|
||||
"@auth/prisma-adapter": "^1.4.0",
|
||||
"@prisma/client": "^5.8.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuração Necessária
|
||||
|
||||
### 1. Variáveis de Ambiente (.env)
|
||||
|
||||
Você precisa configurar o arquivo `.env` na raiz do projeto com:
|
||||
|
||||
```bash
|
||||
# Banco de Dados
|
||||
DB_PASSWORD=sua_senha_aqui
|
||||
DATABASE_URL=postgresql://postgres:sua_senha@postgres:5432/pdimaker_prod
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=sua_senha_redis
|
||||
|
||||
# JWT e Autenticação
|
||||
JWT_SECRET=seu_jwt_secret_seguro
|
||||
NEXTAUTH_SECRET=seu_nextauth_secret_seguro
|
||||
NEXTAUTH_URL=https://pdimaker.com.br
|
||||
|
||||
# Google OAuth (IMPORTANTE!)
|
||||
GOOGLE_CLIENT_ID=seu_google_client_id_aqui
|
||||
GOOGLE_CLIENT_SECRET=seu_google_client_secret_aqui
|
||||
|
||||
# URLs
|
||||
FRONTEND_URL=https://pdimaker.com.br
|
||||
NEXT_PUBLIC_API_URL=https://api.pdimaker.com.br
|
||||
```
|
||||
|
||||
### 2. Configurar Google OAuth
|
||||
|
||||
Para configurar o Google OAuth:
|
||||
|
||||
1. Acesse: https://console.cloud.google.com/
|
||||
2. Crie um novo projeto ou selecione um existente
|
||||
3. Vá em "APIs e Serviços" > "Credenciais"
|
||||
4. Clique em "Criar Credenciais" > "ID do cliente OAuth"
|
||||
5. Configure:
|
||||
- Tipo: Aplicativo da Web
|
||||
- URIs de redirecionamento autorizados:
|
||||
- `https://pdimaker.com.br/api/auth/callback/google`
|
||||
- `http://localhost:3300/api/auth/callback/google` (desenvolvimento)
|
||||
6. Copie o Client ID e Client Secret para o `.env`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Como Usar
|
||||
|
||||
### Acessar a Aplicação
|
||||
- **Frontend**: http://seu-dominio.com ou http://localhost:80
|
||||
- **API**: http://api.seu-dominio.com ou http://localhost:4000
|
||||
- **Frontend Direto**: http://localhost:3300
|
||||
|
||||
### Rotas Protegidas
|
||||
O middleware protege automaticamente:
|
||||
- ✅ Rotas públicas: `/`, `/login`, `/about`, `/api/auth`
|
||||
- 🔒 Todas as outras rotas requerem autenticação
|
||||
- 🔐 Rotas `/workspace/[slug]` verificam permissão do usuário
|
||||
|
||||
### Testar Autenticação
|
||||
1. Acesse: http://localhost:80/login
|
||||
2. Clique em "Entrar com Google"
|
||||
3. Faça login com sua conta Google
|
||||
4. Será redirecionado para a home page autenticado
|
||||
|
||||
---
|
||||
|
||||
## 📊 Banco de Dados
|
||||
|
||||
### Migrations do Prisma
|
||||
|
||||
Para aplicar as migrations do banco:
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
cd /var/www/pdimaker/frontend
|
||||
docker exec pdimaker-web npx prisma db push
|
||||
|
||||
# Backend
|
||||
cd /var/www/pdimaker/backend
|
||||
docker exec pdimaker-api npx prisma db push
|
||||
```
|
||||
|
||||
### Acessar o Banco
|
||||
|
||||
```bash
|
||||
docker exec -it pdimaker-db psql -U postgres -d pdimaker_prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Verificar Logs
|
||||
|
||||
```bash
|
||||
cd /var/www/pdimaker
|
||||
|
||||
# Todos os serviços
|
||||
docker-compose logs -f
|
||||
|
||||
# Serviço específico
|
||||
docker-compose logs -f frontend
|
||||
docker-compose logs -f backend
|
||||
docker-compose logs -f nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Comandos Úteis
|
||||
|
||||
```bash
|
||||
# Ver status dos containers
|
||||
docker-compose ps
|
||||
|
||||
# Reiniciar um serviço
|
||||
docker-compose restart frontend
|
||||
docker-compose restart backend
|
||||
docker-compose restart nginx
|
||||
|
||||
# Parar todos os serviços
|
||||
docker-compose down
|
||||
|
||||
# Iniciar todos os serviços
|
||||
docker-compose up -d
|
||||
|
||||
# Rebuild e restart completo
|
||||
docker-compose down
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Próximos Passos
|
||||
|
||||
1. **Configurar Google OAuth** (obrigatório para login funcionar)
|
||||
2. **Aplicar migrations** do Prisma no banco
|
||||
3. **Configurar SSL** para HTTPS (certificados em `/var/www/pdimaker/ssl/`)
|
||||
4. **Criar usuário admin** inicial no banco
|
||||
5. **Implementar páginas do dashboard** em `frontend/app/(dashboard)/`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Estrutura do Middleware
|
||||
|
||||
O middleware implementado protege:
|
||||
|
||||
```typescript
|
||||
// Rotas públicas (não requerem autenticação)
|
||||
["/", "/login", "/about", "/api/auth"]
|
||||
|
||||
// Rotas protegidas (requerem autenticação)
|
||||
Todas as outras rotas
|
||||
|
||||
// Rotas com verificação de permissão
|
||||
/workspace/[slug] - Verifica se usuário tem acesso ao workspace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Problemas Conhecidos
|
||||
|
||||
1. **Vulnerabilidades no npm**: Execute `npm audit fix` no container
|
||||
2. **OpenSSL warning**: Não afeta o funcionamento, mas considere atualizar a imagem base
|
||||
3. **Google OAuth não configurado**: O login não funcionará até configurar
|
||||
|
||||
---
|
||||
|
||||
## 💡 Dicas
|
||||
|
||||
- Use o Prisma Studio para visualizar dados: `npx prisma studio`
|
||||
- Configure variáveis de ambiente de desenvolvimento no `.env.local`
|
||||
- Logs em tempo real: `docker-compose logs -f --tail=100`
|
||||
- O middleware roda no Edge Runtime do Next.js (mais rápido)
|
||||
|
||||
---
|
||||
|
||||
## 📞 Suporte
|
||||
|
||||
Se tiver problemas:
|
||||
1. Verifique os logs dos containers
|
||||
2. Confirme que todas as variáveis de ambiente estão configuradas
|
||||
3. Teste a conexão com o banco de dados
|
||||
4. Verifique se o Google OAuth está configurado corretamente
|
||||
|
||||
---
|
||||
|
||||
**Data de Setup**: 19 de Novembro de 2025
|
||||
**Versão**: 1.0.0
|
||||
|
||||
257
SISTEMA_AUTENTICACAO.md
Normal file
257
SISTEMA_AUTENTICACAO.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 🔐 Sistema de Autenticação e Workspaces - PDIMaker
|
||||
|
||||
## 📋 Visão Geral
|
||||
|
||||
Sistema completo de autenticação e gerenciamento de workspaces similar ao **mentorado.tech**, onde funcionários, gestores e RH compartilham um workspace privado e isolado.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Funcionalidades Implementadas
|
||||
|
||||
### 1. **Autenticação com Google OAuth**
|
||||
- ✅ Login social com Google
|
||||
- ✅ Criação automática de usuário no primeiro login
|
||||
- ✅ Sessão JWT segura
|
||||
- ✅ Redirecionamento inteligente
|
||||
|
||||
### 2. **Sistema de Workspaces**
|
||||
- ✅ Workspaces privados (1 Funcionário + 1 Gestor + 1 RH opcional)
|
||||
- ✅ Slug único para cada workspace
|
||||
- ✅ Isolamento completo entre workspaces
|
||||
- ✅ Status de workspace (ACTIVE, PENDING_INVITE, ARCHIVED)
|
||||
|
||||
### 3. **Sistema de Convites**
|
||||
- ✅ Geração de código único (24 caracteres)
|
||||
- ✅ Convite por email
|
||||
- ✅ Expiração automática (7 dias)
|
||||
- ✅ Validação de acesso
|
||||
|
||||
### 4. **Fluxo de Onboarding**
|
||||
- ✅ Página inicial para novos usuários
|
||||
- ✅ Opção de aceitar convite
|
||||
- ✅ Opção de criar novo workspace
|
||||
- ✅ Redirecionamento automático
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Fluxo de Uso
|
||||
|
||||
### **Cenário 1: Criar Workspace como Gestor**
|
||||
|
||||
1. Gestor faz login → Vai para `/onboarding`
|
||||
2. Clica em "Criar Workspace"
|
||||
3. Seleciona "Gestor (Mentor)"
|
||||
4. Informa email do funcionário
|
||||
5. Sistema:
|
||||
- Cria o workspace
|
||||
- Gera código de convite
|
||||
- Exibe o código para compartilhar
|
||||
6. Gestor compartilha código com o funcionário
|
||||
|
||||
### **Cenário 2: Aceitar Convite como Funcionário**
|
||||
|
||||
1. Funcionário faz login → Vai para `/onboarding`
|
||||
2. Clica em "Tenho um código"
|
||||
3. Cola o código recebido
|
||||
4. Clica em "Aceitar Convite"
|
||||
5. Sistema:
|
||||
- Valida o código
|
||||
- Ativa o workspace
|
||||
- Redireciona para o workspace
|
||||
|
||||
---
|
||||
|
||||
## 📁 Estrutura de Arquivos Criados
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── app/
|
||||
│ ├── page.tsx # Home (redireciona)
|
||||
│ ├── login/page.tsx # Página de login
|
||||
│ ├── dashboard/page.tsx # Dashboard principal
|
||||
│ ├── onboarding/page.tsx # Primeira vez do usuário
|
||||
│ ├── workspaces/page.tsx # Lista de workspaces
|
||||
│ ├── workspace/
|
||||
│ │ ├── create/page.tsx # Criar workspace
|
||||
│ │ └── [slug]/page.tsx # Workspace individual
|
||||
│ └── api/
|
||||
│ ├── workspaces/
|
||||
│ │ └── create/route.ts # API criar workspace
|
||||
│ └── invites/
|
||||
│ └── accept/route.ts # API aceitar convite
|
||||
│
|
||||
├── lib/
|
||||
│ ├── auth/
|
||||
│ │ └── config.ts # Configuração NextAuth
|
||||
│ ├── types/
|
||||
│ │ └── next-auth.d.ts # Tipos TypeScript
|
||||
│ └── utils/
|
||||
│ └── invite-code.ts # Geração de códigos
|
||||
│
|
||||
└── middleware.ts # Proteção de rotas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Banco de Dados
|
||||
|
||||
### **Tabelas Criadas:**
|
||||
|
||||
1. **`users`** - Usuários do sistema
|
||||
- id, email, name, avatar, role, googleId
|
||||
- createdAt, updatedAt, lastLoginAt
|
||||
|
||||
2. **`workspaces`** - Workspaces privados
|
||||
- id, slug, employeeId, managerId, hrId
|
||||
- status, config, createdAt, updatedAt
|
||||
|
||||
3. **`invites`** - Convites pendentes
|
||||
- id, email, role, token, invitedBy
|
||||
- workspaceId, status, expiresAt, acceptedAt
|
||||
|
||||
### **ENUMs:**
|
||||
- Role: EMPLOYEE, MANAGER, HR_ADMIN
|
||||
- WorkspaceStatus: ACTIVE, PENDING_INVITE, ARCHIVED
|
||||
- InviteStatus: PENDING, ACCEPTED, EXPIRED, CANCELLED
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Segurança
|
||||
|
||||
### **Middleware de Proteção**
|
||||
```typescript
|
||||
// Rotas públicas
|
||||
["/", "/login", "/about", "/api/auth"]
|
||||
|
||||
// Rotas protegidas
|
||||
Todas as outras (requerem autenticação)
|
||||
|
||||
// Verificação de workspace
|
||||
/workspace/[slug] - Valida se usuário tem acesso
|
||||
```
|
||||
|
||||
### **Validações**
|
||||
- ✅ Token JWT seguro
|
||||
- ✅ Verificação de email no convite
|
||||
- ✅ Expiração de convites (7 dias)
|
||||
- ✅ Status de convite (evita reuso)
|
||||
- ✅ Isolamento de workspaces
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Interface
|
||||
|
||||
### **Design Similar ao mentorado.tech:**
|
||||
- Background corporativo
|
||||
- Cards com glassmorphism
|
||||
- Código de convite destacado
|
||||
- Status visual (Mentor/Mentorado/RH)
|
||||
- Cores por role:
|
||||
- 🟣 Roxo: Funcionário/Mentorado
|
||||
- 🟢 Verde: Gestor/Mentor
|
||||
- 🟠 Laranja: RH
|
||||
|
||||
---
|
||||
|
||||
## 📝 Próximos Passos
|
||||
|
||||
### **Funcionalidades Adicionais:**
|
||||
|
||||
1. **Email de Convite**
|
||||
- Integrar SendGrid ou Resend
|
||||
- Template de email com código
|
||||
- Link direto para aceitar
|
||||
|
||||
2. **Dashboard do Workspace**
|
||||
- Diário de Atividades
|
||||
- Metas e PDI
|
||||
- Reuniões 1:1
|
||||
- Testes Vocacionais
|
||||
|
||||
3. **Permissões por Role**
|
||||
- Funcionário: visualizar, criar entradas
|
||||
- Gestor: visualizar tudo, dar feedback
|
||||
- RH: visualizar todos workspaces
|
||||
|
||||
4. **Notificações**
|
||||
- Novo feedback
|
||||
- Reunião agendada
|
||||
- Meta atualizada
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Como Testar
|
||||
|
||||
### **1. Configurar Google OAuth**
|
||||
|
||||
Adicione no `.env`:
|
||||
```bash
|
||||
GOOGLE_CLIENT_ID=seu_client_id
|
||||
GOOGLE_CLIENT_SECRET=seu_client_secret
|
||||
NEXTAUTH_SECRET=seu_secret_seguro
|
||||
NEXTAUTH_URL=https://pdimaker.com.br
|
||||
DATABASE_URL=postgresql://postgres:senha@postgres:5432/pdimaker_prod
|
||||
```
|
||||
|
||||
### **2. Acessar a Aplicação**
|
||||
|
||||
```bash
|
||||
# Acessar
|
||||
https://pdimaker.com.br
|
||||
|
||||
# Fluxo:
|
||||
1. Fazer login com Google
|
||||
2. Vai para /onboarding
|
||||
3. Escolher "Criar Workspace" ou "Tenho um código"
|
||||
4. Criar workspace ou aceitar convite
|
||||
5. Acessar workspace em /workspace/[slug]
|
||||
```
|
||||
|
||||
### **3. Testar Convite**
|
||||
|
||||
```bash
|
||||
# Usuário 1 (Gestor):
|
||||
1. Login → Criar Workspace → Informar email do funcionário
|
||||
2. Copiar código gerado: cmhrtjzk6001jox4z9dzb5al
|
||||
3. Compartilhar com funcionário
|
||||
|
||||
# Usuário 2 (Funcionário):
|
||||
1. Login → Tenho um código
|
||||
2. Colar código: cmhrtjzk6001jox4z9dzb5al
|
||||
3. Aceitar convite
|
||||
4. Workspace ativo!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### **Erro: "Convite não encontrado"**
|
||||
- Verificar se código está correto (24 caracteres)
|
||||
- Verificar se convite não expirou (7 dias)
|
||||
- Verificar no banco: `SELECT * FROM invites WHERE token = 'codigo';`
|
||||
|
||||
### **Erro: "Este convite não é para você"**
|
||||
- Email do usuário logado deve ser o mesmo do convite
|
||||
- Verificar: `SELECT email FROM invites WHERE token = 'codigo';`
|
||||
|
||||
### **Erro: Banco de dados**
|
||||
- Verificar conexão: `docker exec pdimaker-db pg_isready`
|
||||
- Verificar tabelas: `docker exec pdimaker-db psql -U postgres -d pdimaker_prod -c "\dt"`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Estatísticas
|
||||
|
||||
- **Arquivos criados:** 15+
|
||||
- **Linhas de código:** ~2000
|
||||
- **Tabelas do banco:** 3 principais
|
||||
- **Rotas API:** 2 principais
|
||||
- **Páginas:** 6 principais
|
||||
|
||||
---
|
||||
|
||||
**Versão:** 1.0.0
|
||||
**Data:** 19 de Novembro de 2025
|
||||
**Status:** ✅ Pronto para uso
|
||||
|
||||
10
backend/.dockerignore
Normal file
10
backend/.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.vscode
|
||||
coverage
|
||||
14
backend/Dockerfile
Normal file
14
backend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache openssl curl
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install || true
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["node", "dist/main.js"]
|
||||
8
backend/nest-cli.json
Normal file
8
backend/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
27
backend/package.json
Normal file
27
backend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "pdimaker-api",
|
||||
"version": "1.0.0",
|
||||
"description": "PDIMaker Backend API",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.3.0",
|
||||
"@types/node": "^20.11.0",
|
||||
"prisma": "^5.8.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
380
backend/prisma/schema.prisma
Normal file
380
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,380 @@
|
||||
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
|
||||
|
||||
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")
|
||||
}
|
||||
21
backend/src/app.controller.ts
Normal file
21
backend/src/app.controller.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('health')
|
||||
getHealth() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'pdimaker-api'
|
||||
};
|
||||
}
|
||||
}
|
||||
10
backend/src/app.module.ts
Normal file
10
backend/src/app.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
8
backend/src/app.service.ts
Normal file
8
backend/src/app.service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'PDIMaker API is running! 🚀';
|
||||
}
|
||||
}
|
||||
17
backend/src/main.ts
Normal file
17
backend/src/main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.enableCors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 4000;
|
||||
await app.listen(port);
|
||||
|
||||
console.log(`🚀 API rodando na porta ${port}`);
|
||||
}
|
||||
bootstrap();
|
||||
21
backend/tsconfig.json
Normal file
21
backend/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
93
configurar-oauth.sh
Executable file
93
configurar-oauth.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script para configurar Google OAuth no PDIMaker
|
||||
|
||||
echo "🔐 Configuração do Google OAuth - PDIMaker"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Verificar se está na pasta correta
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "❌ Erro: arquivo .env não encontrado!"
|
||||
echo "Execute este script na pasta /var/www/pdimaker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Cole o GOOGLE_CLIENT_ID (algo como: 123456...apps.googleusercontent.com):"
|
||||
read -r CLIENT_ID
|
||||
|
||||
echo ""
|
||||
echo "Cole o GOOGLE_CLIENT_SECRET (algo como: GOCSPX-abc123...):"
|
||||
read -r CLIENT_SECRET
|
||||
|
||||
# Validar se não estão vazios
|
||||
if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then
|
||||
echo "❌ Erro: Client ID ou Client Secret não podem estar vazios!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup do .env
|
||||
cp .env .env.backup.$(date +%Y%m%d_%H%M%S)
|
||||
echo "✅ Backup do .env criado"
|
||||
|
||||
# Atualizar ou adicionar as variáveis
|
||||
if grep -q "GOOGLE_CLIENT_ID=" .env; then
|
||||
sed -i "s|GOOGLE_CLIENT_ID=.*|GOOGLE_CLIENT_ID=$CLIENT_ID|" .env
|
||||
echo "✅ GOOGLE_CLIENT_ID atualizado"
|
||||
else
|
||||
echo "GOOGLE_CLIENT_ID=$CLIENT_ID" >> .env
|
||||
echo "✅ GOOGLE_CLIENT_ID adicionado"
|
||||
fi
|
||||
|
||||
if grep -q "GOOGLE_CLIENT_SECRET=" .env; then
|
||||
sed -i "s|GOOGLE_CLIENT_SECRET=.*|GOOGLE_CLIENT_SECRET=$CLIENT_SECRET|" .env
|
||||
echo "✅ GOOGLE_CLIENT_SECRET atualizado"
|
||||
else
|
||||
echo "GOOGLE_CLIENT_SECRET=$CLIENT_SECRET" >> .env
|
||||
echo "✅ GOOGLE_CLIENT_SECRET adicionado"
|
||||
fi
|
||||
|
||||
# Verificar/adicionar NEXTAUTH_SECRET se não existir
|
||||
if ! grep -q "NEXTAUTH_SECRET=" .env || grep -q "NEXTAUTH_SECRET=seu" .env; then
|
||||
NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
||||
if grep -q "NEXTAUTH_SECRET=" .env; then
|
||||
sed -i "s|NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$NEXTAUTH_SECRET|" .env
|
||||
echo "✅ NEXTAUTH_SECRET gerado e atualizado"
|
||||
else
|
||||
echo "NEXTAUTH_SECRET=$NEXTAUTH_SECRET" >> .env
|
||||
echo "✅ NEXTAUTH_SECRET gerado e adicionado"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verificar/adicionar NEXTAUTH_URL se não existir
|
||||
if ! grep -q "NEXTAUTH_URL=" .env; then
|
||||
echo "NEXTAUTH_URL=https://pdimaker.com.br" >> .env
|
||||
echo "✅ NEXTAUTH_URL adicionado"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔄 Reiniciando container do frontend..."
|
||||
docker restart pdimaker-web
|
||||
|
||||
echo ""
|
||||
echo "⏳ Aguardando container iniciar..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "📋 Verificando logs..."
|
||||
docker logs pdimaker-web --tail=10
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✅ Configuração concluída!"
|
||||
echo ""
|
||||
echo "📝 Próximos passos:"
|
||||
echo "1. Acesse: https://pdimaker.com.br"
|
||||
echo "2. Clique em 'Entrar com Google'"
|
||||
echo "3. Faça login com sua conta Google"
|
||||
echo ""
|
||||
echo "⚠️ IMPORTANTE: Verifique no Google Cloud Console se você adicionou:"
|
||||
echo " Redirect URI: https://pdimaker.com.br/api/auth/callback/google"
|
||||
echo ""
|
||||
echo "🎉 Pronto! O login com Google deve funcionar agora!"
|
||||
|
||||
136
docker-compose.yml
Normal file
136
docker-compose.yml
Normal file
@@ -0,0 +1,136 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# ═══════════════════════════════════════
|
||||
# BANCO DE DADOS POSTGRESQL
|
||||
# ═══════════════════════════════════════
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: pdimaker-db
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: pdimaker_prod
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- pdimaker-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# REDIS (Cache e Sessões)
|
||||
# ═══════════════════════════════════════
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: pdimaker-redis
|
||||
restart: always
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- pdimaker-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# BACKEND API (NestJS)
|
||||
# ═══════════════════════════════════════
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: pdimaker-api
|
||||
restart: always
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 4000
|
||||
DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@postgres:5432/pdimaker_prod
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
|
||||
FRONTEND_URL: https://pdimaker.com.br
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- uploads_data:/app/uploads
|
||||
ports:
|
||||
- "4000:4000"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
networks:
|
||||
- pdimaker-network
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# FRONTEND (Next.js)
|
||||
# ═══════════════════════════════════════
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_API_URL: https://api.pdimaker.com.br
|
||||
container_name: pdimaker-web
|
||||
restart: always
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NEXTAUTH_URL: https://pdimaker.com.br
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||||
DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@postgres:5432/pdimaker_prod
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
ports:
|
||||
- "3300:3000"
|
||||
depends_on:
|
||||
- backend
|
||||
- postgres
|
||||
networks:
|
||||
- pdimaker-network
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# NGINX (Reverse Proxy + SSL)
|
||||
# ═══════════════════════════════════════
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: pdimaker-nginx
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
- /var/log/nginx:/var/log/nginx
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
networks:
|
||||
- pdimaker-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
uploads_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
pdimaker-network:
|
||||
driver: bridge
|
||||
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"]
|
||||
}
|
||||
720
get-docker.sh
Normal file
720
get-docker.sh
Normal file
@@ -0,0 +1,720 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Docker Engine for Linux installation script.
|
||||
#
|
||||
# This script is intended as a convenient way to configure docker's package
|
||||
# repositories and to install Docker Engine, This script is not recommended
|
||||
# for production environments. Before running this script, make yourself familiar
|
||||
# with potential risks and limitations, and refer to the installation manual
|
||||
# at https://docs.docker.com/engine/install/ for alternative installation methods.
|
||||
#
|
||||
# The script:
|
||||
#
|
||||
# - Requires `root` or `sudo` privileges to run.
|
||||
# - Attempts to detect your Linux distribution and version and configure your
|
||||
# package management system for you.
|
||||
# - Doesn't allow you to customize most installation parameters.
|
||||
# - Installs dependencies and recommendations without asking for confirmation.
|
||||
# - Installs the latest stable release (by default) of Docker CLI, Docker Engine,
|
||||
# Docker Buildx, Docker Compose, containerd, and runc. When using this script
|
||||
# to provision a machine, this may result in unexpected major version upgrades
|
||||
# of these packages. Always test upgrades in a test environment before
|
||||
# deploying to your production systems.
|
||||
# - Isn't designed to upgrade an existing Docker installation. When using the
|
||||
# script to update an existing installation, dependencies may not be updated
|
||||
# to the expected version, resulting in outdated versions.
|
||||
#
|
||||
# Source code is available at https://github.com/docker/docker-install/
|
||||
#
|
||||
# Usage
|
||||
# ==============================================================================
|
||||
#
|
||||
# To install the latest stable versions of Docker CLI, Docker Engine, and their
|
||||
# dependencies:
|
||||
#
|
||||
# 1. download the script
|
||||
#
|
||||
# $ curl -fsSL https://get.docker.com -o install-docker.sh
|
||||
#
|
||||
# 2. verify the script's content
|
||||
#
|
||||
# $ cat install-docker.sh
|
||||
#
|
||||
# 3. run the script with --dry-run to verify the steps it executes
|
||||
#
|
||||
# $ sh install-docker.sh --dry-run
|
||||
#
|
||||
# 4. run the script either as root, or using sudo to perform the installation.
|
||||
#
|
||||
# $ sudo sh install-docker.sh
|
||||
#
|
||||
# Command-line options
|
||||
# ==============================================================================
|
||||
#
|
||||
# --version <VERSION>
|
||||
# Use the --version option to install a specific version, for example:
|
||||
#
|
||||
# $ sudo sh install-docker.sh --version 23.0
|
||||
#
|
||||
# --channel <stable|test>
|
||||
#
|
||||
# Use the --channel option to install from an alternative installation channel.
|
||||
# The following example installs the latest versions from the "test" channel,
|
||||
# which includes pre-releases (alpha, beta, rc):
|
||||
#
|
||||
# $ sudo sh install-docker.sh --channel test
|
||||
#
|
||||
# Alternatively, use the script at https://test.docker.com, which uses the test
|
||||
# channel as default.
|
||||
#
|
||||
# --mirror <Aliyun|AzureChinaCloud>
|
||||
#
|
||||
# Use the --mirror option to install from a mirror supported by this script.
|
||||
# Available mirrors are "Aliyun" (https://mirrors.aliyun.com/docker-ce), and
|
||||
# "AzureChinaCloud" (https://mirror.azure.cn/docker-ce), for example:
|
||||
#
|
||||
# $ sudo sh install-docker.sh --mirror AzureChinaCloud
|
||||
#
|
||||
# --setup-repo
|
||||
#
|
||||
# Use the --setup-repo option to configure Docker's package repositories without
|
||||
# installing Docker packages. This is useful when you want to add the repository
|
||||
# but install packages separately:
|
||||
#
|
||||
# $ sudo sh install-docker.sh --setup-repo
|
||||
#
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
# Git commit from https://github.com/docker/docker-install when
|
||||
# the script was uploaded (Should only be modified by upload job):
|
||||
SCRIPT_COMMIT_SHA="7d96bd3c5235ab2121bcb855dd7b3f3f37128ed4"
|
||||
|
||||
# strip "v" prefix if present
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
# The channel to install from:
|
||||
# * stable
|
||||
# * test
|
||||
DEFAULT_CHANNEL_VALUE="stable"
|
||||
if [ -z "$CHANNEL" ]; then
|
||||
CHANNEL=$DEFAULT_CHANNEL_VALUE
|
||||
fi
|
||||
|
||||
DEFAULT_DOWNLOAD_URL="https://download.docker.com"
|
||||
if [ -z "$DOWNLOAD_URL" ]; then
|
||||
DOWNLOAD_URL=$DEFAULT_DOWNLOAD_URL
|
||||
fi
|
||||
|
||||
DEFAULT_REPO_FILE="docker-ce.repo"
|
||||
if [ -z "$REPO_FILE" ]; then
|
||||
REPO_FILE="$DEFAULT_REPO_FILE"
|
||||
# Automatically default to a staging repo fora
|
||||
# a staging download url (download-stage.docker.com)
|
||||
case "$DOWNLOAD_URL" in
|
||||
*-stage*) REPO_FILE="docker-ce-staging.repo";;
|
||||
esac
|
||||
fi
|
||||
|
||||
mirror=''
|
||||
DRY_RUN=${DRY_RUN:-}
|
||||
REPO_ONLY=${REPO_ONLY:-0}
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--channel)
|
||||
CHANNEL="$2"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
;;
|
||||
--mirror)
|
||||
mirror="$2"
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
VERSION="${2#v}"
|
||||
shift
|
||||
;;
|
||||
--setup-repo)
|
||||
REPO_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--*)
|
||||
echo "Illegal option $1"
|
||||
;;
|
||||
esac
|
||||
shift $(( $# > 0 ? 1 : 0 ))
|
||||
done
|
||||
|
||||
case "$mirror" in
|
||||
Aliyun)
|
||||
DOWNLOAD_URL="https://mirrors.aliyun.com/docker-ce"
|
||||
;;
|
||||
AzureChinaCloud)
|
||||
DOWNLOAD_URL="https://mirror.azure.cn/docker-ce"
|
||||
;;
|
||||
"")
|
||||
;;
|
||||
*)
|
||||
>&2 echo "unknown mirror '$mirror': use either 'Aliyun', or 'AzureChinaCloud'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$CHANNEL" in
|
||||
stable|test)
|
||||
;;
|
||||
*)
|
||||
>&2 echo "unknown CHANNEL '$CHANNEL': use either stable or test."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# version_gte checks if the version specified in $VERSION is at least the given
|
||||
# SemVer (Maj.Minor[.Patch]), or CalVer (YY.MM) version.It returns 0 (success)
|
||||
# if $VERSION is either unset (=latest) or newer or equal than the specified
|
||||
# version, or returns 1 (fail) otherwise.
|
||||
#
|
||||
# examples:
|
||||
#
|
||||
# VERSION=23.0
|
||||
# version_gte 23.0 // 0 (success)
|
||||
# version_gte 20.10 // 0 (success)
|
||||
# version_gte 19.03 // 0 (success)
|
||||
# version_gte 26.1 // 1 (fail)
|
||||
version_gte() {
|
||||
if [ -z "$VERSION" ]; then
|
||||
return 0
|
||||
fi
|
||||
version_compare "$VERSION" "$1"
|
||||
}
|
||||
|
||||
# version_compare compares two version strings (either SemVer (Major.Minor.Path),
|
||||
# or CalVer (YY.MM) version strings. It returns 0 (success) if version A is newer
|
||||
# or equal than version B, or 1 (fail) otherwise. Patch releases and pre-release
|
||||
# (-alpha/-beta) are not taken into account
|
||||
#
|
||||
# examples:
|
||||
#
|
||||
# version_compare 23.0.0 20.10 // 0 (success)
|
||||
# version_compare 23.0 20.10 // 0 (success)
|
||||
# version_compare 20.10 19.03 // 0 (success)
|
||||
# version_compare 20.10 20.10 // 0 (success)
|
||||
# version_compare 19.03 20.10 // 1 (fail)
|
||||
version_compare() (
|
||||
set +x
|
||||
|
||||
yy_a="$(echo "$1" | cut -d'.' -f1)"
|
||||
yy_b="$(echo "$2" | cut -d'.' -f1)"
|
||||
if [ "$yy_a" -lt "$yy_b" ]; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$yy_a" -gt "$yy_b" ]; then
|
||||
return 0
|
||||
fi
|
||||
mm_a="$(echo "$1" | cut -d'.' -f2)"
|
||||
mm_b="$(echo "$2" | cut -d'.' -f2)"
|
||||
|
||||
# trim leading zeros to accommodate CalVer
|
||||
mm_a="${mm_a#0}"
|
||||
mm_b="${mm_b#0}"
|
||||
|
||||
if [ "${mm_a:-0}" -lt "${mm_b:-0}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
)
|
||||
|
||||
is_dry_run() {
|
||||
if [ -z "$DRY_RUN" ]; then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
is_wsl() {
|
||||
case "$(uname -r)" in
|
||||
*microsoft* ) true ;; # WSL 2
|
||||
*Microsoft* ) true ;; # WSL 1
|
||||
* ) false;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_darwin() {
|
||||
case "$(uname -s)" in
|
||||
*darwin* ) true ;;
|
||||
*Darwin* ) true ;;
|
||||
* ) false;;
|
||||
esac
|
||||
}
|
||||
|
||||
deprecation_notice() {
|
||||
distro=$1
|
||||
distro_version=$2
|
||||
echo
|
||||
printf "\033[91;1mDEPRECATION WARNING\033[0m\n"
|
||||
printf " This Linux distribution (\033[1m%s %s\033[0m) reached end-of-life and is no longer supported by this script.\n" "$distro" "$distro_version"
|
||||
echo " No updates or security fixes will be released for this distribution, and users are recommended"
|
||||
echo " to upgrade to a currently maintained version of $distro."
|
||||
echo
|
||||
printf "Press \033[1mCtrl+C\033[0m now to abort this script, or wait for the installation to continue."
|
||||
echo
|
||||
sleep 10
|
||||
}
|
||||
|
||||
get_distribution() {
|
||||
lsb_dist=""
|
||||
# Every system that we officially support has /etc/os-release
|
||||
if [ -r /etc/os-release ]; then
|
||||
lsb_dist="$(. /etc/os-release && echo "$ID")"
|
||||
fi
|
||||
# Returning an empty string here should be alright since the
|
||||
# case statements don't act unless you provide an actual value
|
||||
echo "$lsb_dist"
|
||||
}
|
||||
|
||||
echo_docker_as_nonroot() {
|
||||
if is_dry_run; then
|
||||
return
|
||||
fi
|
||||
if command_exists docker && [ -e /var/run/docker.sock ]; then
|
||||
(
|
||||
set -x
|
||||
$sh_c 'docker version'
|
||||
) || true
|
||||
fi
|
||||
|
||||
# intentionally mixed spaces and tabs here -- tabs are stripped by "<<-EOF", spaces are kept in the output
|
||||
echo
|
||||
echo "================================================================================"
|
||||
echo
|
||||
if version_gte "20.10"; then
|
||||
echo "To run Docker as a non-privileged user, consider setting up the"
|
||||
echo "Docker daemon in rootless mode for your user:"
|
||||
echo
|
||||
echo " dockerd-rootless-setuptool.sh install"
|
||||
echo
|
||||
echo "Visit https://docs.docker.com/go/rootless/ to learn about rootless mode."
|
||||
echo
|
||||
fi
|
||||
echo
|
||||
echo "To run the Docker daemon as a fully privileged service, but granting non-root"
|
||||
echo "users access, refer to https://docs.docker.com/go/daemon-access/"
|
||||
echo
|
||||
echo "WARNING: Access to the remote API on a privileged Docker daemon is equivalent"
|
||||
echo " to root access on the host. Refer to the 'Docker daemon attack surface'"
|
||||
echo " documentation for details: https://docs.docker.com/go/attack-surface/"
|
||||
echo
|
||||
echo "================================================================================"
|
||||
echo
|
||||
}
|
||||
|
||||
# Check if this is a forked Linux distro
|
||||
check_forked() {
|
||||
|
||||
# Check for lsb_release command existence, it usually exists in forked distros
|
||||
if command_exists lsb_release; then
|
||||
# Check if the `-u` option is supported
|
||||
set +e
|
||||
lsb_release -a -u > /dev/null 2>&1
|
||||
lsb_release_exit_code=$?
|
||||
set -e
|
||||
|
||||
# Check if the command has exited successfully, it means we're in a forked distro
|
||||
if [ "$lsb_release_exit_code" = "0" ]; then
|
||||
# Print info about current distro
|
||||
cat <<-EOF
|
||||
You're using '$lsb_dist' version '$dist_version'.
|
||||
EOF
|
||||
|
||||
# Get the upstream release info
|
||||
lsb_dist=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')
|
||||
dist_version=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')
|
||||
|
||||
# Print info about upstream distro
|
||||
cat <<-EOF
|
||||
Upstream release is '$lsb_dist' version '$dist_version'.
|
||||
EOF
|
||||
else
|
||||
if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then
|
||||
if [ "$lsb_dist" = "osmc" ]; then
|
||||
# OSMC runs Raspbian
|
||||
lsb_dist=raspbian
|
||||
else
|
||||
# We're Debian and don't even know it!
|
||||
lsb_dist=debian
|
||||
fi
|
||||
dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
|
||||
case "$dist_version" in
|
||||
13)
|
||||
dist_version="trixie"
|
||||
;;
|
||||
12)
|
||||
dist_version="bookworm"
|
||||
;;
|
||||
11)
|
||||
dist_version="bullseye"
|
||||
;;
|
||||
10)
|
||||
dist_version="buster"
|
||||
;;
|
||||
9)
|
||||
dist_version="stretch"
|
||||
;;
|
||||
8)
|
||||
dist_version="jessie"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
do_install() {
|
||||
echo "# Executing docker install script, commit: $SCRIPT_COMMIT_SHA"
|
||||
|
||||
if command_exists docker; then
|
||||
cat >&2 <<-'EOF'
|
||||
Warning: the "docker" command appears to already exist on this system.
|
||||
|
||||
If you already have Docker installed, this script can cause trouble, which is
|
||||
why we're displaying this warning and provide the opportunity to cancel the
|
||||
installation.
|
||||
|
||||
If you installed the current Docker package using this script and are using it
|
||||
again to update Docker, you can ignore this message, but be aware that the
|
||||
script resets any custom changes in the deb and rpm repo configuration
|
||||
files to match the parameters passed to the script.
|
||||
|
||||
You may press Ctrl+C now to abort this script.
|
||||
EOF
|
||||
( set -x; sleep 20 )
|
||||
fi
|
||||
|
||||
user="$(id -un 2>/dev/null || true)"
|
||||
|
||||
sh_c='sh -c'
|
||||
if [ "$user" != 'root' ]; then
|
||||
if command_exists sudo; then
|
||||
sh_c='sudo -E sh -c'
|
||||
elif command_exists su; then
|
||||
sh_c='su -c'
|
||||
else
|
||||
cat >&2 <<-'EOF'
|
||||
Error: this installer needs the ability to run commands as root.
|
||||
We are unable to find either "sudo" or "su" available to make this happen.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if is_dry_run; then
|
||||
sh_c="echo"
|
||||
fi
|
||||
|
||||
# perform some very rudimentary platform detection
|
||||
lsb_dist=$( get_distribution )
|
||||
lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if is_wsl; then
|
||||
echo
|
||||
echo "WSL DETECTED: We recommend using Docker Desktop for Windows."
|
||||
echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop/"
|
||||
echo
|
||||
cat >&2 <<-'EOF'
|
||||
|
||||
You may press Ctrl+C now to abort this script.
|
||||
EOF
|
||||
( set -x; sleep 20 )
|
||||
fi
|
||||
|
||||
case "$lsb_dist" in
|
||||
|
||||
ubuntu)
|
||||
if command_exists lsb_release; then
|
||||
dist_version="$(lsb_release --codename | cut -f2)"
|
||||
fi
|
||||
if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then
|
||||
dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")"
|
||||
fi
|
||||
;;
|
||||
|
||||
debian|raspbian)
|
||||
dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
|
||||
case "$dist_version" in
|
||||
13)
|
||||
dist_version="trixie"
|
||||
;;
|
||||
12)
|
||||
dist_version="bookworm"
|
||||
;;
|
||||
11)
|
||||
dist_version="bullseye"
|
||||
;;
|
||||
10)
|
||||
dist_version="buster"
|
||||
;;
|
||||
9)
|
||||
dist_version="stretch"
|
||||
;;
|
||||
8)
|
||||
dist_version="jessie"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
centos|rhel)
|
||||
if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
|
||||
dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
if command_exists lsb_release; then
|
||||
dist_version="$(lsb_release --release | cut -f2)"
|
||||
fi
|
||||
if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
|
||||
dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
|
||||
fi
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
# Check if this is a forked Linux distro
|
||||
check_forked
|
||||
|
||||
# Print deprecation warnings for distro versions that recently reached EOL,
|
||||
# but may still be commonly used (especially LTS versions).
|
||||
case "$lsb_dist.$dist_version" in
|
||||
centos.8|centos.7|rhel.7)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
debian.buster|debian.stretch|debian.jessie)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
raspbian.buster|raspbian.stretch|raspbian.jessie)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
ubuntu.focal|ubuntu.bionic|ubuntu.xenial|ubuntu.trusty)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
ubuntu.oracular|ubuntu.mantic|ubuntu.lunar|ubuntu.kinetic|ubuntu.impish|ubuntu.hirsute|ubuntu.groovy|ubuntu.eoan|ubuntu.disco|ubuntu.cosmic)
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
;;
|
||||
fedora.*)
|
||||
if [ "$dist_version" -lt 41 ]; then
|
||||
deprecation_notice "$lsb_dist" "$dist_version"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Run setup for each distro accordingly
|
||||
case "$lsb_dist" in
|
||||
ubuntu|debian|raspbian)
|
||||
pre_reqs="ca-certificates curl"
|
||||
apt_repo="deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] $DOWNLOAD_URL/linux/$lsb_dist $dist_version $CHANNEL"
|
||||
(
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c 'apt-get -qq update >/dev/null'
|
||||
$sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pre_reqs >/dev/null"
|
||||
$sh_c 'install -m 0755 -d /etc/apt/keyrings'
|
||||
$sh_c "curl -fsSL \"$DOWNLOAD_URL/linux/$lsb_dist/gpg\" -o /etc/apt/keyrings/docker.asc"
|
||||
$sh_c "chmod a+r /etc/apt/keyrings/docker.asc"
|
||||
$sh_c "echo \"$apt_repo\" > /etc/apt/sources.list.d/docker.list"
|
||||
$sh_c 'apt-get -qq update >/dev/null'
|
||||
)
|
||||
|
||||
if [ "$REPO_ONLY" = "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pkg_version=""
|
||||
if [ -n "$VERSION" ]; then
|
||||
if is_dry_run; then
|
||||
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||
else
|
||||
# Will work for incomplete versions IE (17.12), but may not actually grab the "latest" if in the test channel
|
||||
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/~ce~.*/g' | sed 's/-/.*/g')"
|
||||
search_command="apt-cache madison docker-ce | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
|
||||
pkg_version="$($sh_c "$search_command")"
|
||||
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||
echo "INFO: $search_command"
|
||||
if [ -z "$pkg_version" ]; then
|
||||
echo
|
||||
echo "ERROR: '$VERSION' not found amongst apt-cache madison results"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
if version_gte "18.09"; then
|
||||
search_command="apt-cache madison docker-ce-cli | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
|
||||
echo "INFO: $search_command"
|
||||
cli_pkg_version="=$($sh_c "$search_command")"
|
||||
fi
|
||||
pkg_version="=$pkg_version"
|
||||
fi
|
||||
fi
|
||||
(
|
||||
pkgs="docker-ce${pkg_version%=}"
|
||||
if version_gte "18.09"; then
|
||||
# older versions didn't ship the cli and containerd as separate packages
|
||||
pkgs="$pkgs docker-ce-cli${cli_pkg_version%=} containerd.io"
|
||||
fi
|
||||
if version_gte "20.10"; then
|
||||
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||
fi
|
||||
if version_gte "23.0"; then
|
||||
pkgs="$pkgs docker-buildx-plugin"
|
||||
fi
|
||||
if version_gte "28.2"; then
|
||||
pkgs="$pkgs docker-model-plugin"
|
||||
fi
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pkgs >/dev/null"
|
||||
)
|
||||
echo_docker_as_nonroot
|
||||
exit 0
|
||||
;;
|
||||
centos|fedora|rhel)
|
||||
if [ "$(uname -m)" = "s390x" ]; then
|
||||
echo "Effective v27.5, please consult RHEL distro statement for s390x support."
|
||||
exit 1
|
||||
fi
|
||||
repo_file_url="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE"
|
||||
(
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
if command_exists dnf5; then
|
||||
$sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
|
||||
$sh_c "dnf5 config-manager addrepo --overwrite --save-filename=docker-ce.repo --from-repofile='$repo_file_url'"
|
||||
|
||||
if [ "$CHANNEL" != "stable" ]; then
|
||||
$sh_c "dnf5 config-manager setopt \"docker-ce-*.enabled=0\""
|
||||
$sh_c "dnf5 config-manager setopt \"docker-ce-$CHANNEL.enabled=1\""
|
||||
fi
|
||||
$sh_c "dnf makecache"
|
||||
elif command_exists dnf; then
|
||||
$sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
|
||||
$sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo"
|
||||
$sh_c "dnf config-manager --add-repo $repo_file_url"
|
||||
|
||||
if [ "$CHANNEL" != "stable" ]; then
|
||||
$sh_c "dnf config-manager --set-disabled \"docker-ce-*\""
|
||||
$sh_c "dnf config-manager --set-enabled \"docker-ce-$CHANNEL\""
|
||||
fi
|
||||
$sh_c "dnf makecache"
|
||||
else
|
||||
$sh_c "yum -y -q install yum-utils"
|
||||
$sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo"
|
||||
$sh_c "yum-config-manager --add-repo $repo_file_url"
|
||||
|
||||
if [ "$CHANNEL" != "stable" ]; then
|
||||
$sh_c "yum-config-manager --disable \"docker-ce-*\""
|
||||
$sh_c "yum-config-manager --enable \"docker-ce-$CHANNEL\""
|
||||
fi
|
||||
$sh_c "yum makecache"
|
||||
fi
|
||||
)
|
||||
|
||||
if [ "$REPO_ONLY" = "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pkg_version=""
|
||||
if command_exists dnf; then
|
||||
pkg_manager="dnf"
|
||||
pkg_manager_flags="-y -q --best"
|
||||
else
|
||||
pkg_manager="yum"
|
||||
pkg_manager_flags="-y -q"
|
||||
fi
|
||||
if [ -n "$VERSION" ]; then
|
||||
if is_dry_run; then
|
||||
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
|
||||
else
|
||||
if [ "$lsb_dist" = "fedora" ]; then
|
||||
pkg_suffix="fc$dist_version"
|
||||
else
|
||||
pkg_suffix="el"
|
||||
fi
|
||||
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/\\\\.ce.*/g' | sed 's/-/.*/g').*$pkg_suffix"
|
||||
search_command="$pkg_manager list --showduplicates docker-ce | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
|
||||
pkg_version="$($sh_c "$search_command")"
|
||||
echo "INFO: Searching repository for VERSION '$VERSION'"
|
||||
echo "INFO: $search_command"
|
||||
if [ -z "$pkg_version" ]; then
|
||||
echo
|
||||
echo "ERROR: '$VERSION' not found amongst $pkg_manager list results"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
if version_gte "18.09"; then
|
||||
# older versions don't support a cli package
|
||||
search_command="$pkg_manager list --showduplicates docker-ce-cli | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
|
||||
cli_pkg_version="$($sh_c "$search_command" | cut -d':' -f 2)"
|
||||
fi
|
||||
# Cut out the epoch and prefix with a '-'
|
||||
pkg_version="-$(echo "$pkg_version" | cut -d':' -f 2)"
|
||||
fi
|
||||
fi
|
||||
(
|
||||
pkgs="docker-ce$pkg_version"
|
||||
if version_gte "18.09"; then
|
||||
# older versions didn't ship the cli and containerd as separate packages
|
||||
if [ -n "$cli_pkg_version" ]; then
|
||||
pkgs="$pkgs docker-ce-cli-$cli_pkg_version containerd.io"
|
||||
else
|
||||
pkgs="$pkgs docker-ce-cli containerd.io"
|
||||
fi
|
||||
fi
|
||||
if version_gte "20.10"; then
|
||||
pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
|
||||
fi
|
||||
if version_gte "23.0"; then
|
||||
pkgs="$pkgs docker-buildx-plugin docker-model-plugin"
|
||||
fi
|
||||
if ! is_dry_run; then
|
||||
set -x
|
||||
fi
|
||||
$sh_c "$pkg_manager $pkg_manager_flags install $pkgs"
|
||||
)
|
||||
echo_docker_as_nonroot
|
||||
exit 0
|
||||
;;
|
||||
sles)
|
||||
echo "Effective v27.5, please consult SLES distro statement for s390x support."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [ -z "$lsb_dist" ]; then
|
||||
if is_darwin; then
|
||||
echo
|
||||
echo "ERROR: Unsupported operating system 'macOS'"
|
||||
echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
echo "ERROR: Unsupported distribution '$lsb_dist'"
|
||||
echo
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 1
|
||||
}
|
||||
|
||||
# wrapped up in a function so that we have some protection against only getting
|
||||
# half the file during "curl | sh"
|
||||
do_install
|
||||
80
init-db.sql
Normal file
80
init-db.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
-- Schema PDIMaker
|
||||
-- Criação de tabelas do sistema
|
||||
|
||||
-- Tipos ENUM
|
||||
CREATE TYPE "Role" AS ENUM ('EMPLOYEE', 'MANAGER', 'HR_ADMIN');
|
||||
CREATE TYPE "WorkspaceStatus" AS ENUM ('ACTIVE', 'ARCHIVED', 'PENDING_INVITE');
|
||||
CREATE TYPE "InviteStatus" AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'CANCELLED');
|
||||
CREATE TYPE "JournalVisibility" AS ENUM ('PUBLIC', 'PRIVATE', 'SUMMARY');
|
||||
CREATE TYPE "GoalType" AS ENUM ('TECHNICAL', 'SOFT_SKILL', 'LEADERSHIP', 'CAREER');
|
||||
CREATE TYPE "GoalStatus" AS ENUM ('NOT_STARTED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED');
|
||||
CREATE TYPE "ActionStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'DONE', 'BLOCKED');
|
||||
CREATE TYPE "AssessmentType" AS ENUM ('PERSONALITY', 'BEHAVIORAL', 'MOTIVATIONAL', 'VOCATIONAL', 'CUSTOM');
|
||||
CREATE TYPE "OneOnOneStatus" AS ENUM ('SCHEDULED', 'COMPLETED', 'CANCELLED');
|
||||
|
||||
-- Tabela de Usuários
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"email" TEXT UNIQUE NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"avatar" TEXT,
|
||||
"role" "Role" DEFAULT 'EMPLOYEE',
|
||||
"googleId" TEXT UNIQUE,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastLoginAt" TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON "users"("email");
|
||||
|
||||
-- Tabela de Workspaces
|
||||
CREATE TABLE IF NOT EXISTS "workspaces" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"slug" TEXT UNIQUE NOT NULL,
|
||||
"employeeId" TEXT NOT NULL REFERENCES "users"("id"),
|
||||
"managerId" TEXT NOT NULL REFERENCES "users"("id"),
|
||||
"hrId" TEXT REFERENCES "users"("id"),
|
||||
"status" "WorkspaceStatus" DEFAULT 'ACTIVE',
|
||||
"config" JSONB,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_workspace_employee_manager ON "workspaces"("employeeId", "managerId");
|
||||
CREATE INDEX idx_workspaces_slug ON "workspaces"("slug");
|
||||
CREATE INDEX idx_workspaces_employee ON "workspaces"("employeeId");
|
||||
CREATE INDEX idx_workspaces_manager ON "workspaces"("managerId");
|
||||
|
||||
-- Tabela de Convites
|
||||
CREATE TABLE IF NOT EXISTS "invites" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"email" TEXT NOT NULL,
|
||||
"role" "Role" NOT NULL,
|
||||
"token" TEXT UNIQUE NOT NULL,
|
||||
"invitedBy" TEXT NOT NULL,
|
||||
"workspaceId" TEXT REFERENCES "workspaces"("id"),
|
||||
"status" "InviteStatus" DEFAULT 'PENDING',
|
||||
"expiresAt" TIMESTAMP NOT NULL,
|
||||
"acceptedAt" TIMESTAMP,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_invites_email ON "invites"("email");
|
||||
CREATE INDEX idx_invites_token ON "invites"("token");
|
||||
|
||||
-- Função para gerar IDs únicos (cuid)
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE OR REPLACE FUNCTION generate_cuid() RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
timestamp_part TEXT;
|
||||
counter_part TEXT;
|
||||
random_part TEXT;
|
||||
BEGIN
|
||||
timestamp_part := to_hex(extract(epoch from now())::bigint);
|
||||
counter_part := lpad(to_hex((random() * 65535)::int), 4, '0');
|
||||
random_part := encode(gen_random_bytes(12), 'hex');
|
||||
RETURN 'c' || timestamp_part || counter_part || random_part;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
72
nginx/conf.d/default.conf
Normal file
72
nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,72 @@
|
||||
upstream frontend {
|
||||
server pdimaker-web:3000;
|
||||
}
|
||||
|
||||
upstream backend {
|
||||
server pdimaker-api:4000;
|
||||
}
|
||||
|
||||
# Redirecionar HTTP para HTTPS
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name pdimaker.com.br www.pdimaker.com.br;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS - Frontend
|
||||
server {
|
||||
listen 443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
server_name pdimaker.com.br www.pdimaker.com.br;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
|
||||
# HTTP - API
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.pdimaker.com.br;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS - API
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name api.pdimaker.com.br;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location / {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
38
nginx/nginx.conf
Normal file
38
nginx/nginx.conf
Normal file
@@ -0,0 +1,38 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 20M;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/json application/javascript application/xml+rss
|
||||
application/rss+xml font/truetype font/opentype
|
||||
application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
16
scripts/backup-db.sh
Executable file
16
scripts/backup-db.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="/var/backups/pdimaker"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="$BACKUP_DIR/pdimaker_$DATE.sql.gz"
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
echo "🗄️ Fazendo backup do banco de dados..."
|
||||
docker-compose exec -T postgres pg_dump -U pdimaker pdimaker_prod | gzip > $BACKUP_FILE
|
||||
|
||||
echo "✅ Backup salvo em: $BACKUP_FILE"
|
||||
|
||||
# Manter apenas últimos 7 backups
|
||||
find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete
|
||||
33
scripts/deploy.sh
Executable file
33
scripts/deploy.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Iniciando deploy do PDIMaker..."
|
||||
|
||||
# Carregar variáveis de ambiente
|
||||
export $(cat ../.env.production | xargs)
|
||||
|
||||
# Pull das últimas mudanças
|
||||
echo "📥 Atualizando código..."
|
||||
git pull origin main
|
||||
|
||||
# Rebuild e restart dos containers
|
||||
echo "🐳 Rebuilding containers..."
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
|
||||
# Aguardar containers ficarem healthy
|
||||
echo "⏳ Aguardando containers..."
|
||||
sleep 30
|
||||
|
||||
# Rodar migrations
|
||||
echo "🗄️ Rodando migrations..."
|
||||
docker-compose exec -T backend npx prisma migrate deploy
|
||||
|
||||
# Verificar saúde
|
||||
echo "🏥 Verificando saúde dos serviços..."
|
||||
docker-compose ps
|
||||
|
||||
echo "✅ Deploy concluído!"
|
||||
echo "🌐 Frontend: https://pdimaker.com.br"
|
||||
echo "🔧 Backend: https://api.pdimaker.com.br"
|
||||
9
scripts/logs.sh
Executable file
9
scripts/logs.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
SERVICE=${1:-all}
|
||||
|
||||
if [ "$SERVICE" = "all" ]; then
|
||||
docker-compose logs -f --tail=100
|
||||
else
|
||||
docker-compose logs -f --tail=100 $SERVICE
|
||||
fi
|
||||
11
scripts/restart.sh
Executable file
11
scripts/restart.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
SERVICE=${1:-all}
|
||||
|
||||
if [ "$SERVICE" = "all" ]; then
|
||||
docker-compose restart
|
||||
else
|
||||
docker-compose restart $SERVICE
|
||||
fi
|
||||
|
||||
echo "✅ Serviço(s) reiniciado(s)!"
|
||||
380
shared/prisma/schema.prisma
Normal file
380
shared/prisma/schema.prisma
Normal file
@@ -0,0 +1,380 @@
|
||||
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
|
||||
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user