portal: gate onboarding link; revamp apps
This commit is contained in:
parent
2c52a23d8f
commit
712676a054
@ -39,6 +39,7 @@ def _client_ip() -> str:
|
|||||||
|
|
||||||
|
|
||||||
ONBOARDING_STEPS: tuple[str, ...] = (
|
ONBOARDING_STEPS: tuple[str, ...] = (
|
||||||
|
"keycloak_password_changed",
|
||||||
"vaultwarden_master_password",
|
"vaultwarden_master_password",
|
||||||
"element_recovery_key",
|
"element_recovery_key",
|
||||||
"element_recovery_key_stored",
|
"element_recovery_key_stored",
|
||||||
@ -223,7 +224,7 @@ def register(app) -> None:
|
|||||||
"status": status,
|
"status": status,
|
||||||
"username": row.get("username") or "",
|
"username": row.get("username") or "",
|
||||||
}
|
}
|
||||||
if status in {"accounts_building", "awaiting_onboarding", "ready"}:
|
if status in {"awaiting_onboarding", "ready"}:
|
||||||
response["onboarding_url"] = f"/onboarding?code={code}"
|
response["onboarding_url"] = f"/onboarding?code={code}"
|
||||||
if status in {"awaiting_onboarding", "ready"}:
|
if status in {"awaiting_onboarding", "ready"}:
|
||||||
completed = sorted(_fetch_completed_onboarding_steps(conn, code))
|
completed = sorted(_fetch_completed_onboarding_steps(conn, code))
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
<nav class="links">
|
<nav class="links">
|
||||||
<RouterLink to="/" class="nav-link">Home</RouterLink>
|
<RouterLink to="/" class="nav-link">Home</RouterLink>
|
||||||
<RouterLink to="/about" class="nav-link">About</RouterLink>
|
<RouterLink to="/about" class="nav-link">About</RouterLink>
|
||||||
<a href="https://cloud.bstein.dev" class="nav-link strong" target="_blank" rel="noreferrer">Cloud</a>
|
|
||||||
|
|
||||||
<template v-if="auth.enabled">
|
<template v-if="auth.enabled">
|
||||||
<template v-if="auth.authenticated">
|
<template v-if="auth.authenticated">
|
||||||
|
|||||||
@ -4,34 +4,129 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Atlas</p>
|
<p class="eyebrow">Atlas</p>
|
||||||
<h1>Apps</h1>
|
<h1>Apps</h1>
|
||||||
<p class="lede">Quick links to the lab services. Some apps open in a new tab for security reasons.</p>
|
<p class="lede">
|
||||||
</div>
|
Service shortcuts for Atlas. Nextcloud is the hub, but everything is available directly too.
|
||||||
<div class="hero-actions">
|
</p>
|
||||||
<a class="pill mono" href="https://cloud.bstein.dev" target="_blank" rel="noreferrer">Open Nextcloud</a>
|
|
||||||
<a class="pill mono" href="https://sso.bstein.dev" target="_blank" rel="noreferrer">Open Keycloak</a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section v-for="category in categories" :key="category.title" class="card category">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2>Service Grid</h2>
|
<div>
|
||||||
<span class="pill mono">apps + pipelines + observability</span>
|
<h2>{{ category.title }}</h2>
|
||||||
|
<p class="muted">{{ category.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tiles">
|
||||||
|
<a
|
||||||
|
v-for="app in category.apps"
|
||||||
|
:key="app.name"
|
||||||
|
class="tile"
|
||||||
|
:href="app.url"
|
||||||
|
:target="app.target"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<div class="tile-title">{{ app.name }}</div>
|
||||||
|
<div class="tile-desc">{{ app.description }}</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ServiceGrid :services="displayServices" />
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
const categories = [
|
||||||
import ServiceGrid from "../components/ServiceGrid.vue";
|
{
|
||||||
import { fallbackServices } from "../data/sample.js";
|
title: "Cloud",
|
||||||
|
description: "Files, photos, mail, calendars, and documents — the primary hub for Atlas users.",
|
||||||
const props = defineProps({
|
apps: [
|
||||||
serviceData: Object,
|
{
|
||||||
});
|
name: "Nextcloud",
|
||||||
|
url: "https://cloud.bstein.dev",
|
||||||
const displayServices = computed(() => props.serviceData?.services || fallbackServices().services);
|
target: "_blank",
|
||||||
|
description: "Your personal cloud storage and productivity suite.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Security",
|
||||||
|
description: "Modern password security and secrets tooling.",
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "Vaultwarden",
|
||||||
|
url: "https://vault.bstein.dev",
|
||||||
|
target: "_blank",
|
||||||
|
description: "Password manager (Bitwarden-compatible).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vault",
|
||||||
|
url: "https://secret.bstein.dev",
|
||||||
|
target: "_blank",
|
||||||
|
description: "Secrets management for infrastructure and apps.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Communications",
|
||||||
|
description: "Chat and meetings.",
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "AI Chat",
|
||||||
|
url: "/ai/chat",
|
||||||
|
target: "_self",
|
||||||
|
description: "Chat with Atlas AI (GPU-accelerated).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Meet",
|
||||||
|
url: "https://meet.bstein.dev",
|
||||||
|
target: "_blank",
|
||||||
|
description: "Video meetings (Jitsi).",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Streaming",
|
||||||
|
description: "Family media streaming and upload workflows.",
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "Jellyfin",
|
||||||
|
url: "https://stream.bstein.dev",
|
||||||
|
target: "_blank",
|
||||||
|
description: "Stream videos to desktop, mobile, and TV.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pegasus",
|
||||||
|
url: "https://pegasus.bstein.dev",
|
||||||
|
target: "_blank",
|
||||||
|
description: "Mobile-friendly upload/publish into Jellyfin.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Dev",
|
||||||
|
description: "Source control, CI, registry, GitOps, and observability.",
|
||||||
|
apps: [
|
||||||
|
{ name: "Gitea", url: "https://scm.bstein.dev", target: "_blank", description: "Git hosting and collaboration." },
|
||||||
|
{ name: "Jenkins", url: "https://ci.bstein.dev", target: "_blank", description: "CI pipelines and automation." },
|
||||||
|
{ name: "Harbor", url: "https://registry.bstein.dev", target: "_blank", description: "Artifact registry." },
|
||||||
|
{ name: "GitOps", url: "https://cd.bstein.dev", target: "_blank", description: "GitOps UI for Flux." },
|
||||||
|
{ name: "Grafana", url: "https://metrics.bstein.dev", target: "_blank", description: "Dashboards and monitoring." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Crypto",
|
||||||
|
description: "Local infrastructure for crypto workloads.",
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "Monero Node",
|
||||||
|
url: "https://monero.bstein.dev",
|
||||||
|
target: "_blank",
|
||||||
|
description: "Faster sync using the Atlas Monero node.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -49,6 +144,58 @@ const displayServices = computed(() => props.serviceData?.services || fallbackSe
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
padding: 18px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiles {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
transition: border-color 160ms ease, transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile:hover {
|
||||||
|
border-color: rgba(120, 180, 255, 0.35);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-title {
|
||||||
|
font-weight: 750;
|
||||||
|
color: var(--text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-desc {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
@ -67,10 +214,4 @@ h1 {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -23,6 +23,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="requestUsername" class="status-meta">
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="label mono">Username</span>
|
||||||
|
<span class="value mono">{{ requestUsername }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="status === 'pending'" class="steps">
|
<div v-if="status === 'pending'" class="steps">
|
||||||
<h3>Awaiting approval</h3>
|
<h3>Awaiting approval</h3>
|
||||||
<p class="muted">An Atlas admin has to approve this request before an account can be provisioned.</p>
|
<p class="muted">An Atlas admin has to approve this request before an account can be provisioned.</p>
|
||||||
@ -53,6 +60,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="checklist">
|
<ul class="checklist">
|
||||||
|
<li class="check-item">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="isStepDone('keycloak_password_changed')"
|
||||||
|
:disabled="!auth.authenticated || loading"
|
||||||
|
@change="toggleStep('keycloak_password_changed', $event)"
|
||||||
|
/>
|
||||||
|
<span>Change your Keycloak password</span>
|
||||||
|
</label>
|
||||||
|
<p class="muted">
|
||||||
|
Set a strong account password in Keycloak. Use the
|
||||||
|
<a href="https://sso.bstein.dev/realms/atlas/account" target="_blank" rel="noreferrer">account console</a>.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="check-item">
|
<li class="check-item">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
@ -127,6 +150,7 @@ import { auth, authFetch, login } from "../auth";
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const requestCode = ref("");
|
const requestCode = ref("");
|
||||||
|
const requestUsername = ref("");
|
||||||
const status = ref("");
|
const status = ref("");
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
@ -170,6 +194,7 @@ async function check() {
|
|||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
||||||
status.value = data.status || "unknown";
|
status.value = data.status || "unknown";
|
||||||
|
requestUsername.value = data.username || "";
|
||||||
onboarding.value = data.onboarding || { required_steps: [], completed_steps: [] };
|
onboarding.value = data.onboarding || { required_steps: [], completed_steps: [] };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message || "Failed to check status";
|
error.value = err.message || "Failed to check status";
|
||||||
@ -264,6 +289,29 @@ h1 {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-meta {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row .label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row .value {
|
||||||
|
color: var(--text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user