rewritten DAO, user management
This commit is contained in:
parent
01d961539c
commit
f36ed55788
28
README.md
28
README.md
@ -33,23 +33,33 @@ PORT=8090 mvn -q compile exec:java
|
||||
|
||||
## User Management
|
||||
|
||||
To manage users (add, delete, update passwords), use the `UserManager` CLI tool:
|
||||
User management is now available via `/user` CRUD API protected by a fixed bearer token:
|
||||
- Bearer token: `MujBearer852654`
|
||||
|
||||
```bash
|
||||
# Interactive mode
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager
|
||||
# List users
|
||||
curl -H "Authorization: Bearer MujBearer852654" \
|
||||
http://localhost:8080/user
|
||||
|
||||
# Add a user
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager add username password
|
||||
# Get one user
|
||||
curl -H "Authorization: Bearer MujBearer852654" \
|
||||
"http://localhost:8080/user?username=admin"
|
||||
|
||||
# List all users
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager list
|
||||
# Create user
|
||||
curl -X POST -H "Authorization: Bearer MujBearer852654" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user","password":"pass123"}' \
|
||||
http://localhost:8080/user
|
||||
|
||||
# Update password
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager update username new_password
|
||||
curl -X PUT -H "Authorization: Bearer MujBearer852654" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user","newPassword":"newPass123"}' \
|
||||
http://localhost:8080/user
|
||||
|
||||
# Delete user
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager delete username
|
||||
curl -X DELETE -H "Authorization: Bearer MujBearer852654" \
|
||||
"http://localhost:8080/user?username=user"
|
||||
```
|
||||
|
||||
See [USERS_MANAGEMENT.md](USERS_MANAGEMENT.md) for detailed user management documentation.
|
||||
|
||||
@ -1,122 +1,85 @@
|
||||
# Správa uživatelů - Xtream Player
|
||||
# User Management - Xtream Player
|
||||
|
||||
## Přehled
|
||||
## Overview
|
||||
|
||||
Uživatelé jsou ukládáni v H2 databázi (`~/.xtream-player/users.db`). Aplikace vytvoří výchozího uživatele `admin`/`admin` při prvním spuštění, pokud žádní uživatelé neexistují.
|
||||
Users are stored in H2 database (`~/.xtream-player/users.db`).
|
||||
If no users exist, application creates default user `admin` / `admin` on startup.
|
||||
|
||||
## UserManager - Nástroj pro správu uživatelů
|
||||
## Authentication for User API
|
||||
|
||||
K přidání, smazání nebo úpravě uživatelů slouží třída `UserManager`, kterou lze spustit jako standalone aplikaci.
|
||||
User CRUD API is protected by a fixed bearer token:
|
||||
|
||||
### Spuštění interaktivního režimu
|
||||
- Header: `Authorization: Bearer MujBearer852654`
|
||||
|
||||
Without this header (or with wrong value), API returns `401 Unauthorized`.
|
||||
|
||||
## /user CRUD API
|
||||
|
||||
Base endpoint:
|
||||
|
||||
- `http://localhost:8080/user`
|
||||
|
||||
### List users
|
||||
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager
|
||||
curl -H "Authorization: Bearer MujBearer852654" \
|
||||
http://localhost:8080/user
|
||||
```
|
||||
|
||||
Zobrazí menu s následujícími možnostmi:
|
||||
1. Přidat uživatele
|
||||
2. Smazat uživatele
|
||||
3. Aktualizovat heslo
|
||||
4. Vypsat všechny uživatele
|
||||
5. Ověřit heslo
|
||||
0. Odejít
|
||||
### Get one user
|
||||
|
||||
### Použití z příkazové řádky
|
||||
|
||||
#### Přidat uživatele
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager add username password
|
||||
curl -H "Authorization: Bearer MujBearer852654" \
|
||||
"http://localhost:8080/user?username=admin"
|
||||
```
|
||||
|
||||
#### Smazat uživatele
|
||||
### Create user
|
||||
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager delete username
|
||||
curl -X POST -H "Authorization: Bearer MujBearer852654" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user","password":"pass123"}' \
|
||||
http://localhost:8080/user
|
||||
```
|
||||
|
||||
#### Aktualizovat heslo
|
||||
You can also send URL-encoded form data:
|
||||
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager update username new_password
|
||||
curl -X POST -H "Authorization: Bearer MujBearer852654" \
|
||||
-d "username=user&password=pass123" \
|
||||
http://localhost:8080/user
|
||||
```
|
||||
|
||||
#### Vypsat všechny uživatele
|
||||
### Update password
|
||||
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager list
|
||||
curl -X PUT -H "Authorization: Bearer MujBearer852654" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user","newPassword":"newPass123"}' \
|
||||
http://localhost:8080/user
|
||||
```
|
||||
|
||||
#### Ověřit heslo
|
||||
`password` is accepted as fallback key as well.
|
||||
|
||||
### Delete user
|
||||
|
||||
By query param:
|
||||
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager verify username password
|
||||
curl -X DELETE -H "Authorization: Bearer MujBearer852654" \
|
||||
"http://localhost:8080/user?username=user"
|
||||
```
|
||||
|
||||
## Příklady
|
||||
Or by JSON body:
|
||||
|
||||
### Přidať nového administrátora
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager add admin123 MySecurePassword123
|
||||
curl -X DELETE -H "Authorization: Bearer MujBearer852654" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user"}' \
|
||||
http://localhost:8080/user
|
||||
```
|
||||
|
||||
### Změnit heslo existujícího uživatele
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager update admin newPassword456
|
||||
```
|
||||
## Response Notes
|
||||
|
||||
### Zobrazit všechny uživatele
|
||||
```bash
|
||||
java -cp target/xtream-player-1.0.0.jar cz.kamma.xtreamplayer.UserManager list
|
||||
```
|
||||
|
||||
Výstup:
|
||||
```
|
||||
╔═══╦════════════════════╦═══════════════════════════════╦═══════════════════════════════╗
|
||||
║ ID║ Username ║ Created At ║ Updated At ║
|
||||
╠═══╬════════════════════╬═══════════════════════════════╬═══════════════════════════════╣
|
||||
║ 1║ admin ║ 2026-03-09 10:30:45.123 ║ 2026-03-09 10:30:45.123 ║
|
||||
║ 2║ admin123 ║ 2026-03-09 10:35:12.456 ║ 2026-03-09 10:35:12.456 ║
|
||||
╚═══╩════════════════════╩═══════════════════════════════╩═══════════════════════════════╝
|
||||
Total: 2 user(s)
|
||||
```
|
||||
|
||||
## Architektura
|
||||
|
||||
### UserStore
|
||||
- **Třída**: `cz.kamma.xtreamplayer.UserStore`
|
||||
- **Odpovědnost**: Správa perzistentního úložiště uživatelů v H2 databázi
|
||||
- **Metody**:
|
||||
- `initialize()` - Initialisace databázové tabulky
|
||||
- `createUser(username, password)` - Vytvoří nového uživatele
|
||||
- `updatePassword(username, newPassword)` - Změní heslo
|
||||
- `deleteUser(username)` - Odstraní uživatele
|
||||
- `getUser(username)` - Retrieves uživatele
|
||||
- `getAllUsers()` - Načte všechny uživatele
|
||||
- `verifyPassword(username, password)` - Ověří heslo
|
||||
- `userExists(username)` - Zkontroluje existenci
|
||||
|
||||
### UserAuthenticator
|
||||
- **Třída**: `cz.kamma.xtreamplayer.UserAuthenticator`
|
||||
- **Odpovědnost**: Správa session tokenů a přihlašování
|
||||
- **Metody**:
|
||||
- `authenticate(username, password)` - Přihlášení a vygenerování tokenu
|
||||
- `validateToken(token)` - Ověření tokenu
|
||||
- `isTokenValid(token)` - Kontrola validity tokenu
|
||||
- `revokeToken(token)` - Zrušení tokenu
|
||||
|
||||
### UserManager
|
||||
- **Třída**: `cz.kamma.xtreamplayer.UserManager`
|
||||
- **Odpovědnost**: CLI nástroj pro správu uživatelů
|
||||
- **Režimy**:
|
||||
- Interaktivní - Menu-driven UI
|
||||
- Příkazová řádka - Přímá spuštění příkazů
|
||||
|
||||
## Bezpečnost
|
||||
|
||||
- Hesla jsou hashována pomocí SHA-256 a zakódována v Base64
|
||||
- Session tokeny jsou generovány pomocí `SecureRandom` a mají platnost 24 hodin
|
||||
- Databáze je chráněna stejně jako ostatní aplikační data v `~/.xtream-player/`
|
||||
|
||||
## Migrace ze starý verze
|
||||
|
||||
Pokud upgradujete z verze bez databázové autentizace:
|
||||
1. Aplikace automaticky vytvoří nový soubor `~/.xtream-player/users.db`
|
||||
2. Výchozí uživatel `admin`/`admin` bude vytvořen automaticky
|
||||
3. Můžete přidat nebo upravit uživatele pomocí `UserManager`
|
||||
- Returned user objects include: `id`, `username`, `createdAt`, `updatedAt`
|
||||
- Password hash is never returned by the API.
|
||||
|
||||
@ -3,39 +3,67 @@ package cz.kamma.xtreamplayer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
final class LibraryRepository {
|
||||
private static final Logger LOGGER = LogManager.getLogger(LibraryRepository.class);
|
||||
/**
|
||||
* Unified Data Access Object for both user credentials and library/stream data.
|
||||
* Manages persistent data in H2 database for users, categories, streams, series, and favorites.
|
||||
*/
|
||||
final class ApplicationDao {
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApplicationDao.class);
|
||||
private final Path dbPath;
|
||||
private final String jdbcUrl;
|
||||
|
||||
LibraryRepository(Path dbPath) {
|
||||
public ApplicationDao(Path dbPath) {
|
||||
this.dbPath = dbPath;
|
||||
this.jdbcUrl = "jdbc:h2:file:" + dbPath.toAbsolutePath().toString().replace("\\", "/") + ";AUTO_SERVER=TRUE";
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
public void initialize() {
|
||||
try {
|
||||
Files.createDirectories(dbPath.getParent());
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to create database directory", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
try {
|
||||
try (Connection connection = openConnection(); Statement statement = connection.createStatement()) {
|
||||
// User tables
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id IDENTITY PRIMARY KEY,
|
||||
username VARCHAR(120) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(256) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""");
|
||||
// Library metadata
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS source_meta (
|
||||
meta_key VARCHAR(120) PRIMARY KEY,
|
||||
meta_value CLOB
|
||||
)
|
||||
""");
|
||||
// Categories
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS live_categories (
|
||||
category_id VARCHAR(80) PRIMARY KEY,
|
||||
@ -54,6 +82,7 @@ final class LibraryRepository {
|
||||
category_name VARCHAR(400) NOT NULL
|
||||
)
|
||||
""");
|
||||
// Streams
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS live_streams (
|
||||
stream_id VARCHAR(120) PRIMARY KEY,
|
||||
@ -70,6 +99,7 @@ final class LibraryRepository {
|
||||
container_extension VARCHAR(40)
|
||||
)
|
||||
""");
|
||||
// Series
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS series_items (
|
||||
series_id VARCHAR(120) PRIMARY KEY,
|
||||
@ -87,6 +117,7 @@ final class LibraryRepository {
|
||||
container_extension VARCHAR(40)
|
||||
)
|
||||
""");
|
||||
// Favorites
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
favorite_key VARCHAR(260) PRIMARY KEY,
|
||||
@ -102,14 +133,12 @@ final class LibraryRepository {
|
||||
created_at BIGINT NOT NULL
|
||||
)
|
||||
""");
|
||||
// Indexes
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_live_streams_category ON live_streams(category_id)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_vod_streams_category ON vod_streams(category_id)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_items_category ON series_items(category_id)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_episodes_series ON series_episodes(series_id)");
|
||||
statement.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_series_episodes_series_sort "
|
||||
+ "ON series_episodes(series_id, season, episode_num, title)"
|
||||
);
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_episodes_series_sort ON series_episodes(series_id, season, episode_num, title)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_live_categories_name ON live_categories(category_name)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_vod_categories_name ON vod_categories(category_name)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_categories_name ON series_categories(category_name)");
|
||||
@ -117,16 +146,148 @@ final class LibraryRepository {
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_vod_streams_name ON vod_streams(name)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_series_items_name ON series_items(name)");
|
||||
statement.execute("CREATE INDEX IF NOT EXISTS idx_favorites_created_at ON favorites(created_at DESC)");
|
||||
LOGGER.info("Database tables and indexes initialized at {}", dbPath);
|
||||
}
|
||||
try (Connection connection = openConnection()) {
|
||||
initializeFullText(connection);
|
||||
}
|
||||
LOGGER.info("H2 repository initialized at {}", dbPath);
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to initialize H2 repository.", exception);
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to initialize database", exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== USER MANAGEMENT =====
|
||||
|
||||
public void createUser(String username, String password) {
|
||||
if (username == null || username.isBlank() || password == null || password.isBlank()) {
|
||||
throw new IllegalArgumentException("Username and password must not be null or empty");
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, username);
|
||||
stmt.setString(2, hashPassword(password));
|
||||
stmt.executeUpdate();
|
||||
LOGGER.info("User created: {}", username);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
if (exception.getMessage().contains("Unique constraint")) {
|
||||
throw new IllegalArgumentException("User '" + username + "' already exists");
|
||||
}
|
||||
LOGGER.error("Failed to create user: {}", username, exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePassword(String username, String newPassword) {
|
||||
if (username == null || username.isBlank() || newPassword == null || newPassword.isBlank()) {
|
||||
throw new IllegalArgumentException("Username and password must not be null or empty");
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, hashPassword(newPassword));
|
||||
stmt.setString(2, username);
|
||||
int updated = stmt.executeUpdate();
|
||||
if (updated == 0) {
|
||||
throw new IllegalArgumentException("User '" + username + "' not found");
|
||||
}
|
||||
LOGGER.info("Password updated for user: {}", username);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to update password for user: {}", username, exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteUser(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
throw new IllegalArgumentException("Username must not be null or empty");
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "DELETE FROM users WHERE username = ?";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, username);
|
||||
int deleted = stmt.executeUpdate();
|
||||
if (deleted == 0) {
|
||||
throw new IllegalArgumentException("User '" + username + "' not found");
|
||||
}
|
||||
LOGGER.info("User deleted: {}", username);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to delete user: {}", username, exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<User> getUser(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, username);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.of(new User(
|
||||
rs.getInt("id"),
|
||||
rs.getString("username"),
|
||||
rs.getString("password_hash"),
|
||||
rs.getTimestamp("created_at"),
|
||||
rs.getTimestamp("updated_at")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to get user: {}", username, exception);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public List<User> getAllUsers() {
|
||||
List<User> users = new ArrayList<>();
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "SELECT id, username, password_hash, created_at, updated_at FROM users ORDER BY username";
|
||||
try (Statement stmt = connection.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
while (rs.next()) {
|
||||
users.add(new User(
|
||||
rs.getInt("id"),
|
||||
rs.getString("username"),
|
||||
rs.getString("password_hash"),
|
||||
rs.getTimestamp("created_at"),
|
||||
rs.getTimestamp("updated_at")
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to get all users", exception);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
public boolean verifyPassword(String username, String password) {
|
||||
Optional<User> user = getUser(username);
|
||||
if (user.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return hashPassword(password).equals(user.get().getPasswordHash());
|
||||
}
|
||||
|
||||
public boolean userExists(String username) {
|
||||
return getUser(username).isPresent();
|
||||
}
|
||||
|
||||
// ===== LIBRARY MANAGEMENT =====
|
||||
|
||||
void clearAllSources() {
|
||||
LOGGER.info("Clearing all cached library data in H2");
|
||||
inTransaction(connection -> {
|
||||
@ -704,6 +865,8 @@ final class LibraryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== PRIVATE HELPERS =====
|
||||
|
||||
private int countTable(Connection connection, String tableName) throws SQLException {
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) AS c FROM " + tableName)) {
|
||||
@ -925,7 +1088,7 @@ final class LibraryRepository {
|
||||
}
|
||||
|
||||
private Connection openConnection() throws SQLException {
|
||||
return DriverManager.getConnection(jdbcUrl, "sa", "");
|
||||
return DriverManager.getConnection(jdbcUrl);
|
||||
}
|
||||
|
||||
private void inTransaction(SqlWork work) {
|
||||
@ -945,6 +1108,64 @@ final class LibraryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private String hashPassword(String password) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== DATA CLASSES =====
|
||||
|
||||
public static final class User {
|
||||
private final int id;
|
||||
private final String username;
|
||||
private final String passwordHash;
|
||||
private final Timestamp createdAt;
|
||||
private final Timestamp updatedAt;
|
||||
|
||||
User(int id, String username, String passwordHash, Timestamp createdAt, Timestamp updatedAt) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.passwordHash = passwordHash;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public Timestamp getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Timestamp getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
", createdAt=" + createdAt +
|
||||
", updatedAt=" + updatedAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SqlWork {
|
||||
void execute(Connection connection) throws Exception;
|
||||
@ -7,17 +7,17 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* Authentication provider that manages session tokens.
|
||||
* User credentials are stored in the database via UserStore.
|
||||
* User credentials are stored in the database via ApplicationDao.
|
||||
*/
|
||||
public final class UserAuthenticator {
|
||||
private final UserStore userStore;
|
||||
private final ApplicationDao dao;
|
||||
private final Map<String, String> sessionTokens = new HashMap<>();
|
||||
private final Map<String, Long> tokenExpiry = new HashMap<>();
|
||||
private static final long TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
public UserAuthenticator(UserStore userStore) {
|
||||
this.userStore = userStore;
|
||||
public UserAuthenticator(ApplicationDao dao) {
|
||||
this.dao = dao;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -27,7 +27,7 @@ public final class UserAuthenticator {
|
||||
if (username == null || password == null) {
|
||||
return null;
|
||||
}
|
||||
if (!userStore.verifyPassword(username, password)) {
|
||||
if (!dao.verifyPassword(username, password)) {
|
||||
return null;
|
||||
}
|
||||
// Generate and store session token
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
package cz.kamma.xtreamplayer;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Command-line utility for managing users in the application.
|
||||
*
|
||||
* Usage:
|
||||
* java -cp xtream-player.jar cz.kamma.xtreamplayer.UserManager
|
||||
*/
|
||||
public class UserManager {
|
||||
private final UserStore userStore;
|
||||
private final Scanner scanner;
|
||||
|
||||
public UserManager() {
|
||||
this.userStore = new UserStore(
|
||||
Path.of(System.getProperty("user.home"), ".xtream-player", "users.db")
|
||||
);
|
||||
this.userStore.initialize();
|
||||
this.scanner = new Scanner(System.in);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
UserManager manager = new UserManager();
|
||||
if (args.length > 0) {
|
||||
manager.handleCommand(args);
|
||||
} else {
|
||||
manager.interactiveMode();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCommand(String[] args) {
|
||||
try {
|
||||
String command = args[0].toLowerCase();
|
||||
switch (command) {
|
||||
case "add" -> {
|
||||
if (args.length < 3) {
|
||||
System.err.println("Usage: add <username> <password>");
|
||||
System.exit(1);
|
||||
}
|
||||
addUser(args[1], args[2]);
|
||||
}
|
||||
case "delete" -> {
|
||||
if (args.length < 2) {
|
||||
System.err.println("Usage: delete <username>");
|
||||
System.exit(1);
|
||||
}
|
||||
deleteUser(args[1]);
|
||||
}
|
||||
case "update" -> {
|
||||
if (args.length < 3) {
|
||||
System.err.println("Usage: update <username> <new_password>");
|
||||
System.exit(1);
|
||||
}
|
||||
updatePassword(args[1], args[2]);
|
||||
}
|
||||
case "list" -> listUsers();
|
||||
case "verify" -> {
|
||||
if (args.length < 3) {
|
||||
System.err.println("Usage: verify <username> <password>");
|
||||
System.exit(1);
|
||||
}
|
||||
verifyUser(args[1], args[2]);
|
||||
}
|
||||
default -> {
|
||||
System.err.println("Unknown command: " + command);
|
||||
System.err.println("Available commands: add, delete, update, list, verify");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void interactiveMode() {
|
||||
System.out.println("╔════════════════════════════════════════╗");
|
||||
System.out.println("║ Xtream Player - User Manager ║");
|
||||
System.out.println("╚════════════════════════════════════════╝");
|
||||
System.out.println();
|
||||
System.out.println("Available commands:");
|
||||
System.out.println(" 1. Add user");
|
||||
System.out.println(" 2. Delete user");
|
||||
System.out.println(" 3. Update password");
|
||||
System.out.println(" 4. List all users");
|
||||
System.out.println(" 5. Verify password");
|
||||
System.out.println(" 0. Exit");
|
||||
System.out.println();
|
||||
|
||||
boolean running = true;
|
||||
while (running) {
|
||||
System.out.print("\nSelect option: ");
|
||||
String choice = scanner.nextLine().trim();
|
||||
|
||||
try {
|
||||
switch (choice) {
|
||||
case "1" -> addUserInteractive();
|
||||
case "2" -> deleteUserInteractive();
|
||||
case "3" -> updatePasswordInteractive();
|
||||
case "4" -> listUsers();
|
||||
case "5" -> verifyUserInteractive();
|
||||
case "0" -> {
|
||||
System.out.println("Goodbye!");
|
||||
running = false;
|
||||
}
|
||||
default -> System.out.println("Invalid option. Please try again.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addUserInteractive() {
|
||||
System.out.print("Username: ");
|
||||
String username = scanner.nextLine().trim();
|
||||
System.out.print("Password: ");
|
||||
String password = scanner.nextLine().trim();
|
||||
addUser(username, password);
|
||||
}
|
||||
|
||||
private void addUser(String username, String password) {
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
System.err.println("Username and password cannot be empty.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
userStore.createUser(username, password);
|
||||
System.out.println("✓ User '" + username + "' created successfully.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println("✗ " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteUserInteractive() {
|
||||
System.out.print("Username to delete: ");
|
||||
String username = scanner.nextLine().trim();
|
||||
System.out.print("Are you sure? (yes/no): ");
|
||||
String confirm = scanner.nextLine().trim().toLowerCase();
|
||||
if ("yes".equals(confirm) || "y".equals(confirm)) {
|
||||
deleteUser(username);
|
||||
} else {
|
||||
System.out.println("Cancelled.");
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteUser(String username) {
|
||||
try {
|
||||
userStore.deleteUser(username);
|
||||
System.out.println("✓ User '" + username + "' deleted successfully.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println("✗ " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePasswordInteractive() {
|
||||
System.out.print("Username: ");
|
||||
String username = scanner.nextLine().trim();
|
||||
System.out.print("New password: ");
|
||||
String newPassword = scanner.nextLine().trim();
|
||||
updatePassword(username, newPassword);
|
||||
}
|
||||
|
||||
private void updatePassword(String username, String newPassword) {
|
||||
if (newPassword.isBlank()) {
|
||||
System.err.println("Password cannot be empty.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
userStore.updatePassword(username, newPassword);
|
||||
System.out.println("✓ Password for user '" + username + "' updated successfully.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println("✗ " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void listUsers() {
|
||||
List<UserStore.User> users = userStore.getAllUsers();
|
||||
if (users.isEmpty()) {
|
||||
System.out.println("No users found.");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("╔═══╦════════════════════╦═══════════════════════════════╦═══════════════════════════════╗");
|
||||
System.out.println("║ ID║ Username ║ Created At ║ Updated At ║");
|
||||
System.out.println("╠═══╬════════════════════╬═══════════════════════════════╬═══════════════════════════════╣");
|
||||
|
||||
for (UserStore.User user : users) {
|
||||
System.out.printf("║ %2d║ %-18s ║ %-29s ║ %-29s ║%n",
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getCreatedAt() != null ? user.getCreatedAt().toString() : "N/A",
|
||||
user.getUpdatedAt() != null ? user.getUpdatedAt().toString() : "N/A"
|
||||
);
|
||||
}
|
||||
System.out.println("╚═══╩════════════════════╩═══════════════════════════════╩═══════════════════════════════╝");
|
||||
System.out.println("Total: " + users.size() + " user(s)");
|
||||
}
|
||||
|
||||
private void verifyUserInteractive() {
|
||||
System.out.print("Username: ");
|
||||
String username = scanner.nextLine().trim();
|
||||
System.out.print("Password: ");
|
||||
String password = scanner.nextLine().trim();
|
||||
verifyUser(username, password);
|
||||
}
|
||||
|
||||
private void verifyUser(String username, String password) {
|
||||
if (userStore.verifyPassword(username, password)) {
|
||||
System.out.println("✓ Password is correct for user '" + username + "'.");
|
||||
} else {
|
||||
System.out.println("✗ Password verification failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,273 +0,0 @@
|
||||
package cz.kamma.xtreamplayer;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Database-backed user store using H2 database.
|
||||
* Manages user credentials stored persistently in the database.
|
||||
*/
|
||||
final class UserStore {
|
||||
private static final Logger LOGGER = LogManager.getLogger(UserStore.class);
|
||||
private final Path dbPath;
|
||||
private final String jdbcUrl;
|
||||
|
||||
public UserStore(Path dbPath) {
|
||||
this.dbPath = dbPath;
|
||||
this.jdbcUrl = "jdbc:h2:file:" + dbPath.toAbsolutePath().toString().replace("\\", "/") + ";AUTO_SERVER=TRUE";
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
try {
|
||||
Files.createDirectories(dbPath.getParent());
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to create database directory", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
try {
|
||||
try (Connection connection = openConnection(); Statement statement = connection.createStatement()) {
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id IDENTITY PRIMARY KEY,
|
||||
username VARCHAR(120) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(256) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""");
|
||||
LOGGER.info("Users table initialized");
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to initialize users table", exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user with the given username and password.
|
||||
*/
|
||||
public void createUser(String username, String password) {
|
||||
if (username == null || username.isBlank() || password == null || password.isBlank()) {
|
||||
throw new IllegalArgumentException("Username and password must not be null or empty");
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, username);
|
||||
stmt.setString(2, hashPassword(password));
|
||||
stmt.executeUpdate();
|
||||
LOGGER.info("User created: {}", username);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
if (exception.getMessage().contains("Unique constraint")) {
|
||||
throw new IllegalArgumentException("User '" + username + "' already exists");
|
||||
}
|
||||
LOGGER.error("Failed to create user: {}", username, exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's password.
|
||||
*/
|
||||
public void updatePassword(String username, String newPassword) {
|
||||
if (username == null || username.isBlank() || newPassword == null || newPassword.isBlank()) {
|
||||
throw new IllegalArgumentException("Username and password must not be null or empty");
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, hashPassword(newPassword));
|
||||
stmt.setString(2, username);
|
||||
int updated = stmt.executeUpdate();
|
||||
if (updated == 0) {
|
||||
throw new IllegalArgumentException("User '" + username + "' not found");
|
||||
}
|
||||
LOGGER.info("Password updated for user: {}", username);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to update password for user: {}", username, exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user by username.
|
||||
*/
|
||||
public void deleteUser(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
throw new IllegalArgumentException("Username must not be null or empty");
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "DELETE FROM users WHERE username = ?";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, username);
|
||||
int deleted = stmt.executeUpdate();
|
||||
if (deleted == 0) {
|
||||
throw new IllegalArgumentException("User '" + username + "' not found");
|
||||
}
|
||||
LOGGER.info("User deleted: {}", username);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to delete user: {}", username, exception);
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by username.
|
||||
*/
|
||||
public Optional<User> getUser(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?";
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.setString(1, username);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.of(new User(
|
||||
rs.getInt("id"),
|
||||
rs.getString("username"),
|
||||
rs.getString("password_hash"),
|
||||
rs.getTimestamp("created_at"),
|
||||
rs.getTimestamp("updated_at")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to get user: {}", username, exception);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users.
|
||||
*/
|
||||
public List<User> getAllUsers() {
|
||||
List<User> users = new ArrayList<>();
|
||||
try (Connection connection = openConnection()) {
|
||||
String sql = "SELECT id, username, password_hash, created_at, updated_at FROM users ORDER BY username";
|
||||
try (Statement stmt = connection.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
while (rs.next()) {
|
||||
users.add(new User(
|
||||
rs.getInt("id"),
|
||||
rs.getString("username"),
|
||||
rs.getString("password_hash"),
|
||||
rs.getTimestamp("created_at"),
|
||||
rs.getTimestamp("updated_at")
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Failed to get all users", exception);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify password for a user.
|
||||
*/
|
||||
public boolean verifyPassword(String username, String password) {
|
||||
Optional<User> user = getUser(username);
|
||||
if (user.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return hashPassword(password).equals(user.get().getPasswordHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user exists.
|
||||
*/
|
||||
public boolean userExists(String username) {
|
||||
return getUser(username).isPresent();
|
||||
}
|
||||
|
||||
private Connection openConnection() throws SQLException {
|
||||
return DriverManager.getConnection(jdbcUrl);
|
||||
}
|
||||
|
||||
private String hashPassword(String password) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing a user.
|
||||
*/
|
||||
public static final class User {
|
||||
private final int id;
|
||||
private final String username;
|
||||
private final String passwordHash;
|
||||
private final Timestamp createdAt;
|
||||
private final Timestamp updatedAt;
|
||||
|
||||
User(int id, String username, String passwordHash, Timestamp createdAt, Timestamp updatedAt) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.passwordHash = passwordHash;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public Timestamp getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Timestamp getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
", createdAt=" + createdAt +
|
||||
", updatedAt=" + updatedAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,13 +39,13 @@ final class XtreamLibraryService {
|
||||
private static final String META_LOADED_AT = "source_loaded_at";
|
||||
|
||||
private final ConfigStore configStore;
|
||||
private final LibraryRepository repository;
|
||||
private final ApplicationDao dao;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
XtreamLibraryService(ConfigStore configStore, LibraryRepository repository) {
|
||||
XtreamLibraryService(ConfigStore configStore, ApplicationDao dao) {
|
||||
this.configStore = configStore;
|
||||
this.repository = repository;
|
||||
this.dao = dao;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
@ -73,34 +73,34 @@ final class XtreamLibraryService {
|
||||
XtreamConfig config = requireConfigured();
|
||||
ensureFingerprint(config);
|
||||
if ("live_categories".equals(step)) {
|
||||
repository.setMeta(META_LOADED_AT, "");
|
||||
dao.setMeta(META_LOADED_AT, "");
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case "live_categories" -> {
|
||||
JsonNode response = fetchXtream(config, Map.of("action", "get_live_categories"));
|
||||
repository.replaceLiveCategories(parseCategories(response));
|
||||
dao.replaceLiveCategories(parseCategories(response));
|
||||
}
|
||||
case "live_streams" -> {
|
||||
JsonNode response = fetchXtream(config, Map.of("action", "get_live_streams"));
|
||||
repository.replaceLiveStreams(parseLiveStreams(response));
|
||||
dao.replaceLiveStreams(parseLiveStreams(response));
|
||||
}
|
||||
case "vod_categories" -> {
|
||||
JsonNode response = fetchXtream(config, Map.of("action", "get_vod_categories"));
|
||||
repository.replaceVodCategories(parseCategories(response));
|
||||
dao.replaceVodCategories(parseCategories(response));
|
||||
}
|
||||
case "vod_streams" -> {
|
||||
JsonNode response = fetchXtream(config, Map.of("action", "get_vod_streams"));
|
||||
repository.replaceVodStreams(parseVodStreams(response));
|
||||
dao.replaceVodStreams(parseVodStreams(response));
|
||||
}
|
||||
case "series_categories" -> {
|
||||
JsonNode response = fetchXtream(config, Map.of("action", "get_series_categories"));
|
||||
repository.replaceSeriesCategories(parseCategories(response));
|
||||
dao.replaceSeriesCategories(parseCategories(response));
|
||||
}
|
||||
case "series_items" -> {
|
||||
JsonNode response = fetchXtream(config, Map.of("action", "get_series"));
|
||||
repository.replaceSeriesItems(parseSeriesItems(response));
|
||||
repository.setMeta(META_LOADED_AT, Long.toString(System.currentTimeMillis()));
|
||||
dao.replaceSeriesItems(parseSeriesItems(response));
|
||||
dao.setMeta(META_LOADED_AT, Long.toString(System.currentTimeMillis()));
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unsupported load step: " + step);
|
||||
}
|
||||
@ -114,9 +114,9 @@ final class XtreamLibraryService {
|
||||
|
||||
Map<String, Object> status() {
|
||||
XtreamConfig config = configStore.get();
|
||||
LibraryRepository.LibraryCounts counts = repository.countAll();
|
||||
String storedFingerprint = nullSafe(repository.getMeta(META_FINGERPRINT));
|
||||
String loadedAt = nullSafe(repository.getMeta(META_LOADED_AT));
|
||||
ApplicationDao.LibraryCounts counts = dao.countAll();
|
||||
String storedFingerprint = nullSafe(dao.getMeta(META_FINGERPRINT));
|
||||
String loadedAt = nullSafe(dao.getMeta(META_LOADED_AT));
|
||||
String currentFingerprint = fingerprint(config);
|
||||
|
||||
boolean ready = config.isConfigured()
|
||||
@ -140,39 +140,39 @@ final class XtreamLibraryService {
|
||||
return out;
|
||||
}
|
||||
|
||||
List<LibraryRepository.CategoryRow> listCategories(String type) {
|
||||
return repository.listCategories(type);
|
||||
List<ApplicationDao.CategoryRow> listCategories(String type) {
|
||||
return dao.listCategories(type);
|
||||
}
|
||||
|
||||
List<?> listItems(String type, String categoryId, String search, Integer limit, Integer offset) {
|
||||
String normalizedType = type == null ? "" : type.trim().toLowerCase(Locale.ROOT);
|
||||
return switch (normalizedType) {
|
||||
case "live" -> repository.listLiveStreams(categoryId, search, limit, offset);
|
||||
case "vod" -> repository.listVodStreams(categoryId, search, limit, offset);
|
||||
case "series" -> repository.listSeriesItems(categoryId, search, limit, offset);
|
||||
case "live" -> dao.listLiveStreams(categoryId, search, limit, offset);
|
||||
case "vod" -> dao.listVodStreams(categoryId, search, limit, offset);
|
||||
case "series" -> dao.listSeriesItems(categoryId, search, limit, offset);
|
||||
default -> throw new IllegalArgumentException("Unsupported type: " + type);
|
||||
};
|
||||
}
|
||||
|
||||
List<LibraryRepository.GlobalSearchRow> globalSearch(String query, int limitRaw, int offsetRaw) {
|
||||
List<ApplicationDao.GlobalSearchRow> globalSearch(String query, int limitRaw, int offsetRaw) {
|
||||
String normalizedQuery = nullSafe(query).trim();
|
||||
if (normalizedQuery.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
int limit = Math.max(1, Math.min(limitRaw, 500));
|
||||
int offset = Math.max(0, offsetRaw);
|
||||
return repository.globalSearch(normalizedQuery, limit, offset);
|
||||
return dao.globalSearch(normalizedQuery, limit, offset);
|
||||
}
|
||||
|
||||
List<LibraryRepository.FavoriteRow> listFavorites(String search, int limit, int offset) {
|
||||
return repository.listFavorites(search, limit, offset);
|
||||
List<ApplicationDao.FavoriteRow> listFavorites(String search, int limit, int offset) {
|
||||
return dao.listFavorites(search, limit, offset);
|
||||
}
|
||||
|
||||
int countFavorites(String search) {
|
||||
return repository.countFavorites(search);
|
||||
return dao.countFavorites(search);
|
||||
}
|
||||
|
||||
LibraryRepository.FavoriteRow saveFavorite(
|
||||
ApplicationDao.FavoriteRow saveFavorite(
|
||||
String key,
|
||||
String mode,
|
||||
String id,
|
||||
@ -198,7 +198,7 @@ final class XtreamLibraryService {
|
||||
throw new IllegalArgumentException("Missing favorite title.");
|
||||
}
|
||||
long safeCreatedAt = createdAt == null || createdAt <= 0 ? System.currentTimeMillis() : createdAt;
|
||||
LibraryRepository.FavoriteRow row = new LibraryRepository.FavoriteRow(
|
||||
ApplicationDao.FavoriteRow row = new ApplicationDao.FavoriteRow(
|
||||
normalizedKey,
|
||||
normalizedMode,
|
||||
nullSafe(id).trim(),
|
||||
@ -211,7 +211,7 @@ final class XtreamLibraryService {
|
||||
nullSafe(url).trim(),
|
||||
safeCreatedAt
|
||||
);
|
||||
repository.upsertFavorite(row);
|
||||
dao.upsertFavorite(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
@ -220,16 +220,16 @@ final class XtreamLibraryService {
|
||||
if (normalizedKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing favorite key.");
|
||||
}
|
||||
return repository.deleteFavorite(normalizedKey);
|
||||
return dao.deleteFavorite(normalizedKey);
|
||||
}
|
||||
|
||||
List<LibraryRepository.SeriesEpisodeRow> listSeriesEpisodes(String seriesIdRaw) {
|
||||
List<ApplicationDao.SeriesEpisodeRow> listSeriesEpisodes(String seriesIdRaw) {
|
||||
String seriesId = seriesIdRaw == null ? "" : seriesIdRaw.trim();
|
||||
if (seriesId.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing series_id.");
|
||||
}
|
||||
|
||||
List<LibraryRepository.SeriesEpisodeRow> cached = repository.listSeriesEpisodes(seriesId);
|
||||
List<ApplicationDao.SeriesEpisodeRow> cached = dao.listSeriesEpisodes(seriesId);
|
||||
if (!cached.isEmpty()) {
|
||||
LOGGER.debug("Series episodes served from cache for series_id={}", seriesId);
|
||||
return cached;
|
||||
@ -241,8 +241,8 @@ final class XtreamLibraryService {
|
||||
"action", "get_series_info",
|
||||
"series_id", seriesId
|
||||
));
|
||||
List<LibraryRepository.SeriesEpisodeRow> loaded = parseSeriesEpisodes(seriesId, response.path("episodes"));
|
||||
repository.replaceSeriesEpisodes(seriesId, loaded);
|
||||
List<ApplicationDao.SeriesEpisodeRow> loaded = parseSeriesEpisodes(seriesId, response.path("episodes"));
|
||||
dao.replaceSeriesEpisodes(seriesId, loaded);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@ -261,11 +261,11 @@ final class XtreamLibraryService {
|
||||
return objectMapper.convertValue(response, Object.class);
|
||||
}
|
||||
|
||||
private List<LibraryRepository.CategoryRow> parseCategories(JsonNode node) {
|
||||
private List<ApplicationDao.CategoryRow> parseCategories(JsonNode node) {
|
||||
if (!node.isArray()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, LibraryRepository.CategoryRow> deduplicated = new LinkedHashMap<>();
|
||||
Map<String, ApplicationDao.CategoryRow> deduplicated = new LinkedHashMap<>();
|
||||
for (JsonNode item : node) {
|
||||
String id = text(item, "category_id");
|
||||
if (id.isBlank()) {
|
||||
@ -275,16 +275,16 @@ final class XtreamLibraryService {
|
||||
if (name.isBlank()) {
|
||||
name = "Category " + id;
|
||||
}
|
||||
deduplicated.put(id, new LibraryRepository.CategoryRow(id, name));
|
||||
deduplicated.put(id, new ApplicationDao.CategoryRow(id, name));
|
||||
}
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
}
|
||||
|
||||
private List<LibraryRepository.LiveStreamRow> parseLiveStreams(JsonNode node) {
|
||||
private List<ApplicationDao.LiveStreamRow> parseLiveStreams(JsonNode node) {
|
||||
if (!node.isArray()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, LibraryRepository.LiveStreamRow> deduplicated = new LinkedHashMap<>();
|
||||
Map<String, ApplicationDao.LiveStreamRow> deduplicated = new LinkedHashMap<>();
|
||||
for (JsonNode item : node) {
|
||||
String streamId = text(item, "stream_id");
|
||||
if (streamId.isBlank()) {
|
||||
@ -294,7 +294,7 @@ final class XtreamLibraryService {
|
||||
if (name.isBlank()) {
|
||||
name = "Stream " + streamId;
|
||||
}
|
||||
deduplicated.put(streamId, new LibraryRepository.LiveStreamRow(
|
||||
deduplicated.put(streamId, new ApplicationDao.LiveStreamRow(
|
||||
streamId,
|
||||
name,
|
||||
text(item, "category_id"),
|
||||
@ -304,11 +304,11 @@ final class XtreamLibraryService {
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
}
|
||||
|
||||
private List<LibraryRepository.VodStreamRow> parseVodStreams(JsonNode node) {
|
||||
private List<ApplicationDao.VodStreamRow> parseVodStreams(JsonNode node) {
|
||||
if (!node.isArray()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, LibraryRepository.VodStreamRow> deduplicated = new LinkedHashMap<>();
|
||||
Map<String, ApplicationDao.VodStreamRow> deduplicated = new LinkedHashMap<>();
|
||||
for (JsonNode item : node) {
|
||||
String streamId = text(item, "stream_id");
|
||||
if (streamId.isBlank()) {
|
||||
@ -322,7 +322,7 @@ final class XtreamLibraryService {
|
||||
if (ext.isBlank()) {
|
||||
ext = "mp4";
|
||||
}
|
||||
deduplicated.put(streamId, new LibraryRepository.VodStreamRow(
|
||||
deduplicated.put(streamId, new ApplicationDao.VodStreamRow(
|
||||
streamId,
|
||||
name,
|
||||
text(item, "category_id"),
|
||||
@ -332,11 +332,11 @@ final class XtreamLibraryService {
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
}
|
||||
|
||||
private List<LibraryRepository.SeriesItemRow> parseSeriesItems(JsonNode node) {
|
||||
private List<ApplicationDao.SeriesItemRow> parseSeriesItems(JsonNode node) {
|
||||
if (!node.isArray()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, LibraryRepository.SeriesItemRow> deduplicated = new LinkedHashMap<>();
|
||||
Map<String, ApplicationDao.SeriesItemRow> deduplicated = new LinkedHashMap<>();
|
||||
for (JsonNode item : node) {
|
||||
String seriesId = text(item, "series_id");
|
||||
if (seriesId.isBlank()) {
|
||||
@ -346,7 +346,7 @@ final class XtreamLibraryService {
|
||||
if (name.isBlank()) {
|
||||
name = "Series " + seriesId;
|
||||
}
|
||||
deduplicated.put(seriesId, new LibraryRepository.SeriesItemRow(
|
||||
deduplicated.put(seriesId, new ApplicationDao.SeriesItemRow(
|
||||
seriesId,
|
||||
name,
|
||||
text(item, "category_id")
|
||||
@ -355,11 +355,11 @@ final class XtreamLibraryService {
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
}
|
||||
|
||||
private List<LibraryRepository.SeriesEpisodeRow> parseSeriesEpisodes(String seriesId, JsonNode episodesBySeason) {
|
||||
private List<ApplicationDao.SeriesEpisodeRow> parseSeriesEpisodes(String seriesId, JsonNode episodesBySeason) {
|
||||
if (episodesBySeason == null || episodesBySeason.isMissingNode() || episodesBySeason.isNull()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, LibraryRepository.SeriesEpisodeRow> deduplicated = new LinkedHashMap<>();
|
||||
Map<String, ApplicationDao.SeriesEpisodeRow> deduplicated = new LinkedHashMap<>();
|
||||
episodesBySeason.fields().forEachRemaining(entry -> {
|
||||
String season = entry.getKey();
|
||||
JsonNode episodes = entry.getValue();
|
||||
@ -389,7 +389,7 @@ final class XtreamLibraryService {
|
||||
seasonValue = season;
|
||||
}
|
||||
|
||||
deduplicated.put(episodeId, new LibraryRepository.SeriesEpisodeRow(
|
||||
deduplicated.put(episodeId, new ApplicationDao.SeriesEpisodeRow(
|
||||
episodeId,
|
||||
seriesId,
|
||||
seasonValue,
|
||||
@ -405,12 +405,12 @@ final class XtreamLibraryService {
|
||||
|
||||
private void ensureFingerprint(XtreamConfig config) {
|
||||
String currentFingerprint = fingerprint(config);
|
||||
String storedFingerprint = nullSafe(repository.getMeta(META_FINGERPRINT));
|
||||
String storedFingerprint = nullSafe(dao.getMeta(META_FINGERPRINT));
|
||||
if (!currentFingerprint.equals(storedFingerprint)) {
|
||||
LOGGER.info("Source fingerprint changed. Clearing cached library data.");
|
||||
repository.clearAllSources();
|
||||
repository.setMeta(META_FINGERPRINT, currentFingerprint);
|
||||
repository.setMeta(META_LOADED_AT, "");
|
||||
dao.clearAllSources();
|
||||
dao.setMeta(META_FINGERPRINT, currentFingerprint);
|
||||
dao.setMeta(META_LOADED_AT, "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package cz.kamma.xtreamplayer;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
@ -35,6 +36,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
public final class XtreamPlayerApplication {
|
||||
private static final int DEFAULT_PORT = 8080;
|
||||
private static final String USER_API_BEARER_TOKEN = "MujBearer852654";
|
||||
private static final Set<String> SENSITIVE_KEYS = Set.of("password", "pass", "pwd", "token", "authorization");
|
||||
private static final String ATTR_REQ_START_NANOS = "reqStartNanos";
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
@ -59,29 +61,23 @@ public final class XtreamPlayerApplication {
|
||||
ConfigStore configStore = new ConfigStore(
|
||||
Path.of(System.getProperty("user.home"), ".xtream-player", "config.properties")
|
||||
);
|
||||
LibraryRepository libraryRepository = new LibraryRepository(
|
||||
Path.of(System.getProperty("user.home"), ".xtream-player", "library", "xtream-sources")
|
||||
ApplicationDao applicationDao = new ApplicationDao(
|
||||
Path.of(System.getProperty("user.home"), ".xtream-player", "app.db")
|
||||
);
|
||||
libraryRepository.initialize();
|
||||
XtreamLibraryService libraryService = new XtreamLibraryService(configStore, libraryRepository);
|
||||
|
||||
// Initialize user store
|
||||
UserStore userStore = new UserStore(
|
||||
Path.of(System.getProperty("user.home"), ".xtream-player", "users.db")
|
||||
);
|
||||
userStore.initialize();
|
||||
applicationDao.initialize();
|
||||
|
||||
// Create default admin user if no users exist
|
||||
if (userStore.getAllUsers().isEmpty()) {
|
||||
if (applicationDao.getAllUsers().isEmpty()) {
|
||||
try {
|
||||
userStore.createUser("admin", "admin");
|
||||
applicationDao.createUser("admin", "admin");
|
||||
LOGGER.info("Default admin user created (username: admin, password: admin)");
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// User might already exist
|
||||
}
|
||||
}
|
||||
|
||||
UserAuthenticator userAuthenticator = new UserAuthenticator(userStore);
|
||||
XtreamLibraryService libraryService = new XtreamLibraryService(configStore, applicationDao);
|
||||
UserAuthenticator userAuthenticator = new UserAuthenticator(applicationDao);
|
||||
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||
server.createContext("/api/auth/login", new LoginHandler(userAuthenticator));
|
||||
@ -100,6 +96,7 @@ public final class XtreamPlayerApplication {
|
||||
server.createContext("/api/library/series-episodes", new LibrarySeriesEpisodesHandler(libraryService, userAuthenticator));
|
||||
server.createContext("/api/library/epg", new LibraryEpgHandler(libraryService, userAuthenticator));
|
||||
server.createContext("/api/favorites", new FavoritesHandler(libraryService, userAuthenticator));
|
||||
server.createContext("/user", new UserCrudHandler(applicationDao));
|
||||
server.createContext("/", new StaticHandler(userAuthenticator));
|
||||
server.setExecutor(Executors.newFixedThreadPool(12));
|
||||
server.start();
|
||||
@ -738,7 +735,7 @@ public final class XtreamPlayerApplication {
|
||||
? Map.of()
|
||||
: OBJECT_MAPPER.readValue(body, Map.class);
|
||||
|
||||
LibraryRepository.FavoriteRow saved = libraryService.saveFavorite(
|
||||
ApplicationDao.FavoriteRow saved = libraryService.saveFavorite(
|
||||
asString(payload.get("key")),
|
||||
asString(payload.get("mode")),
|
||||
asString(payload.get("id")),
|
||||
@ -775,6 +772,103 @@ public final class XtreamPlayerApplication {
|
||||
}
|
||||
}
|
||||
|
||||
private record UserCrudHandler(ApplicationDao applicationDao) implements HttpHandler {
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
if (!requireUserApiBearer(exchange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String method = exchange.getRequestMethod().toUpperCase(Locale.ROOT);
|
||||
Map<String, String> query = parseKeyValue(exchange.getRequestURI().getRawQuery());
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case "GET" -> {
|
||||
logApiRequest(exchange, "/user", query);
|
||||
String username = query.getOrDefault("username", "").trim();
|
||||
if (username.isBlank()) {
|
||||
List<Map<String, Object>> users = applicationDao.getAllUsers().stream()
|
||||
.map(XtreamPlayerApplication::toUserResponse)
|
||||
.toList();
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("users", users);
|
||||
out.put("count", users.size());
|
||||
writeJsonObject(exchange, 200, out);
|
||||
return;
|
||||
}
|
||||
var user = applicationDao.getUser(username);
|
||||
if (user.isEmpty()) {
|
||||
writeJson(exchange, 404, errorJson("User not found."));
|
||||
return;
|
||||
}
|
||||
writeJsonObject(exchange, 200, toUserResponse(user.get()));
|
||||
return;
|
||||
}
|
||||
case "POST" -> {
|
||||
Map<String, String> payload = parseRequestPayload(exchange);
|
||||
logApiRequest(exchange, "/user", payload);
|
||||
String username = payload.getOrDefault("username", "").trim();
|
||||
String password = payload.getOrDefault("password", "");
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
writeJson(exchange, 400, errorJson("Fields 'username' and 'password' are required."));
|
||||
return;
|
||||
}
|
||||
applicationDao.createUser(username, password);
|
||||
var created = applicationDao.getUser(username);
|
||||
if (created.isPresent()) {
|
||||
writeJsonObject(exchange, 201, toUserResponse(created.get()));
|
||||
return;
|
||||
}
|
||||
writeJsonObject(exchange, 201, Map.of("created", true, "username", username));
|
||||
return;
|
||||
}
|
||||
case "PUT" -> {
|
||||
Map<String, String> payload = parseRequestPayload(exchange);
|
||||
logApiRequest(exchange, "/user", payload);
|
||||
String username = payload.getOrDefault("username", "").trim();
|
||||
String newPassword = firstNonBlank(payload.get("newPassword"), payload.get("password"));
|
||||
if (username.isBlank() || newPassword == null || newPassword.isBlank()) {
|
||||
writeJson(exchange, 400, errorJson("Fields 'username' and 'newPassword' are required."));
|
||||
return;
|
||||
}
|
||||
applicationDao.updatePassword(username, newPassword);
|
||||
writeJsonObject(exchange, 200, Map.of("updated", true, "username", username));
|
||||
return;
|
||||
}
|
||||
case "DELETE" -> {
|
||||
String username = query.getOrDefault("username", "").trim();
|
||||
Map<String, String> payload = Map.of();
|
||||
if (username.isBlank()) {
|
||||
payload = parseRequestPayload(exchange);
|
||||
username = payload.getOrDefault("username", "").trim();
|
||||
}
|
||||
Map<String, String> logParams = new LinkedHashMap<>(query);
|
||||
logParams.putAll(payload);
|
||||
logApiRequest(exchange, "/user", logParams);
|
||||
if (username.isBlank()) {
|
||||
writeJson(exchange, 400, errorJson("Field 'username' is required."));
|
||||
return;
|
||||
}
|
||||
applicationDao.deleteUser(username);
|
||||
writeJsonObject(exchange, 200, Map.of("deleted", true, "username", username));
|
||||
return;
|
||||
}
|
||||
default -> {
|
||||
logApiRequest(exchange, "/user", query);
|
||||
methodNotAllowed(exchange, "GET, POST, PUT, DELETE");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException exception) {
|
||||
writeJson(exchange, 400, errorJson(exception.getMessage()));
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error("User CRUD endpoint failed", exception);
|
||||
writeJson(exchange, 500, errorJson("User API failed: " + exception.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StaticHandler implements HttpHandler {
|
||||
StaticHandler(UserAuthenticator userAuthenticator) {
|
||||
// Static handler allows unauthenticated access to public files
|
||||
@ -859,6 +953,16 @@ public final class XtreamPlayerApplication {
|
||||
return query.get("token");
|
||||
}
|
||||
|
||||
private static boolean requireUserApiBearer(HttpExchange exchange) throws IOException {
|
||||
String authHeader = exchange.getRequestHeaders().getFirst("Authorization");
|
||||
String expected = "Bearer " + USER_API_BEARER_TOKEN;
|
||||
if (!expected.equals(authHeader)) {
|
||||
writeJson(exchange, 401, errorJson("Unauthorized. Missing or invalid bearer token."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean requireAuth(HttpExchange exchange, UserAuthenticator userAuthenticator) throws IOException {
|
||||
String token = extractAuthToken(exchange);
|
||||
if (token == null || !userAuthenticator.isTokenValid(token)) {
|
||||
@ -1308,6 +1412,33 @@ public final class XtreamPlayerApplication {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseRequestPayload(HttpExchange exchange) throws IOException {
|
||||
String body = readBody(exchange);
|
||||
if (body == null || body.isBlank()) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
String contentType = firstNonBlank(exchange.getRequestHeaders().getFirst("Content-Type"), "").toLowerCase(Locale.ROOT);
|
||||
if (contentType.contains("application/json")) {
|
||||
Map<String, Object> raw = OBJECT_MAPPER.readValue(body, new TypeReference<>() {
|
||||
});
|
||||
Map<String, String> parsed = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : raw.entrySet()) {
|
||||
parsed.put(entry.getKey(), asString(entry.getValue()));
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
return parseKeyValue(body);
|
||||
}
|
||||
|
||||
private static Map<String, Object> toUserResponse(ApplicationDao.User user) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("id", user.getId());
|
||||
out.put("username", user.getUsername());
|
||||
out.put("createdAt", user.getCreatedAt() == null ? null : user.getCreatedAt().toString());
|
||||
out.put("updatedAt", user.getUpdatedAt() == null ? null : user.getUpdatedAt().toString());
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String readBody(HttpExchange exchange) throws IOException {
|
||||
try (InputStream inputStream = exchange.getRequestBody()) {
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user