First Commit

This commit is contained in:
marco
2026-09-01 10:46:29 -03:00
commit bf96b77e9c
10000 changed files with 3508600 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
SysCMS Commercial License
Copyright (c) 2026 Marco Costa. All rights reserved.
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+35
View File
@@ -0,0 +1,35 @@
# Auth Module for SysCMS
Authentication and authorization module for user management.
## Features
- User registration and login
- Password reset functionality
- User profile management
- Role-based access control
- Email verification (optional)
- Session management
## Installation
1. Upload the `auth.zip` file through the module manager or extract it to the `modules/` directory.
2. Run the migrations to register the module and create the database tables.
3. Configure authentication settings in the admin panel.
## Permissions
- `auth.login`: User login
- `auth.logout`: User logout
- `auth.register`: User registration
- `auth.profile`: View and edit profile
- `auth.password.reset`: Request password reset
- `auth.password.change`: Change password
## Configuration
The module supports the following settings:
- `allow_registration`: Enable/disable user registration
- `require_email_verification`: Require email verification before account activation
- `password_min_length`: Minimum password length (default: 8)
- `session_timeout`: Session timeout in minutes (default: 120)
## License
Copyright (c) 2026 Marco Costa. All rights reserved.
Licensed under the SysCMS Commercial License.
File diff suppressed because it is too large Load Diff
+187
View File
@@ -0,0 +1,187 @@
<?php
namespace Modules\Auth\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Modules\Auth\Models\User;
use Kernel\Application;
class AuthMiddleware
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Check if user is authenticated
if (!$this->isAuthenticated()) {
if ($this->expectsJson($request)) {
return new JsonResponse([
'success' => false,
'message' => __('auth.unauthenticated'),
'timestamp' => time()
], 401);
}
// Redirect to login page for web requests
return new Response('', 302, [
'Location' => url('/login')
]);
}
// Check if user session is still valid
if (!$this->isSessionValid()) {
$this->logout();
if ($this->expectsJson($request)) {
return new JsonResponse([
'success' => false,
'message' => __('auth.session_expired'),
'timestamp' => time()
], 401);
}
return new Response('', 302, [
'Location' => url('/login?expired=1')
]);
}
// Set user in request for easy access
$request->attributes->set('user', $this->getCurrentUser());
return $next($request);
}
/**
* Check if user is authenticated.
*
* @return bool
*/
protected function isAuthenticated(): bool
{
if (isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) {
return true;
}
if (!empty($_COOKIE['remember_token'])) {
$secret = $_ENV['APP_KEY'] ?? '';
$parts = explode(':', $_COOKIE['remember_token']);
if (count($parts) === 3) {
[$id, $token, $sig] = $parts;
$payload = $id . ':' . $token;
$expected = hash_hmac('sha256', $payload, $secret);
if (hash_equals($expected, $sig)) {
try {
$user = User::find((int)$id);
if ($user && $user->users_remember_token === $token) {
$_SESSION['user_id'] = $user->users_id;
$_SESSION['user_idbus'] = $user->users_bus_id;
$_SESSION['user_role'] = $user->users_role;
$_SESSION['last_activity'] = time();
return true;
}
} catch (\Throwable $e) {}
}
}
}
return false;
}
/**
* Check if session is still valid.
*
* @return bool
*/
protected function isSessionValid(): bool
{
if (!isset($_SESSION['last_activity'])) {
return false;
}
$timeoutSetting = 120;
try {
if (class_exists('\App\Models\Module')) {
$module = \App\Models\Module::byName('auth')->first();
if ($module) {
$timeoutSetting = (int)($module->getSetting('session_timeout', 120));
}
}
} catch (\Throwable $e) {}
$sessionTimeout = (int)($_ENV['SESSION_LIFETIME'] ?? $timeoutSetting) * 60;
$lastActivity = $_SESSION['last_activity'];
$currentTime = time();
// Check if session has expired
if (($currentTime - $lastActivity) > $sessionTimeout) {
return false;
}
// Update last activity time
$_SESSION['last_activity'] = $currentTime;
return true;
}
/**
* Get current authenticated user.
*
* @return User|null
*/
protected function getCurrentUser(): ?User
{
if (!$this->isAuthenticated()) {
return null;
}
try {
return User::find($_SESSION['user_id']);
} catch (\Exception $e) {
// Log error and invalidate session
error_log("Error loading user: " . $e->getMessage());
$this->logout();
return null;
}
}
/**
* Check if request expects JSON response.
*
* @param Request $request
* @return bool
*/
protected function expectsJson(Request $request): bool
{
return $request->headers->get('Accept') === 'application/json' ||
$request->headers->get('Content-Type') === 'application/json' ||
str_contains($request->getPathInfo(), '/api/');
}
/**
* Logout user and destroy session.
*
* @return void
*/
protected function logout(): void
{
// Unset all session variables
unset($_SESSION['user_id']);
unset($_SESSION['user_idbus']);
unset($_SESSION['user_role']);
unset($_SESSION['last_activity']);
unset($_SESSION['csrf_token']);
// Destroy session
session_destroy();
// Start new session for possible flash messages
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
}
+526
View File
@@ -0,0 +1,526 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace Modules\Auth\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class User extends Model
{
use SoftDeletes;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'users';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'users_id';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'users_name',
'users_email',
'users_password',
'users_role',
'users_bus_id',
'users_avatar',
'users_bio',
'users_phone',
'users_email_verified_at',
'users_last_login_at',
'users_status',
'users_preferences'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'users_password',
'remember_token'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'users_email_verified_at' => 'datetime',
'users_last_login_at' => 'datetime',
'users_preferences' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime'
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'users_email_verified_at',
'users_last_login_at',
'created_at',
'updated_at',
'deleted_at'
];
/**
* Boot the model
*/
protected static function boot()
{
parent::boot();
// Add company scope by default
static::addGlobalScope('company', function ($query) {
if (isset($_SESSION['user_idbus']) && $_SESSION['user_idbus'] !== null && $_SESSION['user_role'] !== 'super_admin') {
$query->where('users_bus_id', $_SESSION['user_idbus']);
}
});
}
/**
* Get the company that owns the user.
*/
public function company(): BelongsTo
{
return $this->belongsTo(\Modules\Company\Models\Company::class, 'users_bus_id', 'bus_id');
}
/**
* Get the permissions for the user.
*/
public function permissions(): HasMany
{
return $this->hasMany(\App\Models\UserPermission::class, 'user_permissions_user_id', 'users_id');
}
/**
* Get the activity logs for the user.
*/
public function activityLogs(): HasMany
{
return $this->hasMany(\App\Models\ActivityLog::class, 'activity_logs_user_id', 'users_id');
}
/**
* Check if user is super admin
*
* @return bool
*/
public function isSuperAdmin(): bool
{
return $this->users_role === 'super_admin';
}
/**
* Check if user is admin
*
* @return bool
*/
public function isAdmin(): bool
{
return $this->users_role === 'admin';
}
/**
* Check if user is member
*
* @return bool
*/
public function isMember(): bool
{
return $this->users_role === 'member';
}
/**
* Check if user is regular user
*
* @return bool
*/
public function isUser(): bool
{
return $this->users_role === 'user';
}
/**
* Check if user has specific permission
*
* @param string $permission
* @return bool
*/
public function hasPermission(string $permission): bool
{
// Super admin has all permissions
if ($this->isSuperAdmin()) {
return true;
}
// Admin role has full access within their company scope
if ($this->isAdmin()) {
return true;
}
// Check direct permissions
$hasDirectPermission = \App\Models\UserPermission::where('user_permissions_user_id', $this->users_id)
->where('user_permissions_permission', $permission)
->where('user_permissions_bus_id', $this->users_bus_id)
->where(function ($query) {
$query->whereNull('user_permissions_expires_at')
->orWhere('user_permissions_expires_at', '>', \Carbon\Carbon::now());
})
->exists();
if ($hasDirectPermission) {
return true;
}
// Check role permissions
return \App\Models\RolePermission::where('role_permissions_role', $this->users_role)
->where('role_permissions_bus_id', $this->users_bus_id)
->where('role_permissions_permission', $permission)
->exists();
}
/**
* Get all permissions for the user as an array of strings.
*
* @return array
*/
public function getPermissions(): array
{
// Se for Super Admin, retorna um token que indica todas as permissões
// (Ou poderíamos retornar todas as permissões do sistema se necessário)
if ($this->isSuperAdmin()) {
return ['*'];
}
// Se for Admin, retorna todas as permissões da empresa (ou um token)
if ($this->isAdmin()) {
return ['*.company'];
}
$directPermissions = \App\Models\UserPermission::where('user_permissions_user_id', $this->users_id)
->where('user_permissions_bus_id', $this->users_bus_id)
->where(function ($query) {
$query->whereNull('user_permissions_expires_at')
->orWhere('user_permissions_expires_at', '>', \Carbon\Carbon::now());
})
->pluck('user_permissions_permission')
->toArray();
$rolePermissions = \App\Models\RolePermission::where('role_permissions_role', $this->users_role)
->where('role_permissions_bus_id', $this->users_bus_id)
->pluck('role_permissions_permission')
->toArray();
return array_unique(array_merge($directPermissions, $rolePermissions));
}
/**
* Get the avatar URL attribute
*
* @return string
*/
public function getAvatarUrlAttribute(): string
{
if ($this->users_avatar) {
return url('storage/uploads/avatars/' . $this->users_avatar);
}
return asset_url('img/default-avatar.png');
}
/**
* Get display name
*
* @return string
*/
public function getDisplayNameAttribute(): string
{
return $this->users_name ?: $this->users_email;
}
/**
* Get id attribute alias
*
* @return int
*/
public function getIdAttribute(): int
{
return $this->users_id;
}
/**
* Hash the password before saving
*
* @param string $value
* @return void
*/
public function setUsersPasswordAttribute($value): void
{
$this->attributes['users_password'] = password_hash($value, PASSWORD_DEFAULT);
}
/**
* Alias for setting password via 'password' attribute
*
* @param string $value
* @return void
*/
public function setPasswordAttribute($value): void
{
$this->setUsersPasswordAttribute($value);
}
/**
* Get the password attribute for authentication
*
* @return string
*/
public function getUsersPasswordAttribute(): string
{
return $this->attributes['users_password'] ?? '';
}
/**
* Alias for getting password via 'password' attribute
*
* @return string
*/
public function getPasswordAttribute(): string
{
return $this->getUsersPasswordAttribute();
}
/**
* Get the email attribute
*
* @return string
*/
public function getEmailAttribute(): string
{
return $this->attributes['users_email'] ?? '';
}
/**
* Get the name attribute
*
* @return string
*/
public function getNameAttribute(): string
{
return $this->attributes['users_name'] ?? '';
}
/**
* Get the role attribute
*
* @return string
*/
public function getRoleAttribute(): string
{
return $this->attributes['users_role'] ?? '';
}
/**
* Get the status attribute
*
* @return string
*/
public function getStatusAttribute(): string
{
return $this->attributes['users_status'] ?? '';
}
/**
* Get the email_verified_at attribute
*
* @return string|null
*/
public function getEmailVerifiedAtAttribute(): ?string
{
return $this->attributes['users_email_verified_at'] ?? null;
}
/**
* Set the email_verified_at attribute
*
* @param string|null $value
* @return void
*/
public function setEmailVerifiedAtAttribute($value): void
{
$this->attributes['users_email_verified_at'] = $value;
}
/**
* Get the avatar attribute
*
* @return string|null
*/
public function getAvatarAttribute(): ?string
{
return $this->attributes['users_avatar'] ?? null;
}
/**
* Set the avatar attribute
*
* @param string|null $value
* @return void
*/
public function setAvatarAttribute($value): void
{
$this->attributes['users_avatar'] = $value;
}
/**
* Get the bio attribute
*
* @return string|null
*/
public function getBioAttribute(): ?string
{
return $this->attributes['users_bio'] ?? null;
}
/**
* Set the bio attribute
*
* @param string|null $value
* @return void
*/
public function setBioAttribute($value): void
{
$this->attributes['users_bio'] = $value;
}
/**
* Get the phone attribute
*
* @return string|null
*/
public function getPhoneAttribute(): ?string
{
return $this->attributes['users_phone'] ?? null;
}
/**
* Set the phone attribute
*
* @param string|null $value
* @return void
*/
public function setPhoneAttribute($value): void
{
$this->attributes['users_phone'] = $value;
}
/**
* Get the preferences attribute
*
* @return array|null
*/
public function getPreferencesAttribute(): ?array
{
$value = $this->attributes['users_preferences'] ?? null;
return $value ? json_decode($value, true) : null;
}
/**
* Set the preferences attribute
*
* @param array|null $value
* @return void
*/
public function setPreferencesAttribute($value): void
{
$this->attributes['users_preferences'] = is_array($value) ? json_encode($value) : $value;
}
/**
* Get users by company
*
* @param int $companyId
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getByCompany(int $companyId)
{
return self::where('users_bus_id', $companyId)->get();
}
/**
* Get active users only
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('users_status', 'active');
}
/**
* Get users by role
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $role
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeRole($query, string $role)
{
return $query->where('users_role', $role);
}
/**
* Update last login timestamp
*
* @return bool
*/
public function updateLastLogin(): bool
{
return $this->update(['users_last_login_at' => \Carbon\Carbon::now()]);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Modules\Auth\Providers;
use Kernel\Application;
use Kernel\View;
use Modules\Auth\Models\User;
use Modules\Auth\Middleware\AuthMiddleware;
class AuthServiceProvider
{
public function register()
{
}
public function boot()
{
$app = Application::getInstance();
// Register Middleware
$app->addProtectedMiddleware(new AuthMiddleware());
// Register Public Routes
$app->addPublicPath('/login');
$app->addPublicPath('/register');
$app->addPublicPath('/forgot-password');
$app->addPublicPath('/reset-password*'); // Prefix
$app->addPublicPath('/verify-email*'); // Prefix
// Inject Auth Global
if (isset($_SESSION['user_id'])) {
try {
if (class_exists(User::class)) {
$user = User::find($_SESSION['user_id']);
if ($user) {
$view = new View();
$view->getEngine()->addGlobal('auth', ['user' => $user]);
}
}
} catch (\Exception $e) {
// Ignore
}
}
}
}
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateUsersTable
{
public function up()
{
if (!Capsule::schema()->hasTable('users')) {
Capsule::schema()->create('users', function (Blueprint $table) {
$table->increments('users_id');
$table->string('users_name');
$table->string('users_email')->unique();
$table->string('users_password');
$table->string('users_role')->default('user'); // super_admin, admin, member, user
$table->integer('users_bus_id')->default(0)->index();
$table->string('users_avatar')->nullable();
$table->text('users_bio')->nullable();
$table->string('users_phone')->nullable();
$table->timestamp('users_email_verified_at')->nullable();
$table->timestamp('users_last_login_at')->nullable();
$table->string('users_status')->default('active'); // active, inactive, banned
$table->json('users_preferences')->nullable();
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
});
}
}
public function down()
{
Capsule::schema()->dropIfExists('users');
}
}
@@ -0,0 +1,66 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateAuthPermissionsTables
{
public function up()
{
// User Permissions
if (!Capsule::schema()->hasTable('user_permissions')) {
Capsule::schema()->create('user_permissions', function (Blueprint $table) {
$table->increments('user_permissions_id');
$table->integer('user_permissions_user_id')->unsigned();
$table->integer('user_permissions_bus_id')->unsigned()->nullable(); // Permission might be global or company specific
$table->string('user_permissions_permission');
$table->integer('user_permissions_granted_by')->unsigned()->nullable();
$table->timestamp('user_permissions_granted_at')->useCurrent();
$table->timestamp('user_permissions_expires_at')->nullable();
$table->timestamps();
$table->foreign('user_permissions_user_id')->references('users_id')->on('users')->onDelete('cascade');
// Optional: foreign key for bus_id if module order permits
});
}
// Role Permissions
if (!Capsule::schema()->hasTable('role_permissions')) {
Capsule::schema()->create('role_permissions', function (Blueprint $table) {
$table->increments('role_permissions_id');
$table->string('role_permissions_role');
$table->integer('role_permissions_bus_id')->unsigned()->nullable();
$table->string('role_permissions_permission');
$table->integer('role_permissions_created_by')->unsigned()->nullable();
$table->timestamps();
});
}
// Activity Logs
if (!Capsule::schema()->hasTable('activity_logs')) {
Capsule::schema()->create('activity_logs', function (Blueprint $table) {
$table->increments('activity_logs_id');
$table->integer('activity_logs_user_id')->unsigned()->nullable();
$table->integer('activity_logs_bus_id')->unsigned()->nullable();
$table->string('activity_logs_action');
$table->string('activity_logs_resource_type')->nullable();
$table->string('activity_logs_resource_id')->nullable();
$table->json('activity_logs_old_values')->nullable();
$table->json('activity_logs_new_values')->nullable();
$table->string('activity_logs_ip_address')->nullable();
$table->string('activity_logs_user_agent')->nullable();
$table->timestamps();
$table->index('activity_logs_user_id');
$table->index('activity_logs_bus_id');
});
}
}
public function down()
{
Capsule::schema()->dropIfExists('activity_logs');
Capsule::schema()->dropIfExists('role_permissions');
Capsule::schema()->dropIfExists('user_permissions');
}
}
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateAuthAttemptsTable
{
public function up(): void
{
if (!Capsule::schema()->hasTable('auth_attempts')) {
Capsule::schema()->create('auth_attempts', function (Blueprint $table) {
$table->id();
$table->string('email')->nullable();
$table->string('ip')->index();
$table->string('action')->index();
$table->boolean('success')->default(false);
$table->timestamp('created_at')->nullable();
});
}
}
public function down(): void
{
Capsule::schema()->dropIfExists('auth_attempts');
}
}
@@ -0,0 +1,88 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use App\Models\Module;
use Modules\Company\Models\Company;
class InsertAuthModule
{
public function up()
{
$moduleData = [
'modules_name' => 'auth',
'modules_display_name' => 'Authentication',
'modules_description' => 'Authentication and authorization module for user management',
'modules_version' => '1.0.0',
'modules_author' => 'SysCMS Team',
'modules_ative' => true,
'modules_public' => true,
'modules_blocked' => true, // Core module, cannot be disabled easily
'modules_dependencies' => [],
'modules_permissions' => [
"auth.login",
"auth.logout",
"auth.register",
"auth.profile",
"auth.password.reset",
"auth.password.change"
],
'modules_settings' => [
"allow_registration" => true,
"require_email_verification" => false,
"password_min_length" => 8,
"session_timeout" => 120
],
'modules_path' => 'modules/auth',
'modules_namespace' => 'Modules\\Auth',
'modules_icon' => 'bi bi-person-check',
'menu_side_bar' => true,
'menu_nav_bar' => true,
'modules_order' => 1,
'modules_install_date' => date('Y-m-d H:i:s'),
'modules_update_date' => date('Y-m-d H:i:s')
];
// Check if module already exists
$existingModule = Module::where('modules_name', 'auth')->first();
if (!$existingModule) {
$module = Module::create($moduleData);
// Associate with all existing companies (bus_modules)
// Assuming Company model and table exist.
if (Capsule::schema()->hasTable('bus')) {
$companies = Company::all();
foreach ($companies as $company) {
$exists = Capsule::table('bus_modules')
->where('bus_modules_bus_id', $company->bus_id)
->where('bus_modules_module_id', $module->modules_id)
->exists();
if (!$exists) {
Capsule::table('bus_modules')->insert([
'bus_modules_bus_id' => $company->bus_id,
'bus_modules_module_id' => $module->modules_id,
'bus_modules_is_enabled' => true,
'bus_modules_settings' => json_encode([]),
'bus_modules_enabled_by' => 1,
'bus_modules_enabled_at' => date('Y-m-d H:i:s'),
'bus_modules_show_public_nav' => true,
'bus_modules_show_dashboard_nav' => true,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
]);
}
}
}
}
}
public function down()
{
$module = Module::where('modules_name', 'auth')->first();
if ($module) {
Capsule::table('bus_modules')->where('bus_modules_module_id', $module->modules_id)->delete();
$module->delete();
}
}
}
+77
View File
@@ -0,0 +1,77 @@
{
"menu": {
"dashboard": "Authentifizierung",
"login": "Anmeldung"
},
"login_title": "Anmeldung - :platformName",
"register_title": "Registrierung - :platformName",
"profile_title": "Mein Profil",
"forgot_password_title": "Passwort vergessen",
"reset_password_title": "Passwort zurücksetzen",
"email": "E-Mail",
"name": "Name",
"password": "Passwort",
"password_confirmation": "Passwort Bestätigung",
"email_placeholder": "Geben Sie Ihre E-Mail ein",
"password_placeholder": "Passwort eingeben",
"password_confirmation_placeholder": "Passwort bestätigen",
"name_placeholder": "Name eingeben",
"current_password_placeholder": "Aktuelles Passwort",
"new_password_placeholder": "Neues Passwort",
"new_password_confirmation_placeholder": "Neues Passwort bestätigen",
"accept_terms": "Ich akzeptiere die Bedingungen",
"read_terms": "Bedingungen lesen",
"privacy_policy": "Datenschutz",
"terms_of_service": "Nutzungsbedingungen",
"cookie_policy": "Cookie-Richtlinie",
"data_protection": "Datenschutz",
"remember_me": "Angemeldet bleiben",
"login_button": "Anmelden",
"register_button": "Registrieren",
"forgot_password": "Passwort vergessen?",
"update_profile": "Profil aktualisieren",
"back_to_login": "Zur Anmeldung",
"logout": "Abmelden",
"send_reset_link": "Link zum Zurücksetzen senden",
"reset_token": "Zurücksetzungs-Token",
"reset_email": "E-Mail zum Zurücksetzen",
"reset_token_invalid": "Ungültiger Token",
"reset_token_expired": "Token abgelaufen",
"reset_email_invalid": "Ungültige E-Mail",
"reset_password_button": "Passwort zurücksetzen",
"invalid_credentials": "Ungültige Anmeldedaten",
"account_inactive": "Konto inaktiv",
"session_expired": "Sitzung abgelaufen. Bitte erneut anmelden.",
"registration_pending": "Registrierung ausstehend",
"registration_error": "Registrierungsfehler",
"registration_success": "Registrierung erfolgreich. Bitte anmelden.",
"logout_success": "Erfolgreich abgemeldet",
"profile_updated": "Profil aktualisiert",
"reset_link_sent": "Link zum Zurücksetzen gesendet",
"current_avatar": "Aktueller Avatar",
"close": "Schließen",
"security_check": "Sicherheitsüberprüfung",
"captcha_solved": "Richtig!",
"captcha_instruction": "Lösen Sie die Summe, um fortzufahren.",
"captcha_placeholder": "Ergebnis eingeben",
"captcha_question": "Wie viel ist :a + :b?",
"invalid_captcha": "Ungültiges Captcha",
"email_verified_success": "E-Mail bestätigt. Bitte anmelden.",
"verification_email_sent": "Bestätigungs-E-Mail gesendet.",
"resend_verification": "Bestätigung erneut senden",
"too_many_attempts": "Zu viele Versuche. Später erneut probieren.",
"captcha_reload": "Herausforderung neu laden",
"terms_and_conditions": "Allgemeine Geschäftsbedingungen",
"privacy_policy_text": "Text der Datenschutzerklärung...",
"terms_of_service_text": "Text der Nutzungsbedingungen...",
"cookie_policy_text": "Text der Cookie-Richtlinie...",
"data_protection_text": "Text zum Datenschutz...",
"phone": "Telefon",
"bio": "Biografie",
"current_password": "Aktuelles Passwort",
"avatar": "Avatar",
"cancel": "Abbrechen",
"preview": "Vorschau",
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
"email_not_found": "E-mail não encontrado."
}
+9
View File
@@ -0,0 +1,9 @@
{
"title": "Authentifizierung",
"description": "Systemauthentifizierungs- und Autorisierungsmodul",
"welcome": "Willkommen beim Content Management System",
"login_required": "Anmeldung erforderlich, um auf diese Seite zuzugreifen",
"access_denied": "Zugriff verweigert",
"invalid_credentials": "Ungültige Anmeldedaten",
"session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an."
}
+77
View File
@@ -0,0 +1,77 @@
{
"menu": {
"dashboard": "Authentication",
"login": "Login"
},
"login_title": "Login - :platformName",
"register_title": "Register - :platformName",
"profile_title": "My Profile",
"forgot_password_title": "Forgot Password",
"reset_password_title": "Reset Password",
"email": "Email",
"name": "Name",
"password": "Password",
"password_confirmation": "Password Confirmation",
"email_placeholder": "Enter your email",
"password_placeholder": "Enter your password",
"password_confirmation_placeholder": "Confirm your password",
"name_placeholder": "Enter your name",
"current_password_placeholder": "Current password",
"new_password_placeholder": "New password",
"new_password_confirmation_placeholder": "Confirm new password",
"accept_terms": "I accept the terms",
"read_terms": "Read terms",
"privacy_policy": "Privacy Policy",
"terms_of_service": "Terms of Service",
"cookie_policy": "Cookie Policy",
"data_protection": "Data Protection",
"remember_me": "Remember me",
"login_button": "Sign In",
"register_button": "Register",
"forgot_password": "Forgot your password?",
"update_profile": "Update Profile",
"back_to_login": "Back to Login",
"logout": "Logout",
"send_reset_link": "Send Reset Link",
"reset_token": "Reset Token",
"reset_email": "Reset Email",
"reset_token_invalid": "Invalid token",
"reset_token_expired": "Token expired",
"reset_email_invalid": "Invalid email",
"reset_password_button": "Reset Password",
"invalid_credentials": "Invalid credentials",
"account_inactive": "Inactive account",
"session_expired": "Session expired. Please log in again.",
"registration_pending": "Registration pending",
"registration_error": "Registration error",
"registration_success": "Registration successful! Please log in to continue.",
"logout_success": "Logout successful",
"profile_updated": "Profile updated successfully",
"reset_link_sent": "Reset link sent successfully",
"current_avatar": "Current avatar",
"close": "Close",
"security_check": "Security Check",
"captcha_solved": "Correct!",
"captcha_instruction": "Solve the sum to continue.",
"captcha_placeholder": "Enter the result",
"captcha_question": "What is :a + :b?",
"invalid_captcha": "Invalid captcha",
"email_verified_success": "Email verified successfully. Please log in.",
"verification_email_sent": "Verification email sent. Check your inbox.",
"resend_verification": "Resend verification",
"too_many_attempts": "Too many attempts. Please try again later.",
"captcha_reload": "Reload challenge",
"terms_and_conditions": "Terms and Conditions",
"privacy_policy_text": "Privacy policy text...",
"terms_of_service_text": "Terms of service text...",
"cookie_policy_text": "Cookie policy text...",
"data_protection_text": "Data protection text...",
"phone": "Phone",
"bio": "Biography",
"current_password": "Current Password",
"avatar": "Avatar",
"cancel": "Cancel",
"preview": "Preview",
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
"email_not_found": "E-mail não encontrado."
}
+9
View File
@@ -0,0 +1,9 @@
{
"title": "Authentication",
"description": "System authentication and authorization module",
"welcome": "Welcome to the Content Management System",
"login_required": "Login required to access this page",
"access_denied": "Access denied",
"invalid_credentials": "Invalid credentials",
"session_expired": "Your session has expired. Please log in again."
}
+77
View File
@@ -0,0 +1,77 @@
{
"menu": {
"dashboard": "Autenticación",
"login": "Login"
},
"login_title": "Inicio de sesión - :platformName",
"register_title": "Registro - :platformName",
"profile_title": "Mi Perfil",
"forgot_password_title": "Olvidé mi contraseña",
"reset_password_title": "Restablecer contraseña",
"email": "Correo Electrónico",
"name": "Nombre",
"password": "Contraseña",
"password_confirmation": "Confirmación de Contraseña",
"email_placeholder": "Introduce tu correo",
"password_placeholder": "Introduce tu contraseña",
"password_confirmation_placeholder": "Confirma tu contraseña",
"name_placeholder": "Introduce tu nombre",
"current_password_placeholder": "Contraseña actual",
"new_password_placeholder": "Nueva contraseña",
"new_password_confirmation_placeholder": "Confirmar nueva contraseña",
"accept_terms": "Acepto los términos",
"read_terms": "Leer términos",
"privacy_policy": "Política de privacidad",
"terms_of_service": "Términos de servicio",
"cookie_policy": "Política de cookies",
"data_protection": "Protección de datos",
"remember_me": "Recordarme",
"login_button": "Entrar",
"register_button": "Registrar",
"forgot_password": "¿Olvidaste tu contraseña?",
"update_profile": "Actualizar perfil",
"back_to_login": "Volver al inicio",
"logout": "Salir",
"send_reset_link": "Enviar enlace de restablecimiento",
"reset_token": "Token de restablecimiento",
"reset_email": "Correo para restablecer",
"reset_token_invalid": "Token inválido",
"reset_token_expired": "Token expirado",
"reset_email_invalid": "Correo inválido",
"reset_password_button": "Restablecer contraseña",
"invalid_credentials": "Credenciales inválidas",
"account_inactive": "Cuenta inactiva",
"session_expired": "Sesión expirada. Por favor, inicia sesión nuevamente.",
"registration_pending": "Registro pendiente",
"registration_error": "Error en el registro",
"registration_success": "Registro exitoso. Inicia sesión para continuar.",
"logout_success": "Salida exitosa",
"profile_updated": "Perfil actualizado",
"reset_link_sent": "Enlace de restablecimiento enviado",
"current_avatar": "Avatar actual",
"close": "Cerrar",
"security_check": "Verificación de seguridad",
"captcha_solved": "¡Correcto!",
"captcha_instruction": "Resuelve la suma para continuar.",
"captcha_placeholder": "Introduce el resultado",
"captcha_question": "¿Cuánto es :a + :b?",
"invalid_captcha": "Captcha inválido",
"email_verified_success": "Correo verificado. Inicia sesión.",
"verification_email_sent": "Correo de verificación enviado.",
"resend_verification": "Reenviar verificación",
"too_many_attempts": "Demasiados intentos. Inténtalo más tarde.",
"captcha_reload": "Recargar desafío",
"terms_and_conditions": "Términos y Condiciones",
"privacy_policy_text": "Texto de la política de privacidad...",
"terms_of_service_text": "Texto de los términos de servicio...",
"cookie_policy_text": "Texto de la política de cookies...",
"data_protection_text": "Texto de protección de datos...",
"phone": "Teléfono",
"bio": "Biografía",
"current_password": "Contraseña actual",
"avatar": "Avatar",
"cancel": "Cancelar",
"preview": "Vista previa",
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
"email_not_found": "E-mail não encontrado."
}
+9
View File
@@ -0,0 +1,9 @@
{
"title": "Autenticación",
"description": "Módulo de autenticación y autorización del sistema",
"welcome": "Bienvenido al Sistema de Gestión de Contenidos",
"login_required": "Es necesario iniciar sesión para acceder a esta página",
"access_denied": "Acceso denegado",
"invalid_credentials": "Credenciales inválidas",
"session_expired": "Tu sesión ha expirado. Por favor, inicia sesión de nuevo."
}
+77
View File
@@ -0,0 +1,77 @@
{
"menu": {
"dashboard": "Authentification",
"login": "Connexion"
},
"login_title": "Connexion - :platformName",
"register_title": "Inscription - :platformName",
"profile_title": "Mon Profil",
"forgot_password_title": "Mot de passe oublié",
"reset_password_title": "Réinitialiser le mot de passe",
"email": "Email",
"name": "Nom",
"password": "Mot de passe",
"password_confirmation": "Confirmation du mot de passe",
"email_placeholder": "Entrez votre email",
"password_placeholder": "Saisissez votre mot de passe",
"password_confirmation_placeholder": "Confirmez votre mot de passe",
"name_placeholder": "Saisissez votre nom",
"current_password_placeholder": "Mot de passe actuel",
"new_password_placeholder": "Nouveau mot de passe",
"new_password_confirmation_placeholder": "Confirmez le nouveau mot de passe",
"accept_terms": "J'accepte les conditions",
"read_terms": "Lire les conditions",
"privacy_policy": "Politique de confidentialité",
"terms_of_service": "Conditions d'utilisation",
"cookie_policy": "Politique de cookies",
"data_protection": "Protection des données",
"remember_me": "Se souvenir de moi",
"login_button": "Se connecter",
"register_button": "S'inscrire",
"forgot_password": "Mot de passe oublié ?",
"update_profile": "Mettre à jour le profil",
"back_to_login": "Retour à la connexion",
"logout": "Se déconnecter",
"send_reset_link": "Envoyer le lien de réinitialisation",
"reset_token": "Jeton de réinitialisation",
"reset_email": "E-mail de réinitialisation",
"reset_token_invalid": "Jeton invalide",
"reset_token_expired": "Jeton expiré",
"reset_email_invalid": "E-mail invalide",
"reset_password_button": "Réinitialiser le mot de passe",
"invalid_credentials": "Identifiants invalides",
"account_inactive": "Compte inactif",
"session_expired": "Session expirée. Veuillez vous reconnecter.",
"registration_pending": "Inscription en attente",
"registration_error": "Erreur d'inscription",
"registration_success": "Inscription réussie ! Connectez-vous pour continuer.",
"logout_success": "Déconnexion réussie",
"profile_updated": "Profil mis à jour avec succès",
"reset_link_sent": "Lien de réinitialisation envoyé avec succès",
"current_avatar": "Avatar actuel",
"close": "Fermer",
"security_check": "Vérification de sécurité",
"captcha_solved": "Correct !",
"captcha_instruction": "Résolvez l'addition pour continuer.",
"invalid_captcha": "Captcha invalide",
"email_verified_success": "E-mail confirmé avec succès. Connectez-vous.",
"verification_email_sent": "E-mail de confirmation envoyé. Vérifiez votre boîte de réception.",
"email_already_verified": "Cet e-mail a déjà été vérifié. Connectez-vous.",
"email_not_found": "E-mail introuvable.",
"resend_verification": "Renvoyer la confirmation",
"too_many_attempts": "Trop de tentatives. Réessayez plus tard.",
"captcha_reload": "Recharger le défi",
"terms_and_conditions": "Termes et Conditions",
"privacy_policy_text": "Texte de la politique de confidentialité...",
"terms_of_service_text": "Texte des conditions d'utilisation...",
"cookie_policy_text": "Texte de la politique de cookies...",
"data_protection_text": "Texte de la protection des données...",
"phone": "Téléphone",
"bio": "Biographie",
"current_password": "Mot de passe actuel",
"avatar": "Avatar",
"cancel": "Annuler",
"preview": "Aperçu",
"captcha_question": "What is :a + :b?",
"captcha_placeholder": "Enter the result"
}
+9
View File
@@ -0,0 +1,9 @@
{
"title": "Authentification",
"description": "Module d'authentification et d'autorisation",
"welcome": "Bienvenue dans le système de gestion de contenu",
"login_required": "Connexion requise pour accéder à cette page",
"access_denied": "Accès refusé",
"invalid_credentials": "Identifiants invalides",
"session_expired": "Votre session a expiré. Veuillez vous reconnecter."
}
+82
View File
@@ -0,0 +1,82 @@
{
"menu": {
"dashboard": "Autenticação",
"login": "Login"
},
"login_title": "Login - :platformName",
"register_title": "Cadastro - :platformName",
"profile_title": "Meu Perfil",
"forgot_password_title": "Esqueci minha senha",
"reset_password_title": "Redefinir senha",
"password_reset_success": "Senha redefinida com sucesso! Faça login para continuar.",
"password_confirmation_error": "As senhas não coincidem.",
"weak_password_error": "A senha deve ter pelo menos 8 caracteres.",
"reset_error": "Ocorreu um erro ao redefinir sua senha. Verifique os dados e tente novamente.",
"email": "E-mail",
"name": "Nome",
"password": "Senha",
"password_confirmation": "Confirmação de Senha",
"email_placeholder": "Digite seu email",
"password_placeholder": "Digite sua senha",
"password_confirmation_placeholder": "Confirme sua senha",
"name_placeholder": "Digite seu nome",
"current_password_placeholder": "Senha atual",
"new_password_placeholder": "Nova senha",
"new_password_confirmation_placeholder": "Confirme a nova senha",
"accept_terms": "Aceito os termos",
"read_terms": "Ler os termos",
"privacy_policy": "Política de Privacidade",
"terms_of_service": "Termos de Serviço",
"cookie_policy": "Política de Cookies",
"data_protection": "Proteção de Dados",
"remember_me": "Lembrar-me",
"login_button": "Entrar",
"register_button": "Cadastrar",
"forgot_password": "Esqueceu a senha?",
"update_profile": "Atualizar Perfil",
"back_to_login": "Voltar ao Login",
"logout": "Sair",
"send_reset_link": "Enviar Link de Redefinição",
"reset_token": "Token de Redefinição",
"reset_email": "Email para Redefinição",
"reset_token_invalid": "Token inválido",
"reset_token_expired": "Token expirado",
"reset_email_invalid": "Email inválido",
"reset_password_button": "Redefinir Senha",
"invalid_credentials": "Credenciais inválidas",
"account_inactive": "Conta inativa",
"session_expired": "Sessão expirada. Por favor, faça login novamente.",
"registration_pending": "Cadastro pendente",
"registration_error": "Erro no cadastro",
"registration_success": "Cadastro realizado com sucesso! Faça login para continuar.",
"logout_success": "Logout realizado com sucesso",
"profile_updated": "Perfil atualizado com sucesso",
"reset_link_sent": "Link de redefinição enviado com sucesso",
"current_avatar": "Avatar atual",
"close": "Fechar",
"security_check": "Verificação de Segurança",
"captcha_solved": "Correto!",
"captcha_instruction": "Resolva a soma para continuar.",
"invalid_captcha": "Captcha inválido",
"email_verified_success": "E-mail confirmado com sucesso. Faça login.",
"verification_email_sent": "E-mail de confirmação enviado. Verifique sua caixa de entrada.",
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
"email_not_found": "E-mail não encontrado.",
"resend_verification": "Reenviar confirmação",
"too_many_attempts": "Muitas tentativas. Tente novamente mais tarde.",
"captcha_reload": "Trocar desafio",
"terms_and_conditions": "Termos e Condições",
"privacy_policy_text": "Texto da política de privacidade...",
"terms_of_service_text": "Texto dos termos de serviço...",
"cookie_policy_text": "Texto da política de cookies...",
"data_protection_text": "Texto da proteção de dados...",
"phone": "Telefone",
"bio": "Biografia",
"current_password": "Senha atual",
"new_password": "Nova senha",
"avatar": "Avatar",
"cancel": "Cancelar",
"preview": "Visualização",
"captcha_question": "What is :a + :b?",
"captcha_placeholder": "Enter the result"
}
+9
View File
@@ -0,0 +1,9 @@
{
"title": "Autenticação",
"description": "Módulo de autenticação e autorização do sistema",
"welcome": "Bem-vindo ao Sistema de Gerenciamento de Conteúdo",
"login_required": "É necessário fazer login para acessar esta página",
"access_denied": "Acesso negado",
"invalid_credentials": "Credenciais inválidas",
"session_expired": "Sua sessão expirou. Por favor, faça login novamente."
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "auth",
"display_name": "Authentication",
"description": "Authentication and authorization module for user management",
"version": "1.0.0",
"author": "SysCMS Team",
"dependencies": [],
"permissions": [
"auth.login",
"auth.logout",
"auth.register",
"auth.profile",
"auth.password.reset",
"auth.password.change"
],
"settings": {
"allow_registration": true,
"require_email_verification": false,
"password_min_length": 8,
"session_timeout": 120
},
"assets": {
"css": [],
"js": []
},
"providers": [
"Modules\\Auth\\Providers\\AuthServiceProvider"
],
"routes": {
"web": "routes/web.php",
"api": "routes/api.php"
},
"menu": {
"dashboard": {
"icon": "bi bi-person-check",
"label": "auth.menu.dashboard",
"route": "admin/dashboard",
"order": 1
},
"public": {
"label": "auth.menu.login",
"route": "login",
"order": 1
}
},
"views": "views",
"locales": "locales",
"module_ative": true,
"module_public": true,
"module_blocked": true,
"ready": true,
"tables": [
"users"
]
}
View File
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Kernel\Application;
/**
* Register auth module routes
*
* @param Application $app
* @return void
*/
return function (Application $app) {
// Authentication routes
$app->addRoute('GET', '/login', [\Modules\Auth\Controllers\AuthController::class, 'showLoginForm']);
$app->addRoute('POST', '/login', [\Modules\Auth\Controllers\AuthController::class, 'login']);
$app->addRoute('GET', '/logout', [\Modules\Auth\Controllers\AuthController::class, 'logout']);
// Registration routes
$app->addRoute('GET', '/register', [\Modules\Auth\Controllers\AuthController::class, 'showRegistrationForm']);
$app->addRoute('POST', '/register', [\Modules\Auth\Controllers\AuthController::class, 'register']);
// Profile routes
$app->addRoute('GET', '/profile', [\Modules\Auth\Controllers\AuthController::class, 'profile']);
$app->addRoute('POST', '/profile', [\Modules\Auth\Controllers\AuthController::class, 'updateProfile']);
// Password reset routes
$app->addRoute('GET', '/forgot-password', [\Modules\Auth\Controllers\AuthController::class, 'showForgotPasswordForm']);
$app->addRoute('POST', '/forgot-password', [\Modules\Auth\Controllers\AuthController::class, 'sendResetLink']);
$app->addRoute('GET', '/reset-password/{token}', [\Modules\Auth\Controllers\AuthController::class, 'showResetPasswordForm']);
$app->addRoute('POST', '/reset-password', [\Modules\Auth\Controllers\AuthController::class, 'resetPassword']);
// Email verification
$app->addRoute('GET', '/verify-email/resend', [\Modules\Auth\Controllers\AuthController::class, 'showResendVerificationForm']);
$app->addRoute('POST', '/verify-email/resend', [\Modules\Auth\Controllers\AuthController::class, 'resendVerification']);
$app->addRoute('GET', '/verify-email/{token}', [\Modules\Auth\Controllers\AuthController::class, 'verifyEmail']);
};
+106
View File
@@ -0,0 +1,106 @@
{% extends "layouts/base.twig" %}
{% block title %}{{ __('auth.forgot_password_title', {'platformName': platformName}) }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-5">
<div class="card-header">
<h3 class="text-center font-weight-light my-4">
{{ __('auth.forgot_password_title', {'platformName': platformName}) }}
</h3>
</div>
<div class="card-body p-4">
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error == 1 ? __('auth.email_not_found') : '' }}
{{ error == 'captcha' ? __('auth.invalid_captcha') : '' }}
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
</div>
{% endif %}
{% if success %}
<div class="alert alert-success" role="alert">
{{ __('auth.reset_link_sent') }}
</div>
{% endif %}
<form method="POST" action="{{ url('forgot-password') }}">
{{ csrf_field() }}
<div class="form-floating mb-3">
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" required>
<label for="email">{{ __('auth.email') }}</label>
</div>
{% if require_captcha_forgot %}
<div class="mb-3" x-data="{
num1: 0,
num2: 0,
userAnswer: '',
solved: false,
generate() {
this.num1 = Math.floor(Math.random() * 10) + 1;
this.num2 = Math.floor(Math.random() * 10) + 1;
this.userAnswer = '';
this.solved = false;
},
check() {
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
this.solved = true;
} else {
this.solved = false;
}
}
}" x-init="generate()">
<div class="mb-3">
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
<div class="d-flex align-items-center gap-2">
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
<span x-text="num1"></span> + <span x-text="num2"></span> =
</div>
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
<i class="bi bi-arrow-clockwise"></i> ↻
</button>
</div>
<div class="mt-2" style="min-height: 24px;">
<span x-show="solved" class="text-success fw-bold small">
✓ {{ __('auth.captcha_solved') }}
</span>
<span x-show="!solved" class="text-muted small">
{{ __('auth.captcha_instruction') }}
</span>
</div>
</div>
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
</div>
{% endif %}
<div class="d-grid">
<button class="btn btn-primary" type="submit">
{{ __('auth.send_reset_link') }}
</button>
<a href="{{ url('login') }}" class="btn btn-link">
{{ __('auth.back_to_login') }}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-focus on email field
document.addEventListener('DOMContentLoaded', function() {
const emailField = document.getElementById('email');
if (emailField) {
emailField.focus();
}
});
</script>
{% endblock %}
View File
+259
View File
@@ -0,0 +1,259 @@
{% extends "layouts/base.twig" %}
{% block title %}{{ __('auth.login_title', {'platformName': platformName}) }}{% endblock %}
{% block head %}
<style>
.login-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
}
.login-card {
border: none;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
max-width: 450px;
width: 100%;
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px 20px;
text-align: center;
}
.login-header h3 {
margin: 0;
font-weight: 600;
}
.login-body {
padding: 40px 30px;
background-color: #fff;
}
.form-floating {
margin-bottom: 20px;
}
.form-control {
border-radius: 10px;
border: 1px solid #e0e0e0;
padding: 12px 15px;
font-size: 16px;
transition: all 0.3s;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 10px;
padding: 12px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-link {
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: all 0.3s;
}
.btn-link:hover {
color: #764ba2;
text-decoration: underline;
}
.form-check-input:checked {
background-color: #667eea;
border-color: #667eea;
}
.alert {
border-radius: 10px;
border: none;
margin-bottom: 20px;
}
.captcha-container {
background-color: #f8f9fa;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.captcha-image {
border-radius: 8px;
overflow: hidden;
display: inline-block;
}
.password-toggle {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #6c757d;
z-index: 10;
}
</style>
{% endblock %}
{% block content %}
<div class="login-container">
<div class="row justify-content-center w-100">
<div class="col-lg-5">
<div class="card login-card">
<div class="login-header">
<h3>{{ __('auth.login_title', {'platformName': platformName}) }}</h3>
</div>
<div class="login-body">
{% if password_reset_success %}
<div class="alert alert-success" role="alert">
{{ __('auth.password_reset_success') }}
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error == 1 ? __('auth.invalid_credentials') : '' }}
{{ error == 2 ? __('auth.account_inactive') : '' }}
{{ error == 3 ? __('auth.registration_pending') : '' }}
{{ error == 'captcha' ? __('auth.invalid_captcha') : '' }}
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
</div>
{% endif %}
{% if expired %}
<div class="alert alert-warning" role="alert">
{{ __('auth.session_expired') }}
</div>
{% endif %}
<form method="POST" action="{{ url('login') }}">
{{ csrf_field() }}
<div class="form-floating mb-3">
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" value="{{ old('email') }}" required>
<label for="email">{{ __('auth.email') }}</label>
</div>
{% include 'components/password-field.twig' with {
id: 'password',
name: 'password',
label: __('auth.password'),
placeholder: __('auth.password_placeholder'),
required: true
} %}
{% if require_captcha_login %}
<div class="captcha-container" x-data="{
num1: 0,
num2: 0,
userAnswer: '',
solved: false,
generate() {
this.num1 = Math.floor(Math.random() * 10) + 1;
this.num2 = Math.floor(Math.random() * 10) + 1;
this.userAnswer = '';
this.solved = false;
},
check() {
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
this.solved = true;
} else {
this.solved = false;
}
}
}" x-init="generate()">
<div class="mb-3">
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
<div class="d-flex align-items-center gap-2">
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
<span x-text="num1"></span> + <span x-text="num2"></span> =
</div>
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
<i class="bi bi-arrow-clockwise"></i> ↻
</button>
</div>
<div class="mt-2" style="min-height: 24px;">
<span x-show="solved" class="text-success fw-bold small">
✓ {{ __('auth.captcha_solved') }}
</span>
<span x-show="!solved" class="text-muted small">
{{ __('auth.captcha_instruction') }}
</span>
</div>
</div>
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
</div>
{% endif %}
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="remember" name="remember">
<label class="form-check-label" for="remember">{{ __('auth.remember_me') }}</label>
</div>
<div class="d-grid gap-3">
<button class="btn btn-primary btn-lg" type="submit">
{{ __('auth.login_button') }}
</button>
<a href="{{ url('forgot-password') }}" class="btn btn-link">
{{ __('auth.forgot_password') }}
</a>
{% if allow_registration %}
<a href="{{ url('register') }}" class="btn btn-link">
{{ __('auth.register_button') }}
</a>
{% endif %}
</div>
</form>
<div class="mt-3 d-grid">
<a href="{{ url('verify-email/resend') }}" class="btn btn-outline-secondary btn-sm">
{{ __('auth.resend_verification') }}
</a>
</div>
{% if success %}
<div class="alert alert-success" role="alert">
{{ __('auth.logout_success') }}
</div>
{% endif %}
{% if registered %}
<div class="alert alert-success" role="alert">
{{ __('auth.registration_success') }}
</div>
{% endif %}
{% if verified %}
<div class="alert alert-success" role="alert">
{{ __('auth.email_verified_success') }}
</div>
{% endif %}
{% if resend %}
<div class="alert alert-info" role="alert">
{{ __('auth.verification_email_sent') }}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-focus on email field
document.addEventListener('DOMContentLoaded', function() {
const emailField = document.getElementById('email');
if (emailField) {
emailField.focus();
}
});
</script>
{% endblock %}
+135
View File
@@ -0,0 +1,135 @@
{% extends "layouts/dashboard.twig" %}
{% block title %}{{ __('auth.profile_title') }} - {% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header">
<h3 class="card-title">{{ __('auth.profile_title') }}</h3>
</div>
<div class="card-body">
{% if success %}
<div class="alert alert-success" role="alert">
{{ __('auth.profile_updated') }}
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endif %}
<form method="POST" action="{{ url('profile') }}" enctype="multipart/form-data">
{{ csrf_field() }}
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">{{ __('auth.name') }}</label>
<input type="text" class="form-control" id="name" name="name" value="{{ user.name }}" required>
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">{{ __('auth.email') }}</label>
<input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">{{ __('auth.phone') }}</label>
<input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone }}">
</div>
<div class="col-md-12 mb-3">
<label for="bio" class="form-label">{{ __('auth.bio') }}</label>
<textarea class="form-control" id="bio" name="bio" rows="4">{{ user.bio }}</textarea>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="avatar" class="form-label">{{ __('auth.avatar') }}</label>
<input type="file" class="form-control" id="avatar" name="avatar" accept="image/*">
<div id="avatarPreview" class="mt-2"></div>
{% if user.avatar %}
<div class="mt-2">
<small class="form-text text-muted">{{ __('auth.current_avatar') }}:</small>
<img src="{{ user.avatar_url }}" alt="{{ __('auth.current_avatar') }}" class="img-thumbnail" style="max-width: 100px;">
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3" x-data="{ show: false }">
<label for="current_password" class="form-label">{{ __('auth.current_password') }}</label>
<div class="input-group">
<input :type="show ? 'text' : 'password'" class="form-control" id="current_password" name="current_password" placeholder="{{ __('auth.current_password_placeholder') }}" autocomplete="new-password" value="">
<button class="btn btn-outline-secondary" type="button" @click="show = !show">
<i :class="show ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3" x-data="{ show: false }">
<label for="password" class="form-label">{{ __('auth.new_password') }}</label>
<div class="input-group">
<input :type="show ? 'text' : 'password'" class="form-control" id="password" name="password" placeholder="{{ __('auth.new_password_placeholder') }}" autocomplete="new-password" value="">
<button class="btn btn-outline-secondary" type="button" @click="show = !show">
<i :class="show ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
</button>
</div>
</div>
<div class="col-md-6 mb-3" x-data="{ show: false }">
<label for="password_confirmation" class="form-label">{{ __('auth.password_confirmation') }}</label>
<div class="input-group">
<input :type="show ? 'text' : 'password'" class="form-control" id="password_confirmation" name="password_confirmation" placeholder="{{ __('auth.password_confirmation_placeholder') }}" autocomplete="new-password" value="">
<button class="btn btn-outline-secondary" type="button" @click="show = !show">
<i :class="show ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button type="submit" class="btn btn-primary">{{ __('auth.update_profile') }}</button>
<a href="{{ url('dashboard') }}" class="btn btn-secondary">{{ __('auth.cancel') }}</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Preview avatar before upload
document.addEventListener('DOMContentLoaded', function() {
const avatarInput = document.getElementById('avatar');
const avatarPreview = document.getElementById('avatarPreview');
if (avatarInput && avatarPreview) {
avatarInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
avatarPreview.innerHTML = `<img src="${e.target.result}" alt="{{ __('auth.preview') }}" class="img-thumbnail" style="max-width: 200px;">`;
};
reader.readAsDataURL(file);
}
});
}
});
</script>
{% endblock %}
+131
View File
@@ -0,0 +1,131 @@
{% extends "layouts/base.twig" %}
{% block title %}{{ __('auth.register_title', {'platformName': platformName}) }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-5">
<div class="card-header">
<h3 class="text-center font-weight-light my-4">
{{ __('auth.register_title', {'platformName': platformName}) }}
</h3>
</div>
<div class="card-body p-4">
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error == 'captcha' ? __('auth.invalid_captcha') : __('auth.registration_error') }}
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
</div>
{% endif %}
<form method="POST" action="{{ url('register') }}">
{{ csrf_field() }}
<div class="form-floating mb-3">
<input type="text" class="form-control" id="name" name="name" placeholder="{{ __('auth.name_placeholder') }}" value="{{ old('name') }}" required>
<label for="name">{{ __('auth.name') }}</label>
</div>
<div class="form-floating mb-3">
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" value="{{ old('email') }}" required>
<label for="email">{{ __('auth.email') }}</label>
</div>
{% include 'components/password-field.twig' with {
id: 'password',
name: 'password',
label: __('auth.password'),
placeholder: __('auth.password_placeholder'),
required: true
} %}
{% include 'components/password-field.twig' with {
id: 'password_confirmation',
name: 'password_confirmation',
label: __('auth.password_confirmation'),
placeholder: __('auth.password_confirmation_placeholder'),
required: true
} %}
{% if require_captcha_register %}
<div class="mb-3" x-data="{
num1: 0,
num2: 0,
userAnswer: '',
solved: false,
generate() {
this.num1 = Math.floor(Math.random() * 10) + 1;
this.num2 = Math.floor(Math.random() * 10) + 1;
this.userAnswer = '';
this.solved = false;
},
check() {
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
this.solved = true;
} else {
this.solved = false;
}
}
}" x-init="generate()">
<div class="mb-3">
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
<div class="d-flex align-items-center gap-2">
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
<span x-text="num1"></span> + <span x-text="num2"></span> =
</div>
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
<i class="bi bi-arrow-clockwise"></i> ↻
</button>
</div>
<div class="mt-2" style="min-height: 24px;">
<span x-show="solved" class="text-success fw-bold small">
✓ {{ __('auth.captcha_solved') }}
</span>
<span x-show="!solved" class="text-muted small">
{{ __('auth.captcha_instruction') }}
</span>
</div>
</div>
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
</div>
{% endif %}
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="terms" name="terms" required>
<label class="form-check-label" for="terms">
{{ __('auth.accept_terms') }}
<a href="/user_terms.html" target="_blank">
{{ __('auth.read_terms') }}
</a>
</label>
</div>
<div class="d-grid">
<button class="btn btn-primary btn-lg" type="submit">
{{ __('auth.register_button') }}
</button>
<a href="{{ url('login') }}" class="btn btn-link">
{{ __('auth.back_to_login') }}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-focus on name field
document.addEventListener('DOMContentLoaded', function() {
const nameField = document.getElementById('name');
if (nameField) {
nameField.focus();
}
});
</script>
{% endblock %}
+107
View File
@@ -0,0 +1,107 @@
{% extends "layouts/base.twig" %}
{% block title %}{{ __('auth.resend_verification') }} - {% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-5">
<div class="card-header">
<h3 class="text-center font-weight-light my-4">
{{ __('auth.resend_verification') }}
</h3>
</div>
<div class="card-body p-4">
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error == 1 ? __('auth.email_not_found') : '' }}
{{ error == 'captcha' ? __('auth.invalid_captcha') : '' }}
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
{{ error == 'verified' ? __('auth.email_already_verified') : '' }}
</div>
{% endif %}
{% if success %}
<div class="alert alert-success" role="alert">
{{ __('auth.verification_email_sent') }}
</div>
{% endif %}
<form method="POST" action="{{ url('verify-email/resend') }}">
{{ csrf_field() }}
<div class="form-floating mb-3">
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" required value="{{ old('email') }}">
<label for="email">{{ __('auth.email') }}</label>
</div>
{% if require_captcha_resend %}
<div class="mb-3" x-data="{
num1: 0,
num2: 0,
userAnswer: '',
solved: false,
generate() {
this.num1 = Math.floor(Math.random() * 10) + 1;
this.num2 = Math.floor(Math.random() * 10) + 1;
this.userAnswer = '';
this.solved = false;
},
check() {
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
this.solved = true;
} else {
this.solved = false;
}
}
}" x-init="generate()">
<div class="mb-3">
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
<div class="d-flex align-items-center gap-2">
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
<span x-text="num1"></span> + <span x-text="num2"></span> =
</div>
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
<i class="bi bi-arrow-clockwise"></i> ↻
</button>
</div>
<div class="mt-2" style="min-height: 24px;">
<span x-show="solved" class="text-success fw-bold small">
✓ {{ __('auth.captcha_solved') }}
</span>
<span x-show="!solved" class="text-muted small">
{{ __('auth.captcha_instruction') }}
</span>
</div>
</div>
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
</div>
{% endif %}
<div class="d-grid">
<button class="btn btn-primary" type="submit">
{{ __('auth.resend_verification') }}
</button>
<a href="{{ url('login') }}" class="btn btn-link">
{{ __('auth.back_to_login') }}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-focus on email field
document.addEventListener('DOMContentLoaded', function() {
const emailField = document.getElementById('email');
if (emailField) {
emailField.focus();
}
});
</script>
{% endblock %}
+163
View File
@@ -0,0 +1,163 @@
{% extends "layouts/base.twig" %}
{% block title %}{{ __('auth.reset_password_title', {'platformName': platformName}) }}{% endblock %}
{% block head %}
<style>
.reset-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
}
.reset-card {
border: none;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
max-width: 450px;
width: 100%;
}
.reset-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px 20px;
text-align: center;
}
.reset-header h3 {
margin: 0;
font-weight: 600;
}
.reset-body {
padding: 40px 30px;
background-color: #fff;
}
.form-floating {
margin-bottom: 20px;
}
.form-control {
border-radius: 10px;
border: 1px solid #e0e0e0;
padding: 12px 15px;
font-size: 16px;
transition: all 0.3s;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 10px;
padding: 12px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-link {
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: all 0.3s;
}
.btn-link:hover {
color: #764ba2;
text-decoration: underline;
}
.alert {
border-radius: 10px;
border: none;
margin-bottom: 20px;
}
</style>
{% endblock %}
{% block content %}
<div class="reset-container">
<div class="row justify-content-center w-100">
<div class="col-lg-5">
<div class="card reset-card">
<div class="reset-header">
<h3>{{ __('auth.reset_password_title', {'platformName': platformName}) }}</h3>
</div>
<div class="reset-body">
{% if error %}
<div class="alert alert-danger" role="alert">
{% if error == 'password_confirmation' %}
{{ __('auth.password_confirmation_error') }}
{% elseif error == 'weak_password' %}
{{ __('auth.weak_password_error') }}
{% elseif error == '4' %}
{{ __('auth.reset_error') }}
{% elseif error == '1' %}
{{ __('auth.reset_token_invalid') }}
{% elseif error == '2' %}
{{ __('auth.reset_token_expired') }}
{% elseif error == '3' %}
{{ __('auth.reset_email_invalid') }}
{% else %}
{{ __('auth.reset_error') }}
{% endif %}
</div>
{% endif %}
<form method="POST" action="{{ url('reset-password') }}">
{{ csrf_field() }}
<input type="hidden" name="token" value="{{ token }}">
<input type="hidden" name="email" value="{{ email }}">
<div class="form-floating mb-3">
<input type="email" class="form-control" id="email_display" value="{{ email }}" disabled>
<label for="email_display">{{ __('auth.email') }}</label>
</div>
{% include 'components/password-field.twig' with {
id: 'password',
name: 'password',
label: __('auth.new_password'),
placeholder: __('auth.new_password_placeholder'),
required: true
} %}
{% include 'components/password-field.twig' with {
id: 'password_confirmation',
name: 'password_confirmation',
label: __('auth.password_confirmation'),
placeholder: __('auth.password_confirmation_placeholder'),
required: true
} %}
<div class="d-grid gap-2">
<button class="btn btn-primary btn-lg" type="submit">
{{ __('auth.reset_password_button') }}
</button>
<a href="{{ url('login') }}" class="btn btn-link text-decoration-none text-center">
{{ __('auth.back_to_login') }}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-focus on password field
document.addEventListener('DOMContentLoaded', function() {
const passwordField = document.getElementById('password');
if (passwordField) {
passwordField.focus();
}
});
</script>
{% endblock %}