瀏覽代碼

login page

master
FlorianEisenmenger 1 天之前
父節點
當前提交
2b1dcf04a6
共有 8 個檔案被更改,包括 180 行新增2 行删除
  1. +2
    -1
      .claude/settings.local.json
  2. +10
    -0
      CLAUDE.md
  3. +32
    -0
      httpdocs/assets/styles/components/_register.scss
  4. +2
    -0
      httpdocs/config/packages/security.yaml
  5. +25
    -0
      httpdocs/src/Controller/RegistrationController.php
  6. +95
    -0
      httpdocs/templates/registration/find_account.html.twig
  7. +1
    -1
      httpdocs/templates/registration/register.html.twig
  8. +13
    -0
      httpdocs/translations/messages.de.yaml

+ 2
- 1
.claude/settings.local.json 查看文件

@@ -25,7 +25,8 @@
"Bash(npx playwright *)",
"Bash(npm init *)",
"Bash(npm install *)",
"Bash(NODE_TLS_REJECT_UNAUTHORIZED=0 node test.mjs)"
"Bash(NODE_TLS_REJECT_UNAUTHORIZED=0 node test.mjs)",
"Bash(open *)"
]
}
}

+ 10
- 0
CLAUDE.md 查看文件

@@ -338,6 +338,16 @@ Billable/Non-Billable Trennung via `Service.billable`. Revenue-Berechnung nutzt
- **Brand-Farbe**: Billable-Balken nutzen `--color-primary`, Non-Billable grau
- **Donut-Charts**: Drei nebeneinander (3-Spalten-Grid, responsive 1-Spalte), eigene Legende mit Farb-Dot, Name, Wert und Prozent. 8-Farben-Palette, „Rest" in Grau

## Account-Finder (Anmelde-Übergangsseite)

Seite unter `/find-account` zur Weiterleitung auf die richtige Subdomain-Login-Seite. Nutzt das Register-Card-Design.

- **Route**: `/find-account` (`app_find_account`) in `RegistrationController`, PUBLIC_ACCESS
- **Template**: `templates/registration/find_account.html.twig` (embed von `register-card.html.twig`)
- **Styles**: `.find-account-input` in `_register.scss` (Input mit `.domain`-Suffix)
- **Slug-Validierung**: `POST /api/check-slug` prüft via `AccountRepository::findBySlug()` ob der Account existiert, bevor weitergeleitet wird. Fehlermeldung bei unbekanntem Slug.
- **Links**: Register-Seite verlinkt „Zur Anmeldung" auf `/find-account`, Find-Account verlinkt „Jetzt registrieren" auf `/register`

## mite-Import

Import von Kunden, Projekten, Leistungen und Zeiteinträgen aus mite-Backups (XML). Erreichbar als eigener Tab in den Account-Einstellungen (nur Admin).


+ 32
- 0
httpdocs/assets/styles/components/_register.scss 查看文件

@@ -150,6 +150,38 @@
}
}

// ─── Find Account ────────────────────────────────────────────────────────
.find-account-input {
display: flex;
align-items: center;
gap: 0;
border: 1px solid $color-input-border;
border-radius: $radius-sm;
background: $color-input-bg;
transition: border-color $transition-fast;

&:focus-within {
border-color: var(--color-primary);
box-shadow: $shadow-focus;
}
}

.find-account-input__field {
flex: 1;
border: none !important;
box-shadow: none !important;
background: transparent;
min-width: 0;
}

.find-account-input__suffix {
padding: 0 $space-4 0 0;
font-size: $font-size-sm;
color: $color-text-muted;
white-space: nowrap;
user-select: none;
}

// ─── Erfolgs-State ────────────────────────────────────────────────────────────
.register-success {
text-align: center;


+ 2
- 0
httpdocs/config/packages/security.yaml 查看文件

@@ -48,7 +48,9 @@ security:
access_control:
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/find-account, roles: PUBLIC_ACCESS }
- { path: ^/api/register, roles: PUBLIC_ACCESS }
- { path: ^/api/check-slug, roles: PUBLIC_ACCESS }
- { path: ^/verify/, roles: PUBLIC_ACCESS }
- { path: ^/invite/, roles: PUBLIC_ACCESS }
- { path: ^/forgot-password, roles: PUBLIC_ACCESS }


+ 25
- 0
httpdocs/src/Controller/RegistrationController.php 查看文件

@@ -2,6 +2,7 @@

namespace App\Controller;

use App\Repository\Central\AccountRepository;
use App\Service\RegistrationService;
use App\Service\SlugGenerator;
use Psr\Log\LoggerInterface;
@@ -17,6 +18,7 @@ class RegistrationController extends AbstractController
public function __construct(
private readonly RegistrationService $registrationService,
private readonly SlugGenerator $slugGenerator,
private readonly AccountRepository $accountRepo,
private readonly TranslatorInterface $translator,
private readonly string $appDomain,
private readonly LoggerInterface $logger,
@@ -30,9 +32,32 @@ class RegistrationController extends AbstractController
]);
}

