diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java index a94e612..dd82a5e 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java @@ -1,13 +1,24 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.bookmarks; -import static de.srsoftware.umbrella.bookmarks.Constants.CONFIG_DATABASE; +import static de.srsoftware.umbrella.bookmarks.Constants.*; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.Constants.URL; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED; +import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; +import de.srsoftware.tools.Path; +import de.srsoftware.tools.SessionToken; import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.api.UserService; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Token; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.io.IOException; +import java.util.Optional; public class BookmarkApi extends BaseHandler { private final BookmarkDb db; @@ -18,4 +29,31 @@ public class BookmarkApi extends BaseHandler { db = new SqliteDb(connect(dbFile)); users = userService; } + + @Override + public boolean doPost(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = users.loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case SAVE -> postBookmark(user.get(),ex); + case null, default -> super.doPost(path,ex); + }; + } catch (NumberFormatException e){ + return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); + } catch (UmbrellaException e){ + return send(ex,e); + } + } + + private boolean postBookmark(UmbrellaUser user, HttpExchange ex) throws IOException { + var json = json(ex); + if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingFieldException(URL); + if (!(json.has(COMMENT) && json.get(COMMENT) instanceof String comment)) throw missingFieldException(COMMENT); + var urlHash = db.save(url,comment, user.id()); + return sendContent(ex,urlHash); + } } diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java index e23a1e0..235a84c 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java @@ -2,4 +2,5 @@ package de.srsoftware.umbrella.bookmarks; public interface BookmarkDb { + String save(String url, String comment, long userId); } diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java index 4e816fd..9252719 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java @@ -2,7 +2,10 @@ package de.srsoftware.umbrella.bookmarks; public class Constants { + public static final String COMMENT = "comment"; public static final String CONFIG_DATABASE = "umbrella.modules.bookmark.database"; + public static final String HASH = "hash"; + public static final String SAVE = "save"; public static final String TABLE_URLS = "urls"; public static final String TABLE_URL_COMMENTS = "url_comments"; diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java index e153593..238158a 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java @@ -1,13 +1,16 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.bookmarks; -import static de.srsoftware.umbrella.bookmarks.Constants.TABLE_URLS; -import static de.srsoftware.umbrella.bookmarks.Constants.TABLE_URL_COMMENTS; +import static de.srsoftware.tools.jdbc.Query.insertInto; +import static de.srsoftware.tools.jdbc.Query.replaceInto; +import static de.srsoftware.umbrella.bookmarks.Constants.*; import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.ERROR_FAILED_CREATE_TABLE; +import static de.srsoftware.umbrella.core.Util.sha1; import static java.lang.System.Logger.Level.ERROR; import de.srsoftware.umbrella.core.BaseDb; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import java.sql.Connection; import java.sql.SQLException; @@ -30,10 +33,10 @@ public class SqliteDb extends BaseDb implements BookmarkDb { private void createUrlCommentsTable() { var sql = """ CREATE TABLE IF NOT EXISTS "url_comments" ( - `url_hash` VARCHAR ( 255 ) NOT NULL, + `hash` VARCHAR ( 255 ) NOT NULL, `user_id` LONG NOT NULL, `comment` TEXT NOT NULL, - PRIMARY KEY (`url_hash`,`user_id`) + PRIMARY KEY (`hash`,`user_id`) )"""; try { var stmt = db.prepareStatement(sql); @@ -56,4 +59,19 @@ CREATE TABLE IF NOT EXISTS "url_comments" ( throw new RuntimeException(e); } } + + @Override + public String save(String url, String comment, long userId) { + var hash = sha1(url); + try { + replaceInto(TABLE_URLS,HASH,URL) + .values(hash,url).execute(db).close(); + replaceInto(TABLE_URL_COMMENTS,HASH,USER_ID,COMMENT) + .values(hash,userId,comment) + .execute(db).close(); + return hash; + } catch (SQLException e) { + throw new UmbrellaException("Failed to store url"); + } + } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f8e0cea..235b074 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -11,6 +11,7 @@ repositories { dependencies { implementation("de.srsoftware:tools.mime:1.1.2") + implementation("de.srsoftware:tools.util:2.0.4") implementation("org.xerial:sqlite-jdbc:3.49.0.0") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Util.java b/core/src/main/java/de/srsoftware/umbrella/core/Util.java index bb2da1a..016519f 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Util.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Util.java @@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core; import static de.srsoftware.tools.MimeType.MIME_FORM_URL; import static de.srsoftware.tools.MimeType.MIME_JSON; +import static de.srsoftware.tools.Strings.hex; import static de.srsoftware.umbrella.core.Constants.*; import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.WARNING; @@ -16,6 +17,8 @@ import java.io.*; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; @@ -29,6 +32,15 @@ public class Util { private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*)@end(\\1)",Pattern.DOTALL); private static File plantumlJar = null; private static final JParsedown MARKDOWN = new JParsedown(); + private static final MessageDigest SHA1; + + static { + try { + SHA1 = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } private Util(){} @@ -155,6 +167,11 @@ public class Util { } } + public static String sha1(String plain){ + var bytes = SHA1.digest(plain.getBytes(UTF_8)); + return hex(bytes); + } + public static void setPlantUmlJar(File file){ LOG.log(INFO,"Using plantuml @ {0}",file.getAbsolutePath()); plantumlJar = file; diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 907258b..0315570 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -7,6 +7,7 @@ import AddDoc from "./routes/document/Add.svelte"; import AddTask from "./routes/task/Add.svelte"; + import Bookmarks from "./routes/bookmark/Index.svelte"; import Callback from "./routes/user/OidcCallback.svelte"; import DocList from "./routes/document/List.svelte"; import EditService from "./routes/user/EditService.svelte"; @@ -50,6 +51,7 @@ + diff --git a/frontend/src/Components/Menu.svelte b/frontend/src/Components/Menu.svelte index d92c793..307a01d 100644 --- a/frontend/src/Components/Menu.svelte +++ b/frontend/src/Components/Menu.svelte @@ -41,6 +41,7 @@ onMount(fetchModules); go('/project')}>{t('projects')} go('/task')}>{t('tasks')} go('/document')}>{t('documents')} + go('/bookmark')}>{t('bookmarks')} go('/notes')}>{t('notes')} {t('tutorial')} {#each modules as module,i}{module.name}{/each} diff --git a/frontend/src/routes/bookmark/Index.svelte b/frontend/src/routes/bookmark/Index.svelte new file mode 100644 index 0000000..a99c19e --- /dev/null +++ b/frontend/src/routes/bookmark/Index.svelte @@ -0,0 +1,43 @@ + + +
+ {t('Bookmarks')} + {#if error} + {error} + {/if} + + + +
\ No newline at end of file diff --git a/translations/src/main/resources/de.json b/translations/src/main/resources/de.json index e98f8da..bb8df03 100644 --- a/translations/src/main/resources/de.json +++ b/translations/src/main/resources/de.json @@ -15,6 +15,7 @@ "base_url": "Basis-URL", "basic_data": "Basis-Daten", "bookmark": "Lesezeichen", + "bookmarks": "Lesezeichen", "by": "von", "client_id": "Client-ID",