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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user