working on search
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Normal file
44
frontend/src/routes/search/Search.svelte
Normal file
@@ -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}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user