diff --git a/README.md b/README.md index af4fcfe..9ce3c4b 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,8 @@ has been built. Local seeded admin: -- email: `admin@visione.local` -- password: `ChangeMe123!` -- the account has `must_change_password = true` and is for local development only. +- email: `admin@visione.gov.it` +- access uses OTP through Mailpit and is for local development only. If you already had a MySQL volume from the old scaffold, create the local database/user once: @@ -55,9 +54,9 @@ docker compose exec -T mysql mysql -uroot -proot -e \ ```bash curl http://localhost:5555/api/v1/health curl http://localhost:5555/api/v1/ready -curl -X POST http://localhost:5555/api/v1/auth/login/password \ +curl -X POST http://localhost:5555/api/v1/auth/otp/request \ -H 'Content-Type: application/json' \ - -d '{"email":"admin@visione.local","password":"ChangeMe123!"}' + -d '{"email":"admin@visione.gov.it"}' ``` Use `-DUSE_SYSTEM_DROGON=ON` only if Drogon is already installed and diff --git a/common/Auth.cc b/common/Auth.cc index 272ffa2..a79800c 100644 --- a/common/Auth.cc +++ b/common/Auth.cc @@ -45,7 +45,7 @@ std::optional authenticate(const drogon::HttpRequestPtr &request) auto db = drogon::app().getDbClient(); const auto userResult = db->execSqlSync( - "SELECT u.id, u.email, u.must_change_password " + "SELECT u.id, u.email " "FROM api_tokens t " "JOIN users u ON u.id = t.user_id " "WHERE t.token_hash = ? AND t.revoked_at IS NULL " @@ -93,7 +93,6 @@ std::optional authenticate(const drogon::HttpRequestPtr &request) context.roles = std::move(roles); context.permissions = std::move(permissions); context.role = primaryRole(context.roles); - context.mustChangePassword = userResult[0]["must_change_password"].as(); return context; } @@ -122,9 +121,4 @@ bool isAdmin(const UserContext &user) { return hasRole(user, "admin") || hasPermission(user, "admin.manage"); } - -bool mustChangePassword(const UserContext &user) -{ - return user.mustChangePassword; -} } // namespace visione::auth diff --git a/common/Auth.h b/common/Auth.h index dcfb530..e61b1b9 100644 --- a/common/Auth.h +++ b/common/Auth.h @@ -15,7 +15,6 @@ struct UserContext std::string role; std::vector roles; std::vector permissions; - bool mustChangePassword{false}; }; std::optional bearerToken(const drogon::HttpRequestPtr &request); @@ -25,5 +24,4 @@ bool hasPermission(const UserContext &user, const std::string &permission); bool canManageReports(const UserContext &user); bool canOperateReports(const UserContext &user); bool isAdmin(const UserContext &user); -bool mustChangePassword(const UserContext &user); } // namespace visione::auth diff --git a/common/Crypto.cc b/common/Crypto.cc index 89afdc9..3cf0e44 100644 --- a/common/Crypto.cc +++ b/common/Crypto.cc @@ -1,10 +1,6 @@ #include "common/Crypto.h" #include - -#include - -#include #include #include #include @@ -14,9 +10,6 @@ namespace visione::crypto { namespace { -constexpr int passwordIterations = 210000; -constexpr size_t passwordHashBytes = 32; - std::string toHex(const unsigned char *data, size_t size) { std::ostringstream stream; @@ -27,22 +20,6 @@ std::string toHex(const unsigned char *data, size_t size) } return stream.str(); } - -std::string pbkdf2(const std::string &password, - const std::string &salt, - int iterations) -{ - std::array output{}; - PKCS5_PBKDF2_HMAC(password.c_str(), - static_cast(password.size()), - reinterpret_cast(salt.data()), - static_cast(salt.size()), - iterations, - EVP_sha256(), - static_cast(output.size()), - output.data()); - return toHex(output.data(), output.size()); -} } // namespace std::string sha256(const std::string &value) @@ -70,42 +47,6 @@ std::string randomToken() return drogon::utils::getUuid() + drogon::utils::getUuid(); } -std::string hashPassword(const std::string &password) -{ - const auto salt = drogon::utils::getUuid() + drogon::utils::getUuid(); - return "pbkdf2_sha256$" + std::to_string(passwordIterations) + "$" + salt + - "$" + pbkdf2(password, salt, passwordIterations); -} - -bool verifyPassword(const std::string &password, const std::string &hash) -{ - constexpr std::string_view pbkdf2Prefix = "pbkdf2_sha256$"; - - if (!hash.starts_with(pbkdf2Prefix)) - { - return false; - } - - const auto iterationsStart = pbkdf2Prefix.size(); - const auto saltStart = hash.find('$', iterationsStart); - if (saltStart == std::string::npos) - { - return false; - } - const auto digestStart = hash.find('$', saltStart + 1); - if (digestStart == std::string::npos) - { - return false; - } - - const auto iterations = - std::stoi(hash.substr(iterationsStart, saltStart - iterationsStart)); - const auto salt = hash.substr(saltStart + 1, digestStart - saltStart - 1); - const auto expected = "pbkdf2_sha256$" + std::to_string(iterations) + "$" + - salt + "$" + pbkdf2(password, salt, iterations); - return expected == hash; -} - std::string hashSecret(const std::string &secret) { return sha256("visione:" + secret); diff --git a/common/Crypto.h b/common/Crypto.h index f184d8c..3382009 100644 --- a/common/Crypto.h +++ b/common/Crypto.h @@ -7,8 +7,5 @@ namespace visione::crypto std::string sha256(const std::string &value); std::string randomNumericCode(int digits); std::string randomToken(); -std::string hashPassword(const std::string &password); -bool verifyPassword(const std::string &password, const std::string &hash); std::string hashSecret(const std::string &secret); } // namespace visione::crypto - diff --git a/controllers/AdminController.cc b/controllers/AdminController.cc index 3acf93d..86e0caf 100644 --- a/controllers/AdminController.cc +++ b/controllers/AdminController.cc @@ -33,19 +33,6 @@ std::optional requireUser( return user; } -bool blockPasswordChangeRequired( - const visione::auth::UserContext &user, - std::function &callback) -{ - if (!visione::auth::mustChangePassword(user)) - { - return false; - } - callback(visione::http::error("password_change_required", - drogon::k403Forbidden)); - return true; -} - void sendAdmin(Json::Value body, std::function &&callback) { @@ -86,7 +73,7 @@ void AdminController::createCategory( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -100,7 +87,7 @@ void AdminController::createArea( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -114,7 +101,7 @@ void AdminController::createTeam( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -129,7 +116,7 @@ void AdminController::updateCategory( long long categoryId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -144,7 +131,7 @@ void AdminController::deleteCategory( long long categoryId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -158,7 +145,7 @@ void AdminController::updateArea( long long areaId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -173,7 +160,7 @@ void AdminController::deleteArea( long long areaId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -187,7 +174,7 @@ void AdminController::updateTeam( long long teamId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -202,7 +189,7 @@ void AdminController::deleteTeam( long long teamId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -215,7 +202,7 @@ void AdminController::overviewStats( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -228,7 +215,7 @@ void AdminController::listModeration( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -246,7 +233,7 @@ void AdminController::updateModeration( long long flagId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -260,7 +247,7 @@ void AdminController::listUsers( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -274,7 +261,7 @@ void AdminController::updateUserRoles( long long userId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -289,7 +276,7 @@ void AdminController::updateUserBan( long long userId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } diff --git a/controllers/AuthController.cc b/controllers/AuthController.cc index 3a4a077..762548c 100644 --- a/controllers/AuthController.cc +++ b/controllers/AuthController.cc @@ -66,43 +66,6 @@ void AuthController::verifyOtp( serviceResponse(service.verifyOtp(body), std::move(callback)); } -void AuthController::loginWithPassword( - const drogon::HttpRequestPtr &req, - std::function &&callback) -{ - const auto body = visione::http::requireJson(req); - if (body.isNull()) - { - callback(visione::http::error("invalid_json", drogon::k400BadRequest)); - return; - } - - visione::services::AuthService service; - serviceResponse(service.loginWithPassword(body), std::move(callback)); -} - -void AuthController::changePassword( - const drogon::HttpRequestPtr &req, - std::function &&callback) -{ - const auto user = visione::auth::authenticate(req); - if (!user) - { - callback(visione::http::error("unauthorized", drogon::k401Unauthorized)); - return; - } - - const auto body = visione::http::requireJson(req); - if (body.isNull()) - { - callback(visione::http::error("invalid_json", drogon::k400BadRequest)); - return; - } - - visione::services::AuthService service; - serviceResponse(service.changePassword(*user, body), std::move(callback)); -} - void AuthController::me( const drogon::HttpRequestPtr &req, std::function &&callback) diff --git a/controllers/AuthController.h b/controllers/AuthController.h index 725b661..e4d2ca8 100644 --- a/controllers/AuthController.h +++ b/controllers/AuthController.h @@ -15,12 +15,6 @@ class AuthController : public drogon::HttpController ADD_METHOD_TO(AuthController::verifyOtp, "/api/v1/auth/otp/verify", drogon::Post); - ADD_METHOD_TO(AuthController::loginWithPassword, - "/api/v1/auth/login/password", - drogon::Post); - ADD_METHOD_TO(AuthController::changePassword, - "/api/v1/auth/change-password", - drogon::Post); ADD_METHOD_TO(AuthController::me, "/api/v1/me", drogon::Get); ADD_METHOD_TO(AuthController::editUser, "/api/v1/me", drogon::Patch); METHOD_LIST_END @@ -34,12 +28,6 @@ class AuthController : public drogon::HttpController void verifyOtp( const drogon::HttpRequestPtr &req, std::function &&callback); - void loginWithPassword( - const drogon::HttpRequestPtr &req, - std::function &&callback); - void changePassword( - const drogon::HttpRequestPtr &req, - std::function &&callback); void me(const drogon::HttpRequestPtr &req, std::function &&callback); void editUser(const drogon::HttpRequestPtr &req, diff --git a/controllers/ReportController.cc b/controllers/ReportController.cc index c4a5c16..739e2c8 100644 --- a/controllers/ReportController.cc +++ b/controllers/ReportController.cc @@ -36,19 +36,6 @@ std::optional requireUser( return user; } -bool blockPasswordChangeRequired( - const visione::auth::UserContext &user, - std::function &callback) -{ - if (!visione::auth::mustChangePassword(user)) - { - return false; - } - callback(visione::http::error("password_change_required", - drogon::k403Forbidden)); - return true; -} - void sendService(Json::Value body, std::function &&callback) { @@ -119,7 +106,7 @@ void ReportController::create( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -139,7 +126,7 @@ void ReportController::list( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -157,7 +144,7 @@ void ReportController::listCategories( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -170,7 +157,7 @@ void ReportController::listTeams( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -183,7 +170,7 @@ void ReportController::assignedReports( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -199,7 +186,7 @@ void ReportController::openReports( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -215,7 +202,7 @@ void ReportController::otherOperatorReports( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -232,7 +219,7 @@ void ReportController::detail( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -246,7 +233,7 @@ void ReportController::interventionSummary( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -260,7 +247,7 @@ void ReportController::teamSuggestions( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -274,7 +261,7 @@ void ReportController::createInterventionLink( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -288,7 +275,7 @@ void ReportController::revokeInterventionLink( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -302,7 +289,7 @@ void ReportController::updateStatus( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -317,7 +304,7 @@ void ReportController::takeReport( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -331,7 +318,7 @@ void ReportController::addMessage( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -346,7 +333,7 @@ void ReportController::listMessages( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -361,7 +348,7 @@ void ReportController::flagMessage( long long messageId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -377,7 +364,7 @@ void ReportController::assignTeam( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -392,7 +379,7 @@ void ReportController::addFeedback( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -407,7 +394,7 @@ void ReportController::flagReport( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -422,7 +409,7 @@ void ReportController::uploadMedia( long long reportId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } diff --git a/controllers/UserController.cc b/controllers/UserController.cc index ec79b95..92b1803 100644 --- a/controllers/UserController.cc +++ b/controllers/UserController.cc @@ -32,19 +32,6 @@ std::optional requireUser( } return user; } - -bool blockPasswordChangeRequired( - const visione::auth::UserContext &user, - std::function &callback) -{ - if (!visione::auth::mustChangePassword(user)) - { - return false; - } - callback(visione::http::error("password_change_required", - drogon::k403Forbidden)); - return true; -} } // namespace void UserController::listNotifications( @@ -52,7 +39,7 @@ void UserController::listNotifications( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -68,7 +55,7 @@ void UserController::markNotificationRead( long long notificationId) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -84,7 +71,7 @@ void UserController::markAllNotificationsRead( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } @@ -97,7 +84,7 @@ void UserController::rewards( std::function &&callback) { auto user = requireUser(req, callback); - if (!user || blockPasswordChangeRequired(*user, callback)) + if (!user) { return; } diff --git a/docs/API_SPEC.md b/docs/API_SPEC.md index b50b98d..aec34c5 100644 --- a/docs/API_SPEC.md +++ b/docs/API_SPEC.md @@ -30,15 +30,11 @@ Authorization: Bearer ## Auth + Profile - `POST /auth/register` - citizen registration + citizen registration without password - `POST /auth/otp/request` sends OTP through Mailpit - `POST /auth/otp/verify` consumes OTP, returns bearer token -- `POST /auth/login/password` - password fallback login -- `POST /auth/change-password` - authenticated password change, also clears `must_change_password` - `GET /me` current user profile, roles, permissions, points - `PATCH /me` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 09a297b..4620b4d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -25,7 +25,6 @@ generate public read-only intervention links. ## Runtime Model - Auth uses bearer tokens from `api_tokens`. - OTP login uses `otp_challenges` and Mailpit in local dev. -- Password hash uses PBKDF2-HMAC-SHA256. Legacy `sha256$` hashes rejected. - DB access uses raw parameterized SQL through `drogon::orm::DbClient`. - No Drogon generated ORM models. No `Mapper`. @@ -37,7 +36,6 @@ generate public read-only intervention links. Effective permission helpers: - `auth::canManageReports(user)`: admin or `reports.manage` - `auth::canOperateReports(user)`: non-admin report manager -- `auth::mustChangePassword(user)`: blocks protected operational/admin flows ## Workflow Ownership Rules - `reports.assigned_operator_id` is single platform owner of report. diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 03c061a..c252896 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -14,7 +14,7 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB ## Core Auth Tables - `users` - identity, profile, password hash, points, `banned`, `must_change_password` + identity, profile, points, `banned` - `roles` - `permissions` - `role_permissions` diff --git a/docs/LLM_CONTEXT.md b/docs/LLM_CONTEXT.md index ae612e8..57fbb84 100644 --- a/docs/LLM_CONTEXT.md +++ b/docs/LLM_CONTEXT.md @@ -29,12 +29,10 @@ public revocable link generated by operator/admin. ## Current Auth Model - Registration creates `citizen`. -- Login paths: +- Login path: - OTP email via Mailpit - - password fallback - Auth token: bearer token from `api_tokens` - Banned users cannot authenticate. -- `must_change_password` blocks most protected operator/admin flows until changed. ## Current Report Workflow ### Citizen flow diff --git a/docs/PROJECT_SPEC.md b/docs/PROJECT_SPEC.md index f00ad1d..d037204 100644 --- a/docs/PROJECT_SPEC.md +++ b/docs/PROJECT_SPEC.md @@ -29,9 +29,7 @@ moderate abuse, and inspect operational stats. ### Auth - citizen registration - OTP login through Mailpit -- password login fallback - bearer token auth -- forced first password change for seed admin - profile read + limited self-edit (`first_name`, `last_name`, `phone`) ### Reports diff --git a/docs/TRACCIA.md b/docs/TRACCIA.md index df926b2..6d49437 100644 --- a/docs/TRACCIA.md +++ b/docs/TRACCIA.md @@ -17,9 +17,9 @@ Dopodichè ho elaborato una serie di feature che il sistema deve implementare an Feature Segnalazioni urbane – Lo sviluppo del della webapp avverrà su una istanza self hosted di Gitea come gesto di ribellione nei confronti di github che userà i dati sulle repo per trainare i modelli di IA (lo faceva già prima ma ora lo dice) – Il progetto sarà diviso tra frontend e backend. Frontend in probabilmente Vue.js e backend in C++, nello specifico Dragon è stato scelto come framework. Abbiamo scelto di sviluppare una webapp perchè il punto importante di un progetto del genere è la versatilità, la compatibilità su tutti i sistemi e la velocità. -– Deve esistere un account amministratore generato dal sistema, appena ci si logga la prima volta ci chiederà di cambiare password, e questo profilo lo dovrà teoricamente avere solo chi il tecnico che gestisce la piattaforma di quel comune, dato che da questo profilo si può fare tutto. E’ diverso da un account operatore perchè l’operatore ha meno permessi rispetto ad un admin, l’admin può decidere chi sono gli operatori. (NOTA: eventualmente ci possono essere i permessi customizzabili, quindi l’admin può creare anche altri utenti che hanno permessi in più) +– Deve esistere un account amministratore generato dal sistema. Questo profilo lo dovrà teoricamente avere solo chi il tecnico che gestisce la piattaforma di quel comune, dato che da questo profilo si può fare tutto. E’ diverso da un account operatore perchè l’operatore ha meno permessi rispetto ad un admin, l’admin può decidere chi sono gli operatori. (NOTA: eventualmente ci possono essere i permessi customizzabili, quindi l’admin può creare anche altri utenti che hanno permessi in più) – Deve essere possibile registrarsi alla piattaforma con tutti i quanti i dati anagrafici possibili e immaginabili, sarebbe figo avere un sistema che dato il codice fiscale ti auto completa i dati deducibili da quest’ultimo. Integrazione frontend con google maps per l’indirizzo. Deve essere possibile anche collegare CIE. Nella registrazione si richiede verifica a due fattori -– Il login deve essere effettuato con email o CIE. Nel caso di email la modalità di default non richiede una password ma un OTP, in quanto dimostrato essere più sicuro. (La password è comunque obbligatoria da settare) +– Il login deve essere effettuato con email o CIE. Nel caso di email la modalità di default usa OTP, in quanto dimostrato essere più sicuro. – Una volta registrati, se siamo un operatore (comunale, poliziotto, pompiere o chiunque) sarà un admin o un’altro operatore con abbastanza permessi ad assegnarci a quel ruolo e sbloccare i permessi per accedere ad altre parti della piattaforma. – Admin/operatori vari devono poter creare le varie zone in base al perimetro, se questo è fattibile tramite una mappa sarebbe l’ideale, assegnare i vari nomi ecc.. Questo serve sia per le segnalazioni e per l’assegnazione dell’agente/squadra che deve intervenire sia per fini statistici – Al sito non si registrano solo gli agenti che intervengono o gli operatori ma anche i cittadini, che possono effettuare delle segnalazioni. Le segnalazioni devono avere una foto, la posizione con coordinate geografiche e via con numero. Il sistema automaticamente capisce a che zona del comune fa parte @@ -69,7 +69,6 @@ erDiagram string nome string cognome string email UK - string password_hash string codice_fiscale UK date data_nascita string luogo_nascita @@ -165,4 +164,4 @@ erDiagram int utente_id FK text testo datetime data_invio - } \ No newline at end of file + } diff --git a/docs/openapi/visione.openapi.yaml b/docs/openapi/visione.openapi.yaml index 79bdad4..4cfd28a 100644 --- a/docs/openapi/visione.openapi.yaml +++ b/docs/openapi/visione.openapi.yaml @@ -66,9 +66,6 @@ "last_name": { "type": "string" }, - "must_change_password": { - "type": "boolean" - }, "phone": { "type": "string" }, @@ -191,25 +188,6 @@ ], "type": "object" }, - "ChangePasswordRequest": { - "additionalProperties": false, - "properties": { - "current_password": { - "format": "password", - "type": "string" - }, - "new_password": { - "format": "password", - "minLength": 10, - "type": "string" - } - }, - "required": [ - "current_password", - "new_password" - ], - "type": "object" - }, "CreateAreaRequest": { "additionalProperties": false, "properties": { @@ -347,9 +325,6 @@ "last_name": { "type": "string" }, - "must_change_password": { - "type": "boolean" - }, "permissions": { "items": { "type": "string" @@ -379,7 +354,6 @@ "email", "fiscal_code", "points", - "must_change_password", "role", "roles", "permissions" @@ -1126,24 +1100,6 @@ ], "type": "object" }, - "PasswordLoginRequest": { - "additionalProperties": false, - "properties": { - "email": { - "format": "email", - "type": "string" - }, - "password": { - "format": "password", - "type": "string" - } - }, - "required": [ - "email", - "password" - ], - "type": "object" - }, "ReadyResponse": { "additionalProperties": false, "properties": { @@ -1207,11 +1163,6 @@ "last_name": { "type": "string" }, - "password": { - "format": "password", - "minLength": 10, - "type": "string" - }, "phone": { "type": "string" } @@ -1220,7 +1171,6 @@ "first_name", "last_name", "email", - "password", "fiscal_code" ], "type": "object" @@ -2434,89 +2384,6 @@ ] } }, - "/api/v1/auth/change-password": { - "post": { - "description": "Changes the authenticated user's password.", - "operationId": "AuthController.changePassword", - "parameters": [ ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChangePasswordRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OkResponse" - } - } - }, - "description": "Password change result." - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - }, - "security": [ - { - "bearerAuth": [ ] - } - ], - "summary": "Change password", - "tags": [ - "Auth" - ] - } - }, - "/api/v1/auth/login/password": { - "post": { - "description": "Password-based login fallback for local development.", - "operationId": "AuthController.loginWithPassword", - "parameters": [ ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PasswordLoginRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TokenResponse" - } - } - }, - "description": "Bearer token when credentials are valid." - }, - "400": { - "$ref": "#/components/responses/BadRequest" - } - }, - "summary": "Password login", - "tags": [ - "Auth" - ] - } - }, "/api/v1/auth/otp/request": { "post": { "description": "Sends a one-time code by email through Mailpit locally.", @@ -2591,7 +2458,7 @@ }, "/api/v1/auth/register": { "post": { - "description": "Creates a citizen account with personal data and password.", + "description": "Creates a citizen account with personal data.", "operationId": "AuthController.registerCitizen", "parameters": [ ], "requestBody": { diff --git a/migrations/001_initial_schema.sql b/migrations/001_initial_schema.sql index 5d6fcb3..47d21c0 100644 --- a/migrations/001_initial_schema.sql +++ b/migrations/001_initial_schema.sql @@ -3,14 +3,12 @@ CREATE TABLE IF NOT EXISTS users ( first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL, - password_hash VARCHAR(255) NOT NULL, fiscal_code VARCHAR(16) NOT NULL, birth_date DATE NULL, birth_place VARCHAR(150) NULL, phone VARCHAR(30) NULL, points INT NOT NULL DEFAULT 0, banned BOOLEAN NOT NULL DEFAULT FALSE, - must_change_password BOOLEAN NOT NULL DEFAULT FALSE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY users_email_unique (email), @@ -223,18 +221,14 @@ INSERT IGNORE INTO users ( first_name, last_name, email, - password_hash, fiscal_code, - phone, - must_change_password + phone ) VALUES ( 'Gennario', 'Vessio', 'admin@visione.gov.it', - 'pbkdf2_sha256$210000$a2c43ef09bd24ea883023e5f57fa6255d8dd317e312a40ddaa1bd3ccd83dbfa5$26D0C9000B603A424C7FB0DA3BE0CDC1798A9CBA34F391F9B8E4C0E1E4A58806', 'VSSGNR94M08A662P', - '380 6180216', - TRUE + '380 6180216' ); INSERT IGNORE INTO user_roles (user_id, role_id) diff --git a/migrations/002_seed_admin_password.sql b/migrations/002_seed_admin_password.sql index 8d26a9c..86b7d9e 100644 --- a/migrations/002_seed_admin_password.sql +++ b/migrations/002_seed_admin_password.sql @@ -3,7 +3,5 @@ SET email = 'admin@visione.gov.it', first_name = 'Gennario', last_name = 'Vessio', fiscal_code = 'VSSGNR94M08A662P', - phone = '380 6180216', - password_hash = 'pbkdf2_sha256$210000$a2c43ef09bd24ea883023e5f57fa6255d8dd317e312a40ddaa1bd3ccd83dbfa5$26D0C9000B603A424C7FB0DA3BE0CDC1798A9CBA34F391F9B8E4C0E1E4A58806', - must_change_password = TRUE + phone = '380 6180216' WHERE email IN ('admin@visione.local', 'admin@visione.gov.it'); diff --git a/migrations/003_admin_pbkdf2_password.sql b/migrations/003_admin_pbkdf2_password.sql index 8d26a9c..86b7d9e 100644 --- a/migrations/003_admin_pbkdf2_password.sql +++ b/migrations/003_admin_pbkdf2_password.sql @@ -3,7 +3,5 @@ SET email = 'admin@visione.gov.it', first_name = 'Gennario', last_name = 'Vessio', fiscal_code = 'VSSGNR94M08A662P', - phone = '380 6180216', - password_hash = 'pbkdf2_sha256$210000$a2c43ef09bd24ea883023e5f57fa6255d8dd317e312a40ddaa1bd3ccd83dbfa5$26D0C9000B603A424C7FB0DA3BE0CDC1798A9CBA34F391F9B8E4C0E1E4A58806', - must_change_password = TRUE + phone = '380 6180216' WHERE email IN ('admin@visione.local', 'admin@visione.gov.it'); diff --git a/migrations/010_drop_password_auth.sql b/migrations/010_drop_password_auth.sql new file mode 100644 index 0000000..957f2ff --- /dev/null +++ b/migrations/010_drop_password_auth.sql @@ -0,0 +1,3 @@ +ALTER TABLE users + DROP COLUMN password_hash, + DROP COLUMN must_change_password; diff --git a/services/AdminService.cc b/services/AdminService.cc index ba72bff..57fe6b4 100644 --- a/services/AdminService.cc +++ b/services/AdminService.cc @@ -80,8 +80,7 @@ const JsonRowSchema &adminUserSchema() static const JsonRowSchema schema{ {"id", JsonFieldType::UInt64}, {"points", JsonFieldType::Int64}, - {"banned", JsonFieldType::Bool}, - {"must_change_password", JsonFieldType::Bool}}; + {"banned", JsonFieldType::Bool}}; return schema; } @@ -591,13 +590,13 @@ Json::Value AdminService::listUsers(const auth::UserContext &user) response["users"] = repositories::resultToJson( drogon::app().getDbClient()->execSqlSync( "SELECT u.id, u.first_name, u.last_name, u.email, u.fiscal_code, " - "u.phone, u.points, u.banned, u.must_change_password, " + "u.phone, u.points, u.banned, " "GROUP_CONCAT(r.name ORDER BY r.name SEPARATOR ',') AS roles " "FROM users u " "LEFT JOIN user_roles ur ON ur.user_id = u.id " "LEFT JOIN roles r ON r.id = ur.role_id " "GROUP BY u.id, u.first_name, u.last_name, u.email, u.fiscal_code, " - "u.phone, u.points, u.banned, u.must_change_password " + "u.phone, u.points, u.banned " "ORDER BY u.created_at DESC LIMIT 200"), adminUserSchema()); return response; diff --git a/services/AuthService.cc b/services/AuthService.cc index 1ed92c9..f01262b 100644 --- a/services/AuthService.cc +++ b/services/AuthService.cc @@ -1,9 +1,10 @@ #include "services/AuthService.h" -#include "common/Crypto.h" #include "common/Email.h" #include "common/Validation.h" +#include "common/Crypto.h" + #include namespace visione::services @@ -62,7 +63,7 @@ Json::Value issueToken(long long userId) Json::Value AuthService::registerCitizen(const Json::Value &body) { const auto missing = validation::missingStrings( - body, {"first_name", "last_name", "email", "password", "fiscal_code"}); + body, {"first_name", "last_name", "email", "fiscal_code"}); if (!missing.empty()) { auto response = failure("missing_fields"); @@ -96,12 +97,11 @@ Json::Value AuthService::registerCitizen(const Json::Value &body) db->execSqlSync( "INSERT INTO users " - "(first_name, last_name, email, password_hash, fiscal_code, birth_date, " - "birth_place, phone) VALUES (?, ?, ?, ?, ?, NULLIF(?, ''), ?, ?)", + "(first_name, last_name, email, fiscal_code, birth_date, " + "birth_place, phone) VALUES (?, ?, ?, ?, NULLIF(?, ''), ?, ?)", body["first_name"].asString(), body["last_name"].asString(), email, - crypto::hashPassword(body["password"].asString()), fiscalCode, body.get("birth_date", "").asString(), body.get("birth_place", "").asString(), @@ -183,68 +183,12 @@ Json::Value AuthService::verifyOtp(const Json::Value &body) return issueToken(challenge[0]["user_id"].as()); } -Json::Value AuthService::loginWithPassword(const Json::Value &body) -{ - const auto missing = validation::missingStrings(body, {"email", "password"}); - if (!missing.empty()) - { - return failure("missing_fields"); - } - - auto db = drogon::app().getDbClient(); - const auto user = db->execSqlSync( - "SELECT id, password_hash FROM users WHERE email = ? AND banned = FALSE " - "LIMIT 1", - body["email"].asString()); - if (user.empty() || - !crypto::verifyPassword(body["password"].asString(), - user[0]["password_hash"].as())) - { - return failure("invalid_credentials"); - } - return issueToken(user[0]["id"].as()); -} - -Json::Value AuthService::changePassword(const auth::UserContext &user, - const Json::Value &body) -{ - const auto missing = - validation::missingStrings(body, {"current_password", "new_password"}); - if (!missing.empty()) - { - return failure("missing_fields"); - } - if (body["new_password"].asString().size() < 10) - { - return failure("weak_password"); - } - - auto db = drogon::app().getDbClient(); - const auto current = db->execSqlSync( - "SELECT password_hash FROM users WHERE id = ? AND banned = FALSE", - user.id); - if (current.empty() || - !crypto::verifyPassword( - body["current_password"].asString(), - current[0]["password_hash"].as())) - { - return failure("invalid_credentials"); - } - - db->execSqlSync( - "UPDATE users SET password_hash = ?, must_change_password = FALSE " - "WHERE id = ?", - crypto::hashPassword(body["new_password"].asString()), - user.id); - return success(); -} - Json::Value AuthService::currentUser(const auth::UserContext &user) { auto db = drogon::app().getDbClient(); const auto result = db->execSqlSync( - "SELECT id, first_name, last_name, email, fiscal_code, phone, points, " - "must_change_password FROM users WHERE id = ?", + "SELECT id, first_name, last_name, email, fiscal_code, phone, points " + "FROM users WHERE id = ?", user.id); auto response = success(); @@ -257,8 +201,6 @@ Json::Value AuthService::currentUser(const auth::UserContext &user) response["user"]["fiscal_code"] = result[0]["fiscal_code"].as(); response["user"]["phone"] = result[0]["phone"].as(); response["user"]["points"] = result[0]["points"].as(); - response["user"]["must_change_password"] = - result[0]["must_change_password"].as(); response["user"]["role"] = user.role; for (const auto &role : user.roles) { diff --git a/services/AuthService.h b/services/AuthService.h index 852e347..695f3c2 100644 --- a/services/AuthService.h +++ b/services/AuthService.h @@ -12,8 +12,6 @@ class AuthService Json::Value registerCitizen(const Json::Value &body); Json::Value requestOtp(const Json::Value &body); Json::Value verifyOtp(const Json::Value &body); - Json::Value loginWithPassword(const Json::Value &body); - Json::Value changePassword(const auth::UserContext &user, const Json::Value &body); Json::Value currentUser(const auth::UserContext &user); Json::Value editUser(const auth::UserContext & user, Json::Value::const_iterator::reference body); }; diff --git a/test/test_main.cc b/test/test_main.cc index c2ce70a..8b56bfb 100644 --- a/test/test_main.cc +++ b/test/test_main.cc @@ -65,7 +65,6 @@ DROGON_TEST(SqlScalarSerializationKeepsTypes) Json::Value value(Json::objectValue); value["id"] = Json::UInt64(7); value["banned"] = false; - value["must_change_password"] = true; value["points"] = Json::Int64(42); value["phone"] = "380 6180216"; return value; @@ -73,7 +72,6 @@ DROGON_TEST(SqlScalarSerializationKeepsTypes) CHECK(json["id"].isUInt64()); CHECK(json["banned"].isBool()); - CHECK(json["must_change_password"].isBool()); CHECK(json["points"].isInt64()); CHECK(json["phone"].isString()); } @@ -163,18 +161,6 @@ DROGON_TEST(GeoJsonAcceptsContainedPolygon) CHECK(!visione::geojson::polygonInsideGeometry(candidate, container)); } -DROGON_TEST(PasswordHashSupportsPbkdf2Only) -{ - const auto hash = visione::crypto::hashPassword("CorrectHorse123!"); - CHECK(hash.rfind("pbkdf2_sha256$", 0) == 0); - CHECK(visione::crypto::verifyPassword("CorrectHorse123!", hash)); - CHECK(!visione::crypto::verifyPassword("wrong", hash)); - - const std::string legacy = - "sha256$local-dev-salt$CD4FF6AC8C9685D08E55AF693970F15F9BFCD4C0DC9F4D3001D202C9DB00291F"; - CHECK(!visione::crypto::verifyPassword("ChangeMe123!", legacy)); -} - int main(int argc, char **argv) { std::promise startedPromise;