Skip to content

Commit 9471007

Browse files
Merge pull request #105 from fivelibx/develop
Develop
2 parents f72dd32 + 71d5112 commit 9471007

27 files changed

Lines changed: 6028 additions & 3304 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<img alt="FiveLib Banner" src="https://github.com/devacademicx/fivelib/blob/main/docs/assets/banner.png"/>
1+
<img alt="FiveLib Banner" src="https://fivelib.vercel.app"/>
22

33
<div align="center">
44
<br/>

‎backend/api/auth.py‎

Lines changed: 247 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,249 @@
1+
import random
2+
from datetime import datetime, timedelta, timezone
3+
from fastapi import APIRouter, HTTPException, status
14
from fastapi.security import OAuth2PasswordBearer
5+
from schemas.auth import LoginRequest, LoginResponse, RegisterRequest, VerifyCodeRequest, ForgotPasswordRequest, ResetPasswordRequest
6+
from services.email_service import enviar_email_verificacao
7+
from database.config import supabase
8+
from api.security import obter_hash_senha, verificar_senha, criar_token_acesso
29

3-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
10+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login")
11+
12+
router = APIRouter(tags=["auth"])
13+
14+
@router.post("/login", response_model=LoginResponse)
15+
async def login(data: LoginRequest):
16+
response = supabase.table("user").select("*").eq("email", data.email).execute()
17+
18+
user_data = response.data
19+
20+
if not user_data:
21+
raise HTTPException(
22+
status_code=status.HTTP_401_UNAUTHORIZED,
23+
detail="E-mail ou senha incorretos"
24+
)
25+
26+
user = user_data[0]
27+
28+
if not verificar_senha(data.password, user["senha"]):
29+
raise HTTPException(
30+
status_code=status.HTTP_401_UNAUTHORIZED,
31+
detail="E-mail ou senha incorretos"
32+
)
33+
34+
if not user.get("is_active", False):
35+
novo_codigo = f"{random.randint(100000, 999999)}"
36+
novo_tempo_expiracao = (datetime.now(timezone.utc) + timedelta(minutes=15)).isoformat()
37+
38+
print("\n" + "="*50)
39+
print(f"🔑 [LOGIN - CONTA INATIVA] NOVO CÓDIGO GERADO PARA {data.email}: {novo_codigo}")
40+
print("="*50 + "\n")
41+
42+
43+
supabase.table("user").update({
44+
"verification_code": novo_codigo,
45+
"verification_expires_at": novo_tempo_expiracao
46+
}).eq("id", user["id"]).execute()
47+
48+
await enviar_email_verificacao(email=user["email"], codigo=novo_codigo)
49+
50+
raise HTTPException(
51+
status_code=status.HTTP_403_FORBIDDEN,
52+
detail={
53+
"error": "requires_verification",
54+
"email": user["email"],
55+
"message": "Esta conta ainda não foi ativada. Um novo código foi gerado."
56+
}
57+
)
58+
59+
payload_usuario = {
60+
"sub": str(user["id"]),
61+
"email": user["email"],
62+
"role": user["perfil"]
63+
}
64+
65+
token_real = criar_token_acesso(data=payload_usuario)
66+
67+
return {
68+
"access_token": token_real,
69+
"token_type": "bearer",
70+
"role": user["perfil"],
71+
"name": user["nome"]
72+
}
73+
74+
@router.post("/register", status_code=status.HTTP_201_CREATED)
75+
async def register(data: RegisterRequest):
76+
if not getattr(data, "accepted_terms", False):
77+
raise HTTPException(
78+
status_code=status.HTTP_400_BAD_REQUEST,
79+
detail="Você precisa aceitar os Termos de Uso e a Política de Privacidade para se cadastrar."
80+
)
81+
82+
user_exists = supabase.table("user").select("id, is_active").eq("email", data.email).execute()
83+
84+
codigo_verificacao = f"{random.randint(100000, 999999)}"
85+
tempo_expiracao = (datetime.now(timezone.utc) + timedelta(minutes=15)).isoformat()
86+
senha_criptografada = obter_hash_senha(data.senha)
87+
timestamp_aceite = datetime.now(timezone.utc).isoformat()
88+
89+
if user_exists.data:
90+
usuario_atual = user_exists.data[0]
91+
92+
if usuario_atual.get("is_active") == True:
93+
raise HTTPException(
94+
status_code=status.HTTP_400_BAD_REQUEST,
95+
detail="Este e-mail já está cadastrado."
96+
)
97+
98+
print("\n" + "="*50)
99+
print(f"🔄 [REENVIO/ATUALIZAÇÃO] NOVO CÓDIGO PARA {data.email}: {codigo_verificacao}")
100+
print("="*50 + "\n")
101+
102+
update_response = supabase.table("user").update({
103+
"nome": data.nome,
104+
"senha": senha_criptografada,
105+
"data_nascimento": data.data_nascimento.isoformat(),
106+
"verification_code": codigo_verificacao,
107+
"verification_expires_at": tempo_expiracao,
108+
"accepted_terms": True,
109+
"accepted_terms_at": timestamp_aceite
110+
}).eq("id", usuario_atual["id"]).execute()
111+
112+
if not update_response.data:
113+
raise HTTPException(
114+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
115+
detail="Erro ao atualizar código de verificação. Tente novamente."
116+
)
117+
118+
await enviar_email_verificacao(email=data.email, codigo=codigo_verificacao)
119+
120+
return {"message": "Um novo código de verificação foi gerado."}
121+
122+
print("\n" + "="*50)
123+
print(f"📧 [NOVO CADASTRO] CÓDIGO PARA {data.email}: {codigo_verificacao}")
124+
print("="*50 + "\n")
125+
126+
novo_usuario = {
127+
"nome": data.nome,
128+
"email": data.email,
129+
"senha": senha_criptografada,
130+
"data_nascimento": data.data_nascimento.isoformat(),
131+
"perfil": "user",
132+
"is_active": False,
133+
"verification_code": codigo_verificacao,
134+
"verification_expires_at": tempo_expiracao,
135+
"accepted_terms": True,
136+
"accepted_terms_at": timestamp_aceite
137+
}
138+
139+
insert_response = supabase.table("user").insert(novo_usuario).execute()
140+
141+
if not insert_response.data:
142+
raise HTTPException(
143+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
144+
detail="Erro ao criar conta. Tente novamente mais tarde."
145+
)
146+
147+
await enviar_email_verificacao(email=data.email, codigo=codigo_verificacao)
148+
149+
return {"message": "Cadastro realizado com sucesso! Verifique seu e-mail para ativar a conta."}
150+
151+
@router.post("/verify-code", status_code=status.HTTP_200_OK)
152+
async def verify_code(data: VerifyCodeRequest):
153+
response = supabase.table("user").select("*").eq("email", data.email).execute()
154+
155+
if not response.data:
156+
raise HTTPException(
157+
status_code=status.HTTP_404_NOT_FOUND,
158+
detail="Usuário não encontrado."
159+
)
160+
161+
user = response.data[0]
162+
163+
if user.get("is_active", False):
164+
return {"message": "Esta conta já está ativa."}
165+
166+
if user.get("verification_code") != data.code:
167+
raise HTTPException(
168+
status_code=status.HTTP_400_BAD_REQUEST,
169+
detail="Código de verificação incorreto."
170+
)
171+
172+
expires_at_str = user.get("verification_expires_at")
173+
if expires_at_str:
174+
expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
175+
176+
if datetime.now(timezone.utc) > expires_at:
177+
raise HTTPException(
178+
status_code=status.HTTP_400_BAD_REQUEST,
179+
detail="O código de verificação expirou. Solicite um novo código."
180+
)
181+
182+
update_response = supabase.table("user").update({
183+
"is_active": True,
184+
"verification_code": None,
185+
"verification_expires_at": None
186+
}).eq("id", user["id"]).execute()
187+
188+
if not update_response.data:
189+
raise HTTPException(
190+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
191+
detail="Erro ao ativar a conta. Tente novamente."
192+
)
193+
194+
return {"message": "Conta ativada com sucesso! Você já pode fazer login."}
195+
196+
@router.post("/forgot-password", status_code=status.HTTP_200_OK)
197+
async def forgot_password(data: ForgotPasswordRequest):
198+
response = supabase.table("user").select("id, is_active").eq("email", data.email).execute()
199+
200+
if not response.data:
201+
return {"message": "Se o e-mail estiver cadastrado, um código de recuperação será enviado."}
202+
203+
user = response.data[0]
204+
205+
codigo_recuperacao = f"{random.randint(100000, 999999)}"
206+
tempo_expiracao = (datetime.now(timezone.utc) + timedelta(minutes=15)).isoformat()
207+
208+
print("\n" + "="*50)
209+
print(f"🔒 [RECUPERAÇÃO DE SENHA] CÓDIGO GERADO PARA {data.email}: {codigo_recuperacao}")
210+
print("="*50 + "\n")
211+
212+
supabase.table("user").update({
213+
"reset_password_code": codigo_recuperacao,
214+
"reset_password_expires_at": tempo_expiracao
215+
}).eq("id", user["id"]).execute()
216+
217+
await enviar_email_verificacao(email=data.email, codigo=codigo_recuperacao)
218+
219+
return {"message": "Código de recuperação gerado com sucesso. Verifique seu e-mail."}
220+
221+
222+
@router.post("/reset-password", status_code=status.HTTP_200_OK)
223+
async def reset_password(data: ResetPasswordRequest):
224+
response = supabase.table("user").select("*").eq("email", data.email).execute()
225+
226+
if not response.data:
227+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Usuário não encontrado.")
228+
229+
user = response.data[0]
230+
231+
if not user.get("reset_password_code") or user.get("reset_password_code") != data.code:
232+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Código de recuperação inválido.")
233+
234+
expires_at_str = user.get("reset_password_expires_at")
235+
if expires_at_str:
236+
expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
237+
if datetime.now(timezone.utc) > expires_at:
238+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="O código de recuperação expirou.")
239+
240+
nova_senha_criptografada = obter_hash_senha(data.nova_senha)
241+
242+
supabase.table("user").update({
243+
"senha": nova_senha_criptografada,
244+
"reset_password_code": None,
245+
"reset_password_expires_at": None,
246+
"is_active": True
247+
}).eq("id", user["id"]).execute()
248+
249+
return {"message": "Senha redefinida com sucesso! Você já pode fazer login."}

