feat: Multi-User/Admin usermanagement + Galaxy Login/Settings + Task detail improvements
- Backend: NEW AdminController with user CRUD (GET/POST/DELETE /api/v1/admin/users)
- Backend: NEW GET /api/dashboard/tasks/{id} single task endpoint
- Backend: NEW POST /api/dashboard/tasks/{id}/activity comment endpoint
- Backend: IUserRepository + UserRepository extended with GetAllAsync, DeleteAsync
- Backend: Admin DTOs (AdminUserInfo, AdminCreateUserRequest, AdminUpdateRoleRequest)
- Frontend: NEW TaskDetailView.vue — URL-based (/tasks/:id) Galaxy-themed task detail
with subtask create/edit/delete, activity with comments, property sidebar
- Frontend: LoginView.vue — полностью Galaxy theme redesign with GalaxyBackground,
glass-morphism card, password toggle, consistent brand
- Frontend: SettingsView.vue — Galaxy theme redesign with glass cards,
admin user management section (create/list users, visible only to owner role)
- Frontend: TaskBoardView.vue — added "Full View" link to URL-based detail page
- Frontend: Router — added /tasks/:id route for TaskDetailView
- Frontend: App.vue — added TaskDetail to standaloneViews whitelist
- Frontend: tasks store — stable
Auth: Admin creates accounts, users log in with existing /api/v1/auth/login.
Login/Settings deliver visible Galaxy-consistent design with nexus-tokens.css tokens.
This commit is contained in:
@@ -32,7 +32,7 @@ const navigate = (label: string) => {
|
||||
}
|
||||
const mobileNavOpen = ref(false)
|
||||
|
||||
const standaloneViews = computed(() => ['Dashboard', 'Settings', 'ProjectDetail', 'Memory', 'Docs', 'Security', 'Incidents', 'Calendar', 'AgentDetail', 'Agents', 'Task Board', 'Notifications'].includes(activeView.value))
|
||||
const standaloneViews = computed(() => ['Dashboard', 'Settings', 'ProjectDetail', 'Memory', 'Docs', 'Security', 'Incidents', 'Calendar', 'AgentDetail', 'Agents', 'Task Board', 'TaskDetail', 'Notifications'].includes(activeView.value))
|
||||
|
||||
onMounted(() => {
|
||||
if (auth.isAuthenticated) store.refresh()
|
||||
|
||||
@@ -12,6 +12,7 @@ import CalendarView from './views/CalendarView.vue'
|
||||
import NexusLayout from './layouts/NexusLayout.vue'
|
||||
import FlowBoard from './views/Dashboard/FlowBoard.vue'
|
||||
import TaskBoardView from './views/TaskBoardView.vue'
|
||||
import TaskDetailView from './views/TaskDetailView.vue'
|
||||
import NotificationsView from './views/NotificationsView.vue'
|
||||
|
||||
const routes = [
|
||||
@@ -36,6 +37,7 @@ const routes = [
|
||||
{ path: '/projects', name: 'Projects', component: { template: '' } },
|
||||
{ path: '/projects/:id', name: 'ProjectDetail', component: ProjectDetailView },
|
||||
{ path: '/tasks', name: 'Task Board', component: TaskBoardView },
|
||||
{ path: '/tasks/:id', name: 'TaskDetail', component: TaskDetailView },
|
||||
{ path: '/agents', name: 'Agents', component: AgentsIndexView },
|
||||
{ path: '/models', name: 'Models', component: { template: '' } },
|
||||
{ path: '/activity', name: 'Activity', component: { template: '' } },
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Fetches tasks from /api/dashboard/tasks and /api/dashboard/tasks/board
|
||||
* and maps them into TaskItem[] format for the TaskStrip component.
|
||||
*
|
||||
* Board state: grouped by column (offen, inProgress, review, done, blocked)
|
||||
* Board state: grouped by column (offen, inProgress, delegated, review, done, blocked)
|
||||
* Auto-refresh: every 30 seconds.
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -13,7 +13,7 @@ import type { TaskItem } from '../components/dashboard/v2/types'
|
||||
|
||||
/* ── API Response Shapes ──────────────────────────── */
|
||||
|
||||
interface DashboardTaskDto {
|
||||
export interface DashboardTaskDto {
|
||||
id: string
|
||||
title: string
|
||||
detail: string | null
|
||||
@@ -21,6 +21,8 @@ interface DashboardTaskDto {
|
||||
state: string
|
||||
priority: string
|
||||
assignedTo: string | null
|
||||
parentTaskId?: string | null
|
||||
dueDate?: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
@@ -228,7 +230,7 @@ export const useTaskStore = defineStore('tasks', {
|
||||
},
|
||||
|
||||
/* ── API: Update task ─────────────────────────── */
|
||||
async updateTask(id: string, updates: { title?: string; detail?: string; priority?: string; assignedTo?: string }) {
|
||||
async updateTask(id: string, updates: { title?: string; detail?: string | null; priority?: string; assignedTo?: string | null; dueDate?: string | null }) {
|
||||
try {
|
||||
const res = await apiFetch(`/api/dashboard/tasks/${id}`, {
|
||||
method: 'PUT',
|
||||
@@ -239,6 +241,29 @@ export const useTaskStore = defineStore('tasks', {
|
||||
await this.fetchBoard()
|
||||
} catch (err) {
|
||||
console.warn('[TaskStore] updateTask failed', err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTaskChildren(id: string) {
|
||||
try {
|
||||
const res = await apiFetch(`/api/dashboard/tasks/${id}/children`)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return await res.json() as DashboardTaskDto[]
|
||||
} catch (err) {
|
||||
console.warn('[TaskStore] fetchTaskChildren failed', err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTaskActivity(id: string) {
|
||||
try {
|
||||
const res = await apiFetch(`/api/dashboard/tasks/${id}/activity`)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return await res.json() as Array<{ id?: string; message?: string; type?: string; createdAt?: string; timestamp?: string }>
|
||||
} catch (err) {
|
||||
console.warn('[TaskStore] fetchTaskActivity failed', err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Command, LockKeyhole } from '@lucide/vue'
|
||||
/**
|
||||
* LoginView – Nexus Mission Control V2 Galaxy Theme
|
||||
*
|
||||
* Vollbild-Login mit GalaxyBackground, Glassmorphismus,
|
||||
* und Consistent Branding.
|
||||
*/
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Mail, LockKeyhole, Command, Eye, EyeOff } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import GalaxyBackground from '../components/background/GalaxyBackground.vue'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const showPassword = ref(false)
|
||||
|
||||
async function submit() {
|
||||
error.value = ''
|
||||
@@ -20,42 +29,345 @@ async function submit() {
|
||||
: '/dashboard'
|
||||
await router.replace(target)
|
||||
} catch (reason) {
|
||||
error.value = reason instanceof Error ? reason.message : 'Login failed.'
|
||||
error.value = reason instanceof Error ? reason.message : 'Login fehlgeschlagen.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="login-page">
|
||||
<section class="login-card">
|
||||
<div class="login-container">
|
||||
<GalaxyBackground />
|
||||
|
||||
<div class="login-content">
|
||||
<!-- Branding -->
|
||||
<div class="login-brand">
|
||||
<div class="brand-mark"><Command :size="20" /></div>
|
||||
<div><strong>NEXUS</strong><span>Noveria Operations</span></div>
|
||||
<div class="brand-icon">
|
||||
<Command :size="22" />
|
||||
</div>
|
||||
<span class="brand-name">NEXUS</span>
|
||||
<span class="brand-sub">Mission Control · Noveria</span>
|
||||
</div>
|
||||
|
||||
<div class="login-heading">
|
||||
<span class="eyebrow">OWNER ACCESS</span>
|
||||
<h1>Sign in to mission control</h1>
|
||||
<p>Use your private owner credentials to continue.</p>
|
||||
<!-- Login Card -->
|
||||
<div class="login-card">
|
||||
<div class="card-header">
|
||||
<span class="eyebrow">AUTHENTIFIZIERUNG</span>
|
||||
<h1>Anmelden</h1>
|
||||
<p>Gib deine Zugangsdaten ein, um auf das Mission Control zuzugreifen.</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit" class="login-form">
|
||||
<div class="field">
|
||||
<label for="email">
|
||||
<Mail :size="14" />
|
||||
<span>E-Mail</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
autocomplete="username"
|
||||
required
|
||||
maxlength="120"
|
||||
placeholder="name@noveria.net"
|
||||
class="field-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">
|
||||
<LockKeyhole :size="14" />
|
||||
<span>Passwort</span>
|
||||
</label>
|
||||
<div class="password-wrap">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
minlength="10"
|
||||
maxlength="200"
|
||||
placeholder="••••••••••"
|
||||
class="field-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-pw"
|
||||
@click="showPassword = !showPassword"
|
||||
:aria-label="showPassword ? 'Passwort verbergen' : 'Passwort anzeigen'"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Eye v-if="!showPassword" :size="16" />
|
||||
<EyeOff v-else :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="login-error" role="alert">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<button type="submit" class="submit-btn" :disabled="auth.loading || !email || !password">
|
||||
<LockKeyhole :size="15" />
|
||||
{{ auth.loading ? 'Anmelden…' : 'Anmelden' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<footer class="card-footer">
|
||||
<span class="lock-icon">🔒</span>
|
||||
<span>Gesicherte Sitzung · Refresh-Token im HTTP-only Cookie</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<label>
|
||||
<span>Email</span>
|
||||
<input v-model="email" type="email" autocomplete="username" required maxlength="120" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Password</span>
|
||||
<input v-model="password" type="password" autocomplete="current-password" required minlength="10" maxlength="200" />
|
||||
</label>
|
||||
<p v-if="error" class="login-error" role="alert">{{ error }}</p>
|
||||
<button type="submit" :disabled="auth.loading">
|
||||
<LockKeyhole :size="15" />
|
||||
{{ auth.loading ? 'Signing in...' : 'Sign in' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<footer>Protected owner session · Refresh token stored in a secure HTTP-only cookie</footer>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
animation: login-fade-in 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes login-fade-in {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Branding ─────────────────────── */
|
||||
.login-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: linear-gradient(135deg, #4f7cff, #b557f6);
|
||||
box-shadow: 0 0 32px -4px rgba(124, 108, 255, 0.6);
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2em;
|
||||
color: #ece9ff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand-sub {
|
||||
font-size: 12px;
|
||||
color: #6f6aa0;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── Login Card ────────────────────── */
|
||||
.login-card {
|
||||
width: 100%;
|
||||
background: linear-gradient(160deg, rgba(20, 17, 48, 0.88), rgba(14, 12, 32, 0.88));
|
||||
border: 1px solid rgba(150, 140, 255, 0.12);
|
||||
border-radius: 20px;
|
||||
padding: 32px 28px;
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 0 0 1px rgba(124, 108, 255, 0.06), 0 24px 80px -12px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
color: #7c6cff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.card-header h1 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: #ece9ff;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.card-header p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6f6aa0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Form ──────────────────────────── */
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #a8a3d6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
padding: 11px 14px;
|
||||
border: 1px solid rgba(150, 140, 255, 0.12);
|
||||
border-radius: 12px;
|
||||
background: rgba(10, 9, 24, 0.55);
|
||||
color: #ece9ff;
|
||||
font-size: 14px;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: #4a4680;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
border-color: rgba(124, 108, 255, 0.5);
|
||||
box-shadow: 0 0 0 3px rgba(124, 108, 255, 0.12);
|
||||
}
|
||||
|
||||
.password-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-wrap .field-input {
|
||||
padding-right: 44px;
|
||||
}
|
||||
|
||||
.toggle-pw {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #6f6aa0;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.toggle-pw:hover {
|
||||
background: rgba(124, 108, 255, 0.08);
|
||||
color: #a8a3d6;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
margin: 0;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(244, 63, 94, 0.1);
|
||||
border: 1px solid rgba(244, 63, 94, 0.2);
|
||||
color: #fda4af;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 13px 20px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #4f7cff, #7c6cff, #b557f6);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.15s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 24px -6px rgba(124, 108, 255, 0.5);
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
opacity: 0.92;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0 32px -4px rgba(124, 108, 255, 0.65);
|
||||
}
|
||||
|
||||
.submit-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ── Footer ────────────────────────── */
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid rgba(150, 140, 255, 0.08);
|
||||
font-size: 10.5px;
|
||||
color: #6f6aa0;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+779
-164
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user