From 08582a4f700ea17c8f8fd41f8ecb9db6e912bddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Guti=C3=A9rrez=20Mu=C3=B1oz?= Date: Thu, 5 Nov 2026 08:28:15 +0000 Subject: [PATCH] Initial commit --- .gitignore | 6 + backend/__init__.py | 0 backend/code/__init__.py | 0 backend/code/config.env | 18 ++ backend/code/core/controller.py | 12 ++ backend/code/enums/__init__.py | 0 backend/code/enums/alert_level_enum.py | 9 + backend/code/enums/animal_sex_enum.py | 6 + backend/code/enums/errors_enum.py | 13 ++ backend/code/enums/message_type_enum.py | 5 + backend/code/main.py | 69 +++++++ .../code/middlewares/private_middleware.py | 19 ++ backend/code/models/__init__,py | 0 .../requests/account_change_password.py | 5 + backend/code/models/requests/account_login.py | 7 + .../code/models/requests/account_recover.py | 6 + .../models/requests/account_reset_password.py | 5 + .../models/requests/animal_associate_alert.py | 0 .../requests/animal_associate_comment.py | 0 .../models/requests/animal_associate_image.py | 0 .../requests/animal_associate_settlement.py | 0 backend/code/models/requests/animal_edit.py | 0 .../code/models/requests/animal_register.py | 0 .../code/models/requests/animals_filter.py | 0 .../code/models/requests/settlement_edit.py | 6 + .../models/requests/settlement_register.py | 7 + .../requests/settlement_register_done,py | 6 + .../models/requests/settlements_filter.py | 0 .../models/requests/user_associate_account.py | 6 + .../requests/user_associate_permissions.py | 6 + backend/code/models/requests/user_edit.py | 7 + backend/code/models/requests/user_register.py | 10 + backend/code/models/requests/users_filter.py | 10 + .../code/models/responses/session_token.py | 4 + .../models/responses/settlement_detail.py | 11 ++ backend/code/models/responses/user_created.py | 11 ++ backend/code/models/responses/user_detail.py | 11 ++ backend/code/modules/accounts/repository.py | 24 +++ backend/code/modules/animal_alerts/router.py | 20 ++ .../code/modules/animal_comments/router.py | 19 ++ backend/code/modules/animal_images/router.py | 28 +++ backend/code/modules/animals/controller.py | 0 backend/code/modules/animals/repository.py | 0 backend/code/modules/animals/router.py | 19 ++ backend/code/modules/database/database.py | 27 +++ .../code/modules/database/database_sqlite.py | 48 +++++ backend/code/modules/miscellaneous/router.py | 13 ++ backend/code/modules/sessions/controller.py | 183 ++++++++++++++++++ backend/code/modules/sessions/repository.py | 89 +++++++++ backend/code/modules/sessions/router.py | 45 +++++ .../code/modules/settlements/controller.py | 91 +++++++++ .../code/modules/settlements/repository.py | 87 +++++++++ backend/code/modules/settlements/router.py | 62 ++++++ backend/code/modules/users/controller.py | 118 +++++++++++ backend/code/modules/users/repository.py | 76 ++++++++ backend/code/modules/users/router.py | 60 ++++++ backend/code/services/environment.py | 20 ++ backend/code/services/mailing.py | 27 +++ backend/code/services/token.py | 22 +++ backend/requeriments.txt | 62 ++++++ database/1_schema_sqlite.sql | 127 ++++++++++++ database/2_population_sqlite.sql | 82 ++++++++ database/settlements.db | Bin 0 -> 118784 bytes database/update_from_sqlite__1_schema.sql | 170 ++++++++++++++++ database/update_from_sqlite__2_population.sql | 92 +++++++++ 65 files changed, 1886 insertions(+) create mode 100644 .gitignore create mode 100644 backend/__init__.py create mode 100644 backend/code/__init__.py create mode 100644 backend/code/config.env create mode 100644 backend/code/core/controller.py create mode 100644 backend/code/enums/__init__.py create mode 100644 backend/code/enums/alert_level_enum.py create mode 100644 backend/code/enums/animal_sex_enum.py create mode 100644 backend/code/enums/errors_enum.py create mode 100644 backend/code/enums/message_type_enum.py create mode 100644 backend/code/main.py create mode 100644 backend/code/middlewares/private_middleware.py create mode 100644 backend/code/models/__init__,py create mode 100644 backend/code/models/requests/account_change_password.py create mode 100644 backend/code/models/requests/account_login.py create mode 100644 backend/code/models/requests/account_recover.py create mode 100644 backend/code/models/requests/account_reset_password.py create mode 100644 backend/code/models/requests/animal_associate_alert.py create mode 100644 backend/code/models/requests/animal_associate_comment.py create mode 100644 backend/code/models/requests/animal_associate_image.py create mode 100644 backend/code/models/requests/animal_associate_settlement.py create mode 100644 backend/code/models/requests/animal_edit.py create mode 100644 backend/code/models/requests/animal_register.py create mode 100644 backend/code/models/requests/animals_filter.py create mode 100644 backend/code/models/requests/settlement_edit.py create mode 100644 backend/code/models/requests/settlement_register.py create mode 100644 backend/code/models/requests/settlement_register_done,py create mode 100644 backend/code/models/requests/settlements_filter.py create mode 100644 backend/code/models/requests/user_associate_account.py create mode 100644 backend/code/models/requests/user_associate_permissions.py create mode 100644 backend/code/models/requests/user_edit.py create mode 100644 backend/code/models/requests/user_register.py create mode 100644 backend/code/models/requests/users_filter.py create mode 100644 backend/code/models/responses/session_token.py create mode 100644 backend/code/models/responses/settlement_detail.py create mode 100644 backend/code/models/responses/user_created.py create mode 100644 backend/code/models/responses/user_detail.py create mode 100644 backend/code/modules/accounts/repository.py create mode 100644 backend/code/modules/animal_alerts/router.py create mode 100644 backend/code/modules/animal_comments/router.py create mode 100644 backend/code/modules/animal_images/router.py create mode 100644 backend/code/modules/animals/controller.py create mode 100644 backend/code/modules/animals/repository.py create mode 100644 backend/code/modules/animals/router.py create mode 100644 backend/code/modules/database/database.py create mode 100644 backend/code/modules/database/database_sqlite.py create mode 100644 backend/code/modules/miscellaneous/router.py create mode 100644 backend/code/modules/sessions/controller.py create mode 100644 backend/code/modules/sessions/repository.py create mode 100644 backend/code/modules/sessions/router.py create mode 100644 backend/code/modules/settlements/controller.py create mode 100644 backend/code/modules/settlements/repository.py create mode 100644 backend/code/modules/settlements/router.py create mode 100644 backend/code/modules/users/controller.py create mode 100644 backend/code/modules/users/repository.py create mode 100644 backend/code/modules/users/router.py create mode 100644 backend/code/services/environment.py create mode 100644 backend/code/services/mailing.py create mode 100644 backend/code/services/token.py create mode 100644 backend/requeriments.txt create mode 100644 database/1_schema_sqlite.sql create mode 100644 database/2_population_sqlite.sql create mode 100644 database/settlements.db create mode 100644 database/update_from_sqlite__1_schema.sql create mode 100644 database/update_from_sqlite__2_population.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bf8346 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +bin +include +lib +lib64 +pyvenv.cfg \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/__init__.py b/backend/code/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/config.env b/backend/code/config.env new file mode 100644 index 0000000..9ae9b36 --- /dev/null +++ b/backend/code/config.env @@ -0,0 +1,18 @@ +# ENVIRONMENT (development, production) +ENVIRONMENT=Development +DOMAIN_URL=https://studio.dotapi.net/proxy/8000/ +SUPPORT_EMAIL=support@dotapi.net + +# TOKEN +TOKEN_SECRET=Desarrollado.por.Albert.Gutiérrez.Muñoz@2026 +TOKEN_ALGORITHM=HS256 +TOKEN_LIVE=60 + +# SESSION +USER_LIMIT_ATTEMPTS=3 + +# MAILING +MAILING_SMTP_SERVER=smtp.gmail.com +MAILING_SMTP_PORT=465 +MAILING_SMTP_USER=albertgmu@gmail.com +MAILING_SMTP_PASSWORD=faqz vrws suaf sqtd \ No newline at end of file diff --git a/backend/code/core/controller.py b/backend/code/core/controller.py new file mode 100644 index 0000000..24d4ae8 --- /dev/null +++ b/backend/code/core/controller.py @@ -0,0 +1,12 @@ +from abc import ABC + +class Controller(ABC): + def _OnProcessParams(self, params: str): + result = {} + params = params.strip() + params = params.split(',') + for param in params: + field, value = param.split('=') + result[field.lower()] = value + + return result \ No newline at end of file diff --git a/backend/code/enums/__init__.py b/backend/code/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/enums/alert_level_enum.py b/backend/code/enums/alert_level_enum.py new file mode 100644 index 0000000..3a061d8 --- /dev/null +++ b/backend/code/enums/alert_level_enum.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class AlertLevelEnum(int, Enum): + Info=0 + Low=1 + Medium=2 + High=3 + Ultra=4 \ No newline at end of file diff --git a/backend/code/enums/animal_sex_enum.py b/backend/code/enums/animal_sex_enum.py new file mode 100644 index 0000000..995b703 --- /dev/null +++ b/backend/code/enums/animal_sex_enum.py @@ -0,0 +1,6 @@ +from enum import Enum + +class AnimalSexEnum(int, Enum): + Female=0 + Male=1 + Unknown=2 \ No newline at end of file diff --git a/backend/code/enums/errors_enum.py b/backend/code/enums/errors_enum.py new file mode 100644 index 0000000..ccc3425 --- /dev/null +++ b/backend/code/enums/errors_enum.py @@ -0,0 +1,13 @@ +from enum import Enum + +class ErrorTypeEnum(int, Enum): + USER_EMAIL_EXISTS = -1 + USER_ACCOUNT_EXISTS = -2 + USER_ID_NOT_EXISTS = -3 + USER_ACCOUNT_LOCKED = -4 + USER_ACCOUNT_WRONG_PASSWORD = -5 + USER_SESSION_NOT_EXISTS = -6 + USER_SESSION_EXPIRED = -7 + USER_ACCOUNT_NO_PASSWORD = -8 + SETTLEMENT_EXISTS = -9 + SETTLEMENT_ID_NOT_EXISTS = -10 \ No newline at end of file diff --git a/backend/code/enums/message_type_enum.py b/backend/code/enums/message_type_enum.py new file mode 100644 index 0000000..cf38f78 --- /dev/null +++ b/backend/code/enums/message_type_enum.py @@ -0,0 +1,5 @@ +from enum import Enum + +class MessageTypeEnum(int, Enum): + Information=0 + Alert=1 \ No newline at end of file diff --git a/backend/code/main.py b/backend/code/main.py new file mode 100644 index 0000000..5e23478 --- /dev/null +++ b/backend/code/main.py @@ -0,0 +1,69 @@ +from fastapi import FastAPI, File, HTTPException, Response, UploadFile, status, Header, Depends + +from backend.code.services.environment import EnvironmentService +from backend.code.middlewares.private_middleware import PrivateMiddleware + +from backend.code.modules.miscellaneous import router as router_miscellaneous +from backend.code.modules.sessions import router as router_sessions +from backend.code.modules.users import router as router_users +from backend.code.modules.animals import router as router_animals +from backend.code.modules.animal_comments import router as router_animal_comments +from backend.code.modules.animal_images import router as router_animal_images +from backend.code.modules.animal_alerts import router as router_animal_alerts +from backend.code.modules.settlements import router as router_settlements + +# +# LOAD CONFIGURATION +# +EnvironmentService().LoadEnvFile('config.env') + +# +# MIDDLEWARES +# +token_middleware = PrivateMiddleware() + +app = FastAPI(root_path="/proxy/8000") + +# +# ENDPOINTS +# + +# +# Session [OK] +# +# app.include_router(router_sessions.routes) + +# +# USERS [OK] +# +# app.include_router(router_users.routes, dependencies=[Depends(token_middleware.HasToken)]) + +# +# SETTLEMENTS +# +app.include_router(router_settlements.routes, dependencies=[Depends(token_middleware.HasToken)]) + +# # +# # ANIMALS +# # +# app.include_router(router_animals.routes, dependencies=[Depends(token_middleware.HasToken)]) + +# # +# # ANIMAL COMMENTS +# # +# app.include_router(router_animal_comments.routes) + +# # +# # ANIMAL IMAGES +# # +# app.include_router(router_animal_images.routes) + +# # +# # ANIMAL ALERTS +# # +# app.include_router(router_animal_alerts.routes) + +# # +# # Miscellaneous +# # +# app.include_router(router_miscellaneous.routes) diff --git a/backend/code/middlewares/private_middleware.py b/backend/code/middlewares/private_middleware.py new file mode 100644 index 0000000..1fcd128 --- /dev/null +++ b/backend/code/middlewares/private_middleware.py @@ -0,0 +1,19 @@ +import jwt +from fastapi import Header, status, HTTPException + +from backend.code.services.token import TokenService + +class PrivateMiddleware: + def __init__(self): + self._tokenService = TokenService() + + def HasToken(self, token: str = Header(...)): + if token is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + + if self._tokenService.ValidateToken(token) == False: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + + return token + + \ No newline at end of file diff --git a/backend/code/models/__init__,py b/backend/code/models/__init__,py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/account_change_password.py b/backend/code/models/requests/account_change_password.py new file mode 100644 index 0000000..25c7fed --- /dev/null +++ b/backend/code/models/requests/account_change_password.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class AccountChangePassword(BaseModel): + OldPassword: str + NewPassword: str \ No newline at end of file diff --git a/backend/code/models/requests/account_login.py b/backend/code/models/requests/account_login.py new file mode 100644 index 0000000..5cf6642 --- /dev/null +++ b/backend/code/models/requests/account_login.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from typing import Optional + +class AccountLoginRequest(BaseModel): + Username: str + Password: str + Domain: Optional[str] = None \ No newline at end of file diff --git a/backend/code/models/requests/account_recover.py b/backend/code/models/requests/account_recover.py new file mode 100644 index 0000000..b1ca03a --- /dev/null +++ b/backend/code/models/requests/account_recover.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel +from typing import Optional + +class AccountRecoverRequest(BaseModel): + Username: str + Domain: Optional[str] \ No newline at end of file diff --git a/backend/code/models/requests/account_reset_password.py b/backend/code/models/requests/account_reset_password.py new file mode 100644 index 0000000..28ace9e --- /dev/null +++ b/backend/code/models/requests/account_reset_password.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class AccountResetPassword(BaseModel): + TokenRequest: str + Password: str \ No newline at end of file diff --git a/backend/code/models/requests/animal_associate_alert.py b/backend/code/models/requests/animal_associate_alert.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/animal_associate_comment.py b/backend/code/models/requests/animal_associate_comment.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/animal_associate_image.py b/backend/code/models/requests/animal_associate_image.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/animal_associate_settlement.py b/backend/code/models/requests/animal_associate_settlement.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/animal_edit.py b/backend/code/models/requests/animal_edit.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/animal_register.py b/backend/code/models/requests/animal_register.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/animals_filter.py b/backend/code/models/requests/animals_filter.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/settlement_edit.py b/backend/code/models/requests/settlement_edit.py new file mode 100644 index 0000000..b4e231d --- /dev/null +++ b/backend/code/models/requests/settlement_edit.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + +class SettlementEditRequest(BaseModel): + Name: str + Address: str + Active: bool \ No newline at end of file diff --git a/backend/code/models/requests/settlement_register.py b/backend/code/models/requests/settlement_register.py new file mode 100644 index 0000000..db722fb --- /dev/null +++ b/backend/code/models/requests/settlement_register.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from typing import Optional + +class SettlementRegisterRequest(BaseModel): + Name: str + Address: str + Active: Optional[bool] diff --git a/backend/code/models/requests/settlement_register_done,py b/backend/code/models/requests/settlement_register_done,py new file mode 100644 index 0000000..f572243 --- /dev/null +++ b/backend/code/models/requests/settlement_register_done,py @@ -0,0 +1,6 @@ +from datetime import datetime +from pydantic import BaseModel + +class SettlementRegisterDoneRequest(BaseModel): + DoneAt: datetime + DoneBy: int \ No newline at end of file diff --git a/backend/code/models/requests/settlements_filter.py b/backend/code/models/requests/settlements_filter.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/models/requests/user_associate_account.py b/backend/code/models/requests/user_associate_account.py new file mode 100644 index 0000000..9d8e6ee --- /dev/null +++ b/backend/code/models/requests/user_associate_account.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel +from typing import Optional + +class UserAssociateAccount(BaseModel): + Username: str + Active: Optional[bool] = True \ No newline at end of file diff --git a/backend/code/models/requests/user_associate_permissions.py b/backend/code/models/requests/user_associate_permissions.py new file mode 100644 index 0000000..09e87cb --- /dev/null +++ b/backend/code/models/requests/user_associate_permissions.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel +from typing import Optional + +class UserAssociatePermissions(BaseModel): + Permissions: Optional[dict[int]] = None + Groups: Optional[dict[int]] = None \ No newline at end of file diff --git a/backend/code/models/requests/user_edit.py b/backend/code/models/requests/user_edit.py new file mode 100644 index 0000000..b9607ab --- /dev/null +++ b/backend/code/models/requests/user_edit.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from typing import Optional + +class UserEditRequest(BaseModel): + Name: Optional[str] = None + Surnames: Optional[str] = None + Email: Optional[str] = None \ No newline at end of file diff --git a/backend/code/models/requests/user_register.py b/backend/code/models/requests/user_register.py new file mode 100644 index 0000000..18c6963 --- /dev/null +++ b/backend/code/models/requests/user_register.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel +from typing import Optional + +class UserRegisterRequest(BaseModel): + Name: str + Surnames: str + Email: str + Username: Optional[str] + Locked: Optional[bool] + Active: Optional[bool] \ No newline at end of file diff --git a/backend/code/models/requests/users_filter.py b/backend/code/models/requests/users_filter.py new file mode 100644 index 0000000..cf07b93 --- /dev/null +++ b/backend/code/models/requests/users_filter.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel +from typing import Optional + +class UserFilter(BaseModel): + Name: Optional[str] = None + Surnames: Optional[str] = None + Email: Optional[str] = None + Username: Optional[str] = None + Locked: Optional[bool] = None + Active: Optional[bool] = None \ No newline at end of file diff --git a/backend/code/models/responses/session_token.py b/backend/code/models/responses/session_token.py new file mode 100644 index 0000000..37f31fa --- /dev/null +++ b/backend/code/models/responses/session_token.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class SessionTokenResponse(BaseModel): + Token: str \ No newline at end of file diff --git a/backend/code/models/responses/settlement_detail.py b/backend/code/models/responses/settlement_detail.py new file mode 100644 index 0000000..eb29c47 --- /dev/null +++ b/backend/code/models/responses/settlement_detail.py @@ -0,0 +1,11 @@ +from datetime import datetime +from pydantic import BaseModel +from typing import Optional + +class SettlementDetailResponse(BaseModel): + Id: int + Name: str + Address: str + LastDone: Optional[datetime] + LastDoneBy: Optional[int] + Active: bool \ No newline at end of file diff --git a/backend/code/models/responses/user_created.py b/backend/code/models/responses/user_created.py new file mode 100644 index 0000000..4a98772 --- /dev/null +++ b/backend/code/models/responses/user_created.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel +from typing import Optional + +class UserCreatedResponse(BaseModel): + Id: int + Name: str + Surnames: str + Email: str + Username: Optional[str] + Locked: Optional[bool] + Active: Optional[bool] diff --git a/backend/code/models/responses/user_detail.py b/backend/code/models/responses/user_detail.py new file mode 100644 index 0000000..ff64230 --- /dev/null +++ b/backend/code/models/responses/user_detail.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel +from typing import Optional + +class UserDetailResponse(BaseModel): + Id: int + Name: str + Surnames: str + Email: str + Username: Optional[str] + Locked: Optional[bool] + Active: Optional[bool] \ No newline at end of file diff --git a/backend/code/modules/accounts/repository.py b/backend/code/modules/accounts/repository.py new file mode 100644 index 0000000..3359310 --- /dev/null +++ b/backend/code/modules/accounts/repository.py @@ -0,0 +1,24 @@ +from backend.code.modules.database.database import DatabaseModule + +class AccountRepository(): + def __init__(self, repo: DatabaseModule): + self.conn = repo.conn + self.cursor = repo.cursor + + def SelectByUsername(self, username: str): + sql = ''' + SELECT * + FROM user_accounts + WHERE username = ? + ''' + self.cursor.execute(sql, [username]) + return self.cursor.fetchone() + + def InsertAccount(self, user_id: int, username: str, locked: bool = False, active: bool = True): + sql = ''' + INSERT INTO user_accounts(user_id, username, locked, active) + VALUES(?, ?, ?, ?) + ''' + self.cursor.execute(sql, [user_id, username, locked, active]) + self.conn.commit() + return self.cursor.lastrowid \ No newline at end of file diff --git a/backend/code/modules/animal_alerts/router.py b/backend/code/modules/animal_alerts/router.py new file mode 100644 index 0000000..9cca333 --- /dev/null +++ b/backend/code/modules/animal_alerts/router.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter + +routes = APIRouter(prefix="/animals/{animalId}/alerts", tags=['Animal alert']) + + +@routes.get("") +def ListAnimalAlerts(animalId: str): + pass + +@routes.post("") +def RegisterAnimalAlert(animalId: str, data): + pass + +@routes.put("/{id}") +def EditAnimalAlert(animalId: str, id: str, data): + pass + +@routes.delete("/{id}") +def DisableAnimalAlert(animalId: str, id: str): + pass \ No newline at end of file diff --git a/backend/code/modules/animal_comments/router.py b/backend/code/modules/animal_comments/router.py new file mode 100644 index 0000000..bed25fe --- /dev/null +++ b/backend/code/modules/animal_comments/router.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter + +routes = APIRouter(prefix="/animals/{animalId}/comments", tags=['Animal comment']) + +@routes.get("") +def ListAnimalComments(animalId: str): + pass + +@routes.post("") +def RegisterAnimalComment(animalId: str, data): + pass + +@routes.put("/{id}") +def EditAnimalComment(animalId: str, id: str, data): + pass + +@routes.delete("/{id}") +def DisableAnimalComment(animalId: str, id: str): + pass \ No newline at end of file diff --git a/backend/code/modules/animal_images/router.py b/backend/code/modules/animal_images/router.py new file mode 100644 index 0000000..849519c --- /dev/null +++ b/backend/code/modules/animal_images/router.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter, UploadFile, File, HTTPException + +ALLOWED_IMAGES = { + "image/jpeg", + "image/png", + "image/webp", +} + +routes = APIRouter(prefix="/animals/{animalId}/images", tags=['Animal image']) + +@routes.get("") +def ListAnimalImages(animalId: str): + pass + +@routes.post("") +def RegisterAnimalImage(animalId: str, file: UploadFile = File(...)): + if file.content_type not in ALLOWED_IMAGES: + raise HTTPException(status_code=400, detail="File must be an image") + + return {"data": file.filename} + +@routes.put("/{id}") +def EditAnimalImage(animalId: str, id: str, data): + pass + +@routes.delete("/{id}") +def DisableAnimalImage(animalId: str, id: str): + pass \ No newline at end of file diff --git a/backend/code/modules/animals/controller.py b/backend/code/modules/animals/controller.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/modules/animals/repository.py b/backend/code/modules/animals/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code/modules/animals/router.py b/backend/code/modules/animals/router.py new file mode 100644 index 0000000..9e5a4c2 --- /dev/null +++ b/backend/code/modules/animals/router.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, status + +routes = APIRouter(prefix="/animals", tags=['Animal']) + +@routes.get("", status_code = status.HTTP_200_OK) +def FilterAnimals(data): + pass + +@routes.post("", status_code = status.HTTP_201_CREATED) +def RegisterAnimal(data): + pass + +@routes.put("/{id}", status_code = status.HTTP_204_NO_CONTENT) +def EditAnimal(id: str, data): + pass + +@routes.delete("/{id}", status_code = status.HTTP_204_NO_CONTENT) +def DisableAnimal(id: str): + pass diff --git a/backend/code/modules/database/database.py b/backend/code/modules/database/database.py new file mode 100644 index 0000000..20e6848 --- /dev/null +++ b/backend/code/modules/database/database.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod + +class DatabaseModule(ABC): + + @abstractmethod + def SelectById(self): + raise NotImplementedError("Select by id method not implemented") + + @abstractmethod + def SelectByCode(self): + raise NotImplementedError("Select by code method not implemented") + + @abstractmethod + def Filter(self, data): + raise NotImplementedError("Filter method not implemented") + + @abstractmethod + def Register(self, data): + raise NotImplementedError("Register method not implemented") + + @abstractmethod + def Edit(self, register, data): + raise NotImplementedError("Edit method not implemented") + + @abstractmethod + def Disable(self, register): + raise NotImplementedError("Disable method not implemented") diff --git a/backend/code/modules/database/database_sqlite.py b/backend/code/modules/database/database_sqlite.py new file mode 100644 index 0000000..fc0f14d --- /dev/null +++ b/backend/code/modules/database/database_sqlite.py @@ -0,0 +1,48 @@ +import os +from backend.code.modules.database.database import DatabaseModule +import sqlite3 + +class DatabaseSqliteModule(DatabaseModule): + def __init__(self): + self.database = '/projects/database/settlements.db' + self.schema = '/projects/database/1_schema_sqlite.sql' + self.basic_data = '/projects/database/2_population_sqlite.sql' + self.Initialize() + + def Initialize(self): + exists = os.path.isfile(self.database) + self.conn = sqlite3.connect(self.database, check_same_thread=False) + self.cursor = self.conn.cursor() + if exists is False: + self.GenerateSchema() + self.PopulateDatabase() + + def GenerateSchema(self): + with open(self.schema, 'r', encoding='utf-8') as file: + sql = file.read() + self.cursor.executescript(sql) + self.conn.commit() + + def PopulateDatabase(self): + with open(self.basic_data, 'r', encoding='utf-8') as file: + sql = file.read() + self.cursor.executescript(sql) + self.conn.commit() + + def SelectById(self): + raise NotImplementedError("Select by id method not implemented") + + def SelectByCode(self): + raise NotImplementedError("Select by code method not implemented") + + def Filter(self, data): + raise NotImplementedError("Filter method not implemented") + + def Register(self, data): + raise NotImplementedError("Register method not implemented") + + def Edit(self, register, data): + raise NotImplementedError("Edit method not implemented") + + def Disable(self, register): + raise NotImplementedError("Disable method not implemented") \ No newline at end of file diff --git a/backend/code/modules/miscellaneous/router.py b/backend/code/modules/miscellaneous/router.py new file mode 100644 index 0000000..28522d7 --- /dev/null +++ b/backend/code/modules/miscellaneous/router.py @@ -0,0 +1,13 @@ +from datetime import datetime +from fastapi import APIRouter, status, HTTPException + +routes = APIRouter(prefix="", tags=['Miscellaneous']) + +@routes.api_route('/', methods=['GET', 'POST', 'PUT', 'DELETE'], status_code=status.HTTP_400_BAD_REQUEST) +def RootPathLocked(): + pass + +@routes.get('/isalive', status_code=status.HTTP_200_OK) +def CheckServerRunning(): + now = datatime.now().strftime('%Y-%m-%d %H:%M:%S') + return {'data': f'Last server update: {now}'} diff --git a/backend/code/modules/sessions/controller.py b/backend/code/modules/sessions/controller.py new file mode 100644 index 0000000..c9dbde5 --- /dev/null +++ b/backend/code/modules/sessions/controller.py @@ -0,0 +1,183 @@ +import os +from datetime import datetime, timedelta +from backend.code.modules.database.database_sqlite import DatabaseSqliteModule +from backend.code.modules.sessions.repository import SessionRepository +from backend.code.services.token import TokenService +from backend.code.services.mailing import MailingService +from backend.code.enums.errors_enum import ErrorTypeEnum +from backend.code.models.responses.session_token import SessionTokenResponse + + +from backend.code.modules.users.repository import UserRepository + +class SessionsController(): + def __init__(self): + database = DatabaseSqliteModule() + self.repo = SessionRepository(database) + self.token_service = TokenService() + self.mailing_service = MailingService() + + def Login(self, user: str, password: str, domain: str = None): + account = self.repo.SelectByUsername(user) + if account is None: + return + + # Array to properties + account_id = account[0] + user_id = account[1] + _password = account[3] + attempts = account[4] + locked = account[5] + name = account[7] + surnames = account[8] + email = account[9] + + # Validations + if locked: + return ErrorTypeEnum.USER_ACCOUNT_LOCKED + + # Default user has no password + if _password is None: + return ErrorTypeEnum.USER_ACCOUNT_NO_PASSWORD + + if password != _password: + attempts = attempts + 1 + self._OnUpdateAttempts(account_id, attempts) + return ErrorTypeEnum.USER_ACCOUNT_WRONG_PASSWORD + + # Reset when password is correct and registered some attempts + if attempts > 0: + self._OnUpdateAttempts(account_id, 0) + + # Generate token + payload = { + 'UserId': user_id, + 'Name': name, + 'Surnames': surnames, + 'Email': email + } + token = self.token_service.GenerateToken(payload) + + session = self.repo.RegisterSession(account_id, token) + return SessionTokenResponse(Token=token) + + def ValidateToken(self, token: str): + exists = self.repo.SelectByToken(token) + if exists is None: + return ErrorTypeEnum.USER_SESSION_NOT_EXISTS + + # Array to properties + account_id = exists[0] + user_id = exists[1] + + # Get metadata token + payload = self.token_service.ValidateToken(token) + if payload == False: + return + + # Check token integrity + if payload['UserId'] != user_id: + return + + autorefresh = self._OnCheckTokenAutoRefresh(payload['exp']) + if autorefresh: + token = self._OnRefreshToken(account_id, token, payload) + + return token + + def Logout(self, token: str): + exists = self.repo.SelectByToken(token) + if exists is None: + return + + disabled = self.repo.DisableToken(token) + if disabled is None: + return + + return disabled == 1 + + def RecoverAccount(self, username: str, domain: str = None): + account = self.repo.SelectByUsername(username) + if account is None: + return + + # Array to properties + AccountId = account[0] + Fullname = f'{account[7]} {account[8]}' + Email = account[9] + now = datetime.now() + + # Generate token + payload = {'Account': AccountId} + token = self.token_service.GenerateToken(payload) + url = f'{os.getenv('DOMAIN_URL')}/restore/{token}' + + registered = self.repo.InsertRecoverRequest(AccountId, token) + if registered is None: + return + + # Prepare notification + template = ''' + Hola {}, + + Has pedido restaurar tu contraseña de {}. Accede al siguiente enlace para establecer una nueva contraseña: + {} + + Si no has realizado esta petición de restauración de la cuenta, envia un correo a: {} + ''' + message = template.format(Fullname, '[APP_NAME]', url, os.getenv('SUPPORT_EMAIL')) + + # Send notification + self.mailing_service.SendMessage( + Email, + 'Petición restaurar cuenta [APP_NAME]', + message + ) + + def ValidateTokenRecoverAccount(self, token: str): + validated = self.token_service.ValidateToken(token) + if validated is False: + return + + exists = self.repo.SelectRecoverRequest(validated['Account'], token) + if exists is None: + return + + disabled = self.repo.DisableRecoverRequest(token) + if disabled == 0: + return + + return True + + # region Utilities + def _OnUpdateAttempts(self, account_id: int, attempts: int): + locked = False + limit = int(os.getenv('USER_LIMIT_ATTEMPTS')) + + if attempts > limit: + attempts = limit + locked = True + + return self.repo.RefreshAttempts(account_id, attempts, locked) + + def _OnCheckTokenAutoRefresh(self, expiration: int): + limit_minutes_left = 10 + now = datetime.now() + expires_at = datetime.fromtimestamp(expiration) + difference = ((expires_at - now).total_seconds()) / 60 # seconds to minutes + return difference <= limit_minutes_left + + def _OnRefreshToken(self, account_id: int, token: str, payload: dict): + old_disabled = self.repo.DisableToken(token) + if old_disabled is None: + return + + new_token = self.token_service.GenerateToken(payload) + + new_registered = self.repo.RegisterSession(account_id, new_token) + if new_registered is None: + return + + return new_token + + # endregion diff --git a/backend/code/modules/sessions/repository.py b/backend/code/modules/sessions/repository.py new file mode 100644 index 0000000..4b94e18 --- /dev/null +++ b/backend/code/modules/sessions/repository.py @@ -0,0 +1,89 @@ +from backend.code.modules.database.database import DatabaseModule + +class SessionRepository(): + def __init__(self, repo: DatabaseModule): + self.conn = repo.conn + self.cursor = repo.cursor + + def SelectByUsername(self, username: str): + sql = ''' + SELECT a.*, u.name, u.surnames, u.email + FROM user_accounts a + JOIN users u ON u.id = a.user_id + WHERE a.username = ? + AND a.active + ''' + self.cursor.execute(sql, [username]) + return self.cursor.fetchone() + + def SelectByToken(self, token: str): + sql = ''' + SELECT uas.account_id, u.id + FROM user_account_sessions uas + JOIN user_accounts ua ON uas.account_id = ua.id + JOIN users u ON ua.user_id = u.id + WHERE token = ? + AND uas.active + ''' + self.cursor.execute(sql, [token]) + return self.cursor.fetchone() + + def RefreshAttempts(self, account_id: int, attempts: int, locked: bool): + sql = ''' + UPDATE user_accounts + SET attempts = ?, locked = ? + WHERE id = ? + ''' + self.cursor.execute(sql, [attempts, locked, account_id]) + self.conn.commit() + return self.cursor.rowcount + + def RegisterSession(self, accountId: int, token: str): + sql = ''' + INSERT INTO user_account_sessions(account_id, token) + VALUES (?, ?) + ''' + self.cursor.execute(sql, [accountId, token]) + self.conn.commit() + return self.cursor.rowcount + + def DisableToken(self, token: str): + sql = ''' + UPDATE user_account_sessions + SET active = false + WHERE token = ? + ''' + self.cursor.execute(sql, [token]) + self.conn.commit() + return self.cursor.rowcount + + def InsertRecoverRequest(self, accountId: int, token: str): + sql = ''' + INSERT INTO user_account_recover_requests(account_id, token) + VALUES(?, ?) + ''' + self.cursor.execute(sql, [accountId, token]) + self.conn.commit() + return self.cursor.rowcount + + def SelectRecoverRequest(self, accountId: int, token: str): + sql = ''' + SELECT * + FROM user_account_recover_requests + WHERE account_id = ? + AND token = ? + AND active + ''' + self.cursor.execute(sql, [accountId, token]) + return self.cursor.fetchone() + + def DisableRecoverRequest(self, token: str): + sql = ''' + UPDATE user_account_recover_requests + SET active = false + WHERE token = ? + AND active + ''' + self.cursor.execute(sql, [token]) + self.conn.commit() + self.cursor.rowcount \ No newline at end of file diff --git a/backend/code/modules/sessions/router.py b/backend/code/modules/sessions/router.py new file mode 100644 index 0000000..8f51aec --- /dev/null +++ b/backend/code/modules/sessions/router.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, status, Depends, HTTPException, Header +from backend.code.middlewares.private_middleware import PrivateMiddleware +from backend.code.models.requests.account_login import AccountLoginRequest +from backend.code.models.requests.account_recover import AccountRecoverRequest +from backend.code.models.responses.session_token import SessionTokenResponse +from backend.code.modules.sessions.controller import SessionsController + +routes = APIRouter(prefix="/account", tags=['Session']) +middleware = PrivateMiddleware() +controller = SessionsController() + +@routes.post('/login', status_code = status.HTTP_200_OK, response_model = SessionTokenResponse) +def Login(data: AccountLoginRequest): + session = controller.Login(data.Username, data.Password, data.Domain) + if type(session) is not SessionTokenResponse: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + + return session + +@routes.post('/token/validate', status_code = status.HTTP_200_OK, response_model = SessionTokenResponse) +def ValidateToken(token: str = Depends(middleware.HasToken)): + validation = controller.ValidateToken(token) + if type(validation) is not SessionTokenResponse: + raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED) + + return validation + +@routes.post('/logout', status_code = status.HTTP_204_NO_CONTENT) +def Logout(token: str = Depends(middleware.HasToken)): + closed = controller.Logout(token) + if closed is None: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + +@routes.post('/recover', status_code = status.HTTP_200_OK) +def RecoverAccount(data: AccountRecoverRequest): + recovered = controller.RecoverAccount(data.Username, data.Domain) + if recovered is None: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + + +@routes.post("/recover/validate/{token}", status_code=status.HTTP_204_NO_CONTENT) +def ValidateTokenRecoverAccount(token: str): + account = controller.ValidateTokenRecoverAccount(token) + if account is None: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) diff --git a/backend/code/modules/settlements/controller.py b/backend/code/modules/settlements/controller.py new file mode 100644 index 0000000..bf33367 --- /dev/null +++ b/backend/code/modules/settlements/controller.py @@ -0,0 +1,91 @@ +from backend.code.enums.errors_enum import ErrorTypeEnum +from backend.code.core.controller import Controller +from backend.code.enums.errors_enum import ErrorTypeEnum +from backend.code.modules.database.database_sqlite import DatabaseSqliteModule +from backend.code.modules.settlements.repository import SettlementRepository +from backend.code.models.responses.settlement_detail import SettlementDetailResponse + +class SettlementController(Controller): + def __init__(self): + database = DatabaseSqliteModule() + self.repo = SettlementRepository(database) + + def ListSettlements(self, params: str = None): + result = [] + + if params is not None: + params = self._OnProcessParams(params) + + settlements = self.repo.FilterSettlements(params) + if len(settlements) == 0: + return result + + for _settlement in settlements: + settlement = SettlementDetailResponse( + Id = _settlement[0], + Name = _settlement[1], + Address = _settlement[2], + LastDone = _settlement[4], + LastDoneBy = _settlement[5], + Active = _settlement[6] + ) + result.append(settlement) + + return result + + def SelectSettlement(self, id: int): + settlement = self.repo.SelectSettlementById(id) + if settlement is None: + return + + return SettlementDetailResponse( + Id = settlement[0], + Name = settlement[1], + Address = settlement[2], + LastDone = settlement[4], + LastDoneBy = settlement[5], + Active = settlement[6] + ) + + def RegisterSettlement(self, name: str, address: str, active: bool = True): + exists = self.repo.FilterSettlements({'name': name, 'address': address}) + if len(exists) > 0: + return ErrorTypeEnum.SETTLEMENT_EXISTS + + settlement_id = self.repo.InsertSettlement(name, address, active) + if settlement_id is None: + return + + return SettlementDetailResponse( + Id = settlement_id, + Name = name, + Address = address, + LastDone = None, + LastDoneBy = None, + Active = active + ) + + def EditSettlement(self, id: int, name: str, address: str, active: bool = True): + exists = self.repo.SelectSettlementById(id) + if exists is None: + return ErrorTypeEnum.SETTLEMENT_ID_NOT_EXISTS + + edited = self.repo.UpdateSettlement(id, name, address, active) + if edited == 0: + return + + return True + + def RegisterSettlementDone(self, id: int, doneAt: datetime, doneBy: int): + pass + + def DeleteSettlement(self, id: int): + exists = self.repo.SelectSettlementById(id) + if exists is None: + return ErrorTypeEnum.SETTLEMENT_ID_NOT_EXISTS + + deleted = self.repo.DeleteSettlement(id) + if deleted == 0: + return + + return True \ No newline at end of file diff --git a/backend/code/modules/settlements/repository.py b/backend/code/modules/settlements/repository.py new file mode 100644 index 0000000..013861b --- /dev/null +++ b/backend/code/modules/settlements/repository.py @@ -0,0 +1,87 @@ +from backend.code.modules.database.database import DatabaseModule +from datetime import datetime + +class SettlementRepository(): + def __init__(self, repo: DatabaseModule): + self.conn = repo.conn + self.cursor = repo.cursor + + def FilterSettlements(self, filters: dict = None): + params = list() + + sql = ''' + SELECT * + FROM settlements + WHERE 1=1 + ''' + + if filters is not None: + if 'name' in filters: + sql = sql + ' AND name LIKE ?' + params.append(filters['name']) + + if 'address' in filters: + sql = sql + ' AND address LIKE ?' + params.append(filters['address']) + + if 'last_done_by' in filters: + sql = sql + ' AND last_done_by LIKE ?' + params.append(filters['last_done_by']) + + if 'last_done' in filters: + sql = sql + ' AND last_done = ?' + params.append(filters['last_done']) + + # print(sql) + + self.cursor.execute(sql, params) + return self.cursor.fetchall() + + def SelectSettlementById(self, id: int): + sql = ''' + SELECT * + FROM settlements + WHERE id = ? + AND active + ''' + self.cursor.execute(sql, [id]) + return self.cursor.fetchone() + + def InsertSettlement(self, name: str, address: str, active: bool = True): + sql = ''' + INSERT INTO settlements(name, address, active) + VALUES(?, ?, ?) + ''' + self.cursor.execute(sql, [name, address, active]) + self.conn.commit() + return self.cursor.lastrowid + + def UpdateSettlement(self, id: int, name: str, address: str, active: bool = True): + sql = ''' + UPDATE settlements + set name = ?, address = ?, active = ? + WHERE id = ? + ''' + self.cursor.execute(sql, [name, address, active, id]) + self.conn.commit() + return self.cursor.rowcount + + def RegisterSettlementDone(self, id: int, user_done: int, done_at: datetime = datetime.now()): + sql = ''' + UPDATE settlements + SET last_done_by = ?, last_done = ? + WHERE id = ? + ''' + self.cursor.execute(sql, [done_at, user_done, id]) + self.conn.commit() + return self.cursor.rowcount + + def DeleteSettlement(self, id: int): + sql = ''' + UPDATE settlements + SET active = false + WHERE id = ? + ''' + self.cursor.execute(sql, [id]) + self.conn.commit() + return self.cursor.rowcount \ No newline at end of file diff --git a/backend/code/modules/settlements/router.py b/backend/code/modules/settlements/router.py new file mode 100644 index 0000000..479c352 --- /dev/null +++ b/backend/code/modules/settlements/router.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, Query, status, Depends, Response, HTTPException + +from backend.code.modules.settlements.controller import SettlementController +from backend.code.models.responses.settlement_detail import SettlementDetailResponse +from backend.code.models.requests.settlement_register import SettlementRegisterRequest +from backend.code.models.requests.settlement_edit import SettlementEditRequest +from backend.code.models.requests.settlement_register_done import SettlementRegisterDoneRequest + +routes = APIRouter(prefix="/settlements", tags=['Settlement']) +controller = SettlementController() + +@routes.get("", status_code = status.HTTP_200_OK, response_model = list[SettlementDetailResponse]) +def ListSettlements(params: str | None = Query(default = None, alias = 'query')): + settlements = controller.ListSettlements(params) + if settlements is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if len(settlements) == 0: + return Response(status_code = status.HTTP_204_NO_CONTENT) + + return settlements + +@routes.get("/{id}", status_code = status.HTTP_200_OK, response_model = SettlementDetailResponse) +def GetSettlement(id: int): + settlement = controller.SelectSettlement(id) + if settlement is None: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + + return settlement + +@routes.post('', status_code = status.HTTP_201_CREATED, response_model = SettlementDetailResponse) +def RegisterSettlement(data: SettlementRegisterRequest): + created = controller.RegisterSettlement(data.Name, data.Address, data.Active) + if created is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if created == -9: + raise HTTPException(status_code = status.HTTP_409_CONFLICT, detail = 'Settlement name or address exists') + + return created + +@routes.put("/{id}", status_code = status.HTTP_204_NO_CONTENT) +def EditSettlement(id: str, data: SettlementEditRequest): + edited = controller.EditSettlement(id, data.Name, data.Address, data.Active) + if edited is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if deleted == -10: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + +@routes.post('/done', status_code = status.HTTP_200_OK) +def RegisterSettlementDone(data: SettlementRegisterDoneRequest): + registered = controller. + +@routes.delete("/{id}", status_code = status.HTTP_204_NO_CONTENT) +def DisableSettlement(id: str): + deleted = controller.DeleteSettlement(id) + if deleted is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if deleted == -10: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) diff --git a/backend/code/modules/users/controller.py b/backend/code/modules/users/controller.py new file mode 100644 index 0000000..3c1663d --- /dev/null +++ b/backend/code/modules/users/controller.py @@ -0,0 +1,118 @@ +from backend.code.enums.errors_enum import ErrorTypeEnum +from backend.code.modules.database.database_sqlite import DatabaseSqliteModule +from backend.code.modules.users.repository import UserRepository +from backend.code.modules.accounts.repository import AccountRepository +from backend.code.models.responses.user_detail import UserDetailResponse + +class UserController(): + def __init__(self): + database = DatabaseSqliteModule() + self.repo = UserRepository(database) + self.account_repo = AccountRepository(database) + + def ListUsers(self, params: str = None): + result = [] + + # Process filters + if params is not None: + params = self._OnProcessParams(params) + + users = self.repo.FilterUsers(params) + if len(users) == 0: + return result + + for _user in users: + user = UserDetailResponse( + Id = _user[0], + Name = _user[1], + Surnames = _user[2], + Email = _user[3], + Username = _user[4], + Locked = _user[5], + Active = _user[6] + ) + result.append(user) + + + return result + + def SelectUser(self, id: int): + user = self.repo.SelectUserById(id) + if user is None: + return + + return UserDetailResponse( + Id = user[0], + Name = user[1], + Surnames = user[2], + Email = user[3], + Username = user[4], + Locked = user[5], + Active = user[6] + ) + + def RegisterUser(self, name: str, surnames: str, email: str, username: str = None, locked: bool = False, active: bool = True): + create_account = (username is not None) + + # Check not exists + user_exists = self.repo.FilterUsers({'email': email}) + if len(user_exists) > 0: + return ErrorTypeEnum.USER_EMAIL_EXISTS + + if create_account: + account_exists = self.account_repo.SelectByUsername(username) + if account_exists is not None: + return ErrorTypeEnum.USER_ACCOUNT_EXISTS + + # Register + user_id = self.repo.InsertUser(name, surnames, email) + if user_id is None: + return + + if create_account: + account_id = self.account_repo.InsertAccount(user_id, username, locked, active) + if account_id is None: + return + + return UserDetailResponse( + Id = user_id, + Name = name, + Surnames = surnames, + Email = email, + Username = username, + Locked = locked, + Active = active + ) + + def EditUser(self, id: int, name: str, surnames: str, email: str): + exists = self.repo.SelectUserById(id) + if exists is None: + return ErrorTypeEnum.USER_ID_NOT_EXISTS + + edited = self.repo.UpdateUser(id, name, surnames, email) + if edited == 0: + return + + return True + + def DeleteUser(self, id: int): + exists = self.repo.SelectUserById(id) + if exists is None: + return ErrorTypeEnum.USER_ID_NOT_EXISTS + + deleted = self.repo.DeleteUser(id) + if deleted == 0: + return + + return True + + #region Utilities + def _OnProcessParams(self, params: str): + result = {} + params = params.strip() + params = params.split(',') + for param in params: + field,value=param.split('=') + result[field.lower()]=value + return result + #endregion \ No newline at end of file diff --git a/backend/code/modules/users/repository.py b/backend/code/modules/users/repository.py new file mode 100644 index 0000000..a094e23 --- /dev/null +++ b/backend/code/modules/users/repository.py @@ -0,0 +1,76 @@ +from backend.code.modules.database.database import DatabaseModule + +class UserRepository(): + def __init__(self, repo: DatabaseModule): + self.conn = repo.conn + self.cursor = repo.cursor + + def FilterUsers(self, filters: dict = None): + params = list() + + sql = ''' + SELECT u.*, a.username, a.locked, a.active + FROM users u + LEFT JOIN user_accounts a ON a.user_id = u.id + WHERE 1=1 + ''' + + if filters is not None: + if 'name' in filters: + sql = sql + " AND u.name LIKE ?" + params.append(filters['name']) + + if 'surnames' in filters: + sql = sql + " AND u.surnames LIKE ?" + params.append(filters['surnames']) + + if 'email' in filters: + sql = sql + " AND u.email LIKE ?" + params.append(filters['email']) + + if 'username' in filters: + sql = sql + " AND a.username LIKE ?" + params.append(filters['username']) + + # print(sql) + + self.cursor.execute(sql, params) + return self.cursor.fetchall() + + def SelectUserById(self, id: int): + sql = ''' + SELECT u.*, a.username, a.locked, a.active + FROM users u + LEFT JOIN user_accounts a ON a.user_id = u.id + WHERE u.id = ? + ''' + self.cursor.execute(sql, [id]) + return self.cursor.fetchone() + + def InsertUser(self, name: str, surnames: str, email: str): + sql = ''' + INSERT INTO users(name, surnames, email) + VALUES(?, ?, ?) + ''' + self.cursor.execute(sql, [name, surnames, email]) + self.conn.commit() + return self.cursor.lastrowid + + def UpdateUser(self, id: int, name: str, surnames: str, email: str): + sql = ''' + UPDATE users + SET name = ?, surnames = ?, email = ? + WHERE id = ? + ''' + self.cursor.execute(sql, [name, surnames, email, id]) + self.conn.commit() + return self.cursor.rowcount + + def DeleteUser(self, id: int): + sql = ''' + DELETE FROM users + WHERE id = ? + ''' + self.cursor.execute(sql, [id]) + self.conn.commit() + return self.cursor.rowcount \ No newline at end of file diff --git a/backend/code/modules/users/router.py b/backend/code/modules/users/router.py new file mode 100644 index 0000000..89e5153 --- /dev/null +++ b/backend/code/modules/users/router.py @@ -0,0 +1,60 @@ +from fastapi import Query, APIRouter, status, Depends, Response, HTTPException +from backend.code.modules.users.controller import UserController + +from backend.code.models.requests.user_register import UserRegisterRequest +from backend.code.models.requests.user_edit import UserEditRequest +from backend.code.models.responses.user_detail import UserDetailResponse +from backend.code.models.responses.user_created import UserCreatedResponse + +routes = APIRouter(prefix="/users", tags=["User"]) +controller = UserController() + +@routes.get('', status_code = status.HTTP_200_OK, response_model = list[UserDetailResponse]) +def ListUsers(params: str | None = Query(default = None, alias = "query")): + users = controller.ListUsers(params) + if users is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if len(users) == 0: + return Response(status_code = status.HTTP_204_NO_CONTENT) + + return users + +@routes.get('/{id}', status_code = status.HTTP_200_OK, response_model = UserDetailResponse) +def GetUser(id: int): + user = controller.SelectUser(id) + if user is None: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + + return user + +@routes.post('', status_code = status.HTTP_201_CREATED, response_model = UserCreatedResponse) +def RegisterUser(data: UserRegisterRequest): + created = controller.RegisterUser(data.Name, data.Surnames, data.Email, data.Username, data.Locked, data.Active) + if created is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if created == -1: + raise HTTPException(status_code = status.HTTP_409_CONFLICT, detail = 'Email exists') + elif created == -2: + raise HTTPException(status_code = status.HTTP_409_CONFLICT, detail = 'Username exists') + + return created + +@routes.put('/{id}', status_code = status.HTTP_204_NO_CONTENT) +def EditUser(id: int, data: UserEditRequest): + edited = controller.EditUser(id, data.Name, data.Surnames, data.Email) + if edited is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if deleted == -3: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + +@routes.delete('/{id}', status_code = status.HTTP_204_NO_CONTENT) +def DeleteUser(id: int): + deleted = controller.DeleteUser(id) + if deleted is None: + raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST) + + if deleted == -3: + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) \ No newline at end of file diff --git a/backend/code/services/environment.py b/backend/code/services/environment.py new file mode 100644 index 0000000..6a3b9a3 --- /dev/null +++ b/backend/code/services/environment.py @@ -0,0 +1,20 @@ +import os + +class EnvironmentService: + def __init__(self): + self.LINE_COMMENTED = '#' + self.VALUE_SEPARATOR = '=' + + def LoadEnvFile(self, filepath:str): + with open(filepath, 'r') as file: + for line in file: + line = line.strip() + if line and not line.startswith(self.LINE_COMMENTED) and self.VALUE_SEPARATOR in line: + key,value = line.split(self.VALUE_SEPARATOR, 1) + self.Set(key, value) # os.environ[key] = value + + def Get(self, key:str): + return os.getenv(key) + + def Set(self, key: str, value): + os.environ[key] = value \ No newline at end of file diff --git a/backend/code/services/mailing.py b/backend/code/services/mailing.py new file mode 100644 index 0000000..15b4de0 --- /dev/null +++ b/backend/code/services/mailing.py @@ -0,0 +1,27 @@ +import os +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +# https://realpython.com/python-send-email/ +class MailingService: + def __init__(self): + self.server = os.getenv('MAILING_SMTP_SERVER') + self.port = os.getenv('MAILING_SMTP_PORT') + self.user = os.getenv('MAILING_SMTP_USER') + self.password = os.getenv('MAILING_SMTP_PASSWORD') + + def SendMessage(self, address: str, subject: str, content: str): + try: + message = MIMEMultipart() + message['From'] = self.user + message['To'] = address + message['Subject'] = subject + message.attach(MIMEText(content, 'plain')) + + with smtplib.SMTP_SSL(self.server, self.port) as server: + server.login(self.user, self.password) + server.send_message(message) + server.quit() + except Exception as e: + print(e) \ No newline at end of file diff --git a/backend/code/services/token.py b/backend/code/services/token.py new file mode 100644 index 0000000..4352c8f --- /dev/null +++ b/backend/code/services/token.py @@ -0,0 +1,22 @@ +import os +import jwt +from datetime import datetime, timedelta + +class TokenService: + def __init__(self): + self.SECRET = os.getenv('TOKEN_SECRET') + self.ALGORITHM = os.getenv('TOKEN_ALGORITHM') + self.LIVE_HOUR_MINUTES = os.getenv('TOKEN_LIVE') + + def GenerateToken(self, payload): + payload['exp'] = datetime.now() + timedelta(minutes = int(self.LIVE_HOUR_MINUTES)) + return jwt.encode(payload, self.SECRET, self.ALGORITHM) + + def ValidateToken(self, token: str): + try: + if os.getenv('ENVIRONMENT') == 'Development' and token == "1": + return True + + return jwt.decode(token, self.SECRET, algorithms = self.ALGORITHM) + except jwt.InvalidTokenError as e: + return False \ No newline at end of file diff --git a/backend/requeriments.txt b/backend/requeriments.txt new file mode 100644 index 0000000..c3bd094 --- /dev/null +++ b/backend/requeriments.txt @@ -0,0 +1,62 @@ +agent-detector==2.0.0 +annotated-doc==0.0.5 +annotated-types==0.8.0 +anyio==4.15.1 +certifi==2026.10.23 +charset-normalizer==3.5.2 +click==8.5.0 +colorama==0.4.6 +detect-installer==0.2.1 +dnspython==2.9.0 +email-validator==2.3.0 +fastapi==0.162.0 +fastapi-cli==0.0.32 +fastapi-cloud-cli==0.26.0 +fastar==0.12.0 +googleapis-common-protos==1.76.0 +h11==0.16.0 +httpcore==1.0.9 +httpcore2==2.16.0 +httptools==0.9.0 +httpx==0.28.1 +httpx2==2.16.0 +idna==3.20 +Jinja2==3.1.6 +markdown-it-py==4.2.0 +MarkupSafe==3.0.4 +mdurl==0.1.2 +opentelemetry-api==1.45.1 +opentelemetry-exporter-http-transport==0.66b1 +opentelemetry-exporter-otlp-common==0.66b1 +opentelemetry-exporter-otlp-proto-common==1.45.1 +opentelemetry-exporter-otlp-proto-http==1.45.1 +opentelemetry-proto==1.45.1 +opentelemetry-sdk==1.45.1 +opentelemetry-semantic-conventions==0.66b1 +protobuf==7.36.2 +pydantic==2.14.1 +pydantic-extra-types==2.11.1 +pydantic-settings==2.16.1 +pydantic_core==2.50.1 +Pygments==2.21.0 +PyJWT==2.15.1 +pysqlite3==0.6.1 +python-dotenv==1.2.4 +python-multipart==0.0.32 +PyYAML==6.0.3 +requests==2.34.2 +rich==15.0.0 +rich-toolkit==0.20.6 +rignore==0.8.1 +sentry-sdk==2.73.0 +shellingham==1.5.4 +starlette==1.10.0 +truststore==0.10.5 +typer==0.27.3 +typing-inspection==0.4.4 +typing_extensions==4.16.0 +urllib3==2.8.0 +uvicorn==0.54.0 +uvloop==0.23.0 +watchfiles==1.3.0 +websockets==17.3 diff --git a/database/1_schema_sqlite.sql b/database/1_schema_sqlite.sql new file mode 100644 index 0000000..7bd988c --- /dev/null +++ b/database/1_schema_sqlite.sql @@ -0,0 +1,127 @@ +-- drop database if exists settlementsint +-- create database settlements; + +create table users ( + id integer primary key autoincrement, + name varchar, + surnames varchar, + email varchar +); + +create table user_accounts ( + id integer primary key autoincrement, + user_id bigint references users (id) unique, + username varchar unique not null, + password varchar default null, + attempts smallint default 0, + locked bool default false, + active bool default true +); + +create table permissions ( + id integer primary key autoincrement, + code varchar unique not null, + description varchar default null +); + +create table groups ( + id integer primary key autoincrement, + code varchar unique not null, + description varchar default null +); + +create table user_account_groups ( + account_id bigint references user_accounts (id), + group_id bigint references groups (id) +); + +create table user_account_permissions ( + account_id bigint references user_accounts (id), + permission_id bigint references permissions (id) +); + +create table group_permissions ( + group_id bigint references groups (id), + permission_id bigint references permissions (id) +); + +create table user_account_sessions ( + id integer primary key autoincrement, + account_id bigint not null references user_accounts (id), + token varchar not null unique, + created_at timestamp default current_timestamp, + active bool default true +); + +create table user_account_recover_requests ( + id integer primary key autoincrement, + account_id bigint not null references user_accounts (id), + token varchar not null unique, + created_at timestamp default current_timestamp, + active bool default true +); + +create table settlements ( + id integer primary key autoincrement, + name varchar not null unique, + address varchar not null unique, + created_at timestamp not null default current_timestamp, + last_done timestamp default null, + last_done_by bigint references users (id), + active bool default true +); + +create table animal_coats ( + id integer primary key autoincrement, + code varchar unique not null, + description varchar default null +); + +create table animals ( + id integer primary key autoincrement, + name varchar not null, + microchip varchar default null, + coat bigint not null references animal_coats (id), + sex int not null check(sex in ('female', 'male', 'unknown')) default 'unknown', + settlement_id bigint references settlements (id), + has_ear_mark bool default false, + controlled bool default false, + castrated bool default false, + adopted bool default false, + friendly bool default null, + friendly_children bool default null, + friendly_dogs bool default null, + first_view timestamp default null, + last_view timestamp default null, + created_at timestamp not null default current_timestamp, + active bool default true +); + +create table animal_comments ( + id integer primary key autoincrement, + animal_id bigint not null references animals (id), + _type int not null check(_type in ('info', 'alert')) default 'info', + message varchar not null, + created_at timestamp default current_timestamp, + public bool default false, + active bool default true +); + +create table animal_alerts ( + id integer primary key autoincrement, + animal_id bigint not null references animals (id), + _type int not null check(_type in ('info', 'low', 'medium', 'high', 'ultra')) default 'high', + comment varchar not null, + created_at timestamp default current_timestamp, + active bool default true +); + +create table animal_images ( + id integer primary key autoincrement, + animal_id bigint not null references animals (id), + image varchar not null unique, + created_at timestamp default current_timestamp, + is_primary bool default false, + public bool default false, + active bool default true +); diff --git a/database/2_population_sqlite.sql b/database/2_population_sqlite.sql new file mode 100644 index 0000000..cc8dc0b --- /dev/null +++ b/database/2_population_sqlite.sql @@ -0,0 +1,82 @@ +insert into + animal_coats (code) +values + ('NEGRO'), + ('NARANJA'), + ('BLANCO'), + ('CHOCOLATE'); + +insert into + animal_coats (code, description) +values + ('CAREY', 'Mezcla irregular de negro y rojo'), + ('AZUL', 'Gris diluido'), + ('CANELA', 'Marrón claro con toque dorado'), + ('LILA', 'Crema con tono lavanda'), + ('CREMA', 'Naranja pálido'), + ('CALICO', 'Combina blanco, negro y naranja'), + ('ATIGRADO', 'Con rayas o espirales definidas'), + ( + 'COLORPOINT', + 'Cuerpo claro con extremidades oscuras' + ), + ( + 'AHUMADO', + 'Pelaje blanco en la base con puntas de color' + ); + +insert into + permissions (code) +values + ('LOGIN'), + ('USERS_ADMINISTRATE'), + ('USERS_CREATE'), + ('USERS_EDIT'), + ('USERS_DISABLE'), + ('USERS_FORCE_RESET_PASSWORD'), + ('ANIMALS_ADMINISTRATE'), + ('ANIMALS_CREATE'), + ('ANIMALS_EDIT'), + ('ANIMALS_DISABLE'), + ('ANIMALS_SET_PUBLIC'), + ('ANIMAL_IMAGES_ADMINISTRATE'), + ('ANIMAL_IMAGES_UPLOAD'), + ('ANIMAL_IMAGES_EDIT'), + ('ANIMAL_IMAGES_DISABLE'), + ('ANIMAL_IMAGES_SET_PUBLIC'), + ('ANIMAL_COMMENTS_ADMINISTRATE'), + ('ANIMAL_COMMENTS_CREATE'), + ('ANIMAL_COMMENTS_EDIT'), + ('ANIMAL_COMMENTS_DISABLE'), + ('ANIMAL_COMMENTS_SET_PUBLIC'), + ('ANIMAL_ALERTS_ADMINISTRATE'), + ('ANIMAL_ALERTS_CREATE'), + ('ANIMAL_ALERTS_EDIT'), + ('ANIMAL_ALERTS_DISABLE'), + ('SETTLEMENTS_ADMINISTRATE'), + ('SETTLEMENTS_CREATE'), + ('SETTLEMENTS_EDIT'), + ('SETTLEMENTS_DISABLE'); + +insert into + groups (code) +values + ('ADMINISTRATORS'), + ('WORKERS'), + ('VOLUNTEERS'), + ('ADOPTERS'); + +insert into + users (id, name, surnames, email) +values + ( + 1, + 'Albert', + 'Gutiérrez Muñoz', + 'albertgmu@gmail.com' + ); + +insert into + user_accounts (user_id, username, password) +values + (1, 'devadmin', 'fe452152faec60d73c3f00c3bb985fe9'); \ No newline at end of file diff --git a/database/settlements.db b/database/settlements.db new file mode 100644 index 0000000000000000000000000000000000000000..988a6a5e9b49a185b118b78e124cd72f6d368e79 GIT binary patch literal 118784 zcmeI*eQX@-;d!W&HJV_?8 zXWW@_Qb)j%-0q3fA>c2BfPWUjX;(;xc>4#?!Lrf`gkX8qEn47?`(y7A=s@W9PGUjg z8ISGE)qC#=XqwH=lRUzap!q{ai$+k%7zqNQniv`1S8BThGm&g2ZId5 zyhQ)>(m#y{p)XvG4f@R5ULW-GCFa)p7u;rH@_||AqV;r8ail{40uX=z1Rwwb2tWV= z5P$##AaMKw7l@A@lqGL74xs^ZsmrZMRZBmWmVD5czSQG1zm2g&rwF_ z+BvS9UDnh}S$B~Z+JdsAm1Jwj-cu}hjTIERBouj36%5^HuK#o74Tk&``2+fd2LvDh z0SG_<0uX=z1Rwwb2tWV=AEyA}JPg~-c)Z?L-OyxZ`E(0SxWafC#yzJN=@f#X$pfs1 z@%E-Y9&bTf7YZd=S&{~$;ZQWZBuL`WKw&r{MwSK!#7I6L8;LGSv3A-gj{Fxx{+E1@ zKH&iY2tWV=5P$##AOHafKmY;|fWUJg;O1Dil{5c#w{V=-Yu*39%aHfTcgefYK?l(V z2tWV=5P$##AOHafKmY;|fWRk9pq>68fE6mh`M4`=Gf1GNgX?X^2bl5yy}o{iyib4r z|1ZcNlGn-45QRJ>_eg~F_&@OftN+jaU-tikU-9SsBmN%W2flZGZ~ETw{hY7tTkwt2 zUGabb1Rwwb2tWV=5P$##An<$%1bSUegqcpx%uFWJ$;`|&KbD$~k4`4fUUD%v9T~~7 z)Xe!F7ZY-%CUVL6O!Cr27xS_sJwBFBWm3~KIhxzu?P7+RcqWyOPxA4}WNxRL^B2qm zIeIulLyRaxquy zNoIwoII{`XA*|}ox0pS!QtXv^Sr-#vCbQ$IjJf{b>li=oKbOa*lA9L_!y z&~b*lQEyyxBB1>g{fa%S!PC6id+%wkznNh5+OyLP=PW>**^GlH0b0B0*Zo=9b#u3$ zyIM~%>vEZu+h+hcv*CLu09rg;z+1cjH`o7NiD60(NEqxDKv|txo{7baDM&=l#FA{?D8`OdTi#0SG_<0uX=z1Rwwb2tWV= z5P-l*6~O)fNj-CDHv}L60SG_<0uX=z1Rwwb2teTQ0=WM_ycJ4900Izz00bZa0SG_< z0uX=z1Wu}edH?Tj|0d)AAK!O7ztiz;`VtQaKmY;|fB*y_009V`V1aGU-Ntx7$45L; zbz&tyF3MSXA~n023T5P!o>FpAF_9Wdt(6z%5)-jow9uRwo~usJKajJlNj==mxuH}t6k519yOADW ze4LM{=011x^LNX6v$E1%eX%rGEsPh8!tAJGrAJru;Q_N;QMwm5>yBh+(&6;#ayT9|MzqJXBbkTV zU?!c7Rc;p7(@uqfkvrk&HpiWCg=%&z7rQlIel${^UayQTm6gr4x#3YQJiTCyrjy3_ zgenyHm4S_LC_TRvt}gFV7`!t$xXrbkaD~U|)s@(-$jqEkRb%VfQ1zj%t!U|~`vVIP z3xyG7+!!eHBhit#IGf1kO7qEG3gJ5gBikH%vK1Z-W;W>sYd9y*s1pzKv&Q0Pd3Yr^ zD?eD~C#351qAtWHMstH3siB1j((1h-)viDZhVMkf=J)@(J038kt25s5z<<>jI>DEE zG#vsEfB*y_009U<00KX3fi1=LIiCH*Of|Sqc7KmNso*-<{XO#Z0e7EX??l)0}}i}K`# zJQ~eD8Z1u;vCSoAWM;OxP8$i*u!X^4`v2A$Gk)V3TP4!y656VJHy5gUOsPywtR|3@?uI z^Q-yA@y8>p`5|?FF%=t(MCOJTa_dofc|p&L_wMRD@eMVraQIG?w#tleIL=muGpjV} zV(IR~hn4(Dw6d5Up1Hf0*T)`B4G!rGktu0t_~Bw^ZgKfRY&dy;RnT`?h3*UtZk;jX z4UV%_)wF4q6{C5+oLOC3OO!U^`pt#$wc*);rHA)oBhpwtE#zf`9~WlJv9RcD6%7rF z4%gQIeV=Ct=YPfTCfoES9uR;41Rwwb2tWV=5P$##An=h1{FH0!vxkluIrixW_i@m7D-2?Xv~t3UEgLCa6lbARY~gQuK(@yJsGKJr@(M-mMA8rh^zct8LG5P$## zAOHafKmY;|fB*y_u)jc*^DtL#M;OL?x1z{mr6_3eby-=i6olZ6oReHR(<8<2&8FjH*(s?g ztV+RrQBXuRC@I0B5X=j@6ckk@Sgt6Bpa%=)R#DX=oi66;d?GuU%}r%fnVCdI(#mR3 zr1ACiZ(3e@Y-m!6#@81lJ*etpMHBQJ9WJK#etaf1o}<+zXemvo(u2~<^s=l8MOw5V zEy)VqP491aG2QnQ@yS$z7A@svg|?>Fv=@Vlv`nk11{FaQlvUw^*Tn>GBy!1gJY%H< z%Wu6_lnd%*kBd1Mn@ml{6SOr#tz|TLU!`4J7nFiF7{bDF_p<(l&eZDyJ| zVbEGDX3i0~&B*`e6oAVtYs~;~X65w>fEEuI@Ye4C&Gmogl&y()2>}Q|00Izz00bZa z0SG_<0uX?}b0^?yGZ@kuWJr-*^iBJ}=Ks}B*>|bqmpXr>{a@NUymM`DdplZ#ZH8;g z{crBy;RIKJ$&=I19RoT90SG_<0=opZACR`d_&EE@RYS-ZrHU?Tyda7y4b|f{NmSSA zwkFXSHr>#ln0bm$V$+zk;7ojUG8ueg(cty=AbrS%psX0uvZMvenp_gJYH&@e28D{D z%8E#1?Igu`u~xFtAi76hUZy(*6_x&}6pKMkT9PzL5oy?;y-q!NT`u%ndmHK+4MbcQ zG;xK7)HU~{-{niYWfh^d35HalzZf}a$R*lWp;R`*2!%?~2-3JiT7$tia~h2@Yx?W2cpkno{>lgU=Nvn_D@HoJYMb{c3TX{F4UC9Nb| zr(jQl&%wVZyH0PV?~HQW;I3uaNA*r^{JuNv*B4Xj)ObWP(I+QbBI3FjS+sfceC5QJ0nQ9N_ZXZYwUHzOQxvO*x|-C&$`q#FMZbaiw0GTnp`&M zC8Jreamh0;t?m6cysd#B?LO!YV>OHqSWN8ArbM z97q46ZLNXS3kUVj);d09+wg4P<=O&$ee5gu_Y6A6wO4mJdTSqvbkra5eAf2f_l<7m zSnr~s{UgmxR)o@120>ZS^$k@k>^lPLpL2#GmCA-5)JsCKXny!MM#+Fxps0##68&Vh ze-JJSMZNynDu@StPR87=fpp(N{thob?AUCLxemXp;~K8J=|{20{5YO!X${=C!7kQL zV*TSlH~*z${$s-WbZBgHjuW{n^SY`0A}!j#)y1|20s(fLZ4QjufY4hU``1s{5eI<% zTw3mWrFMe##<{an>5nML#YR$lKi2<`>z`gY4g??o0SG_<0uX=z1Rwwb2teT374Wrw zo#}8V8S)$cgum1G556m%?|HxF5n8|HPPYE~v)gX83<3~V$VpmY; zYITvPe=SS8W8;!zqSujiOo*d>l&yA7-^%*7?RF{XrA` z(?AiOUILDh`9r^jUj2#lZGoO1_KR8DS*Yvlfqm4Ez5;1qfNG9hYebb~ zQB%bgx!j~^ED@Ru$@NjE(%_mWMDNt!*CH)uzntmP%!lA!T4P-*SJiyZyhGDA~~#V4$XiUR#* z-Db4Wfd`>q6;^&y{*oq3N}*VF9$<&Mk zr_v(r*3(Pvd+lk4WHm<3kt42#&$R~bUpr_e**>(IA0V}Xc77{zwk>e&8oS+Zd*AT2 zJo{ef_IU?crH*{jI+nnNGfxU!unk{6Y}+^+J$=~SLblz`Z1Y{U$c|UtH6vS=?d55Q zkS*iLK>J>29RGe13UpBG&2{hQ#mG_U2O4R$!CX)k>FGn>EXC*Pd(jF^+!Z2ZzknS4 z8M=6?HBcHqX!zPAANIs#?ut;`9kSVl_5Ww$4g!sU00bZa0SG_<0uX=z1Rwwb2z;ah zxc~o1d2w(EKmY;|fB*y_009U<00Izzz%wCW{{BCF|Nk>_D$ocBKmY;|fB*y_009U< M00Izzz(*$V|9*lXR{#J2 literal 0 HcmV?d00001 diff --git a/database/update_from_sqlite__1_schema.sql b/database/update_from_sqlite__1_schema.sql new file mode 100644 index 0000000..2ebeb10 --- /dev/null +++ b/database/update_from_sqlite__1_schema.sql @@ -0,0 +1,170 @@ +-- drop database if exists settlements; +-- create database settlements; +drop type if exists alert_types cascade; + +create type alert_types as enum ('info', 'low', 'medium', 'high', 'ultra'); + +drop type if exists message_types cascade; + +create type message_types as enum ('info', 'alert'); + +drop type if exists animal_sexs cascade; + +create type animal_sexs as enum ('female', 'male', 'unknown'); + +drop table if exists users cascade; + +drop table if exists user_accounts cascade; + +drop table if exists permissions cascade; + +drop table if exists groups cascade; + +drop table if exists user_groups cascade; + +drop table if exists user_permissions cascade; + +drop table if exists group_permissions cascade; + +drop table if exists user_groups cascade; + +drop table if exists user_sessions cascade; + +drop table if exists user_recover_requests cascade; + +drop table if exists settlements cascade; + +drop table if exists animals cascade; + +drop table if exists animal_comments cascade; + +drop table if exists animal_alerts cascade; + +drop table if exists animal_images cascade; + +drop table if exists animal_coats cascade; + +create table users ( + id bigint generated by default as identity primary key, + name varchar, + surnames varchar, + email varchar +); + +create table user_accounts ( + id bigint generated by default as identity primary key, + user_id bigint references users (id), + username varchar unique not null, + password varchar not null, + attempts smallint default 0, + locked bool default false, + active bool default true +); + +create table permissions ( + id bigint generated by default as identity primary key, + code varchar unique not null, + description varchar default null +); + +create table groups ( + id bigint generated by default as identity primary key, + code varchar unique not null, + description varchar default null +); + +create table user_groups ( + user_id bigint references users (id), + group_id bigint references groups (id) +); + +create table user_permissions ( + user_id bigint references users (id), + permission_id bigint references permissions (id) +); + +create table group_permissions ( + group_id bigint references groups (id), + permission_id bigint references permissions (id) +); + +create table user_sessions ( + id bigint generated by default as identity primary key, + user_id bigint not null references users (id), + token varchar not null unique, + created_at timestamp default current_timestamp, + active bool default true +); + +create table user_recover_requests ( + id bigint generated by default as identity primary key, + user_id bigint not null references users (id), + token varchar not null unique, + created_at timestamp default current_timestamp, + active bool default true +); + +create table settlements ( + id bigint generated by default as identity primary key, + name varchar not null unique, + address varchar not null unique, + created_at timestamp not null default current_timestamp, + last_done timestamp default null, + last_done_by bigint references users (id), + active bool default true +); + +create table animal_coats ( + id bigint generated by default as identity primary key, + code varchar unique not null, + description varchar default null +); + +create table animals ( + id bigint generated by default as identity primary key, + name varchar not null, + microchip varchar default null, + coat bigint not null references animal_coats (id), + sex animal_sexs default 'unknown', + settlement_id bigint references settlements (id), + has_ear_mark bool default false, + controlled bool default false, + castrated bool default false, + adopted bool default false, + friendly bool default null, + friendly_children bool default null, + friendly_dogs bool default null, + first_view timestamp default null, + last_view timestamp default null, + created_at timestamp not null default current_timestamp, + active bool default true +); + +create table animal_comments ( + id bigint generated by default as identity primary key, + animal_id bigint not null references animals (id), + type message_types default 'info', + message varchar not null, + created_at timestamp default current_timestamp, + public bool default false, + active bool default true +); + +create table animal_alerts ( + id bigint generated by default as identity primary key, + animal_id bigint not null references animals (id), + type alert_types default 'high', + comment varchar not null, + created_at timestamp default current_timestamp, + active bool default true +); + +create table animal_images ( + id bigint generated by default as identity primary key, + animal_id bigint not null references animals (id), + image varchar not null unique, + created_at timestamp default current_timestamp, + is_primary bool default false, + public bool default false, + active bool default true +); diff --git a/database/update_from_sqlite__2_population.sql b/database/update_from_sqlite__2_population.sql new file mode 100644 index 0000000..c30b0bc --- /dev/null +++ b/database/update_from_sqlite__2_population.sql @@ -0,0 +1,92 @@ +truncate table animal_coats cascade; + +truncate table permissions cascade; + +truncate table groups cascade; + +truncate table users cascade; + +truncate table user_accounts cascade; + +insert into + animal_coats (code) +values + ('NEGRO'), + ('NARANJA'), + ('BLANCO'), + ('CHOCOLATE'); + +insert into + animal_coats (code, description) +values + ('CAREY', 'Mezcla irregular de negro y rojo'), + ('AZUL', 'Gris diluido'), + ('CANELA', 'Marrón claro con toque dorado'), + ('LILA', 'Crema con tono lavanda'), + ('CREMA', 'Naranja pálido'), + ('CALICO', 'Combina blanco, negro y naranja'), + ('ATIGRADO', 'Con rayas o espirales definidas'), + ( + 'COLORPOINT', + 'Cuerpo claro con extremidades oscuras' + ), + ( + 'AHUMADO', + 'Pelaje blanco en la base con puntas de color' + ); + +insert into + permissions (code) +values + ('LOGIN'), + ('USERS_ADMINISTRATE'), + ('USERS_CREATE'), + ('USERS_EDIT'), + ('USERS_DISABLE'), + ('USERS_FORCE_RESET_PASSWORD'), + ('ANIMALS_ADMINISTRATE'), + ('ANIMALS_CREATE'), + ('ANIMALS_EDIT'), + ('ANIMALS_DISABLE'), + ('ANIMALS_SET_PUBLIC'), + ('ANIMAL_IMAGES_ADMINISTRATE'), + ('ANIMAL_IMAGES_UPLOAD'), + ('ANIMAL_IMAGES_EDIT'), + ('ANIMAL_IMAGES_DISABLE'), + ('ANIMAL_IMAGES_SET_PUBLIC'), + ('ANIMAL_COMMENTS_ADMINISTRATE'), + ('ANIMAL_COMMENTS_CREATE'), + ('ANIMAL_COMMENTS_EDIT'), + ('ANIMAL_COMMENTS_DISABLE'), + ('ANIMAL_COMMENTS_SET_PUBLIC'), + ('ANIMAL_ALERTS_ADMINISTRATE'), + ('ANIMAL_ALERTS_CREATE'), + ('ANIMAL_ALERTS_EDIT'), + ('ANIMAL_ALERTS_DISABLE'), + ('SETTLEMENTS_ADMINISTRATE'), + ('SETTLEMENTS_CREATE'), + ('SETTLEMENTS_EDIT'), + ('SETTLEMENTS_DISABLE'); + +insert into + groups (code) +values + ('ADMINISTRATORS'), + ('WORKERS'), + ('VOLUNTEERS'), + ('ADOPTERS'); + +insert into + users (id, name, surnames, email) +values + ( + 1, + 'Albert', + 'Gutiérrez Muñoz', + 'albertgmu@gmail.com' + ); + +insert into + user_accounts (user_id, username, password) +values + (1, 'devadmin', encode (sha256 (''), 'hex')); \ No newline at end of file