Browse Source

working on search

feature/document
Stephan Richter 4 months ago
parent
commit
b07533eaa9
  1. 4
      core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java
  2. 10
      frontend/src/App.svelte
  3. 44
      frontend/src/routes/search/Search.svelte
  4. 210
      legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java
  5. 15
      user/src/main/java/de/srsoftware/umbrella/user/UserModule.java
  6. 19
      web/src/main/java/de/srsoftware/umbrella/web/WebHandler.java

4
core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java

@ -15,7 +15,7 @@ import java.util.List;
public abstract class BaseHandler extends PathHandler { public abstract class BaseHandler extends PathHandler {
private static HttpExchange addCors(HttpExchange ex){ public HttpExchange addCors(HttpExchange ex){
var headers = ex.getRequestHeaders(); var headers = ex.getRequestHeaders();
var origin = nullable(headers.get("Origin")).orElse(List.of()).stream().filter(url -> url.contains("://localhost")||url.contains("://127.0.0.1")).findAny(); var origin = nullable(headers.get("Origin")).orElse(List.of()).stream().filter(url -> url.contains("://localhost")||url.contains("://127.0.0.1")).findAny();
if (origin.isPresent()) { if (origin.isPresent()) {
@ -25,7 +25,7 @@ public abstract class BaseHandler extends PathHandler {
headers.add("Access-Control-Allow-Origin", url); headers.add("Access-Control-Allow-Origin", url);
headers.add("Access-Control-Allow-Headers", "Content-Type"); headers.add("Access-Control-Allow-Headers", "Content-Type");
headers.add("Access-Control-Allow-Credentials", "true"); headers.add("Access-Control-Allow-Credentials", "true");
headers.add("Access-Control-Allow-Methods","GET, POST, PATCH"); headers.add("Access-Control-Allow-Methods","DELETE, GET, POST, PATCH");
} }
return ex; return ex;
} }

10
frontend/src/App.svelte

@ -8,6 +8,7 @@
import Footer from "./Components/Footer.svelte"; import Footer from "./Components/Footer.svelte";
import Login from "./Components/Login.svelte"; import Login from "./Components/Login.svelte";
import Menu from "./Components/Menu.svelte"; import Menu from "./Components/Menu.svelte";
import Search from "./routes/search/Search.svelte";
import User from "./routes/user/User.svelte"; import User from "./routes/user/User.svelte";
import UserEdit from "./routes/user/Edit.svelte"; import UserEdit from "./routes/user/Edit.svelte";
@ -31,10 +32,11 @@
{#if user.name } {#if user.name }
<!-- https://github.com/notnotsamuel/svelte-tiny-router --> <!-- https://github.com/notnotsamuel/svelte-tiny-router -->
<Menu /> <Menu />
<Route path="/user" component={User} /> <Route path="/search" component={Search} />
<Route path="/user/login" component={User} /> <Route path="/user" component={User} />
<Route path="/user/:user_id/edit" component={UserEdit} /> <Route path="/user/login" component={User} />
<Route path="/user/oidc/add" component={EditService} /> <Route path="/user/:user_id/edit" component={UserEdit} />
<Route path="/user/oidc/add" component={EditService} />
<Route path="/user/oidc/edit/:serviceName" component={EditService} /> <Route path="/user/oidc/edit/:serviceName" component={EditService} />
<Route> <Route>
<p>Page not found</p> <p>Page not found</p>

44
frontend/src/routes/search/Search.svelte

@ -0,0 +1,44 @@
<script>
import {t} from '../../translations.svelte.js';
let key = "";
let fulltext = false;
let html = "";
async function doSearch(ev){
ev.preventDefault();
const params = new URLSearchParams()
params.set('key', key);
if (fulltext) params.set('fulltext',true);
const url = `${location.protocol}//${location.host.replace('5173','8080')}/legacy/search?${params.toString()}`;
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
html = await resp.text();
if (!html) html = t('search.nothing_found');
}
}
</script>
<fieldset class="search">
<legend>{t('search.search')}</legend>
<form onsubmit={doSearch}>
<label>
{t('search.key')}
<input type="text" bind:value={key} />
</label>
<label>
<input type="checkbox" bind:checked={fulltext} />
{t('search.fulltext')}
</label>
<button type="submit">{t('search.go')}</button>
</form>
</fieldset>
{#if html}
<fieldset>
<legend>
{t('search.results')}
</legend>
{@html html}
</fieldset>
{/if}

210
legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java

@ -55,11 +55,11 @@ public class LegacyApi extends BaseHandler {
allowOrigin(ex, "*"); // add CORS header allowOrigin(ex, "*"); // add CORS header
yield load(path,ex); yield load(path,ex);
} }
case LOGIN -> getLegacyLogin(ex); case LOGIN -> getLogin(ex);
case LOGOUT-> logout(ex); case LOGOUT-> logout(ex);
case SEARCH -> { case SEARCH -> {
try { try {
yield legacySearch(ex); yield search(ex);
} catch (UmbrellaException e){ } catch (UmbrellaException e){
yield send(ex,e); yield send(ex,e);
} }
@ -68,54 +68,21 @@ public class LegacyApi extends BaseHandler {
}; };
} }
private boolean legacySearch(HttpExchange ex) throws IOException, UmbrellaException { @Override
var optToken = SessionToken.from(ex).map(Token::of); public boolean doPost(Path path, HttpExchange ex) throws IOException {
if (optToken.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); try {
var token = optToken.get(); return switch (path.pop()) {
var params = queryParam(ex); case JSON -> jsonUser(ex);
var key = params.get(KEY) instanceof String s ? s : null; case NOTIFY -> legacyNotify(ex);
if (key == null) throw new UmbrellaException(HTTP_BAD_REQUEST,"Missing search key"); case VALIDATE_TOKEN -> validateToken(ex);
var fulltext = key.contains("+") || "on".equals(params.get("fulltext")); default -> super.doPost(path,ex);
StringBuilder searchResult = new StringBuilder(); };
if (fulltext){ } catch (UmbrellaException e){
for (var module : config.keys()){
var baseUrl = config.get(module + ".baseUrl");
if (baseUrl.isEmpty()) continue;
var res = request(baseUrl.get()+"/search",token.asMap().plus(KEY,key),MIME_FORM_URL,token.asBearer());
if (!(res instanceof String content) || content.isBlank()) continue;
searchResult.append("<fieldset><label>").append(module).append("</label>");
searchResult.append(content).append("</fieldset>\n");
}
} else {
var bookmark = config.get("bookmark.baseUrl");
if (bookmark.isEmpty()) throw new UmbrellaException(500,"Tag search not available: Bookmark module not configured!");
var res = request(bookmark.get()+"/search",token.asMap().plus(KEY,key),MIME_FORM_URL,null);
if (!(res instanceof String content)) throw new UmbrellaException(500,"Search did not return html content!");
searchResult.append(content);
}
return sendContent(ex,searchResult.toString());
};
private boolean logout(HttpExchange ex) throws IOException {
var returnTo = queryParam(ex).get("returnTo");
var optToken = SessionToken.from(ex).map(Token::of);
if (optToken.isPresent()) try{
var token = optToken.get();
users.dropSession(token);
var expiredToken = new SessionToken(token.toString(),"/", Instant.now().minus(1, DAYS),true);
expiredToken.addTo(ex);
if (returnTo instanceof String location) return sendRedirect(ex,location);
return sendContent(ex, Map.of(REDIRECT,"home"));
} catch (UmbrellaException e) {
return send(ex,e); return send(ex,e);
} }
if (returnTo instanceof String location) return sendRedirect(ex,location);
return sendContent(ex,Map.of(REDIRECT,"home"));
} }
private boolean getLegacyLogin(HttpExchange ex) throws IOException { private boolean getLogin(HttpExchange ex) throws IOException {
var returnTo = queryParam(ex).get("returnTo"); var returnTo = queryParam(ex).get("returnTo");
if (returnTo instanceof String url) { if (returnTo instanceof String url) {
var token = SessionToken.from(ex).map(SessionToken::sessionId) var token = SessionToken.from(ex).map(SessionToken::sessionId)
@ -131,59 +98,8 @@ public class LegacyApi extends BaseHandler {
return sendRedirect(ex,location); return sendRedirect(ex,location);
} }
@Override private boolean jsonUser(HttpExchange ex) throws UmbrellaException, IOException {
public boolean doPost(Path path, HttpExchange ex) throws IOException { Map<String, Object> data = null;
try {
return switch (path.pop()) {
case JSON -> legacyJson(ex);
case NOTIFY -> legacyNotify(ex);
case VALIDATE_TOKEN -> validateToken(ex);
default -> super.doPost(path,ex);
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
private static String stripTrailingSlash(Object o){
String url = o.toString();
if (url.endsWith("/")) return url.substring(0,url.length()-1);
return url;
}
private boolean validateToken(HttpExchange ex) throws UmbrellaException, IOException {
String body;
try {
body = body(ex);
} catch (Exception e){
throw new UmbrellaException(400,"Failed to read request body").causedBy(e);
}
var map = decode(body);
LOG.log(DEBUG,"validateToken(…, {0}), data: {1}",map);
String domain = stripTrailingSlash(map.get(DOMAIN) instanceof String s ? s : "");
var keys = config.keys();
var match = false;
for (var key : keys){
var baseUrl = config.get(key + ".baseUrl").map(LegacyApi::stripTrailingSlash).orElse(null);
if (domain.equals(baseUrl)){
match = true;
break;
}
}
if (!match) throw new UmbrellaException(500,"Failed to verify request domain!");
var o = map.get(TOKEN);
if (!(o instanceof String token)) throw new UmbrellaException(500,"Request did not contain token!");
var session = users.load(Token.of(token));
var user = users.load(session);
var userMap = user.toMap();
userMap.put(TOKEN,Map.of(TOKEN,token,EXPIRATION,session.expiration().getEpochSecond()));
return sendContent(ex,userMap);
}
private boolean legacyJson(HttpExchange ex) throws UmbrellaException, IOException {
Map<String, Object> data = null;
try { try {
data = decode(body(ex)); data = decode(body(ex));
} catch (IOException e) { } catch (IOException e) {
@ -228,12 +144,6 @@ public class LegacyApi extends BaseHandler {
return sendContent(ex, userList.getFirst().toMap()); return sendContent(ex, userList.getFirst().toMap());
} }
protected Session requestSession(Token token) throws UmbrellaException {
var session = users.load(token);
session = users.extend(session);
return session;
}
private boolean legacyNotify(HttpExchange ex) throws UmbrellaException, IOException { private boolean legacyNotify(HttpExchange ex) throws UmbrellaException, IOException {
if (messageUrl == null) throw new UmbrellaException(500,"missing configuration: umbrella.modules.message.baseUrl"); if (messageUrl == null) throw new UmbrellaException(500,"missing configuration: umbrella.modules.message.baseUrl");
var mime = contentType(ex).orElse(null); var mime = contentType(ex).orElse(null);
@ -291,4 +201,94 @@ public class LegacyApi extends BaseHandler {
// TODO: should we return json? // TODO: should we return json?
return sendEmptyResponse(HTTP_OK,ex); return sendEmptyResponse(HTTP_OK,ex);
} }
private boolean logout(HttpExchange ex) throws IOException {
var returnTo = queryParam(ex).get("returnTo");
var optToken = SessionToken.from(ex).map(Token::of);
if (optToken.isPresent()) try{
var token = optToken.get();
users.dropSession(token);
var expiredToken = new SessionToken(token.toString(),"/", Instant.now().minus(1, DAYS),true);
expiredToken.addTo(ex);
if (returnTo instanceof String location) return sendRedirect(ex,location);
return sendContent(ex, Map.of(REDIRECT,"home"));
} catch (UmbrellaException e) {
return send(ex,e);
}
if (returnTo instanceof String location) return sendRedirect(ex,location);
return sendContent(ex,Map.of(REDIRECT,"home"));
}
protected Session requestSession(Token token) throws UmbrellaException {
var session = users.load(token);
session = users.extend(session);
return session;
}
private boolean search(HttpExchange ex) throws IOException, UmbrellaException {
var optToken = SessionToken.from(ex).map(Token::of);
if (optToken.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
var token = optToken.get();
var params = queryParam(ex);
var key = params.get(KEY) instanceof String s ? s : null;
if (key == null) throw new UmbrellaException(HTTP_BAD_REQUEST,"Missing search key");
var fulltext = key.contains("+") || "on".equals(params.get("fulltext"));
StringBuilder searchResult = new StringBuilder();
if (fulltext){
for (var module : config.keys()){
var baseUrl = config.get(module + ".baseUrl");
if (baseUrl.isEmpty()) continue;
var res = request(baseUrl.get()+"/search",token.asMap().plus(KEY,key),MIME_FORM_URL,token.asBearer());
if (!(res instanceof String content) || content.isBlank()) continue;
searchResult.append("<fieldset><label>").append(module).append("</label>");
searchResult.append(content).append("</fieldset>\n");
}
} else {
var bookmark = config.get("bookmark.baseUrl");
if (bookmark.isEmpty()) throw new UmbrellaException(500,"Tag search not available: Bookmark module not configured!");
var res = request(bookmark.get()+"/search",token.asMap().plus(KEY,key),MIME_FORM_URL,null);
if (!(res instanceof String content)) throw new UmbrellaException(500,"Search did not return html content!");
searchResult.append(content);
}
addCors(ex);
return sendContent(ex,searchResult.toString());
};
private static String stripTrailingSlash(Object o){
String url = o.toString();
if (url.endsWith("/")) return url.substring(0,url.length()-1);
return url;
}
private boolean validateToken(HttpExchange ex) throws UmbrellaException, IOException {
String body;
try {
body = body(ex);
} catch (Exception e){
throw new UmbrellaException(400,"Failed to read request body").causedBy(e);
}
var map = decode(body);
LOG.log(DEBUG,"validateToken(…, {0}), data: {1}",map);
String domain = stripTrailingSlash(map.get(DOMAIN) instanceof String s ? s : "");
var keys = config.keys();
var match = false;
for (var key : keys){
var baseUrl = config.get(key + ".baseUrl").map(LegacyApi::stripTrailingSlash).orElse(null);
if (domain.equals(baseUrl)){
match = true;
break;
}
}
if (!match) throw new UmbrellaException(500,"Failed to verify request domain!");
var o = map.get(TOKEN);
if (!(o instanceof String token)) throw new UmbrellaException(500,"Request did not contain token!");
var session = users.load(Token.of(token));
var user = users.load(session);
var userMap = user.toMap();
userMap.put(TOKEN,Map.of(TOKEN,token,EXPIRATION,session.expiration().getEpochSecond()));
return sendContent(ex,userMap);
}
} }

15
user/src/main/java/de/srsoftware/umbrella/user/UserModule.java

@ -72,20 +72,7 @@ public class UserModule extends BaseHandler {
logins = loginDb; logins = loginDb;
} }
private HttpExchange addCors(HttpExchange ex){
var headers = ex.getRequestHeaders();
var origin = nullable(headers.get("Origin")).orElse(List.of()).stream().filter(url -> url.contains("://localhost")||url.contains("://127.0.0.1")).findAny();
if (origin.isPresent()) {
var url = origin.get();
headers = ex.getResponseHeaders();
headers.add("Allow-Origin", url);
headers.add("Access-Control-Allow-Origin", url);
headers.add("Access-Control-Allow-Headers", "Content-Type");
headers.add("Access-Control-Allow-Credentials", "true");
headers.add("Access-Control-Allow-Methods","DELETE, GET, POST, PATCH");
}
return ex;
}
private boolean deleteOIDC(HttpExchange ex, UmbrellaUser user, Path path) throws IOException { private boolean deleteOIDC(HttpExchange ex, UmbrellaUser user, Path path) throws IOException {
var head = path.pop(); var head = path.pop();

19
web/src/main/java/de/srsoftware/umbrella/web/WebHandler.java

@ -9,26 +9,13 @@ import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.tools.Path; import de.srsoftware.tools.Path;
import de.srsoftware.tools.PathHandler; import de.srsoftware.tools.PathHandler;
import de.srsoftware.umbrella.core.BaseHandler;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
public class WebHandler extends PathHandler { public class WebHandler extends BaseHandler {
private HttpExchange addCors(HttpExchange ex){
var headers = ex.getRequestHeaders();
var origin = nullable(headers.get("Origin")).orElse(List.of()).stream().filter(url -> url.contains("://localhost")||url.contains("://127.0.0.1")).findAny();
if (origin.isPresent()) {
var url = origin.get();
headers = ex.getResponseHeaders();
headers.add("Allow-Origin", url);
headers.add("Access-Control-Allow-Origin", url);
headers.add("Access-Control-Allow-Headers", "Content-Type");
headers.add("Access-Control-Allow-Credentials", "true");
headers.add("Access-Control-Allow-Methods","GET, POST, PATCH");
}
return ex;
}
@Override @Override
public boolean doGet(Path path, HttpExchange ex) throws IOException { public boolean doGet(Path path, HttpExchange ex) throws IOException {

Loading…
Cancel
Save