implemented user login.
next: find a solution for routing
This commit is contained in:
@@ -12,9 +12,13 @@ application{
|
||||
}
|
||||
|
||||
dependencies{
|
||||
implementation(project(":core"))
|
||||
implementation(project(":translations"))
|
||||
implementation(project(":user"))
|
||||
implementation(project(":web"))
|
||||
|
||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||
implementation("de.srsoftware:tools.util:2.0.3")
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
|
||||
@@ -6,8 +6,10 @@ import static java.lang.System.Logger.Level.INFO;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import de.srsoftware.tools.ColorLogger;
|
||||
import de.srsoftware.umbrella.core.ConnectionProvider;
|
||||
import de.srsoftware.umbrella.translations.Translations;
|
||||
import de.srsoftware.umbrella.user.UserModule;
|
||||
import de.srsoftware.umbrella.user.sqlite.SqliteDB;
|
||||
import de.srsoftware.umbrella.web.WebHandler;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
@@ -15,15 +17,18 @@ import java.util.concurrent.Executors;
|
||||
|
||||
public class Application {
|
||||
private static final System.Logger LOG = System.getLogger("Umbrella");
|
||||
private static final String USER_DB = "/home/srichter/workspace/umbrella/data/umbrella.db";
|
||||
public static void main(String[] args) throws IOException {
|
||||
ColorLogger.setRootLogLevel(DEBUG);
|
||||
LOG.log(INFO, "Starting Umbrella:");
|
||||
var port = 8080;
|
||||
var threads = 16;
|
||||
var port = 8080;
|
||||
var threads = 16;
|
||||
var connectionProvider = new ConnectionProvider();
|
||||
var userDb = new SqliteDB(connectionProvider.get(USER_DB));
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||
server.setExecutor(Executors.newFixedThreadPool(threads));
|
||||
new WebHandler().bindPath("/").on(server);
|
||||
new UserModule().bindPath("/api/user").on(server);
|
||||
new UserModule(userDb).bindPath("/api/user").on(server);
|
||||
new Translations().bindPath("/api/translations").on(server);
|
||||
LOG.log(INFO,"Started web server at {0}",port);
|
||||
server.start();
|
||||
|
||||
@@ -40,7 +40,7 @@ subprojects {
|
||||
dependencies {
|
||||
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
implementation("de.srsoftware:tools.http:6.0.2")
|
||||
implementation("de.srsoftware:tools.http:6.0.3")
|
||||
implementation("de.srsoftware:tools.logging:1.3.2")
|
||||
}
|
||||
|
||||
|
||||
20
core/build.gradle.kts
Normal file
20
core/build.gradle.kts
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id("java")
|
||||
}
|
||||
|
||||
group = "de.srsoftware"
|
||||
version = "unspecified"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
implementation("org.xerial:sqlite-jdbc:3.49.0.0")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class AddableMap extends HashMap<String,Object> {
|
||||
public AddableMap plus(Object...param){
|
||||
if (param.length % 2 == 1) throw new RuntimeException("Expectirg number of parameters to be even");
|
||||
for (int i=0; i<param.length; i+=2) put(param[i].toString(),param[i+1]);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import org.sqlite.SQLiteDataSource;
|
||||
|
||||
public class ConnectionProvider extends HashMap<File, Connection> {
|
||||
public Connection get(Object o) {
|
||||
if (o instanceof String filename) o = new File(filename);
|
||||
if (o instanceof File dbFile) try {
|
||||
var conn = super.get(dbFile);
|
||||
if (conn == null) put(dbFile, conn = open(dbFile));
|
||||
return conn;
|
||||
} catch (SQLException sqle) {
|
||||
throw new RuntimeException(sqle);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Connection open(File dbFile) throws SQLException {
|
||||
SQLiteDataSource dataSource = new SQLiteDataSource();
|
||||
dataSource.setUrl("jdbc:sqlite:%s".formatted(dbFile));
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public class Constants {
|
||||
|
||||
public static final String ADDRESS = "address";
|
||||
public static final String ATTACHMENTS = "attachments";
|
||||
public static final String BODY = "body";
|
||||
public static final String DATA = "data";
|
||||
public static final String DATE = "date";
|
||||
public static final String DEFAULT_LANGUAGE = "en";
|
||||
public static final String DEFAULT_THEME = "winter";
|
||||
public static final String DESCRIPTION = "description";
|
||||
public static final String DOMAIN = "domain";
|
||||
public static final String EMAIL = "email";
|
||||
|
||||
public static final String ERROR_FAILED_CREATE_TABLE = "Failed to create \"{0}\" table!";
|
||||
public static final String ERROR_INVALID_FIELD = "Expected {0} to be {1}!";
|
||||
public static final String ERROR_MISSING_FIELD = "Json is missing {0} field!";
|
||||
public static final String ERROR_READ_TABLE = "Failed to read {0} from {1} table";
|
||||
|
||||
public static final String EXPIRATION = "expiration";
|
||||
public static final String ID = "id";
|
||||
public static final String KEY = "key";
|
||||
public static final String LANGUAGE = "language";
|
||||
public static final String LOGIN = "login";
|
||||
public static final String MESSAGES = "messages";
|
||||
public static final String NAME = "name";
|
||||
public static final String MIME = "mime";
|
||||
public static final String NUMBER = "number";
|
||||
public static final String OPTIONAL = "optional";
|
||||
public static final String PASSWORD = "password";
|
||||
|
||||
public static final String RECEIVERS = "receivers";
|
||||
public static final String REDIRECT = "redirect";
|
||||
public static final String SENDER = "sender";
|
||||
public static final String SETTINGS = "settings";
|
||||
public static final String STATE = "state";
|
||||
public static final String STATUS_CODE = "code";
|
||||
public static final String STRING = "string";
|
||||
public static final String SUBJECT = "subject";
|
||||
|
||||
public static final String TABLE_SETTINGS = "settings";
|
||||
|
||||
public static final String TEMPLATE = "template";
|
||||
public static final String TEXT = "text";
|
||||
public static final String THEME = "theme";
|
||||
public static final String TITLE = "title";
|
||||
public static final String TOKEN = "token";
|
||||
public static final String UMBRELLA = "Umbrella";
|
||||
public static final String URL = "url";
|
||||
public static final String USER = "user";
|
||||
public static final String USER_ID = "user_id";
|
||||
public static final String USER_LIST = "user_list";
|
||||
public static final String UTF8 = UTF_8.displayName();
|
||||
public static final String VALUE = "value";
|
||||
}
|
||||
19
core/src/main/java/de/srsoftware/umbrella/core/Paths.java
Normal file
19
core/src/main/java/de/srsoftware/umbrella/core/Paths.java
Normal file
@@ -0,0 +1,19 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
public class Paths {
|
||||
private Paths(){};
|
||||
|
||||
public static final String ADD = "add";
|
||||
public static final String CSS = "css";
|
||||
public static final String JSON = "json";
|
||||
public static final String LEGACY = "legacy";
|
||||
public static final String LIST = "list";
|
||||
public static final String LOGOUT = "logout";
|
||||
public static final String SEARCH = "search";
|
||||
public static final String SERVICE = "service";
|
||||
public static final String SETTINGS = "settings";
|
||||
public static final String SUBMIT = "submit";
|
||||
public static final String TOKEN = "token";
|
||||
public static final String VIEW = "view";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
public class ResponseCode {
|
||||
public static final int OK = 200;
|
||||
public static final int REDIRECT = 302;
|
||||
public static final int BAD_REQUEST = 400;
|
||||
public static final int UNAUTHORIZED = 401;
|
||||
public static final int FORBIDDEN = 403;
|
||||
public static final int NOT_FOUND = 404;
|
||||
public static final int UNPROCESSABLE = 422;
|
||||
public static final int SERVER_ERROR = 500;
|
||||
public static final int NOT_IMPLEMENTED = 501;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
|
||||
public class UmbrellaException extends Exception{
|
||||
private final int statusCode;
|
||||
|
||||
public UmbrellaException(int statusCode, String message){
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public UmbrellaException(int statusCode, String message, Object ... fills){
|
||||
this(statusCode,format(message,fills));
|
||||
}
|
||||
|
||||
public UmbrellaException causedBy(Exception e) {
|
||||
initCause(e);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int statusCode(){
|
||||
return statusCode;
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,20 @@
|
||||
translations_ready = true;
|
||||
});
|
||||
|
||||
import Footer from "./Components/Footer.svelte";
|
||||
import Homepage from "./Components/Homepage.svelte";
|
||||
import Login from "./Components/Login.svelte";
|
||||
import Menu from "./Components/Menu.svelte";
|
||||
</script>
|
||||
|
||||
{#if translations_ready }
|
||||
{#if user.username }
|
||||
<Menu />
|
||||
<Homepage />
|
||||
{:else}
|
||||
<Login />
|
||||
{/if}
|
||||
<Menu />
|
||||
{#if user.name }
|
||||
<Homepage />
|
||||
{:else}
|
||||
<Login />
|
||||
{/if}
|
||||
<Footer />
|
||||
{:else}
|
||||
<p>Loading translations...</p>
|
||||
{/if}
|
||||
6
frontend/src/Components/Footer.svelte
Normal file
6
frontend/src/Components/Footer.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import { t } from '../translations.svelte.js';
|
||||
</script>
|
||||
<footer>
|
||||
{@html t('footer.message','<a href="https://srsoftware.de">SRSoftware</a>')}
|
||||
</footer>
|
||||
@@ -2,4 +2,4 @@
|
||||
import { t } from '../translations.svelte.js';
|
||||
import { user } from '../user.svelte.js';
|
||||
</script>
|
||||
<h1>{t('home.Welcome')}, {user.username}</h1>
|
||||
<h1>{t('home.Welcome',user.name)}</h1>
|
||||
@@ -3,11 +3,13 @@
|
||||
import { user } from '../user.svelte.js';
|
||||
|
||||
function logout(){
|
||||
user.username = null;
|
||||
user.name = null;
|
||||
}
|
||||
</script>
|
||||
<nav>
|
||||
<a href="/">{t('nav.Home')}</a>
|
||||
<a href="https://svelte.dev/tutorial/svelte/state" target="_blank">{t('nav.Tutorial')}</a>
|
||||
{#if user.name }
|
||||
<a href="#" on:click={logout}>Logout</a>
|
||||
{/if}
|
||||
</nav>
|
||||
@@ -27,4 +27,11 @@ button{
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: yellow red red yellow;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 5px;
|
||||
}
|
||||
@@ -7,12 +7,13 @@ export async function loadTranslation(lang){
|
||||
translations.values = await fetch(url).then(resp => resp.json());
|
||||
}
|
||||
|
||||
export function t(key){
|
||||
export function t(key,...args){
|
||||
let set = translations.values;
|
||||
var keys = key.split('.');
|
||||
let keys = key.split('.');
|
||||
for (let key of keys){
|
||||
if (!set[key]) return keys[keys.length-1];
|
||||
set = set[key];
|
||||
}
|
||||
for (var i in args) set = set.replace(`{${i}}`,args[i]);
|
||||
return set;
|
||||
}
|
||||
@@ -4,3 +4,5 @@ include("backend")
|
||||
include("translations")
|
||||
include("user")
|
||||
include("web")
|
||||
|
||||
include("core")
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"footer": {
|
||||
"message" : "Umbrella ist ein Produkt von {0}."
|
||||
},
|
||||
"home" : {
|
||||
"Welcome" : "Willkommen"
|
||||
"Welcome" : "Willkommen, {0}"
|
||||
},
|
||||
"login" : {
|
||||
"do_login" : "anmelden",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
description = "Umbrella : User"
|
||||
|
||||
dependencies{
|
||||
implementation(project(":core"))
|
||||
implementation("de.srsoftware:tools.jdbc:1.3.2")
|
||||
implementation("de.srsoftware:tools.mime:1.1.2")
|
||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||
implementation("de.srsoftware:tools.util:2.0.3")
|
||||
implementation("org.json:json:20240303")
|
||||
implementation("org.xerial:sqlite-jdbc:3.49.0.0")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user;/* © SRSoftware 2025 */
|
||||
|
||||
import static de.srsoftware.tools.Strings.hex;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import de.srsoftware.tools.PasswordHasher;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* This is a bad hasher for several reasons:
|
||||
* First, it uses sha-1, which is regarded outdated nowadays.
|
||||
* Second, it does not use salted passwords, which makes it prone to rainbow table attacks
|
||||
*/
|
||||
public class BadHasher implements PasswordHasher<String> {
|
||||
private static final String SHA1 = "SHA-1";
|
||||
|
||||
private final MessageDigest digest;
|
||||
|
||||
/**
|
||||
* Create a new instance
|
||||
* @throws NoSuchAlgorithmException if SHA256 cannot be instantiated
|
||||
*/
|
||||
public BadHasher() throws NoSuchAlgorithmException {
|
||||
digest = MessageDigest.getInstance(SHA1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hash(String password, String ignored) {
|
||||
var bytes = digest.digest(password.getBytes(UTF_8));
|
||||
|
||||
return hex(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String salt(String hashedPassword) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class Constants {
|
||||
private Constants(){}
|
||||
|
||||
public static final String AUTH_ENDPOINT = "authorization_endpoint";
|
||||
public static final String AUTHORIZATION_CODE = "authorization_code";
|
||||
|
||||
public static final String CLIENT_ID = "client_id";
|
||||
public static final String CLIENT_SECRET = "client_secret";
|
||||
public static final String CODE = "code";
|
||||
|
||||
|
||||
public static final String DB_VERSION = "user_db_version";
|
||||
public static final String DEFAULT_FIELD = "sub";
|
||||
public static final Duration DEFAULT_SESSION_DURATION = Duration.ofMinutes(20);
|
||||
|
||||
public static final String FOREIGN_ID = "foreign_id";
|
||||
public static final String GRANT_TYPE = "grant_type";
|
||||
|
||||
public static final String IDS = "ids";
|
||||
public static final String ID_TOKEN = "id_token";
|
||||
public static final String JWKS_ENDPOINT = "jwks_uri";
|
||||
|
||||
public static final String LAST_LOGOFF = "last_logoff";
|
||||
public static final String LOGIN_SERVICES = "login_services";
|
||||
public static final String MESSAGE_DELIVERY = "message_delivery";
|
||||
public static final String OIDC_CALLBACK = "redirect_uri";
|
||||
public static final String OIDC_LINK = "oidc_link";
|
||||
public static final String OIDC_SCOPE = "openid";
|
||||
|
||||
public static final String PASS = "pass";
|
||||
|
||||
public static final String PATH_CALLBACK = "callback";
|
||||
public static final String PATH_DASH = "dash";
|
||||
public static final String PATH_IMPERSONATE = "impersonate";
|
||||
public static final String PATH_INSTALL = "install";
|
||||
public static final String PATH_JAVASCRIPT = "js";
|
||||
public static final String PATH_LOGIN = "login";
|
||||
public static final String PATH_MENU = "menu";
|
||||
public static final String PATH_NOTIFY = "notify";
|
||||
public static final String PATH_OIDC_BUTTONS = "oidc_buttons";
|
||||
public static final String PATH_OIDC_LOGIN = "oidc_login";
|
||||
public static final String PATH_OPENID_LOGIN = "openid_login";
|
||||
public static final String PATH_SESSION = "session";
|
||||
public static final String PATH_VALIDATE_TOKEN = "validateToken";
|
||||
|
||||
|
||||
public static final String REDIRECT_URI = "redirect_uri";
|
||||
public static final String RESPONSE_TYPE = "response_type";
|
||||
public static final String SCOPE = "scope";
|
||||
public static final String SERVICE = "service";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
|
||||
public static final String TABLE_LOGIN_SERVICES = "user_login_services";
|
||||
public static final String TABLE_SERVICE_IDS_USERS = "user_service_ids_users";
|
||||
public static final String TABLE_TOKENS = "tokens";
|
||||
public static final String TABLE_TOKEN_USES = "token_uses";
|
||||
public static final String TABLE_USERS = "users";
|
||||
|
||||
public static final String TOKEN_ENDPOINT = "token_endpoint";
|
||||
public static final String USER_INFO_FIELD = "user_info_field";
|
||||
public static final String USERNAME = "username";
|
||||
}
|
||||
@@ -1,21 +1,36 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.PASSWORD;
|
||||
import static de.srsoftware.umbrella.core.ResponseCode.*;
|
||||
import static de.srsoftware.umbrella.user.Constants.*;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.tools.MimeType;
|
||||
import de.srsoftware.tools.Path;
|
||||
import de.srsoftware.tools.PathHandler;
|
||||
import de.srsoftware.tools.SessionToken;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.user.api.UserDb;
|
||||
import de.srsoftware.umbrella.user.model.Password;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static de.srsoftware.tools.MimeType.MIME_JSON;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
|
||||
public class UserModule extends PathHandler {
|
||||
private static final BadHasher BAD_HASHER;
|
||||
private static final System.Logger LOG = System.getLogger("User");
|
||||
private final UserDb users;
|
||||
|
||||
static {
|
||||
try {
|
||||
BAD_HASHER = new BadHasher();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public UserModule(UserDb userDb){
|
||||
users = userDb;
|
||||
}
|
||||
|
||||
private HttpExchange addCors(HttpExchange ex){
|
||||
var headers = ex.getResponseHeaders();
|
||||
@@ -35,20 +50,25 @@ public class UserModule extends PathHandler {
|
||||
addCors(ex);
|
||||
var p = path.toString();
|
||||
switch (p){
|
||||
case "login": return postLogin(ex);
|
||||
case PATH_LOGIN: return postLogin(ex);
|
||||
}
|
||||
return super.doPost(path, ex);
|
||||
}
|
||||
|
||||
private boolean postLogin(HttpExchange ex) throws IOException {
|
||||
var json = json(ex);
|
||||
if (!(json.has("username") && json.get("username") instanceof String username)) return sendContent(ex,402,"Username missing");
|
||||
if (!(json.has("password") && json.get("password") instanceof String password)) return sendContent(ex,402,"Password missing");
|
||||
|
||||
if (!username.equals(password)) return sendContent(ex,401,"Login failed");
|
||||
var sessionId = UUID.randomUUID().toString();
|
||||
new SessionToken(sessionId).addTo(ex);
|
||||
ex.getResponseHeaders().add("Content-Type", MIME_JSON);
|
||||
return sendContent(ex,200,new JSONObject(Map.of("username",username)).toString());
|
||||
if (!(json.has(USERNAME) && json.get(USERNAME) instanceof String username)) return sendContent(ex,UNPROCESSABLE,"Username missing");
|
||||
if (!(json.has(PASSWORD) && json.get(PASSWORD) instanceof String password)) return sendContent(ex,UNPROCESSABLE,"Password missing");
|
||||
if (password.isBlank()) return sendContent(ex,UNAUTHORIZED,"Password must not be blank");
|
||||
var hashedPass = Password.of(BAD_HASHER.hash(password,null));
|
||||
try {
|
||||
var user = users.load(username, hashedPass);
|
||||
users.getSession(user)
|
||||
.cookie()
|
||||
.addTo(ex.getResponseHeaders());
|
||||
return sendContent(ex,200,user);
|
||||
} catch (UmbrellaException ue){
|
||||
return sendContent(ex,ue.statusCode(),ue.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.api;
|
||||
|
||||
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.user.model.ForeignLogin;
|
||||
import de.srsoftware.umbrella.user.model.LoginService;
|
||||
import java.util.List;
|
||||
|
||||
public interface LoginServiceDb {
|
||||
String delete(String serviceName) throws UmbrellaException;
|
||||
long getUserId(String loginService, String foreignUserId) throws UmbrellaException;
|
||||
List<ForeignLogin> listAssignments(long userId) throws UmbrellaException;
|
||||
List<LoginService> listLoginServices() throws UmbrellaException;
|
||||
LoginService loadLoginService(String id) throws UmbrellaException;
|
||||
ForeignLogin save(ForeignLogin foreignLogin) throws UmbrellaException;
|
||||
LoginService save(LoginService service) throws UmbrellaException;
|
||||
ForeignLogin unlink(ForeignLogin of) throws UmbrellaException;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.api;
|
||||
|
||||
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.user.model.DbUser;
|
||||
import de.srsoftware.umbrella.user.model.Password;
|
||||
import de.srsoftware.umbrella.user.model.Session;
|
||||
import de.srsoftware.umbrella.user.model.Token;
|
||||
import de.srsoftware.umbrella.user.model.UmbrellaUser;
|
||||
import java.util.List;
|
||||
|
||||
public interface UserDb {
|
||||
|
||||
Long delete(Long userId) throws UmbrellaException;
|
||||
|
||||
Boolean dropSession(Token token) throws UmbrellaException;
|
||||
|
||||
/**
|
||||
* Get a session for the provided user.
|
||||
* @param user
|
||||
* @return
|
||||
*/
|
||||
Session getSession(UmbrellaUser user) throws UmbrellaException;
|
||||
|
||||
Session extend(Session session) throws UmbrellaException;
|
||||
|
||||
List<UmbrellaUser> list(Integer start, Integer limit, Long ... ids) throws UmbrellaException;
|
||||
|
||||
Session load(Token token) throws UmbrellaException;
|
||||
|
||||
UmbrellaUser load(Long id) throws UmbrellaException;
|
||||
|
||||
UmbrellaUser load(Session session) throws UmbrellaException;
|
||||
|
||||
UmbrellaUser load(String key, Password password) throws UmbrellaException;
|
||||
|
||||
UmbrellaUser save(DbUser user) throws UmbrellaException;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class DbUser extends UmbrellaUser {
|
||||
|
||||
public enum PERMISSION {
|
||||
CREATE_USERS,
|
||||
DELETE_USERS,
|
||||
LIST_USERS,
|
||||
IMPERSONATE,
|
||||
MANAGE_LOGIN_SERVICES
|
||||
}
|
||||
|
||||
private final Set<PERMISSION> permissions;
|
||||
private final Password hashedPass;
|
||||
private final Long lastLogoff;
|
||||
|
||||
public DbUser(long id, String name, String email, Password hashedPassword, String theme, String languageCode, Set<PERMISSION> permissions, Long lastLogoff) {
|
||||
super(id, name, email, theme, languageCode);
|
||||
this.hashedPass = hashedPassword;
|
||||
this.permissions = permissions;
|
||||
this.lastLogoff = lastLogoff;
|
||||
}
|
||||
|
||||
public Password hashedPassword(){
|
||||
return hashedPass;
|
||||
}
|
||||
|
||||
public Long lastLogoff() {
|
||||
return lastLogoff;
|
||||
}
|
||||
|
||||
public boolean may(PERMISSION permission){
|
||||
return permissions.contains(permission);
|
||||
}
|
||||
|
||||
public Set<PERMISSION> permissions() {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
var map = super.toMap();
|
||||
map.put("permissions",permissions);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.USER_ID;
|
||||
import static de.srsoftware.umbrella.user.Constants.FOREIGN_ID;
|
||||
import static de.srsoftware.umbrella.user.Constants.SERVICE;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import java.util.Map;
|
||||
|
||||
public record ForeignLogin(String loginService, String foreingId, Long userId) implements Mappable {
|
||||
public static ForeignLogin of(String serviceName, String externalId, long userId) {
|
||||
return new ForeignLogin(serviceName,externalId,userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
return Map.of(SERVICE,loginService,FOREIGN_ID,foreingId, USER_ID,userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
|
||||
import static de.srsoftware.tools.Strings.base64;
|
||||
import static de.srsoftware.umbrella.core.Constants.NAME;
|
||||
import static de.srsoftware.umbrella.core.Constants.URL;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record LoginService(String name, String url, String clientId, String clientSecret, String userInfoField) implements Mappable {
|
||||
|
||||
@Override
|
||||
public Map<String,Object> toMap() {
|
||||
var map = new HashMap<String,Object>();
|
||||
map.put(NAME,name);
|
||||
map.put(URL,url);
|
||||
map.put("clientId",clientId);
|
||||
map.put("clientSecret",clientSecret);
|
||||
map.put("userInfoField",userInfoField);
|
||||
return map;
|
||||
}
|
||||
|
||||
public String basicAuth() {
|
||||
return "Basic "+base64((clientId+":"+clientSecret).getBytes(UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
public class Password implements CharSequence{
|
||||
|
||||
private final String pass;
|
||||
|
||||
private Password(String val){
|
||||
this.pass = val;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return pass.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int i) {
|
||||
return pass.charAt(i);
|
||||
}
|
||||
|
||||
public static Password of(String val){
|
||||
return new Password(val);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
return pass.subSequence(start,end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return pass;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
import de.srsoftware.tools.SessionToken;
|
||||
import java.time.Instant;
|
||||
|
||||
/* © SRSoftware 2025 */
|
||||
public record Session(UmbrellaUser user, Token token, Instant expiration) {
|
||||
public Session extended(Instant newExpiration) {
|
||||
return new Session(user,token,newExpiration);
|
||||
}
|
||||
|
||||
public SessionToken cookie() {
|
||||
return new SessionToken(token.toString(),"/",expiration,true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.TOKEN;
|
||||
|
||||
import de.srsoftware.tools.SessionToken;
|
||||
import de.srsoftware.umbrella.core.AddableMap;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Token implements CharSequence{
|
||||
|
||||
private final String token;
|
||||
|
||||
public Token(){
|
||||
token = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private Token(String value){
|
||||
this.token = value;
|
||||
}
|
||||
|
||||
public String asBearer() {
|
||||
return "Bearer "+token;
|
||||
}
|
||||
|
||||
public AddableMap asMap(){
|
||||
return new AddableMap().plus(TOKEN,token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int i) {
|
||||
return token.charAt(i);
|
||||
}
|
||||
|
||||
public static Token of(String val){
|
||||
return new Token(val);
|
||||
}
|
||||
|
||||
public static Token of(SessionToken sessionToken) {
|
||||
return new Token(sessionToken.sessionId());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return token.length();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
return token.subSequence(start,end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/* © SRSoftware 2025 */
|
||||
public class UmbrellaUser extends User implements Mappable {
|
||||
|
||||
private final long id;
|
||||
private final String theme, lang;
|
||||
|
||||
public UmbrellaUser(long id, String name, String email, String theme, String languageCode) {
|
||||
super(name,email);
|
||||
this.id = id;
|
||||
this.theme = theme;
|
||||
this.lang = languageCode;
|
||||
}
|
||||
|
||||
|
||||
public long id(){
|
||||
return id;
|
||||
}
|
||||
|
||||
public String language(){
|
||||
return lang;
|
||||
}
|
||||
|
||||
|
||||
public String theme(){
|
||||
return theme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String,Object> toMap() {
|
||||
var map = new HashMap<String,Object>();
|
||||
map.put(ID,id);
|
||||
map.put(LOGIN, name()); // this is still used by old umbrella modules
|
||||
map.put(NAME, name());
|
||||
map.put(EMAIL,email());
|
||||
map.put(THEME,theme);
|
||||
map.put(LANGUAGE,lang);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
import static de.srsoftware.tools.Optionals.nullable;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class User {
|
||||
|
||||
private final String email, name;
|
||||
|
||||
public User(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String email(){
|
||||
return email;
|
||||
}
|
||||
public String name(){
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof User user)) return false;
|
||||
return Objects.equals(email, user.email) && Objects.equals(name, user.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(email, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return format("{1}({0})", nullable(name()).orElse(email()),getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.sqlite;
|
||||
|
||||
import static de.srsoftware.tools.jdbc.Condition.*;
|
||||
import static de.srsoftware.tools.jdbc.Query.*;
|
||||
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.user.Constants.*;
|
||||
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.*;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import de.srsoftware.tools.jdbc.Query;
|
||||
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.user.api.LoginServiceDb;
|
||||
import de.srsoftware.umbrella.user.api.UserDb;
|
||||
import de.srsoftware.umbrella.user.model.*;
|
||||
import de.srsoftware.umbrella.user.model.Session;
|
||||
import de.srsoftware.umbrella.user.model.Token;
|
||||
import de.srsoftware.umbrella.user.model.UmbrellaUser;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
|
||||
public class SqliteDB implements LoginServiceDb, UserDb {
|
||||
private static final System.Logger LOG = System.getLogger(SqliteDB.class.getSimpleName());
|
||||
|
||||
private static final int INITIAL_DB_VERSION = 4;
|
||||
|
||||
private HashMap<String,Session> sessions = new HashMap<>();
|
||||
private final Connection db;
|
||||
|
||||
public SqliteDB(Connection conn){
|
||||
db = conn;
|
||||
init();
|
||||
}
|
||||
|
||||
|
||||
private void createLoginServiceTables() {
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
{1} VARCHAR(255),
|
||||
{2} TEXT,
|
||||
{3} VARCHAR(255),
|
||||
{4} VARCHAR(255),
|
||||
{5} VARCHAR(255),
|
||||
PRIMARY KEY ({1})
|
||||
)""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_LOGIN_SERVICES, NAME, URL,CLIENT_ID,CLIENT_SECRET, USER_INFO_FIELD));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_LOGIN_SERVICES,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
{1} VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
{2} INT NOT NULL
|
||||
)""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_SERVICE_IDS_USERS, SERVICE_ID, USER_ID));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SERVICE_IDS_USERS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
{1} VARCHAR(255) NOT NULL,
|
||||
{2} TEXT NOT NULL
|
||||
)""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_TOKEN_USES, TOKEN, DOMAIN));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_TOKEN_USES,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private int createSettingsTable() {
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL);
|
||||
""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_SETTINGS, KEY, VALUE));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Integer version = null;
|
||||
try {
|
||||
var rs = Query.select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
|
||||
if (rs.next()) version = rs.getInt(VALUE);
|
||||
rs.close();
|
||||
if (version == null) {
|
||||
version = INITIAL_DB_VERSION;
|
||||
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
|
||||
}
|
||||
|
||||
return version;
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_READ_TABLE,DB_VERSION,TABLE_SETTINGS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private int createTables() {
|
||||
createUserTables();
|
||||
createLoginServiceTables();
|
||||
return createSettingsTable();
|
||||
}
|
||||
|
||||
private void createUserTables() {
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
`{1}` INTEGER,
|
||||
`{2}` VARCHAR(255) NOT NULL,
|
||||
`{3}` VARCHAR(255) NOT NULL,
|
||||
`{4}` VARCHAR(50),
|
||||
`{5}` VARCHAR(255),
|
||||
`{6}` VARCHAR(100) DEFAULT "DELIVER INSTANTLY",
|
||||
`{7}` INT DEFAULT NULL,
|
||||
`{8}` TEXT,
|
||||
PRIMARY KEY({1})
|
||||
)""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_USERS, ID, LOGIN, PASSWORD, THEME, EMAIL, MESSAGE_DELIVERY, LAST_LOGOFF, SETTINGS));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_USERS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
{1} INT NOT NULL PRIMARY KEY,
|
||||
{2} VARCHAR(255),
|
||||
{3} INTEGER NOT NULL
|
||||
)""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_TOKENS, USER_ID, TOKEN, EXPIRATION));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_USERS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long delete(Long userId) throws UmbrellaException {
|
||||
try {
|
||||
Query.delete().from(TABLE_USERS).where(ID,equal(userId)).execute(db);
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to delete user with id = {0}!",userId,e);
|
||||
throw new UmbrellaException(500,"Failed to delete user with id = {0}!",userId).causedBy(e);
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String delete(String serviceName) throws UmbrellaException {
|
||||
try {
|
||||
Query.delete().from(TABLE_LOGIN_SERVICES).where(NAME,equal(serviceName)).execute(db);
|
||||
Query.delete().from(TABLE_SERVICE_IDS_USERS).where(SERVICE_ID,like(serviceName+":%")).execute(db);
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to delete login service {0}!",serviceName,e);
|
||||
throw new UmbrellaException(500,"Failed to delete login service {0}!",serviceName).causedBy(e);
|
||||
}
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
private void dropExpiredSessions() {
|
||||
LOG.log(DEBUG,"Dropping expired sessions.");
|
||||
try {
|
||||
var tokens = new HashSet<String>();
|
||||
var rs = select(TOKEN).from(TABLE_TOKENS).where(EXPIRATION,lessThan(now())).exec(db);
|
||||
while (rs.next()) tokens.add(rs.getString(TOKEN));
|
||||
rs.close();
|
||||
Query.delete().from(TABLE_TOKEN_USES).where(TOKEN,in(tokens.toArray())).execute(db);
|
||||
Query.delete().from(TABLE_TOKENS).where(TOKEN,in(tokens.toArray())).execute(db);
|
||||
} catch (SQLException sqle){
|
||||
LOG.log(WARNING,"Failed to drop expired sessions!",sqle);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean dropSession(Token token) throws UmbrellaException {
|
||||
LOG.log(DEBUG,"Dropping session associated with {0}.",token);
|
||||
try {
|
||||
return Query.delete().from(TABLE_TOKENS).where(TOKEN, equal(token)).execute(db);
|
||||
} catch (SQLException e){
|
||||
throw new UmbrellaException(500,"Failed to drop session token").causedBy(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session extend(Session session) throws UmbrellaException {
|
||||
try {
|
||||
Instant newExpiration = then();
|
||||
update(TABLE_TOKENS).set(EXPIRATION).where(TOKEN, equal(session.token())).prepare(db).apply(newExpiration.getEpochSecond());
|
||||
LOG.log(DEBUG,"Extended session of user {0} until {1}",session.user().name(),then());
|
||||
return session.extended(newExpiration);
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to extend session {0}",session.token());
|
||||
throw new UmbrellaException(500,"Failed to extend session {0}",session.token()).causedBy(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session getSession(UmbrellaUser user) throws UmbrellaException {
|
||||
if (user == null) throw new UmbrellaException(400,"User must not be null");
|
||||
dropExpiredSessions();
|
||||
LOG.log(DEBUG,"Trying to get session for \"{0}\"",user.name());
|
||||
Session session = null;
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_TOKENS).where(USER_ID,equal(user.id())).exec(db);
|
||||
if (rs.next()) session = new Session(user,Token.of(rs.getString(TOKEN)),Instant.ofEpochSecond(rs.getLong(EXPIRATION)));
|
||||
rs.close();
|
||||
if (session != null) return session;
|
||||
session = new Session(user,new Token(),then());
|
||||
insertInto(TABLE_TOKENS, USER_ID, TOKEN, EXPIRATION).values(session.user().id(),session.token(),session.expiration().getEpochSecond()).execute(db);
|
||||
return session;
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to request session for \"{0}\" from database",user.name());
|
||||
throw new UmbrellaException(500,"Failed to request session for \"{0}\" from database",user.name()).causedBy(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUserId(String loginService, String foreignUserId) throws UmbrellaException {
|
||||
Long userId = null;
|
||||
try {
|
||||
var rs = select(USER_ID).from(TABLE_SERVICE_IDS_USERS).where(SERVICE_ID,equal(loginService+":"+foreignUserId)).exec(db);
|
||||
if (rs.next()) userId = rs.getLong(USER_ID);
|
||||
rs.close();
|
||||
} catch (SQLException sqle){
|
||||
LOG.log(WARNING,"Failed to load user_id for '{0}' @ '{1}'!",foreignUserId,loginService);
|
||||
}
|
||||
if (userId == null) throw new UmbrellaException(500,"Failed to load user_id for \"{0}\" @ \"{1}\"!",foreignUserId,loginService);
|
||||
return userId;
|
||||
}
|
||||
|
||||
private void init(){
|
||||
var version = createTables();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<UmbrellaUser> list(Integer start, Integer limit, Long ... ids) throws UmbrellaException {
|
||||
var list = new ArrayList<UmbrellaUser>();
|
||||
try {
|
||||
var query = select(ALL).from(TABLE_USERS);
|
||||
if (start != null && start > 0) query.skip(start);
|
||||
if (limit != null && limit > 0) query.limit(limit);
|
||||
if (ids != null && ids.length>0) query.where(ID,in((Object[]) ids));
|
||||
var rs = query.exec(db);
|
||||
while (rs.next()) list.add(toUser(rs));
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to load user list from database!",e);
|
||||
throw new UmbrellaException(500,"Failed to load user list from database!").causedBy(e);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ForeignLogin> listAssignments(long userId) throws UmbrellaException {
|
||||
try {
|
||||
var list = new ArrayList<ForeignLogin>();
|
||||
var rs = select(ALL).from(TABLE_SERVICE_IDS_USERS).where(USER_ID,equal(userId)).exec(db);
|
||||
while (rs.next()) list.add(toForeignLogin(rs));
|
||||
rs.close();
|
||||
return list;
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to fetch {0} for user {1}",TABLE_SERVICE_IDS_USERS,userId,e);
|
||||
throw new UmbrellaException(500,"Failed to fetch {0} for user {1}",TABLE_SERVICE_IDS_USERS,userId).causedBy(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LoginService> listLoginServices() throws UmbrellaException {
|
||||
var list = new ArrayList<LoginService>();
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_LOGIN_SERVICES).exec(db);
|
||||
while (rs.next()) list.add(toLoginService(rs));
|
||||
rs.close();
|
||||
} catch (SQLException e){
|
||||
LOG.log(WARNING,"Failed to load login service list from database!",e);
|
||||
throw new UmbrellaException(500,"Failed to load login service list from database!");
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginService loadLoginService(String name) throws UmbrellaException {
|
||||
LoginService loginService = null;
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_LOGIN_SERVICES).where(NAME,equal(name)).exec(db);
|
||||
if (rs.next()) loginService = toLoginService(rs);
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to load login service \"{0}\"!",name);
|
||||
}
|
||||
if (loginService == null) throw new UmbrellaException(500,"Failed to load login service \"{0}\"!",name);
|
||||
return loginService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UmbrellaUser load(Long id) throws UmbrellaException {
|
||||
if (id == null) throw new UmbrellaException(400,"Id must not be null!");
|
||||
UmbrellaUser user = null;
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_USERS).where(ID,equal(id)).exec(db);
|
||||
if (rs.next()) user = toUser(rs);
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to load user \"{0}\"!",id);
|
||||
}
|
||||
if (user == null) throw new UmbrellaException(500,"Failed to load user \"{0}\"!",id);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UmbrellaUser load(Session session) throws UmbrellaException {
|
||||
if (session == null) throw new UmbrellaException(500,"Session must not be null!");
|
||||
UmbrellaUser user = null;
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_USERS).where(ID,equal(session.user().id())).exec(db);
|
||||
if (rs.next()) user = toUser(rs);
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to load user for session {0}!",session.token());
|
||||
}
|
||||
if (user == null) throw new UmbrellaException(500,"Failed to load user for session {0}!",session.token());
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UmbrellaUser load(String key, Password password) throws UmbrellaException {
|
||||
UmbrellaUser user = null;
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_USERS).where(PASS,equal(password)).exec(db);
|
||||
while (rs.next()){
|
||||
var email = rs.getString(EMAIL);
|
||||
if (key.equalsIgnoreCase(email)) {
|
||||
user = toUser(rs);
|
||||
break;
|
||||
}
|
||||
var login = rs.getString(LOGIN);
|
||||
if (key.equals(login)) {
|
||||
user = toUser(rs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to load user \"{0}\"!",key,e);
|
||||
|
||||
}
|
||||
if (user == null) throw new UmbrellaException(401,"Failed to load user \"{0}\"!",key);
|
||||
return user;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session load(Token token) throws UmbrellaException {
|
||||
dropExpiredSessions();
|
||||
Session session = null;
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_TOKENS).leftJoin(USER_ID,TABLE_USERS,ID).where(TOKEN,equal(token)).exec(db);
|
||||
if (rs.next()) session = new Session(toUser(rs),token,Instant.ofEpochSecond(rs.getLong(EXPIRATION)));
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to request session ({0}) from database",token);
|
||||
throw new UmbrellaException(500,"Failed to request session ({0}) from database",token);
|
||||
}
|
||||
if (session == null) throw new UmbrellaException(500,"Failed to request session ({0}) from database",token);
|
||||
return session;
|
||||
}
|
||||
|
||||
public long now() {
|
||||
return LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignLogin save(ForeignLogin assignment) throws UmbrellaException {
|
||||
try {
|
||||
insertInto(TABLE_SERVICE_IDS_USERS, SERVICE_ID, USER_ID).values(assignment.loginService()+":"+assignment.foreingId(),assignment.userId()).execute(db).close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to insert assignment into table {0}!",TABLE_SERVICE_IDS_USERS,e);
|
||||
throw new UmbrellaException(500,"Failed to insert assignment into table {0}!",TABLE_SERVICE_IDS_USERS).causedBy(e);
|
||||
}
|
||||
return assignment;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LoginService save(LoginService service) throws UmbrellaException {
|
||||
try {
|
||||
replaceInto(TABLE_LOGIN_SERVICES, NAME, URL,CLIENT_ID,CLIENT_SECRET)
|
||||
.values(service.name(),service.url(),service.clientId(),service.clientSecret())
|
||||
.execute(db)
|
||||
.close();
|
||||
} catch (SQLException e){
|
||||
LOG.log(WARNING,"Failed to store login service data for {0}!",service.name(),e);
|
||||
throw new UmbrellaException(500,"Failed to store login service for {0}!",service.name()).causedBy(e);
|
||||
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UmbrellaUser save(DbUser user) throws UmbrellaException {
|
||||
try {
|
||||
Long id = user.id();
|
||||
if (id<1) id = null;
|
||||
replaceInto(TABLE_USERS, ID, LOGIN, PASSWORD, THEME, EMAIL, LAST_LOGOFF)
|
||||
.values(id,user.name(),user.hashedPassword(),user.theme(),user.email(),user.lastLogoff())
|
||||
.execute(db)
|
||||
.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to store user data for {0}!",user.name(),e);
|
||||
throw new UmbrellaException(500,"Failed to store user data for {0}!",user.name()).causedBy(e);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public Instant then(){
|
||||
return LocalDateTime.now().plus(DEFAULT_SESSION_DURATION).toInstant(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private ForeignLogin toForeignLogin(ResultSet rs) throws SQLException {
|
||||
var serviceId = rs.getString(SERVICE_ID);
|
||||
var userId = rs.getLong(USER_ID);
|
||||
var parts = serviceId.split(":",2);
|
||||
var serviceName = parts[0];
|
||||
var externalId = parts[1];
|
||||
return ForeignLogin.of(serviceName,externalId,userId);
|
||||
}
|
||||
|
||||
private LoginService toLoginService(ResultSet rs) throws SQLException {
|
||||
return new LoginService(
|
||||
rs.getString(NAME),
|
||||
rs.getString(URL),
|
||||
rs.getString(CLIENT_ID),
|
||||
rs.getString(CLIENT_SECRET),
|
||||
rs.getString(USER_INFO_FIELD)
|
||||
);
|
||||
}
|
||||
|
||||
private DbUser toUser(ResultSet rs) throws SQLException {
|
||||
long id = rs.getLong(ID);
|
||||
Set<DbUser.PERMISSION> perms = id == 1 ? Set.of(CREATE_USERS,DELETE_USERS,IMPERSONATE, MANAGE_LOGIN_SERVICES,LIST_USERS) : Set.of();
|
||||
return new DbUser(
|
||||
id,
|
||||
rs.getString(LOGIN),
|
||||
rs.getString(EMAIL),
|
||||
Password.of(rs.getString(PASS)),
|
||||
rs.getString(THEME),
|
||||
"de", // TODO: save in DB
|
||||
perms,
|
||||
rs.getLong(LAST_LOGOFF)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignLogin unlink(ForeignLogin assignment) throws UmbrellaException {
|
||||
try {
|
||||
Query.delete().from(TABLE_SERVICE_IDS_USERS)
|
||||
.where(SERVICE_ID,equal(assignment.loginService()+":"+assignment.foreingId()))
|
||||
.where(USER_ID,equal(assignment.userId()))
|
||||
.execute(db);
|
||||
return assignment;
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to drop foreign login assignment: {0} - {1}",assignment.loginService(),assignment.foreingId(),e);
|
||||
throw new UmbrellaException(500,"Failed to drop foreign login assignment: {0} - {1}",assignment.loginService(),assignment.foreingId()).causedBy(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user