‎backend/api/routes/users.py‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
1-
from fastapi import APIRouter, Depends, HTTPException
1+
from fastapi import APIRouter, Depends, HTTPException, status
22
from api.auth import oauth2_scheme
3+
from api.security import decodificar_token_acesso
4+
from database.config import supabase
35

46
router = APIRouter()
57

8+
async def obter_usuario_atual(token: str = Depends(oauth2_scheme)):
9+
payload = decodificar_token_acesso(token)
10+
11+
user_id = payload.get("sub")
12+
if not user_id:
13+
raise HTTPException(
14+
status_code=status.HTTP_401_UNAUTHORIZED,
15+
detail="Token inválido: ID de usuário ausente no payload."
16+
)
17+
18+
response = supabase.table("user").select("id", "nome", "email", "perfil").eq("id", user_id).execute()
19+
20+
if not response.data:
21+
raise HTTPException(
22+
status_code=status.HTTP_401_UNAUTHORIZED,
23+
detail="Usuário dono deste token não foi encontrado."
24+
)
25+
26+
return response.data[0]
27+
28+
629
@router.get("/me")
7-
async def read_users_me(token: str = Depends(oauth2_scheme)):
8-
return {"user": "Lucas", "token_received": token}
30+
async def read_users_me(current_user: dict = Depends(obter_usuario_atual)):
31+
return {
32+
"id": current_user["id"],
33+
"name": current_user["nome"],
34+
"email": current_user["email"],
35+
"role": current_user["perfil"]
36+
}

