Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,15 @@ static final class Dashboard implements Callable<Integer> {
@Option(names = "--static", description = "Directory of the prebuilt SPA.")
String staticDir;

@Option(
names = "--insecure-cookies",
description = "Drop the Secure cookie attribute (for local HTTP development).")
boolean insecureCookies;

@Override
public Integer call() throws Exception {
try (Taskito queue = parent.open();
DashboardServer server = DashboardServer.start(queue, port, token, staticDir)) {
DashboardServer server = DashboardServer.start(queue, port, token, staticDir, !insecureCookies)) {
System.out.println("dashboard on http://localhost:" + server.port());
new CountDownLatch(1).await();
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.byteveda.taskito.dashboard;
package org.byteveda.taskito.dashboard.api;

import java.util.LinkedHashMap;
import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.byteveda.taskito.dashboard.api;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.byteveda.taskito.Taskito;
import org.byteveda.taskito.model.JobFilter;
import org.byteveda.taskito.model.JobStatus;

/**
* Read and action handlers for jobs, queues, dead-letters, metrics, and
* workers. Each returns the snake_case JSON body (via {@link Contract}) or
* {@code null} to signal 404.
*/
public final class CoreHandlers {
private static final long DEFAULT_LIMIT = 50;

private final Taskito queue;

public CoreHandlers(Taskito queue) {
this.queue = queue;
}

public Object stats() {
return Contract.stats(queue.stats());
}

public Object statsByQueue() {
Map<String, Object> out = new LinkedHashMap<>();
queue.statsAllQueues().forEach((name, stats) -> out.put(name, Contract.stats(stats)));
return out;
}

public Object queuesPaused() {
return queue.listPausedQueues();
}

public Object listJobs(Map<String, String> query) {
JobFilter.Builder filter = JobFilter.builder();
if (query.containsKey("status")) {
filter.status(JobStatus.fromWire(query.get("status")));
}
if (query.containsKey("queue")) {
filter.queue(query.get("queue"));
}
if (query.containsKey("task")) {
filter.task(query.get("task"));
}
if (query.containsKey("limit")) {
filter.limit(Integer.parseInt(query.get("limit")));
}
if (query.containsKey("offset")) {
filter.offset(Integer.parseInt(query.get("offset")));
}
return queue.listJobs(filter.build()).stream().map(Contract::job).collect(Collectors.toList());
}

public Object job(String id) {
return queue.getJob(id).map(Contract::job).orElse(null);
}

public Object listDead(Map<String, String> query) {
long limit = longParam(query, "limit", DEFAULT_LIMIT);
long offset = longParam(query, "offset", 0);
return queue.listDead(limit, offset).stream().map(Contract::dead).collect(Collectors.toList());
}

public Object listMetrics(Map<String, String> query) {
long since = longParam(query, "since", 0);
return queue.metrics(query.get("task"), since).stream()
.map(Contract::metric)
.collect(Collectors.toList());
}

public Object listWorkers() {
return queue.listWorkers().stream().map(Contract::worker).collect(Collectors.toList());
}

public Object cancel(String id) {
return Map.of("cancelled", queue.cancel(id));
}

public Object retryDead(String id) {
return Map.of("id", queue.retryDead(id));
}

public Object pause(String name) {
queue.queue(name).pause();
return Map.of("ok", true);
}

public Object resume(String name) {
queue.queue(name).resume();
return Map.of("ok", true);
}

private static long longParam(Map<String, String> query, String key, long fallback) {
String value = query.get(key);
return value == null ? fallback : Long.parseLong(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Feature endpoint handlers and the snake_case wire contract mappers. */
package org.byteveda.taskito.dashboard.api;
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.byteveda.taskito.dashboard.auth;

import java.util.LinkedHashMap;
import java.util.Map;
import org.byteveda.taskito.dashboard.support.DashboardError;

/**
* The {@code /api/auth/*} session endpoints: status, setup, login, logout,
* whoami, change-password. Handlers return the JSON body; the server applies
* cookie side effects (setting them on login, clearing them on logout) and
* redacts the raw session token from the login response.
*/
public final class AuthHandlers {
private final AuthStore store;

public AuthHandlers(AuthStore store) {
this.store = store;
}

public Map<String, Object> status() {
return Map.of("setup_required", store.countUsers() == 0);
}

public Map<String, Object> setup(Map<String, Object> body) {
if (store.countUsers() != 0) {
throw DashboardError.badRequest("setup already complete");
}
String username = requireField(body, "username");
String password = requireField(body, "password");
User user = store.createUser(username, password, AuthStore.ROLE_ADMIN);
return Map.of("user", serializeUser(user));
}

public Map<String, Object> login(Map<String, Object> body) {
if (store.countUsers() == 0) {
throw DashboardError.badRequest("setup_required");
}
String username = requireField(body, "username");
String password = requireField(body, "password");
User user = store.authenticate(username, password);
if (user == null) {
throw DashboardError.badRequest("invalid_credentials");
}
Session session = store.createSession(user.username(), user.role());
Map<String, Object> out = new LinkedHashMap<>();
out.put("user", serializeUser(user));
out.put("session", serializeSessionWithToken(session));
return out;
}

public Map<String, Object> logout(RequestContext ctx) {
if (ctx.session() != null) {
store.deleteSession(ctx.session().token());
}
return Map.of("ok", true);
}

public Map<String, Object> whoami(RequestContext ctx) {
Session session = ctx.session();
User user = store.getUser(session.username()).orElse(null);
if (user == null) {
store.deleteSession(session.token());
throw DashboardError.notFound("not_authenticated");
}
Map<String, Object> out = new LinkedHashMap<>();
out.put("user", serializeUser(user));
out.put("csrf_token", session.csrfToken());
out.put("expires_at", session.expiresAt());
return out;
}

public Map<String, Object> changePassword(RequestContext ctx, Map<String, Object> body) {
Session session = ctx.session();
String oldPassword = requireField(body, "old_password");
String newPassword = requireField(body, "new_password");
User user = store.getUser(session.username()).orElse(null);
if (user == null) {
throw DashboardError.badRequest("not_authenticated");
}
if (!store.verifyPassword(user, oldPassword)) {
throw DashboardError.badRequest("invalid_credentials");
}
store.updatePassword(session.username(), newPassword);
return Map.of("ok", true);
}

// ---- serialization -----------------------------------------------------

private static Map<String, Object> serializeUser(User user) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("username", user.username());
m.put("role", user.role());
m.put("created_at", user.createdAt());
m.put("last_login_at", user.lastLoginAt());
return m;
}

/** Login session body — includes the raw {@code token}, redacted by the server. */
private static Map<String, Object> serializeSessionWithToken(Session session) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("username", session.username());
m.put("role", session.role());
m.put("expires_at", session.expiresAt());
m.put("csrf_token", session.csrfToken());
m.put("token", session.token());
return m;
}

private static String requireField(Map<String, Object> body, String key) {
Object value = body == null ? null : body.get(key);
if (value instanceof String s && !s.isEmpty()) {
return s;
}
throw DashboardError.badRequest("missing or empty field '" + key + "'");
}
}
Loading