#[Route('/find-account', name: 'app_find_account')]
public function findAccount(): Response
{
return $this->render('registration/find_account.html.twig', [
'appDomain' => $this->appDomain,
]);
}

/**
* Live-Vorschau des Slugs während der User tippt.
*/
#[Route('/api/check-slug', name: 'api_check_slug', methods: ['POST'])]
public function checkSlug(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$slug = trim($data['slug'] ?? '');

if ($slug === '') {
return $this->json(['exists' => false]);
}

$account = $this->accountRepo->findBySlug($slug);

return $this->json(['exists' => $account !== null]);
}

#[Route('/api/register/preview-slug', name: 'api_register_preview_slug', methods: ['POST'])]
public function previewSlug(Request $request): JsonResponse
{


+ 95
- 0
httpdocs/templates/registration/find_account.html.twig 查看文件

@@ -0,0 +1,95 @@
{# templates/registration/find_account.html.twig #}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ 'app.find_account.page_title'|trans }}</title>
{{ encore_entry_link_tags('app') }}
</head>
<body class="register-body">

{% embed '_components/register-card.html.twig' %}
{% block content %}

<div class="register-card__brand">
<a href="{{ path('app_home') }}">spawntree Timetracker</a>
</div>
<h1 class="register-card__title">{{ 'app.find_account.title'|trans }}</h1>
<p class="register-card__sub">{{ 'app.find_account.subtitle'|trans }}</p>

<div id="find-account-error" class="register-errors" role="alert"></div>

<form id="find-account-form" novalidate>
<div class="register-field">
<label class="register-field__label" for="account-slug">{{ 'app.find_account.label_slug'|trans }}</label>
<div class="find-account-input">
<input class="input find-account-input__field"
type="text"
id="account-slug"
name="slug"
placeholder="{{ 'app.find_account.placeholder_slug'|trans }}"
autocomplete="off"
autocapitalize="none"
spellcheck="false"
autofocus
required />
<span class="find-account-input__suffix">.{{ appDomain }}</span>
</div>
</div>

<div class="register-actions">
<button type="submit" class="btn btn-primary register-actions__submit">
{{ 'app.find_account.btn_submit'|trans }}
</button>
<p class="register-actions__login">
{{ 'app.find_account.no_account'|trans }} <a href="{{ path('app_register') }}">{{ 'app.find_account.link_register'|trans }}</a>
</p>
</div>
</form>

{% endblock %}
{% endembed %}

<script>
document.getElementById('find-account-form').addEventListener('submit', function(e) {
e.preventDefault();
var slug = document.getElementById('account-slug').value.trim().toLowerCase();
var errorEl = document.getElementById('find-account-error');
var btn = document.querySelector('.register-actions__submit');
errorEl.textContent = '';

if (!slug) {
errorEl.innerHTML = '<p>{{ 'app.find_account.error_empty'|trans }}</p>';
return;
}

if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(slug)) {
errorEl.innerHTML = '<p>{{ 'app.find_account.error_invalid'|trans }}</p>';
return;
}

btn.disabled = true;
fetch('/api/check-slug', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug: slug }),
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.exists) {
window.location.href = 'https://' + slug + '.{{ appDomain }}/login';
} else {
errorEl.innerHTML = '<p>{{ 'app.find_account.error_not_found'|trans }}</p>';
btn.disabled = false;
}
})
.catch(function() {
errorEl.innerHTML = '<p>{{ 'app.find_account.error_not_found'|trans }}</p>';
btn.disabled = false;
});
});
</script>

</body>
</html>

+ 1
- 1
httpdocs/templates/registration/register.html.twig 查看文件

@@ -81,7 +81,7 @@
{{ 'app.register.btn_submit'|trans }}
</button>
<p class="register-actions__login">
{{ 'app.register.already_registered'|trans }} <a href="{{ path('app_home') }}">{{ 'app.register.link_login'|trans }}</a>
{{ 'app.register.already_registered'|trans }} <a href="{{ path('app_find_account') }}">{{ 'app.register.link_login'|trans }}</a>
</p>
</div>



+ 13
- 0
httpdocs/translations/messages.de.yaml 查看文件

@@ -413,6 +413,19 @@ app:
already_registered: "Bereits registriert?"
link_login: "Zur Anmeldung"

find_account:
page_title: "Anmelden – spawntree Timetracker"
title: "Anmelden"
subtitle: "Gib den Namen deines Accounts ein, um zur Anmeldeseite weitergeleitet zu werden."
label_slug: "Account-Name"
placeholder_slug: "meine-firma"
btn_submit: "Weiter zur Anmeldung"
error_empty: "Bitte gib deinen Account-Namen ein."
error_invalid: "Der Account-Name darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten."
error_not_found: "Dieser Account wurde nicht gefunden. Bitte überprüfe den Namen."
no_account: "Noch kein Konto?"
link_register: "Jetzt registrieren"

confirm_error:
page_title: "Fehler – spawntree Timetracker"
title: "Link ungültig"


Loading…
取消
儲存