implemented storing of bookmarks
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -1,13 +1,24 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.bookmarks;
|
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.ConnectionProvider.connect;
|
||||||
|
import static de.srsoftware.umbrella.core.Constants.URL;
|
||||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
|
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.configuration.Configuration;
|
||||||
|
import de.srsoftware.tools.Path;
|
||||||
|
import de.srsoftware.tools.SessionToken;
|
||||||
import de.srsoftware.umbrella.core.BaseHandler;
|
import de.srsoftware.umbrella.core.BaseHandler;
|
||||||
import de.srsoftware.umbrella.core.api.UserService;
|
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 {
|
public class BookmarkApi extends BaseHandler {
|
||||||
private final BookmarkDb db;
|
private final BookmarkDb db;
|
||||||
@@ -18,4 +29,31 @@ public class BookmarkApi extends BaseHandler {
|
|||||||
db = new SqliteDb(connect(dbFile));
|
db = new SqliteDb(connect(dbFile));
|
||||||
users = userService;
|
users = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doPost(Path path, HttpExchange ex) throws IOException {
|
||||||
|
addCors(ex);
|
||||||
|
try {
|
||||||
|
Optional<Token> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
package de.srsoftware.umbrella.bookmarks;
|
package de.srsoftware.umbrella.bookmarks;
|
||||||
|
|
||||||
public interface BookmarkDb {
|
public interface BookmarkDb {
|
||||||
|
String save(String url, String comment, long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
package de.srsoftware.umbrella.bookmarks;
|
package de.srsoftware.umbrella.bookmarks;
|
||||||
|
|
||||||
public class Constants {
|
public class Constants {
|
||||||
|
public static final String COMMENT = "comment";
|
||||||
public static final String CONFIG_DATABASE = "umbrella.modules.bookmark.database";
|
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_URLS = "urls";
|
||||||
public static final String TABLE_URL_COMMENTS = "url_comments";
|
public static final String TABLE_URL_COMMENTS = "url_comments";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.bookmarks;
|
package de.srsoftware.umbrella.bookmarks;
|
||||||
|
|
||||||
import static de.srsoftware.umbrella.bookmarks.Constants.TABLE_URLS;
|
import static de.srsoftware.tools.jdbc.Query.insertInto;
|
||||||
import static de.srsoftware.umbrella.bookmarks.Constants.TABLE_URL_COMMENTS;
|
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.*;
|
||||||
import static de.srsoftware.umbrella.core.Constants.ERROR_FAILED_CREATE_TABLE;
|
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 static java.lang.System.Logger.Level.ERROR;
|
||||||
|
|
||||||
import de.srsoftware.umbrella.core.BaseDb;
|
import de.srsoftware.umbrella.core.BaseDb;
|
||||||
|
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
@@ -30,10 +33,10 @@ public class SqliteDb extends BaseDb implements BookmarkDb {
|
|||||||
private void createUrlCommentsTable() {
|
private void createUrlCommentsTable() {
|
||||||
var sql = """
|
var sql = """
|
||||||
CREATE TABLE IF NOT EXISTS "url_comments" (
|
CREATE TABLE IF NOT EXISTS "url_comments" (
|
||||||
`url_hash` VARCHAR ( 255 ) NOT NULL,
|
`hash` VARCHAR ( 255 ) NOT NULL,
|
||||||
`user_id` LONG NOT NULL,
|
`user_id` LONG NOT NULL,
|
||||||
`comment` TEXT NOT NULL,
|
`comment` TEXT NOT NULL,
|
||||||
PRIMARY KEY (`url_hash`,`user_id`)
|
PRIMARY KEY (`hash`,`user_id`)
|
||||||
)""";
|
)""";
|
||||||
try {
|
try {
|
||||||
var stmt = db.prepareStatement(sql);
|
var stmt = db.prepareStatement(sql);
|
||||||
@@ -56,4 +59,19 @@ CREATE TABLE IF NOT EXISTS "url_comments" (
|
|||||||
throw new RuntimeException(e);
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("de.srsoftware:tools.mime:1.1.2")
|
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")
|
implementation("org.xerial:sqlite-jdbc:3.49.0.0")
|
||||||
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
|||||||
@@ -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_FORM_URL;
|
||||||
import static de.srsoftware.tools.MimeType.MIME_JSON;
|
import static de.srsoftware.tools.MimeType.MIME_JSON;
|
||||||
|
import static de.srsoftware.tools.Strings.hex;
|
||||||
import static de.srsoftware.umbrella.core.Constants.*;
|
import static de.srsoftware.umbrella.core.Constants.*;
|
||||||
import static java.lang.System.Logger.Level.*;
|
import static java.lang.System.Logger.Level.*;
|
||||||
import static java.lang.System.Logger.Level.WARNING;
|
import static java.lang.System.Logger.Level.WARNING;
|
||||||
@@ -16,6 +17,8 @@ import java.io.*;
|
|||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
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 final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*)@end(\\1)",Pattern.DOTALL);
|
||||||
private static File plantumlJar = null;
|
private static File plantumlJar = null;
|
||||||
private static final JParsedown MARKDOWN = new JParsedown();
|
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(){}
|
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){
|
public static void setPlantUmlJar(File file){
|
||||||
LOG.log(INFO,"Using plantuml @ {0}",file.getAbsolutePath());
|
LOG.log(INFO,"Using plantuml @ {0}",file.getAbsolutePath());
|
||||||
plantumlJar = file;
|
plantumlJar = file;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import AddDoc from "./routes/document/Add.svelte";
|
import AddDoc from "./routes/document/Add.svelte";
|
||||||
import AddTask from "./routes/task/Add.svelte";
|
import AddTask from "./routes/task/Add.svelte";
|
||||||
|
import Bookmarks from "./routes/bookmark/Index.svelte";
|
||||||
import Callback from "./routes/user/OidcCallback.svelte";
|
import Callback from "./routes/user/OidcCallback.svelte";
|
||||||
import DocList from "./routes/document/List.svelte";
|
import DocList from "./routes/document/List.svelte";
|
||||||
import EditService from "./routes/user/EditService.svelte";
|
import EditService from "./routes/user/EditService.svelte";
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
<!-- https://github.com/notnotsamuel/svelte-tiny-router -->
|
<!-- https://github.com/notnotsamuel/svelte-tiny-router -->
|
||||||
<Menu />
|
<Menu />
|
||||||
<Route path="/" component={User} />
|
<Route path="/" component={User} />
|
||||||
|
<Route path="/bookmark" component={Bookmarks} />
|
||||||
<Route path="/document" component={DocList} />
|
<Route path="/document" component={DocList} />
|
||||||
<Route path="/document/add" component={AddDoc} />
|
<Route path="/document/add" component={AddDoc} />
|
||||||
<Route path="/document/:id/send" component={SendDoc} />
|
<Route path="/document/:id/send" component={SendDoc} />
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ onMount(fetchModules);
|
|||||||
<a href="#" onclick={() => go('/project')}>{t('projects')}</a>
|
<a href="#" onclick={() => go('/project')}>{t('projects')}</a>
|
||||||
<a href="#" onclick={() => go('/task')}>{t('tasks')}</a>
|
<a href="#" onclick={() => go('/task')}>{t('tasks')}</a>
|
||||||
<a href="#" onclick={() => go('/document')}>{t('documents')}</a>
|
<a href="#" onclick={() => go('/document')}>{t('documents')}</a>
|
||||||
|
<a href="#" onclick={() => go('/bookmark')}>{t('bookmarks')}</a>
|
||||||
<a href="#" onclick={() => go('/notes')}>{t('notes')}</a>
|
<a href="#" onclick={() => go('/notes')}>{t('notes')}</a>
|
||||||
<a href="https://svelte.dev/tutorial/svelte/state" target="_blank">{t('tutorial')}</a>
|
<a href="https://svelte.dev/tutorial/svelte/state" target="_blank">{t('tutorial')}</a>
|
||||||
{#each modules as module,i}<a href={module.url}>{module.name}</a>{/each}
|
{#each modules as module,i}<a href={module.url}>{module.name}</a>{/each}
|
||||||
|
|||||||
43
frontend/src/routes/bookmark/Index.svelte
Normal file
43
frontend/src/routes/bookmark/Index.svelte
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script>
|
||||||
|
import { api } from '../../urls.svelte.js';
|
||||||
|
import { t } from '../../translations.svelte.js';
|
||||||
|
|
||||||
|
import Editor from '../../Components/MarkdownEditor.svelte';
|
||||||
|
|
||||||
|
let comment = $state({source:null,rendered:null});
|
||||||
|
let error = $state(null);
|
||||||
|
let link = $state(null);
|
||||||
|
|
||||||
|
async function onclick(ev){
|
||||||
|
let data = {
|
||||||
|
url : link,
|
||||||
|
comment : comment.source
|
||||||
|
};
|
||||||
|
const url = api('bookmark/save');
|
||||||
|
const resp = await fetch(url,{
|
||||||
|
credentials : 'include',
|
||||||
|
method : 'POST',
|
||||||
|
body : JSON.stringify(data)
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
} else {
|
||||||
|
error = await resp.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t('Bookmarks')}</legend>
|
||||||
|
{#if error}
|
||||||
|
<span class="error">{error}</span>
|
||||||
|
{/if}
|
||||||
|
<label>
|
||||||
|
{t('URL')}
|
||||||
|
<input bind:value={link} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{t('Comment')}
|
||||||
|
<Editor simple={true} bind:value={comment} />
|
||||||
|
</label>
|
||||||
|
<button {onclick}>{t('save')}</button>
|
||||||
|
</fieldset>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"base_url": "Basis-URL",
|
"base_url": "Basis-URL",
|
||||||
"basic_data": "Basis-Daten",
|
"basic_data": "Basis-Daten",
|
||||||
"bookmark": "Lesezeichen",
|
"bookmark": "Lesezeichen",
|
||||||
|
"bookmarks": "Lesezeichen",
|
||||||
"by": "von",
|
"by": "von",
|
||||||
|
|
||||||
"client_id": "Client-ID",
|
"client_id": "Client-ID",
|
||||||
|
|||||||
Reference in New Issue
Block a user