‎backend/api/security.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import os
2+
import bcrypt
3+
import jwt
4+
from datetime import datetime, timedelta, timezone
5+
from fastapi import HTTPException, status
6+
7+
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "chave_padrao_desenvolvimento_local_123456")
8+
ALGORITHM = "HS256"
9+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
10+
11+
def obter_hash_senha(senha: str) -> str:
12+
senha_bytes = senha.encode('utf-8')
13+
salt = bcrypt.gensalt()
14+
hash_bytes = bcrypt.hashpw(senha_bytes, salt)
15+
return hash_bytes.decode('utf-8')
16+
17+
def verificar_senha(senha_plana: str, senha_hash: str) -> bool:
18+
try:
19+
senha_bytes = senha_plana.encode('utf-8')
20+
hash_bytes = senha_hash.encode('utf-8')
21+
return bcrypt.checkpw(senha_bytes, hash_bytes)
22+
except Exception:
23+
return False
24+
25+
26+
def criar_token_acesso(data: dict) -> str:
27+
dados_para_criptografar = data.copy()
28+
29+
expiracao = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
30+
dados_para_criptografar.update({"exp": expiracao})
31+
32+
token_jwt = jwt.encode(dados_para_criptografar, SECRET_KEY, algorithm=ALGORITHM)
33+
return token_jwt
34+
35+
def decodificar_token_acesso(token: str) -> dict:
36+
try:
37+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
38+
return payload
39+
except jwt.ExpiredSignatureError:
40+
raise HTTPException(
41+
status_code=status.HTTP_401_UNAUTHORIZED,
42+
detail="O token de acesso expirou. Faça login novamente.",
43+
headers={"WWW-Authenticate": "Bearer"},
44+
)
45+
except jwt.InvalidTokenError:
46+
raise HTTPException(
47+
status_code=status.HTTP_401_UNAUTHORIZED,
48+
detail="Token de acesso inválido.",
49+
headers={"WWW-Authenticate": "Bearer"},
50+
)

‎backend/main.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# Rotas da API
66
from api.routes.health import router as health_router
7+
from api.auth import router as auth_router
78
from api.routes.users import router as users_router
89
from api.routes.tools import router as tools_router
910

@@ -31,7 +32,8 @@
3132
async def root():
3233
return {"message": "FiveLib API is running"}
3334

34-
# NOTE: Registro dos roteadores.
35-
app.include_router(health_router, prefix="/health")
36-
app.include_router(users_router, prefix="/users")
37-
app.include_router(tools_router, prefix="/resources", tags=["Tools"])
35+
# NOTE: Registro dos roteadores com o prefixo da versão.
36+
app.include_router(health_router, prefix="/api/v1/health")
37+
app.include_router(auth_router, prefix="/api/v1/auth")
38+
app.include_router(users_router, prefix="/api/v1/users")
39+
app.include_router(tools_router, prefix="/api/v1/resources", tags=["Tools"])

0 commit comments

Comments
 (0)