refractor(auth): removed password from everywhere

This commit is contained in:
gabdevele
2026-06-16 10:37:31 +02:00 Unverified
parent 4f80d7f995
commit bb7ed18539
25 changed files with 69 additions and 454 deletions
+4 -5
View File
@@ -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
+1 -7
View File
@@ -45,7 +45,7 @@ std::optional<UserContext> 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<UserContext> 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<bool>();
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
-2
View File
@@ -15,7 +15,6 @@ struct UserContext
std::string role;
std::vector<std::string> roles;
std::vector<std::string> permissions;
bool mustChangePassword{false};
};
std::optional<std::string> 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
-59
View File
@@ -1,10 +1,6 @@
#include "common/Crypto.h"
#include <drogon/utils/Utilities.h>
#include <openssl/evp.h>
#include <array>
#include <iomanip>
#include <random>
#include <sstream>
@@ -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<unsigned char, passwordHashBytes> output{};
PKCS5_PBKDF2_HMAC(password.c_str(),
static_cast<int>(password.size()),
reinterpret_cast<const unsigned char *>(salt.data()),
static_cast<int>(salt.size()),
iterations,
EVP_sha256(),
static_cast<int>(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);
-3
View File
@@ -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
+15 -28
View File
@@ -33,19 +33,6 @@ std::optional<visione::auth::UserContext> requireUser(
return user;
}
bool blockPasswordChangeRequired(
const visione::auth::UserContext &user,
std::function<void(const HttpResponsePtr &)> &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<void(const HttpResponsePtr &)> &&callback)
{
@@ -86,7 +73,7 @@ void AdminController::createCategory(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -100,7 +87,7 @@ void AdminController::createArea(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -114,7 +101,7 @@ void AdminController::createTeam(
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -228,7 +215,7 @@ void AdminController::listModeration(
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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;
}
-37
View File
@@ -66,43 +66,6 @@ void AuthController::verifyOtp(
serviceResponse(service.verifyOtp(body), std::move(callback));
}
void AuthController::loginWithPassword(
const drogon::HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&callback)
-12
View File
@@ -15,12 +15,6 @@ class AuthController : public drogon::HttpController<AuthController>
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<AuthController>
void verifyOtp(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
void loginWithPassword(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
void changePassword(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
void me(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
void editUser(const drogon::HttpRequestPtr &req,
+21 -34
View File
@@ -36,19 +36,6 @@ std::optional<visione::auth::UserContext> requireUser(
return user;
}
bool blockPasswordChangeRequired(
const visione::auth::UserContext &user,
std::function<void(const HttpResponsePtr &)> &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<void(const HttpResponsePtr &)> &&callback)
{
@@ -119,7 +106,7 @@ void ReportController::create(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -139,7 +126,7 @@ void ReportController::list(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -157,7 +144,7 @@ void ReportController::listCategories(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -170,7 +157,7 @@ void ReportController::listTeams(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -183,7 +170,7 @@ void ReportController::assignedReports(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -199,7 +186,7 @@ void ReportController::openReports(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -215,7 +202,7 @@ void ReportController::otherOperatorReports(
std::function<void(const HttpResponsePtr &)> &&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;
}
+4 -17
View File
@@ -32,19 +32,6 @@ std::optional<visione::auth::UserContext> requireUser(
}
return user;
}
bool blockPasswordChangeRequired(
const visione::auth::UserContext &user,
std::function<void(const HttpResponsePtr &)> &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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
@@ -97,7 +84,7 @@ void UserController::rewards(
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto user = requireUser(req, callback);
if (!user || blockPasswordChangeRequired(*user, callback))
if (!user)
{
return;
}
+1 -5
View File
@@ -30,15 +30,11 @@ Authorization: Bearer <token>
## 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`
-2
View File
@@ -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<T>`.
@@ -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.
+1 -1
View File
@@ -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`
+1 -3
View File
@@ -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
-2
View File
@@ -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
+3 -4
View File
@@ -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
}
}
+1 -134
View File
@@ -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": {
+2 -8
View File
@@ -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',
'[email protected]',
'pbkdf2_sha256$210000$a2c43ef09bd24ea883023e5f57fa6255d8dd317e312a40ddaa1bd3ccd83dbfa5$26D0C9000B603A424C7FB0DA3BE0CDC1798A9CBA34F391F9B8E4C0E1E4A58806',
'VSSGNR94M08A662P',
'380 6180216',
TRUE
'380 6180216'
);
INSERT IGNORE INTO user_roles (user_id, role_id)
+1 -3
View File
@@ -3,7 +3,5 @@ SET email = '[email protected]',
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 ('[email protected]', '[email protected]');
+1 -3
View File
@@ -3,7 +3,5 @@ SET email = '[email protected]',
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 ('[email protected]', '[email protected]');
+3
View File
@@ -0,0 +1,3 @@
ALTER TABLE users
DROP COLUMN password_hash,
DROP COLUMN must_change_password;
+3 -4
View File
@@ -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;
+7 -65
View File
@@ -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 <drogon/drogon.h>
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<long long>());
}
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<std::string>()))
{
return failure("invalid_credentials");
}
return issueToken(user[0]["id"].as<long long>());
}
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<std::string>()))
{
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<std::string>();
response["user"]["phone"] = result[0]["phone"].as<std::string>();
response["user"]["points"] = result[0]["points"].as<int>();
response["user"]["must_change_password"] =
result[0]["must_change_password"].as<bool>();
response["user"]["role"] = user.role;
for (const auto &role : user.roles)
{
-2
View File
@@ -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);
};
-14
View File
@@ -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<void> startedPromise;