add user privileges & add translations

This commit is contained in:
2026-04-20 19:12:27 +03:00
parent f16a830eb2
commit fc96a95335
17 changed files with 1073 additions and 426 deletions

View File

@@ -18,6 +18,7 @@ import io.vertx.ext.web.sstore.SessionStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import su.xserver.iikocon.config.AppConfig;
import su.xserver.iikocon.handler.AdminHandler;
import su.xserver.iikocon.handler.AuthHandler;
import su.xserver.iikocon.handler.SecurityHandler;
import su.xserver.iikocon.handler.SetupHandler;
@@ -194,7 +195,50 @@ public class MainVerticle extends AbstractVerticle {
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
}));
// В initRouter после настройки authHandler, до объявления /api/admin/*:
router.route("/api/admin/profile").handler(authHandler::requireAuth);
router.get("/api/admin/profile").handler(rc -> {
Integer userId = rc.session().get("userId");
userService.getProfile(userId)
.onSuccess(profile -> rc.response().putHeader("Content-Type", "application/json").end(profile.encode()))
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.put("/api/admin/profile").handler(rc -> {
Integer userId = rc.session().get("userId");
JsonObject body = rc.body().asJsonObject();
String email = body.getString("email");
String password = body.getString("password");
String language = body.getString("language");
userService.updateProfile(userId, email, password, language)
.onSuccess(v -> {
if (language != null) rc.session().put("language", language);
rc.response().end(new JsonObject().put("success", true).encode());
})
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.put("/api/admin/language").handler(rc -> {
Integer userId = rc.session().get("userId");
JsonObject body = rc.body().asJsonObject();
String language = body.getString("language");
if (language == null || (!"en".equals(language) && !"ru".equals(language))) {
rc.response().setStatusCode(400).end("Invalid language");
return;
}
userService.updateLanguage(userId, language)
.onSuccess(v -> {
rc.session().put("language", language);
rc.response().end(new JsonObject().put("success", true).encode());
})
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
// Затем существующий блок router.route("/api/admin/*").handler(authHandler::requireAuth);
router.route("/api/admin/*").handler(authHandler::requireAuth);
// Добавить проверку роли для чувствительных эндпоинтов:
// router.route("/api/admin/users*").handler(AdminHandler::requireAdmin);
// router.route("/api/admin/restaurants*").handler(AdminHandler::requireAdmin);
// router.route("/api/admin/settings*").handler(AdminHandler::requireAdmin);
// router.route("/api/admin/active-sessions").handler(AdminHandler::requireAdmin);
router.get("/api/admin/users").handler(rc -> userService.getAllUsers().onComplete(ar -> {
if (ar.succeeded()) {
@@ -211,13 +255,14 @@ public class MainVerticle extends AbstractVerticle {
String login = body.getString("login");
String email = body.getString("email");
String password = body.getString("password");
String role = body.getString("role");
String ip = rc.request().remoteAddress().host();
if (login == null || email == null || password == null) {
rc.response().setStatusCode(400).end("Missing login, email or password");
return;
}
// Создаём активного пользователя (active = true)
userService.createUser(login, email, password, ip, true)
if (role == null || role.isEmpty()) role = "user";
userService.createUser(login, email, password, ip, true, role)
.onSuccess(v -> rc.response().setStatusCode(201).end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
@@ -228,12 +273,13 @@ public class MainVerticle extends AbstractVerticle {
String login = body.getString("login");
String email = body.getString("email");
String password = body.getString("password");
String role = body.getString("role");
String ip = rc.request().remoteAddress().host();
if (login == null || email == null) {
rc.response().setStatusCode(400).end("Missing login or email");
return;
}
userService.updateUser(id, login, email, password, ip)
userService.updateUser(id, login, email, password, ip, role)
.onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});

View File

@@ -0,0 +1,15 @@
package su.xserver.iikocon.handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
public class AdminHandler {
public static void requireAdmin(RoutingContext ctx) {
String role = ctx.session().get("role");
if (!"admin".equals(role)) {
ctx.response().setStatusCode(403).end(new JsonObject().put("error", "Admin access required").encode());
return;
}
ctx.next();
}
}

View File

@@ -53,6 +53,8 @@ public class AuthHandler {
Session session = ctx.session();
session.put("userId", user.getInteger("id"));
session.put("login", user.getString("login"));
session.put("role", user.getString("role"));
session.put("language", user.getString("language"));
ctx.response().end(new JsonObject().put("success", true).put("login", user.getString("login")).encode());
} else {
ctx.response().setStatusCode(401).end("Invalid credentials");

View File

@@ -56,7 +56,7 @@ public class SetupHandler {
if (clientIp == null) {
clientIp = ctx.request().remoteAddress().host();
}
userService.createUser(login, email, password, clientIp, true).onComplete(cr -> {
userService.createUser(login, email, password, clientIp, true, "admin").onComplete(cr -> {
if (cr.succeeded()) {
ctx.response().setStatusCode(201)
.end(new JsonObject().put("success", true).encode());

View File

@@ -28,6 +28,8 @@ public class UserService {
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
active BOOLEAN DEFAULT FALSE,
role VARCHAR(50) DEFAULT 'user',
language VARCHAR(5) DEFAULT 'en',
ip VARCHAR(45),
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
@@ -42,24 +44,28 @@ public class UserService {
.map(rows -> rows.iterator().next().getLong("cnt"));
}
public Future<Void> createUser(String login, String email, String password, String ip, boolean active) {
public Future<Void> createUser(String login, String email, String password, String ip, boolean active, String role) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt());
Map<String, Object> params = Map.of(
"login", login,
"email", email,
"password", hash,
"ip", ip,
"active", active
"active", active,
"role", role
);
return SqlTemplate.forUpdate(pool,
"INSERT INTO users (login, email, password, ip, active) VALUES (#{login}, #{email}, #{password}, #{ip}, #{active})")
"INSERT INTO users (login, email, password, ip, active, role) VALUES (#{login}, #{email}, #{password}, #{ip}, #{active}, #{role})")
.execute(params)
.mapEmpty();
}
// Существующий метод оставляем, но он будет создавать неактивного пользователя (active = false)
public Future<Void> createUser(String login, String email, String password, String ip, boolean active) {
return createUser(login, email, password, ip, active, "user");
}
public Future<Void> createUser(String login, String email, String password, String ip) {
return createUser(login, email, password, ip, false);
return createUser(login, email, password, ip, false, "user");
}
public Future<Void> setActive(int id, boolean active) {
@@ -68,7 +74,7 @@ public class UserService {
}
public Future<JsonObject> findByLoginOrEmail(String loginOrEmail) {
String sql = "SELECT id, login, email, password, active, ip, created, updated FROM users WHERE login = ? OR email = ?";
String sql = "SELECT id, login, email, password, active, role, language, ip, created, updated FROM users WHERE login = ? OR email = ?";
return pool.preparedQuery(sql)
.execute(Tuple.of(loginOrEmail, loginOrEmail))
.map(rows -> {
@@ -80,31 +86,8 @@ public class UserService {
});
}
public Future<JsonObject> findByEmail(String email) {
return SqlTemplate.forQuery(pool, "SELECT id, login, email, password, active, ip, created, updated FROM users WHERE email = #{email}")
.mapTo(this::toJson)
.execute(Map.of("email", email))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
}
public Future<JsonObject> findByLogin(String login) {
return SqlTemplate.forQuery(pool,
"SELECT id, login, password, created, updated, ip FROM users WHERE login = #{login}")
.mapTo(row -> new JsonObject()
.put("id", row.getInteger("id"))
.put("login", row.getString("login"))
.put("password", row.getString("password"))
.put("created", row.getLocalDateTime("created") != null ?
row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ?
row.getLocalDateTime("updated").toString() : null)
.put("ip", row.getString("ip")))
.execute(Collections.singletonMap("login", login))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
}
public Future<JsonArray> getAllUsers() {
return pool.query("SELECT id, login, email, active, ip, created, updated FROM users ORDER BY id")
return pool.query("SELECT id, login, email, active, role, language, ip, created, updated FROM users ORDER BY id")
.execute()
.map(rows -> {
JsonArray array = new JsonArray();
@@ -114,6 +97,8 @@ public class UserService {
.put("login", row.getString("login"))
.put("email", row.getString("email"))
.put("active", row.getBoolean("active"))
.put("role", row.getString("role"))
.put("language", row.getString("language"))
.put("ip", row.getString("ip"))
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null));
@@ -122,19 +107,23 @@ public class UserService {
});
}
public Future<Void> updateUser(int id, String login, String email, String password, String ip) {
public Future<Void> updateUser(int id, String login, String email, String password, String ip, String role) {
Map<String, Object> params = new HashMap<>();
params.put("id", id);
params.put("login", login);
params.put("email", email);
params.put("ip", ip);
if (role != null) params.put("role", role);
String sql;
if (password != null && !password.isEmpty()) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt());
params.put("password", hash);
sql = "UPDATE users SET login = #{login}, email = #{email}, password = #{password}, ip = #{ip} WHERE id = #{id}";
sql = "UPDATE users SET login = #{login}, email = #{email}, password = #{password}, ip = #{ip}"
+ (role != null ? ", role = #{role}" : "") + " WHERE id = #{id}";
} else {
sql = "UPDATE users SET login = #{login}, email = #{email}, ip = #{ip} WHERE id = #{id}";
sql = "UPDATE users SET login = #{login}, email = #{email}, ip = #{ip}"
+ (role != null ? ", role = #{role}" : "") + " WHERE id = #{id}";
}
return SqlTemplate.forUpdate(pool, sql).execute(params).mapEmpty();
}
@@ -151,6 +140,44 @@ public class UserService {
.mapEmpty();
}
public Future<JsonObject> getProfile(int userId) {
return SqlTemplate.forQuery(pool,
"SELECT id, login, email, role, language, ip, created, updated FROM users WHERE id = #{id}")
.mapTo(row -> new JsonObject()
.put("id", row.getInteger("id"))
.put("login", row.getString("login"))
.put("email", row.getString("email"))
.put("role", row.getString("role"))
.put("language", row.getString("language"))
.put("ip", row.getString("ip"))
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null))
.execute(Map.of("id", userId))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
}
public Future<Void> updateProfile(int userId, String email, String password, String language) {
Map<String, Object> params = new HashMap<>();
params.put("id", userId);
params.put("email", email);
if (language != null) params.put("language", language);
String sql;
if (password != null && !password.isEmpty()) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt());
params.put("password", hash);
sql = "UPDATE users SET email = #{email}, password = #{password}, language = COALESCE(#{language}, language) WHERE id = #{id}";
} else {
sql = "UPDATE users SET email = #{email}, language = COALESCE(#{language}, language) WHERE id = #{id}";
}
return SqlTemplate.forUpdate(pool, sql).execute(params).mapEmpty();
}
public Future<Void> updateLanguage(int userId, String language) {
return SqlTemplate.forUpdate(pool, "UPDATE users SET language = #{lang} WHERE id = #{id}")
.execute(Map.of("id", userId, "lang", language))
.mapEmpty();
}
public boolean checkPassword(String plain, String hash) {
try {
return BCrypt.checkpw(plain, hash);
@@ -164,8 +191,10 @@ public class UserService {
.put("id", row.getInteger("id"))
.put("login", row.getString("login"))
.put("email", row.getString("email"))
.put("password", row.getString("password")) // ← ДОБАВИТЬ ЭТУ СТРОКУ
.put("password", row.getString("password"))
.put("active", row.getBoolean("active"))
.put("role", row.getString("role"))
.put("language", row.getString("language"))
.put("ip", row.getString("ip"))
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null);