@@ -48,6 +52,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute
|
+
|
diff --git a/app/Domains/UserManagement/Views/Registration.vue b/app/Domains/UserManagement/Views/Registration.vue
new file mode 100644
index 0000000..b00b885
--- /dev/null
+++ b/app/Domains/UserManagement/Views/Registration.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/Domains/UserManagement/Views/ResetPassword.vue b/app/Domains/UserManagement/Views/ResetPassword.vue
new file mode 100644
index 0000000..34a95cb
--- /dev/null
+++ b/app/Domains/UserManagement/Views/ResetPassword.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/Domains/UserManagement/Views/VerifyEmail.vue b/app/Domains/UserManagement/Views/VerifyEmail.vue
new file mode 100644
index 0000000..f719d8e
--- /dev/null
+++ b/app/Domains/UserManagement/Views/VerifyEmail.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/Enumerations/MessageType.php b/app/Enumerations/MessageType.php
new file mode 100644
index 0000000..b4ba22f
--- /dev/null
+++ b/app/Enumerations/MessageType.php
@@ -0,0 +1,8 @@
+ 'wilde-moehre',
- 'local_group_name' => 'Stamm Wilde Möhre',
+ 'name' => 'Stamm Wilde Möhre',
'url' => 'wilde-moehre.mareike.local',
'account_iban' => 'DE12345678901234567890',
'email' => 'test@example1.com',
@@ -32,11 +32,11 @@ class DevelopmentDataSeeder {
User::create([
'firstname' => 'Development',
'lastname' => 'User',
- 'user_role' => UserRole::USER_ROLE_ADMIN,
- 'tenant' => 'lv',
+ 'user_role_main' => UserRole::USER_ROLE_ADMIN,
+ 'user_role_local_group' => UserRole::USER_ROLE_GROUP_LEADER,
+ 'local_group' => 'wilde-moehre',
'email' => 'th.guenther@saale-mail.de',
'password' => bcrypt('development'),
- 'local_group_id' => 1,
'username' => 'development',
]);
}
diff --git a/app/Installer/ProductionDataSeeder.php b/app/Installer/ProductionDataSeeder.php
index dcebdd5..21797a8 100644
--- a/app/Installer/ProductionDataSeeder.php
+++ b/app/Installer/ProductionDataSeeder.php
@@ -58,7 +58,7 @@ class ProductionDataSeeder {
private function installTenants() {
Tenant::create([
'slug' => 'lv',
- 'local_group_name' => 'Landesunmittelbare Mitglieder',
+ 'name' => 'Landesunmittelbare Mitglieder',
'url' => 'mareike.local',
'account_iban' => 'DE12345678901234567890',
'email' => 'test@example.com',
diff --git a/app/MessageTemplates/MessageTemplate.php b/app/MessageTemplates/MessageTemplate.php
new file mode 100644
index 0000000..8ba81ba
--- /dev/null
+++ b/app/MessageTemplates/MessageTemplate.php
@@ -0,0 +1,37 @@
+subject;
+ }
+
+ public function setSubject(string $subject): void
+ {
+ $this->subject = $subject;
+ }
+
+ public function getMessage(): string
+ {
+ return $this->message;
+ }
+
+ public function setMessage(string $message): void
+ {
+ $this->message = $message;
+ }
+
+
+
+}
diff --git a/app/MessageTemplates/Registration/InformAdminAboutNewUserTemplate.php b/app/MessageTemplates/Registration/InformAdminAboutNewUserTemplate.php
new file mode 100644
index 0000000..5c8affd
--- /dev/null
+++ b/app/MessageTemplates/Registration/InformAdminAboutNewUserTemplate.php
@@ -0,0 +1,59 @@
+composeMessage($user);
+ return $template;
+ }
+
+ public function __construct() {
+ $this->subject = "Eine Person hat sich auf mareike registriert";
+ }
+
+ public function composeMessage(User $user): void {
+ $this->message =
+ <<Eine Person hat sich auf mareike angemeldet
+Soeben hat sich eine neue Person auf mareike angemeldet:
+
+
+ | Name: |
+ %1\$s |
+
+
+ | Email: |
+ %2\$s |
+
+
+ | Stamm: |
+ %3\$s |
+
+
+ | Freischaltcode: |
+ %4\$s |
+
+
+
+
+Sollte die Person unberechtigt angemeldet sein, lösche den Account.
+
+HTML;
+
+ $this->message = sprintf($this->message,
+ $user->getFullname(),
+ $user->email,
+ $user->local_group,
+ $user->activation_token
+ );
+ }
+}
diff --git a/app/MessageTemplates/activationCodeTemplate.php b/app/MessageTemplates/activationCodeTemplate.php
new file mode 100644
index 0000000..fe27f2a
--- /dev/null
+++ b/app/MessageTemplates/activationCodeTemplate.php
@@ -0,0 +1,23 @@
+composeMessage($emailAddress, $activationCode);
+ return $template;
+ }
+
+ public function __construct() {
+ $this->subject = "Dein Aktivierungscode";
+ }
+
+ public function composeMessage(EmailAddress $emailAddress, string $activationCode): void {
+ $this->message = "Dein Aktivierungscode lautet: {$activationCode}" . PHP_EOL .
+ "Gib diesen zusammen mit der Mailadresse {$emailAddress->getValue()} ein.";
+ }
+}
diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php
index 35bed28..b46ff1f 100644
--- a/app/Models/Tenant.php
+++ b/app/Models/Tenant.php
@@ -24,7 +24,7 @@ class Tenant extends CommonModel
protected $fillable = [
'slug',
- 'local_group_name',
+ 'name',
'email',
'url',
'account_iban',
diff --git a/app/Models/User.php b/app/Models/User.php
index f93e9d6..e17b389 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -5,6 +5,34 @@ namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
+/**
+ * @property string $username
+ * @property string $local_group
+ * @property string $firstname
+ * @property string $nickname
+ * @property string $lastname
+ * @property string $membership_id
+ * @property string $address_1
+ * @property string $address_2
+ * @property string $postcode
+ * @property string $city
+ * @property string $email
+ * @property string $phone
+ * @property string $birthday
+ * @property string $medications
+ * @property string $allergies
+ * @property string $intolerances
+ * @property string $eating_habits
+ * @property string $swimming_permission
+ * @property string $first_aid_permission
+ * @property string $bank_account_iban
+ * @property string $password
+ * @property boolean $active
+ * @property string $user_role_main
+ * @property string $user_role_local_group
+ * @property string $activation_token
+ * @property string $activation_token_expires_at
+ */
class User extends Authenticatable
{
use Notifiable;
@@ -15,13 +43,15 @@ class User extends Authenticatable
* @var list
*/
protected $fillable = [
- 'tenant_id',
- 'user_role',
+ 'user_role_main',
+ 'user_role_local_group',
+ 'activation_token',
+ 'activation_token_expires_at',
'username',
+ 'local_group',
'firstname',
'nickname',
'lastname',
- 'local_group_id',
'membership_id',
'address_1',
'address_2',
@@ -58,8 +88,19 @@ class User extends Authenticatable
protected function casts(): array
{
return [
- 'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
+
+ public function getFullname() : string {
+ return sprintf('%1$1s %2$s %3$s',
+ $this->firstname,
+ $this->nickname !== '' ? '(' . $this->nickname . ')' : '',
+ $this->lastname
+ );
+ }
+
+ public function getNicename() : string {
+ return $this->nickname !== '' ? $this->nickname : $this->firstname;
+ }
}
diff --git a/app/Providers/AuthCheckProvider.php b/app/Providers/AuthCheckProvider.php
index c93b018..4cb38a7 100644
--- a/app/Providers/AuthCheckProvider.php
+++ b/app/Providers/AuthCheckProvider.php
@@ -10,6 +10,11 @@ class AuthCheckProvider {
$user = auth()->user();
$tenant = app('tenant');
+ if ($tenant->slug === 'lv') {
+ return $user->active;
+ }
+
+
return $user->active && $tenant->slug === $user->tenant;
}
@@ -18,6 +23,10 @@ class AuthCheckProvider {
return null;
}
- return auth()->user()->user_role;
+ if (app('tenant')->slug === 'lv') {
+ return auth()->user()->user_role_main;
+ }
+
+ return auth()->user()->user_role_local_group;
}
}
diff --git a/app/Providers/InertiaProvider.php b/app/Providers/InertiaProvider.php
index 49be165..ea8b422 100644
--- a/app/Providers/InertiaProvider.php
+++ b/app/Providers/InertiaProvider.php
@@ -2,6 +2,7 @@
namespace App\Providers;
+use App\Models\Tenant;
use App\Models\User;
use Inertia\Inertia;
use Inertia\Response;
@@ -21,9 +22,10 @@ final class InertiaProvider
public function render() : Response {
$this->props['navbar'] = $this->generateNavbar();
- $this->props['tenant'] = app('tenant')->local_group_name;
+ $this->props['tenant'] = app('tenant');
$this->props['user'] = $this->user;
$this->props['currentPath'] = request()->path();
+ $this->props['availableLocalGroups'] = Tenant::where(['is_active_local_group' => true])->get();
return Inertia::render(
str_replace('/', '/Views/', $this->vueFile),
diff --git a/app/Providers/TenantUserProvider.php b/app/Providers/TenantUserProvider.php
index c15b9cc..909769c 100644
--- a/app/Providers/TenantUserProvider.php
+++ b/app/Providers/TenantUserProvider.php
@@ -8,6 +8,8 @@ class TenantUserProvider extends EloquentUserProvider
{
public function retrieveByCredentials(array $credentials)
{
+ $credentials['active'] = true;
+
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
@@ -16,8 +18,12 @@ class TenantUserProvider extends EloquentUserProvider
}
}
+ if (app('tenant')->slug === 'lv') {
+ return $query->first();
+ }
+
$query->where([
- 'tenant' => app('tenant')->slug,
+ 'local_group' => app('tenant')->slug,
'active' => true
]);
diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php
new file mode 100644
index 0000000..a96bfb6
--- /dev/null
+++ b/app/Repositories/UserRepository.php
@@ -0,0 +1,20 @@
+ $username])->first();
+ }
+
+ public function checkVerificationToken(User $user, string $token) : bool {
+ if (new DateTime() > DateTime::createFromFormat('Y-m-d H:i:s', $user->activation_token_expires_at)) {
+ return false;
+ }
+
+ return $token === $user->activation_token;
+ }
+}
diff --git a/app/Scopes/CommonController.php b/app/Scopes/CommonController.php
index 18f4c94..ee0e119 100644
--- a/app/Scopes/CommonController.php
+++ b/app/Scopes/CommonController.php
@@ -3,8 +3,15 @@
namespace App\Scopes;
use App\Providers\AuthCheckProvider;
+use App\Repositories\UserRepository;
abstract class CommonController {
+ protected UserRepository $users;
+
+ public function __construct() {
+ $this->users = new UserRepository();
+ }
+
protected function checkAuth() {
$authCheckProvider = new AuthCheckProvider;
return $authCheckProvider->checkLoggedIn();
diff --git a/app/ValueObjects/EmailAddress.php b/app/ValueObjects/EmailAddress.php
new file mode 100644
index 0000000..7309928
--- /dev/null
+++ b/app/ValueObjects/EmailAddress.php
@@ -0,0 +1,26 @@
+setValue($value);
+ return $emailAddress;
+ }
+
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ public function setValue(string $value): void
+ {
+ $this->value = $value;
+ }
+}
diff --git a/app/ValueObjects/MessageRecipient.php b/app/ValueObjects/MessageRecipient.php
new file mode 100644
index 0000000..ade85dc
--- /dev/null
+++ b/app/ValueObjects/MessageRecipient.php
@@ -0,0 +1,50 @@
+user = null;
+ $this->emailAddresses = [];
+ $this->name = "";
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function setUser(?User $user): void
+ {
+ $this->user = $user;
+ }
+
+ public function getEmailAddresses(): array
+ {
+ return $this->emailAddresses;
+ }
+
+ public function addEmailAddress(EmailAddress $emailAddresses): void
+ {
+ $this->emailAddresses[] = $emailAddresses;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+
+}
diff --git a/app/Views/Components/ErrorText.vue b/app/Views/Components/ErrorText.vue
new file mode 100644
index 0000000..2e53b14
--- /dev/null
+++ b/app/Views/Components/ErrorText.vue
@@ -0,0 +1,10 @@
+
+
+
+ {{ props.message }}
+
+
diff --git a/config/app.php b/config/app.php
index 423eed5..6fad0ed 100644
--- a/config/app.php
+++ b/config/app.php
@@ -65,7 +65,7 @@ return [
|
*/
- 'timezone' => 'UTC',
+ 'timezone' => 'Europe/Berlin',
/*
|--------------------------------------------------------------------------
diff --git a/database/migrations/2026_01_30_140001_create_tenants.php b/database/migrations/2026_01_30_140001_create_tenants.php
index ddd4f7e..10982f2 100644
--- a/database/migrations/2026_01_30_140001_create_tenants.php
+++ b/database/migrations/2026_01_30_140001_create_tenants.php
@@ -9,7 +9,7 @@ return new class extends Migration {
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
- $table->string('local_group_name');
+ $table->string('name');
$table->string('email');
$table->string('url');
$table->string('account_iban');
diff --git a/database/migrations/2026_01_30_140002_create_users_table.php b/database/migrations/2026_01_30_140002_create_users_table.php
index b745d24..0a9c1ce 100644
--- a/database/migrations/2026_01_30_140002_create_users_table.php
+++ b/database/migrations/2026_01_30_140002_create_users_table.php
@@ -38,14 +38,14 @@ return new class extends Migration
Schema::create('users', function (Blueprint $table) {
$table->id();
- $table->string('tenant');
- $table->string('user_role');
+ $table->string('user_role_main');
+ $table->string('user_role_local_group');
$table->string('username')->unique();
$table->string('password')->nullable();
+ $table->string('local_group');
$table->string('firstname');
$table->string('nickname')->nullable();
$table->string('lastname');
- $table->foreignId('local_group_id')->references('id')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate();
$table->string('membership_id')->nullable();
$table->string('address_1')->nullable();
$table->string('address_2')->nullable();
@@ -62,10 +62,12 @@ return new class extends Migration
$table->string('first_aid_permission')->nullable();
$table->string('bank_account_iban')->nullable();
$table->string('activation_token')->nullable();
+ $table->dateTime('activation_token_expires_at')->nullable()->default(date('Y-m-d H:i:s', strtotime('+3 days')));
$table->boolean('active')->default(false);
- $table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate();
- $table->foreign('user_role')->references('slug')->on('user_roles')->cascadeOnDelete()->cascadeOnUpdate();
+ $table->foreign('local_group')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate();
+ $table->foreign('user_role_main')->references('slug')->on('user_roles')->cascadeOnDelete()->cascadeOnUpdate();
+ $table->foreign('user_role_local_group')->references('slug')->on('user_roles')->cascadeOnDelete()->cascadeOnUpdate();
$table->foreign('swimming_permission')->references('slug')->on('swimming_permissions')->cascadeOnDelete()->cascadeOnUpdate();
$table->foreign('eating_habits')->references('slug')->on('eating_habits')->cascadeOnDelete()->cascadeOnUpdate();
$table->foreign('first_aid_permission')->references('slug')->on('first_aid_permissions')->cascadeOnDelete()->cascadeOnUpdate();
diff --git a/docker-compose.dev b/docker-compose.dev
index ea842a7..338256a 100644
--- a/docker-compose.dev
+++ b/docker-compose.dev
@@ -18,7 +18,7 @@ services:
labels:
- "traefik.enable=true"
- - "traefik.http.routers.mareike.rule=Host(`mareike.local`) || Host(`admin.mareike.local`) || Host(`wilde-moehre.mareike.local`)"
+ - "traefik.http.routers.mareike.rule=Host(`mareike.local`) || Host(`admin.mareike.local`) || Host(`wilde-moehre.mareike.local`) || Host(`fennek.mareike.local`)"
- "traefik.http.routers.mareike.entrypoints=websecure"
- "traefik.http.routers.mareike.tls=true"
- "traefik.http.services.mareike.loadbalancer.server.port=80"
@@ -46,13 +46,6 @@ services:
- ./:/var/www/html
command: >
sh -c "
- npm install &&
- npm install vue3-toastify && npm install @inertiajs/progress && npm install @inertiajs/progress &&
- npm install @fortawesome/fontawesome-svg-core &&
- npm install @fortawesome/free-solid-svg-icons &&
- npm install @fortawesome/vue-fontawesome@latest &&
-
-
while true; do
npm run build
echo 'Vite Dev-Server beendet. Neustart in 3 Sekunden...'
diff --git a/public/css/app.css b/public/css/app.css
index fc27ddc..27dced7 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -118,6 +118,7 @@ html {
th {
text-align: left;
padding-right: 1em;
+ vertical-align: top;
}
th:after {
diff --git a/public/css/elements.css b/public/css/elements.css
index 56e0e27..3edacfb 100644
--- a/public/css/elements.css
+++ b/public/css/elements.css
@@ -12,20 +12,28 @@
input[type="text"],
input[type="email"],
-input[type="password"] {
+input[type="password"],
+select {
width: 100%;
font-size: 13pt;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
color: #656363;
+ background-color: #ffffff;
+}
+
+select {
+ width: calc(100% + 10px);
}
input[type="text"]:focus,
input[type="email"]:focus,
-input[type="password"]:focus {
+input[type="password"]:focus,
+select:focus {
outline: none;
border-color: #1d4899;
+ color: #1d4899;
}
@@ -33,7 +41,8 @@ table {
width: 100%;
}
-button, input[type="submit"] {
+input[type="button"],
+input[type="submit"] {
cursor: pointer;
background-color: #ffffff;
border: 1px solid #809dd5 !important;
@@ -41,7 +50,13 @@ button, input[type="submit"] {
font-weight: bold;
}
-button:hover, input[type="submit"]:hover {
+input[type="button"]:hover,
+input[type="submit"]:hover {
background-color: #1d4899;
color: #ffffff;
}
+
+.error_text {
+ color: red;
+ display: block;
+}
diff --git a/resources/js/components/ajaxHandler.js b/resources/js/components/ajaxHandler.js
new file mode 100644
index 0000000..6f208cb
--- /dev/null
+++ b/resources/js/components/ajaxHandler.js
@@ -0,0 +1,80 @@
+import { ref } from "vue"
+
+export function useAjax() {
+ const loading = ref(false)
+ const error = ref(null)
+ const data = ref(null)
+
+ async function request(url, options = {}) {
+ loading.value = true
+ error.value = null
+ data.value = null
+
+ try {
+ const response = await fetch(url, {
+ method: options.method || "GET",
+ headers: {
+ "Content-Type": "application/json",
+ ...(options.headers || {}),
+ },
+ body: options.body ? JSON.stringify(options.body) : null,
+ })
+
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
+ const result = await response.json()
+ data.value = result
+ return result
+ } catch (err) {
+ error.value = err
+ console.error("AJAX Error:", err)
+ return null
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function download(url, options = {}) {
+ loading.value = true
+ error.value = null
+
+ try {
+ const response = await fetch(url, {
+ method: options.method || "GET",
+ headers: {
+ ...(options.headers || {}),
+ },
+ body: options.body ? JSON.stringify(options.body) : null,
+ })
+
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
+
+ const blob = await response.blob()
+ const filename =
+ options.filename ||
+ response.headers
+ .get("Content-Disposition")
+ ?.split("filename=")[1]
+ ?.replace(/["']/g, "") ||
+ "download.bin"
+
+ const downloadUrl = window.URL.createObjectURL(blob)
+ const a = document.createElement("a")
+ a.href = downloadUrl
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ a.remove()
+ window.URL.revokeObjectURL(downloadUrl)
+
+ return true
+ } catch (err) {
+ error.value = err
+ console.error("Download Error:", err)
+ return false
+ } finally {
+ loading.value = false
+ }
+ }
+
+ return {data, loading, error, request, download}
+}
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php
index d63bb32..90c776a 100644
--- a/resources/views/app.blade.php
+++ b/resources/views/app.blade.php
@@ -1,8 +1,8 @@
-
-
+
+
diff --git a/routes/api.php b/routes/api.php
index 1b1d16a..3877b9c 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -1,15 +1,3 @@
config('app.name'),
- ]);
-});
+require __DIR__.'/../app/Domains/UserManagement/Routes/api.php';
diff --git a/routes/web.php b/routes/web.php
index c85a4b6..6510d5b 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1,23 +1,24 @@
group(function () {
Route::get('/', DashboardController::class);
-
-
Route::middleware(['auth'])->group(function () {
Route::get('/messages', fn () => inertia('Messages'));
- Route::post('/logout', [LogoutController::class, 'logout']);
});
@@ -27,9 +28,6 @@ Route::middleware(IdentifyTenant::class)->group(function () {
- route::get('/logout', LogOutController::class);
- route::post('/login', [LoginController::class, 'doLogin']);
- route::get('/login', [LoginController::class, 'loginForm']);
Route::get('/messages', [TestRenderInertiaProvider::class, 'index']);