Files
nexus/frontend/src/views/LoginView.vue
T
devops e4091eee80
CI - Build & Test / Backend (.NET) (push) Successful in 35s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 20s
CI - Build & Test / Security Check (push) Successful in 4s
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.
2026-06-20 14:24:40 +02:00

374 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 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 = ''
try {
await auth.login(email.value.trim(), password.value)
const target = typeof route.query.redirect === 'string' && route.query.redirect.startsWith('/')
? route.query.redirect
: '/dashboard'
await router.replace(target)
} catch (reason) {
error.value = reason instanceof Error ? reason.message : 'Login fehlgeschlagen.'
}
}
</script>
<template>
<div class="login-container">
<GalaxyBackground />
<div class="login-content">
<!-- Branding -->
<div class="login-brand">
<div class="brand-icon">
<Command :size="22" />
</div>
<span class="brand-name">NEXUS</span>
<span class="brand-sub">Mission Control · Noveria</span>
</div>
<!-- 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>
</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>