auth
This commit is contained in:
parent
937827d08b
commit
643203b8a5
9 changed files with 822 additions and 44 deletions
302
Cargo.lock
generated
302
Cargo.lock
generated
|
|
@ -208,6 +208,36 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"multer",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
|
|
@ -292,8 +322,11 @@ name = "bot"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"base64",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"jsonwebtoken",
|
||||
"migration",
|
||||
"reqwest",
|
||||
"sea-orm",
|
||||
|
|
@ -519,6 +552,18 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
|
|
@ -529,6 +574,33 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
|
|
@ -681,6 +753,44 @@ version = "0.15.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
|
|
@ -690,6 +800,27 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"hkdf",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"sec1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
|
|
@ -743,6 +874,22 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
|
|
@ -896,6 +1043,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -927,6 +1075,17 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
|
|
@ -987,6 +1146,30 @@ dependencies = [
|
|||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"headers-core",
|
||||
"http",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"sha1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers-core"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||
dependencies = [
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1348,6 +1531,29 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "10.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"ed25519-dalek",
|
||||
"getrandom 0.2.17",
|
||||
"hmac",
|
||||
"js-sys",
|
||||
"p256",
|
||||
"p384",
|
||||
"pem",
|
||||
"rand",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"signature",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
|
@ -1472,6 +1678,23 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http",
|
||||
"httparse",
|
||||
"memchr",
|
||||
"mime",
|
||||
"spin",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
|
|
@ -1651,6 +1874,30 @@ dependencies = [
|
|||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p384"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
|
|
@ -1680,6 +1927,16 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
|
|
@ -1785,6 +2042,15 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||
dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
|
|
@ -2005,6 +2271,16 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
|
|
@ -2332,6 +2608,20 @@ version = "4.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
|
|
@ -2490,6 +2780,18 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.2"
|
||||
|
|
|
|||
|
|
@ -20,3 +20,6 @@ uuid = { version = "1.8", features = ["v4", "serde"] }
|
|||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tokio-cron-scheduler = "0.15.1"
|
||||
dashmap = "6.1.0"
|
||||
jsonwebtoken = { version = "10.3.0", features = ["rsa", "rust_crypto"] }
|
||||
base64 = "0.22.1"
|
||||
axum-extra = { version = "0.9", features = ["typed-header"] }
|
||||
|
|
|
|||
|
|
@ -14,7 +14,23 @@
|
|||
</head>
|
||||
|
||||
<body class="dark-theme">
|
||||
<div id="app">
|
||||
<div id="login-overlay" class="login-overlay glass hidden">
|
||||
<div class="login-box">
|
||||
<span class="logo-icon">▲</span>
|
||||
<div class="login-title">Agency</div>
|
||||
<p>Secure Agent Dashboard</p>
|
||||
<button id="login-btn" class="btn btn-primary">Login with Authentik</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="callback-overlay" class="login-overlay glass hidden">
|
||||
<div class="login-box">
|
||||
<div class="loader"></div>
|
||||
<p>Authenticating with Authentik...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app" class="hidden">
|
||||
<aside class="sidebar glass">
|
||||
<header class="sidebar-header">
|
||||
<a href="#" id="logo-link" class="logo">
|
||||
|
|
@ -31,6 +47,11 @@
|
|||
<!-- Tasks will be injected here -->
|
||||
</ul>
|
||||
</nav>
|
||||
<footer class="sidebar-footer">
|
||||
<button id="logout-btn" class="btn btn-ghost btn-sm logout-btn">
|
||||
Logout
|
||||
</button>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
|
|
@ -127,7 +148,7 @@
|
|||
<div class="form-group">
|
||||
<label>Execution Schedule</label>
|
||||
<div id="schedule-presets" class="preset-group">
|
||||
<button type="button" class="btn btn-preset active" data-cron="">One-time</button>
|
||||
<button type="button" class="btn btn-preset active" data-cron="">Manual</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 */5 * * * *">Every 5m</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 0 * * * *">Hourly</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 0 0 * * *">Daily</button>
|
||||
|
|
@ -147,8 +168,7 @@
|
|||
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-ghost" id="cancel-task">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="submit-task-btn">Execute
|
||||
Directive</button>
|
||||
<button type="submit" class="btn btn-primary" id="submit-task-btn">Save Task</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@ import { marked } from 'marked';
|
|||
import DOMPurify from 'dompurify';
|
||||
|
||||
const API_URL = 'http://localhost:3000';
|
||||
// These should ideally be environment-specific
|
||||
const AUTH_CONFIG = {
|
||||
issuer: 'https://idm.flegr.me/application/o/bot/',
|
||||
clientId: 'CicDk8mpSBY1SW4ofCamO3B583ttmvKTSPDQwvpb',
|
||||
redirectUri: window.location.origin + '/callback',
|
||||
authorizeEndpoint: 'https://idm.flegr.me/application/o/authorize/',
|
||||
};
|
||||
|
||||
const state = {
|
||||
tasks: [],
|
||||
|
|
@ -9,9 +16,15 @@ const state = {
|
|||
selectedRunId: null,
|
||||
currentView: 'dashboard', // 'dashboard' or 'task'
|
||||
isEditing: false,
|
||||
token: localStorage.getItem('auth_token')
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
const loginOverlay = document.getElementById('login-overlay');
|
||||
const callbackOverlay = document.getElementById('callback-overlay');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const appEl = document.getElementById('app');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const taskListEl = document.getElementById('task-list');
|
||||
const newTaskBtn = document.getElementById('new-task-btn');
|
||||
const modalContainer = document.getElementById('modal-container');
|
||||
|
|
@ -42,9 +55,31 @@ const toggleCustomCronBtn = document.getElementById('toggle-custom-cron');
|
|||
const customCronContainer = document.getElementById('custom-cron-container');
|
||||
const presetBtns = document.querySelectorAll('.btn-preset');
|
||||
|
||||
// Wrapper for fetch to include Authorization header
|
||||
async function fetchWithAuth(url, options = {}) {
|
||||
if (!state.token) {
|
||||
showLogin();
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${state.token}`
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function fetchTasks() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks`);
|
||||
const response = await fetchWithAuth(`${API_URL}/tasks`);
|
||||
const newTasks = await response.json();
|
||||
|
||||
// Check if we should follow the latest run (if we were already watching it)
|
||||
|
|
@ -88,7 +123,7 @@ async function fetchTasks() {
|
|||
|
||||
async function fetchRecentRuns() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/runs/recent`);
|
||||
const response = await fetchWithAuth(`${API_URL}/runs/recent`);
|
||||
const recentRuns = await response.json();
|
||||
renderDashboard(recentRuns);
|
||||
} catch (error) {
|
||||
|
|
@ -230,8 +265,11 @@ function showTaskView(task) {
|
|||
|
||||
if (!run) {
|
||||
viewStatusEl.textContent = 'No runs';
|
||||
logsOutputEl.innerHTML = '';
|
||||
answerContainerEl.classList.add('hidden');
|
||||
viewStatusEl.className = 'status-badge pending';
|
||||
viewDateEl.textContent = '-';
|
||||
logsOutputEl.innerHTML = '<div class="waiting-placeholder">No logs available. Click "Run Task" to start the agent.</div>';
|
||||
answerContainerEl.classList.remove('hidden');
|
||||
answerOutputEl.innerHTML = '<div class="waiting-placeholder">Waiting for the first execution...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -245,12 +283,12 @@ function showTaskView(task) {
|
|||
minute: '2-digit'
|
||||
});
|
||||
|
||||
if (run.answer) {
|
||||
answerContainerEl.classList.remove('hidden');
|
||||
if (run.answer) {
|
||||
const rawHtml = marked.parse(run.answer);
|
||||
answerOutputEl.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||
} else {
|
||||
answerContainerEl.classList.add('hidden');
|
||||
answerOutputEl.innerHTML = '<div class="waiting-placeholder">Agent is working on the final answer...</div>';
|
||||
}
|
||||
|
||||
// Check if we should auto-scroll
|
||||
|
|
@ -279,7 +317,7 @@ rerunBtn.addEventListener('click', async () => {
|
|||
if (!state.selectedTaskId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
|
||||
const response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const updatedTask = await response.json();
|
||||
|
|
@ -289,15 +327,15 @@ rerunBtn.addEventListener('click', async () => {
|
|||
}
|
||||
selectTask(updatedTask.id);
|
||||
} catch (error) {
|
||||
console.error('Error re-running task:', error);
|
||||
alert('Failed to re-run task.');
|
||||
console.error('Error running task:', error);
|
||||
alert('Failed to run task.');
|
||||
}
|
||||
});
|
||||
|
||||
newTaskBtn.addEventListener('click', () => {
|
||||
state.isEditing = false;
|
||||
modalTitle.textContent = 'New Agent Task';
|
||||
submitTaskBtn.textContent = 'Execute Directive';
|
||||
submitTaskBtn.textContent = 'Save Task';
|
||||
goalTextarea.value = '';
|
||||
cronInput.value = '';
|
||||
updateScheduleUI('');
|
||||
|
|
@ -316,8 +354,8 @@ editTaskBtn.addEventListener('click', () => {
|
|||
if (!task) return;
|
||||
|
||||
state.isEditing = true;
|
||||
modalTitle.textContent = 'Edit Directive';
|
||||
submitTaskBtn.textContent = 'Update Directive';
|
||||
modalTitle.textContent = 'Edit Agent Task';
|
||||
submitTaskBtn.textContent = 'Save Task';
|
||||
goalTextarea.value = task.goal;
|
||||
cronInput.value = task.cron || '';
|
||||
updateScheduleUI(task.cron || '');
|
||||
|
|
@ -383,13 +421,13 @@ newTaskForm.addEventListener('submit', async (e) => {
|
|||
try {
|
||||
let response;
|
||||
if (state.isEditing) {
|
||||
response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}`, {
|
||||
response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${API_URL}/tasks`, {
|
||||
response = await fetchWithAuth(`${API_URL}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
|
|
@ -415,11 +453,6 @@ newTaskForm.addEventListener('submit', async (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
// Initial fetch
|
||||
fetchTasks();
|
||||
|
||||
// Auto-refresh every 3 seconds
|
||||
let isPolling = false;
|
||||
async function startAutoRefresh() {
|
||||
setInterval(async () => {
|
||||
|
|
@ -433,4 +466,71 @@ async function startAutoRefresh() {
|
|||
}, 3000);
|
||||
}
|
||||
|
||||
async function showLogin() {
|
||||
loginOverlay.classList.remove('hidden');
|
||||
appEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
state.token = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
showLogin();
|
||||
}
|
||||
|
||||
async function handleCallback() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get('code');
|
||||
if (!code) return;
|
||||
|
||||
window.history.replaceState({}, document.title, "/");
|
||||
callbackOverlay.classList.remove('hidden');
|
||||
loginOverlay.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/callback?code=${code}&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.access_token) {
|
||||
state.token = data.access_token;
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
callbackOverlay.classList.add('hidden');
|
||||
appEl.classList.remove('hidden');
|
||||
initializeApp();
|
||||
} else {
|
||||
throw new Error('No access token in response');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth callback failed:', error);
|
||||
alert('Authentication failed.');
|
||||
showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
loginBtn.addEventListener('click', () => {
|
||||
const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?client_id=${AUTH_CONFIG.clientId}&response_type=code&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}&scope=openid profile email`;
|
||||
window.location.href = authUrl;
|
||||
});
|
||||
|
||||
logoutBtn.addEventListener('click', () => {
|
||||
logout();
|
||||
});
|
||||
|
||||
async function initializeApp() {
|
||||
if (!state.token) {
|
||||
showLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
appEl.classList.remove('hidden');
|
||||
loginOverlay.classList.add('hidden');
|
||||
|
||||
await fetchTasks();
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
// Check for callback on load
|
||||
if (window.location.pathname === '/callback' || window.location.search.includes('code=')) {
|
||||
handleCallback();
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -434,6 +434,21 @@ body {
|
|||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.answer-output {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.waiting-placeholder {
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.answer-output h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
|
@ -712,3 +727,90 @@ textarea:focus {
|
|||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-dim);
|
||||
}
|
||||
|
||||
.login-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
background: rgba(10, 10, 15, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-box .logo-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.login-box p {
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.1);
|
||||
border-left-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
/* Styles for logout button, based on common patterns */
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
|
@ -22,7 +22,8 @@ impl Agent {
|
|||
) -> Self {
|
||||
let intro = format!(
|
||||
"You are an autonomous agent. You have access to tools that can help
|
||||
you achieve your goals. Use them wisely. Do not ask for clarification and use the
|
||||
you achieve your goals. Use them wisely. The user is unable to respond to you
|
||||
so do not ask for clarification and use the
|
||||
answer tool once you to give your final answer. current date is {}",
|
||||
Utc::now().to_rfc3339()
|
||||
);
|
||||
|
|
|
|||
149
src/auth.rs
Normal file
149
src/auth.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
pub exp: usize,
|
||||
pub iat: usize,
|
||||
pub iss: String,
|
||||
pub aud: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Jwk {
|
||||
kty: String,
|
||||
kid: String,
|
||||
n: String,
|
||||
e: String,
|
||||
alg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Jwks {
|
||||
keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
pub struct JwksVerifier {
|
||||
issuer: String,
|
||||
jwks_uri: String,
|
||||
keys: Arc<RwLock<Vec<Jwk>>>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl JwksVerifier {
|
||||
pub async fn new(issuer: String) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
// Authentik OIDC discovery
|
||||
let discovery_url = format!(
|
||||
"{}/.well-known/openid-configuration",
|
||||
issuer.trim_end_matches('/')
|
||||
);
|
||||
let config: serde_json::Value = client.get(&discovery_url).send().await?.json().await?;
|
||||
|
||||
let jwks_uri = config["jwks_uri"]
|
||||
.as_str()
|
||||
.ok_or("Missing jwks_uri in discovery")?
|
||||
.to_string();
|
||||
|
||||
let verifier = Self {
|
||||
issuer,
|
||||
jwks_uri,
|
||||
keys: Arc::new(RwLock::new(Vec::new())),
|
||||
client,
|
||||
};
|
||||
|
||||
verifier.refresh_keys().await?;
|
||||
Ok(verifier)
|
||||
}
|
||||
|
||||
pub async fn refresh_keys(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let jwks: Jwks = self.client.get(&self.jwks_uri).send().await?.json().await?;
|
||||
let mut keys = self.keys.write().await;
|
||||
*keys = jwks.keys;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify(&self, token: &str) -> Result<Claims, Box<dyn std::error::Error>> {
|
||||
let header = decode_header(token)?;
|
||||
let kid = header.kid.ok_or("Missing kid in token header")?;
|
||||
|
||||
let keys = self.keys.read().await;
|
||||
let jwk = keys
|
||||
.iter()
|
||||
.find(|k| k.kid == kid)
|
||||
.ok_or("Key not found in JWKS")?;
|
||||
|
||||
let decoding_key = DecodingKey::from_rsa_components(&jwk.n, &jwk.e)?;
|
||||
|
||||
let mut validation = Validation::new(Algorithm::RS256);
|
||||
validation.set_issuer(&[self.issuer.clone()]);
|
||||
// Aud validation might need careful config, usually it's the client_id
|
||||
validation.validate_aud = false;
|
||||
|
||||
let token_data = decode::<Claims>(token, &decoding_key, &validation)?;
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Authenticator {
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
token_url: String,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Authenticator {
|
||||
pub async fn new(
|
||||
issuer: String,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
let discovery_url = format!(
|
||||
"{}/.well-known/openid-configuration",
|
||||
issuer.trim_end_matches('/')
|
||||
);
|
||||
let config: serde_json::Value = client.get(&discovery_url).send().await?.json().await?;
|
||||
|
||||
let token_url = config["token_endpoint"]
|
||||
.as_str()
|
||||
.ok_or("Missing token_endpoint in discovery")?
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
client_id,
|
||||
client_secret,
|
||||
token_url,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn exchange_code(
|
||||
&self,
|
||||
code: String,
|
||||
redirect_uri: String,
|
||||
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
let params = [
|
||||
("grant_type", "authorization_code"),
|
||||
("code", &code),
|
||||
("redirect_uri", &redirect_uri),
|
||||
("client_id", &self.client_id),
|
||||
("client_secret", &self.client_secret),
|
||||
];
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(&self.token_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod agent;
|
||||
mod api;
|
||||
mod auth;
|
||||
mod entities;
|
||||
mod scheduler;
|
||||
mod server;
|
||||
|
|
|
|||
130
src/server.rs
130
src/server.rs
|
|
@ -1,16 +1,20 @@
|
|||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
Json, RequestPartsExt, Router,
|
||||
extract::{FromRef, FromRequestParts, Path, Query, State},
|
||||
http::{StatusCode, request::Parts},
|
||||
routing::{get, post},
|
||||
};
|
||||
use axum_extra::{
|
||||
TypedHeader,
|
||||
headers::{Authorization, authorization::Bearer},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, Database, DatabaseConnection, EntityTrait, QueryOrder, QuerySelect, Set,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agent::Agent;
|
||||
|
|
@ -25,6 +29,8 @@ pub struct AppState {
|
|||
pub scheduler: Arc<Scheduler>,
|
||||
pub zen_api_key: Option<String>,
|
||||
pub tavily_api_key: Option<String>,
|
||||
pub verifier: Arc<crate::auth::JwksVerifier>,
|
||||
pub authenticator: Arc<crate::auth::Authenticator>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -84,20 +90,43 @@ pub async fn start(db_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
let authentik_issuer =
|
||||
std::env::var("AUTHENTIK_ISSUER").map_err(|_| "AUTHENTIK_ISSUER not set")?;
|
||||
let authentik_client_id =
|
||||
std::env::var("AUTHENTIK_CLIENT_ID").map_err(|_| "AUTHENTIK_CLIENT_ID not set")?;
|
||||
let authentik_client_secret =
|
||||
std::env::var("AUTHENTIK_CLIENT_SECRET").map_err(|_| "AUTHENTIK_CLIENT_SECRET not set")?;
|
||||
|
||||
let verifier = Arc::new(crate::auth::JwksVerifier::new(authentik_issuer.clone()).await?);
|
||||
let authenticator = Arc::new(
|
||||
crate::auth::Authenticator::new(
|
||||
authentik_issuer,
|
||||
authentik_client_id,
|
||||
authentik_client_secret,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
db,
|
||||
scheduler,
|
||||
zen_api_key,
|
||||
tavily_api_key,
|
||||
verifier,
|
||||
authenticator,
|
||||
});
|
||||
|
||||
let cors = CorsLayer::permissive();
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/tasks", post(create_task).get(list_tasks))
|
||||
.route("/tasks/:id", get(get_task).put(update_task))
|
||||
.route("/tasks/:id/runs", post(rerun_task))
|
||||
.route("/runs/recent", get(get_recent_runs))
|
||||
.route("/auth/callback", get(auth_callback))
|
||||
.layer(cors)
|
||||
.with_state(state);
|
||||
|
||||
|
|
@ -109,6 +138,7 @@ pub async fn start(db_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
async fn list_tasks(
|
||||
_user: AuthenticatedUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<TaskResponse>>, (StatusCode, String)> {
|
||||
let tasks = Task::find()
|
||||
|
|
@ -142,6 +172,7 @@ async fn list_tasks(
|
|||
}
|
||||
|
||||
async fn create_task(
|
||||
_user: AuthenticatedUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<CreateTaskRequest>,
|
||||
) -> Result<Json<TaskResponse>, (StatusCode, String)> {
|
||||
|
|
@ -166,10 +197,11 @@ async fn create_task(
|
|||
let _ = state.scheduler.remove_task_job(task_id).await;
|
||||
}
|
||||
|
||||
execute_agent_run(state, task_id, payload.goal).await
|
||||
get_task_inner(task_id, &state).await.map(Json)
|
||||
}
|
||||
|
||||
async fn rerun_task(
|
||||
_user: AuthenticatedUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<TaskResponse>, (StatusCode, String)> {
|
||||
|
|
@ -234,10 +266,11 @@ async fn execute_agent_run(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
get_task(Path(task_id), State(state)).await
|
||||
get_task_inner(task_id, &state).await.map(Json)
|
||||
}
|
||||
|
||||
async fn update_task(
|
||||
_user: AuthenticatedUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateTaskRequest>,
|
||||
|
|
@ -262,29 +295,95 @@ async fn update_task(
|
|||
let _ = state.scheduler.remove_task_job(id).await;
|
||||
}
|
||||
|
||||
get_task(Path(id), State(state)).await
|
||||
get_task_inner(id, &state).await.map(Json)
|
||||
}
|
||||
|
||||
pub struct AuthenticatedUser(pub crate::auth::Claims);
|
||||
|
||||
#[axum::async_trait]
|
||||
impl<S> FromRequestParts<S> for AuthenticatedUser
|
||||
where
|
||||
Arc<AppState>: axum::extract::FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, String);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let app_state = Arc::<AppState>::from_ref(state);
|
||||
|
||||
let TypedHeader(Authorization(bearer)) = parts
|
||||
.extract::<TypedHeader<Authorization<Bearer>>>()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Missing or invalid Authorization header".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let claims = app_state
|
||||
.verifier
|
||||
.verify(bearer.token())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("Token verification failed: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(AuthenticatedUser(claims))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AuthCallbackQuery {
|
||||
pub code: String,
|
||||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
async fn auth_callback(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<AuthCallbackQuery>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
state
|
||||
.authenticator
|
||||
.exchange_code(query.code, query.redirect_uri)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Token exchange failed: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_task(
|
||||
_user: AuthenticatedUser,
|
||||
Path(id): Path<Uuid>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<TaskResponse>, (StatusCode, String)> {
|
||||
get_task_inner(id, &state).await.map(Json)
|
||||
}
|
||||
|
||||
async fn get_task_inner(id: Uuid, state: &AppState) -> Result<TaskResponse, (StatusCode, String)> {
|
||||
let results = Task::find_by_id(id)
|
||||
.find_with_related(TaskRun)
|
||||
.all(&state.db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let (task, runs) = results
|
||||
let (t, runs) = results
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or((StatusCode::NOT_FOUND, "Task not found".to_string()))?;
|
||||
|
||||
Ok(Json(TaskResponse {
|
||||
id: task.id,
|
||||
goal: task.goal,
|
||||
cron: task.cron,
|
||||
created_at: task.created_at,
|
||||
Ok(TaskResponse {
|
||||
id: t.id,
|
||||
goal: t.goal,
|
||||
cron: t.cron,
|
||||
created_at: t.created_at,
|
||||
runs: runs
|
||||
.into_iter()
|
||||
.map(|r| TaskRunResponse {
|
||||
|
|
@ -295,10 +394,11 @@ async fn get_task(
|
|||
created_at: r.created_at,
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_recent_runs(
|
||||
_user: AuthenticatedUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<RecentRunResponse>>, (StatusCode, String)> {
|
||||
let results = TaskRun::find()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue