跳到內容

用戶管理

XOOPS用戶管理系統為處理用戶註冊、身份驗證、檔案管理和用戶偏好提供了完整的框架。本文件涵蓋用戶物件結構、註冊流程和實際實施範例。

XOOPS中的核心用戶物件是XoopsUser類別,它封裝了所有用戶資料和方法。

CREATE TABLE xoops_users (
uid INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
uname VARCHAR(25) NOT NULL UNIQUE,
email VARCHAR(60) NOT NULL,
pass VARCHAR(255) NOT NULL,
pass_expired DATETIME DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_login DATETIME DEFAULT NULL,
login_attempts INT(11) DEFAULT 0,
user_avatar VARCHAR(255) NOT NULL DEFAULT 'blank.gif',
user_regdate INT(11) NOT NULL DEFAULT 0,
user_icq VARCHAR(15) NOT NULL DEFAULT '',
user_from VARCHAR(100) NOT NULL DEFAULT '',
user_sig TEXT,
user_sig_smilies TINYINT(1) NOT NULL DEFAULT 1,
user_viewemail TINYINT(1) NOT NULL DEFAULT 0,
user_attachsig TINYINT(1) NOT NULL DEFAULT 0,
user_theme VARCHAR(32) NOT NULL DEFAULT '',
user_language VARCHAR(32) NOT NULL DEFAULT '',
user_openid VARCHAR(255) NOT NULL DEFAULT '',
user_notify_method TINYINT(1) NOT NULL DEFAULT 0,
user_notify_interval INT(11) NOT NULL DEFAULT 0
);
class XoopsUser
{
protected $uid;
protected $uname;
protected $email;
protected $pass;
protected $pass_expired;
protected $created_at;
protected $updated_at;
protected $last_login;
protected $login_attempts;
protected $user_avatar;
protected $user_regdate;
protected $user_icq;
protected $user_from;
protected $user_sig;
protected $user_sig_smilies;
protected $user_viewemail;
protected $user_attachsig;
protected $user_theme;
protected $user_language;
protected $user_openid;
protected $user_notify_method;
protected $user_notify_interval;
}
sequenceDiagram
participant User as Web User
participant Browser
participant Server as XOOPS Server
participant DB as Database
participant Email as Email Service
User->>Browser: Fill Registration Form
Browser->>Server: POST /register
Server->>Server: Validate Input
note over Server: Check username,<br/>email, password<br/>requirements
alt Validation Fails
Server-->>Browser: Show Errors
else Validation Passes
Server->>Server: Hash Password
Server->>DB: Insert User Record
DB-->>Server: Success (uid)
Server->>Server: Generate Verification Token
Server->>Email: Send Activation Email
Email-->>User: Activation Link
Server-->>Browser: Registration Confirmation
User->>Email: Click Activation Link
Email-->>Browser: Redirect to Activate URL
Browser->>Server: GET /activate?token=xxx
Server->>DB: Mark User as Active
DB-->>Server: Confirmed
Server-->>Browser: Account Activated
Browser-->>User: Show Success Message
end
<?php
/**
* User Registration Handler
*/
class RegistrationHandler
{
private $userHandler;
private $configHandler;
public function __construct()
{
$this->userHandler = xoops_getHandler('user');
$this->configHandler = xoops_getHandler('config');
}
/**
* Validate registration input
*
* @param array $data Registration form data
* @return array Validation errors, empty if valid
*/
public function validateInput(array $data): array
{
$errors = [];
// Username validation
if (empty($data['uname'])) {
$errors[] = 'Username is required';
} elseif (strlen($data['uname']) < 3) {
$errors[] = 'Username must be at least 3 characters';
} elseif (!preg_match('/^[a-zA-Z0-9_-]+$/', $data['uname'])) {
$errors[] = 'Username contains invalid characters';
} elseif ($this->userHandler->getUserByName($data['uname'])) {
$errors[] = 'Username already exists';
}
// Email validation
if (empty($data['email'])) {
$errors[] = 'Email is required';
} elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Invalid email format';
} elseif ($this->userHandler->getUserByEmail($data['email'])) {
$errors[] = 'Email already registered';
}
// Password validation
if (empty($data['password'])) {
$errors[] = 'Password is required';
} elseif (strlen($data['password']) < 8) {
$errors[] = 'Password must be at least 8 characters';
} elseif ($data['password'] !== $data['password_confirm']) {
$errors[] = 'Passwords do not match';
}
return $errors;
}
/**
* Register new user
*
* @param array $data Registration data
* @return XoopsUser|false User object or false on failure
*/
public function registerUser(array $data)
{
// Validate input
$errors = $this->validateInput($data);
if (!empty($errors)) {
return false;
}
// Create user object
$user = $this->userHandler->create();
$user->setVar('uname', $data['uname']);
$user->setVar('email', $data['email']);
$user->setVar('user_regdate', time());
// Hash password using bcrypt
$hashedPassword = password_hash(
$data['password'],
PASSWORD_BCRYPT,
['cost' => 12]
);
$user->setVar('pass', $hashedPassword);
// Set default preferences
$user->setVar('user_theme', $this->configHandler->getConfig('default_theme'));
$user->setVar('user_language', $this->configHandler->getConfig('default_language'));
// Save user
if ($this->userHandler->insertUser($user)) {
$uid = $user->getVar('uid');
// Generate verification token
$token = bin2hex(random_bytes(32));
$this->saveVerificationToken($uid, $token);
// Send verification email
$this->sendVerificationEmail($user, $token);
return $user;
}
return false;
}
/**
* Save verification token
*
* @param int $uid User ID
* @param string $token Verification token
*/
private function saveVerificationToken(int $uid, string $token): void
{
$tokenHandler = xoops_getHandler('usertoken');
$tokenHandler->saveToken($uid, $token, 'email_verification', 24); // 24 hours
}
/**
* Send verification email
*
* @param XoopsUser $user User object
* @param string $token Verification token
*/
private function sendVerificationEmail(XoopsUser $user, string $token): void
{
global $xoopsConfig;
$siteUrl = $xoopsConfig['siteurl'];
$activationUrl = $siteUrl . '/user.php?op=activate&token=' . $token;
$subject = 'Email Verification - ' . $xoopsConfig['sitename'];
$body = "Hello " . $user->getVar('uname') . ",\n\n";
$body .= "Please click the link below to verify your email:\n";
$body .= $activationUrl . "\n\n";
$body .= "This link will expire in 24 hours.\n\n";
$body .= "Regards,\n" . $xoopsConfig['sitename'];
$mailHandler = xoops_getHandler('mail');
$mailHandler->send($user->getVar('email'), $subject, $body);
}
}
graph TD
A["Login Form"] --> B["Username/Email & Password"]
B --> C{"Input Validation"}
C -->|Invalid| D["Show Error"]
C -->|Valid| E["Query User Database"]
E --> F{"User Found?"}
F -->|No| G["Invalid Credentials"]
G --> H["Log Failed Attempt"]
H --> I{"Max Attempts?"}
I -->|Yes| J["Account Locked"]
I -->|No| D
F -->|Yes| K["Verify Password Hash"]
K --> L{"Hash Match?"}
L -->|No| H
L -->|Yes| M["Check Account Status"]
M --> N{"Account Active?"}
N -->|No| O["Account Inactive/Suspended"]
N -->|Yes| P["Create Session"]
P --> Q["Store Session Token"]
Q --> R{"Remember Me?"}
R -->|Yes| S["Set Long-lived Cookie"]
R -->|No| T["Set Session Cookie"]
S --> U["Update Last Login"]
T --> U
U --> V["Redirect to Dashboard"]
<?php
/**
* 身份驗證處理器
*/
class AuthenticationHandler
{
private $userHandler;
private $maxLoginAttempts = 5;
private $lockoutDuration = 900; // 15分鐘
public function __construct()
{
$this->userHandler = xoops_getHandler('user');
}
/**
* 用用戶名/電子郵件和密碼驗證用戶
*
* @param string $username 用戶名或電子郵件
* @param string $password 純文字密碼
* @param bool $rememberMe 記住登入
* @return XoopsUser|false 經過驗證的用戶或false
*/
public function authenticate(
string $username,
string $password,
bool $rememberMe = false
)
{
// 檢查帳戶鎖定
if ($this->isAccountLocked($username)) {
throw new Exception('由於登入嘗試失敗,帳戶暫時鎖定');
}
// 按用戶名或電子郵件尋找用戶
$user = $this->userHandler->getUserByName($username);
if (!$user) {
$user = $this->userHandler->getUserByEmail($username);
}
if (!$user) {
$this->recordFailedAttempt($username);
return false;
}
// 驗證密碼
if (!password_verify($password, $user->getVar('pass'))) {
$this->recordFailedAttempt($username);
return false;
}
// 檢查帳戶狀態
if ($user->getVar('level') == 0) {
throw new Exception('帳戶未啟用');
}
// 清除失敗的嘗試
$this->clearFailedAttempts($user->getVar('uid'));
// 更新最後登入
$user->setVar('last_login', date('Y-m-d H:i:s'));
$this->userHandler->insertUser($user);
// 建立會話
$this->createSession($user, $rememberMe);
return $user;
}
/**
* 建立經過驗證的會話
*
* @param XoopsUser $user 用戶物件
* @param bool $rememberMe 啟用持久登入
*/
private function createSession(XoopsUser $user, bool $rememberMe = false): void
{
// 產生會話令牌
$token = bin2hex(random_bytes(32));
$_SESSION['xoopsUserId'] = $user->getVar('uid');
$_SESSION['xoopsUserName'] = $user->getVar('uname');
$_SESSION['xoopsSessionToken'] = $token;
$_SESSION['xoopsSessionCreated'] = time();
// 在資料庫中儲存令牌以供驗證
$this->storeSessionToken($user->getVar('uid'), $token);
if ($rememberMe) {
// 建立持久登入cookie(14天)
$cookieToken = bin2hex(random_bytes(32));
setcookie(
'xoops_persistent_login',
$cookieToken,
time() + (14 * 24 * 60 * 60),
'/',
'',
true, // 僅HTTPS
true // HttpOnly
);
// 儲存cookie令牌雜湊
$this->storePersistentToken(
$user->getVar('uid'),
hash('sha256', $cookieToken)
);
}
}
/**
* 記錄失敗的登入嘗試
*
* @param string $username 用戶名或電子郵件
*/
private function recordFailedAttempt(string $username): void
{
$key = 'login_attempt_' . md5($username);
$attempts = apcu_fetch($key) ?: 0;
apcu_store($key, $attempts + 1, $this->lockoutDuration);
}
/**
* 檢查帳戶是否被鎖定
*
* @param string $username 用戶名或電子郵件
* @return bool 如果被鎖定則為true
*/
private function isAccountLocked(string $username): bool
{
$key = 'login_attempt_' . md5($username);
$attempts = apcu_fetch($key) ?: 0;
return $attempts >= $this->maxLoginAttempts;
}
/**
* 清除失敗的嘗試
*
* @param int $uid 用戶ID
*/
private function clearFailedAttempts(int $uid): void
{
$user = $this->userHandler->getUser($uid);
$user->setVar('login_attempts', 0);
$this->userHandler->insertUser($user);
}
/**
* 儲存會話令牌
*
* @param int $uid 用戶ID
* @param string $token 會話令牌
*/
private function storeSessionToken(int $uid, string $token): void
{
// 在資料庫或快取中儲存
$tokenData = [
'uid' => $uid,
'token' => hash('sha256', $token),
'created' => time(),
'expires' => time() + (8 * 60 * 60) // 8小時
];
$db = XoopsDatabaseFactory::getDatabaseConnection();
$db->query("INSERT INTO xoops_sessions (uid, token, created, expires)
VALUES (?, ?, ?, ?)",
array($uid, $tokenData['token'], $tokenData['created'], $tokenData['expires']));
}
}
<?php
/**
* 用戶檔案管理
*/
class ProfileManager
{
private $userHandler;
private $avatarHandler;
public function __construct()
{
$this->userHandler = xoops_getHandler('user');
$this->avatarHandler = xoops_getHandler('avatar');
}
/**
* Update user profile
*
* @param int $uid User ID
* @param array $data Profile data
* @return bool Success status
*/
public function updateProfile(int $uid, array $data): bool
{
$user = $this->userHandler->getUser($uid);
if (!$user) {
return false;
}
// Update profile fields
if (isset($data['email'])) {
// Verify email is unique (excluding current user)
$existingUser = $this->userHandler->getUserByEmail($data['email']);
if ($existingUser && $existingUser->getVar('uid') !== $uid) {
throw new Exception('Email already in use');
}
$user->setVar('email', $data['email']);
}
if (isset($data['user_icq'])) {
$user->setVar('user_icq', sanitize_text_field($data['user_icq']));
}
if (isset($data['user_from'])) {
$user->setVar('user_from', sanitize_text_field($data['user_from']));
}
if (isset($data['user_sig'])) {
$sig = $data['user_sig'];
if (strlen($sig) > 500) {
throw new Exception('Signature too long');
}
$user->setVar('user_sig', $sig);
}
if (isset($data['user_sig_smilies'])) {
$user->setVar('user_sig_smilies', (int)$data['user_sig_smilies']);
}
if (isset($data['user_viewemail'])) {
$user->setVar('user_viewemail', (int)$data['user_viewemail']);
}
if (isset($data['user_attachsig'])) {
$user->setVar('user_attachsig', (int)$data['user_attachsig']);
}
if (isset($data['user_theme'])) {
$user->setVar('user_theme', $data['user_theme']);
}
if (isset($data['user_language'])) {
$user->setVar('user_language', $data['user_language']);
}
return $this->userHandler->insertUser($user);
}
/**
* Change user password
*
* @param int $uid User ID
* @param string $currentPassword Current password
* @param string $newPassword New password
* @return bool Success status
*/
public function changePassword(
int $uid,
string $currentPassword,
string $newPassword
): bool
{
$user = $this->userHandler->getUser($uid);
if (!$user) {
return false;
}
// Verify current password
if (!password_verify($currentPassword, $user->getVar('pass'))) {
throw new Exception('Current password is incorrect');
}
// Validate new password
if (strlen($newPassword) < 8) {
throw new Exception('New password must be at least 8 characters');
}
// Hash new password
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]);
$user->setVar('pass', $hashedPassword);
return $this->userHandler->insertUser($user);
}
/**
* Get user profile data
*
* @param int $uid User ID
* @return array Profile data
*/
public function getProfile(int $uid): array
{
$user = $this->userHandler->getUser($uid);
if (!$user) {
return [];
}
return [
'uid' => $user->getVar('uid'),
'uname' => $user->getVar('uname'),
'email' => $user->getVar('email'),
'user_regdate' => $user->getVar('user_regdate'),
'user_icq' => $user->getVar('user_icq'),
'user_from' => $user->getVar('user_from'),
'user_sig' => $user->getVar('user_sig'),
'user_viewemail' => $user->getVar('user_viewemail'),
'user_attachsig' => $user->getVar('user_attachsig'),
'user_theme' => $user->getVar('user_theme'),
'user_language' => $user->getVar('user_language'),
'last_login' => $user->getVar('last_login'),
'avatar' => $user->getVar('user_avatar')
];
}
}
<?php
/**
* 用戶頭像處理器
*/
class AvatarHandler
{
private $avatarPath = '/uploads/avatars/';
private $maxSize = 2097152; // 2MB
private $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
/**
* Upload user avatar
*
* @param int $uid User ID
* @param array $file $_FILES array
* @return string|false Avatar filename or false
*/
public function uploadAvatar(int $uid, array $file)
{
// Validate file
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception('File upload error: ' . $file['error']);
}
if ($file['size'] > $this->maxSize) {
throw new Exception('File too large (max 2MB)');
}
if (!in_array($file['type'], $this->allowedTypes)) {
throw new Exception('Invalid file type');
}
// Verify MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $this->allowedTypes)) {
throw new Exception('Invalid file content');
}
// Generate unique filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'avatar_' . $uid . '_' . time() . '.' . $extension;
// Create upload directory
$uploadDir = XOOPS_ROOT_PATH . $this->avatarPath;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$filepath = $uploadDir . $filename;
// Move uploaded file
if (!move_uploaded_file($file['tmp_name'], $filepath)) {
throw new Exception('Failed to move uploaded file');
}
// Resize image to standard size (150x150)
$this->resizeImage($filepath, 150, 150);
// Update user avatar
$userHandler = xoops_getHandler('user');
$user = $userHandler->getUser($uid);
// Delete old avatar if exists
$oldAvatar = $user->getVar('user_avatar');
if ($oldAvatar && $oldAvatar !== 'blank.gif') {
$oldPath = $uploadDir . $oldAvatar;
if (file_exists($oldPath)) {
unlink($oldPath);
}
}
// Save new avatar
$user->setVar('user_avatar', $filename);
$userHandler->insertUser($user);
return $filename;
}
/**
* Resize image to specified dimensions
*
* @param string $filepath Path to image file
* @param int $width Target width
* @param int $height Target height
*/
private function resizeImage(string $filepath, int $width, int $height): void
{
if (!extension_loaded('gd')) {
return; // GD not available, skip resizing
}
$image = imagecreatefromstring(file_get_contents($filepath));
if (!$image) {
return;
}
$resized = imagecreatetruecolor($width, $height);
// Preserve transparency for PNG and GIF
$format = mime_content_type($filepath);
if ($format === 'image/png' || $format === 'image/gif') {
imagealphablending($resized, false);
imagesavealpha($resized, true);
}
imagecopyresampled(
$resized, $image,
0, 0, 0, 0,
$width, $height,
imagesx($image), imagesy($image)
);
// Save resized image
$ext = pathinfo($filepath, PATHINFO_EXTENSION);
if (strtolower($ext) === 'png') {
imagepng($resized, $filepath, 9);
} else {
imagejpeg($resized, $filepath, 90);
}
imagedestroy($image);
imagedestroy($resized);
}
/**
* Delete user avatar
*
* @param int $uid User ID
* @return bool Success status
*/
public function deleteAvatar(int $uid): bool
{
$userHandler = xoops_getHandler('user');
$user = $userHandler->getUser($uid);
if (!$user) {
return false;
}
$avatar = $user->getVar('user_avatar');
if ($avatar && $avatar !== 'blank.gif') {
$filepath = XOOPS_ROOT_PATH . $this->avatarPath . $avatar;
if (file_exists($filepath)) {
unlink($filepath);
}
}
$user->setVar('user_avatar', 'blank.gif');
return $userHandler->insertUser($user);
}
}
<?php
/**
* 用戶偏好處理器
*/
class UserPreferencesHandler
{
private $userHandler;
private $prefixCache = 'user_pref_';
public function __construct()
{
$this->userHandler = xoops_getHandler('user');
}
/**
* Get user preference
*
* @param int $uid User ID
* @param string $prefKey Preference key
* @param mixed $default Default value
* @return mixed Preference value
*/
public function getPreference(int $uid, string $prefKey, $default = null)
{
// Try cache first
$cacheKey = $this->prefixCache . $uid . '_' . $prefKey;
$cached = apcu_fetch($cacheKey);
if ($cached !== false) {
return $cached;
}
// Get from database
$db = XoopsDatabaseFactory::getDatabaseConnection();
$result = $db->query(
"SELECT pref_value FROM xoops_user_preferences
WHERE uid = ? AND pref_key = ?",
array($uid, $prefKey)
);
if ($result && $db->getRowCount($result) > 0) {
$row = $db->fetchArray($result);
$value = unserialize($row['pref_value']);
apcu_store($cacheKey, $value, 3600); // Cache for 1 hour
return $value;
}
return $default;
}
/**
* Set user preference
*
* @param int $uid User ID
* @param string $prefKey Preference key
* @param mixed $prefValue Preference value
* @return bool Success status
*/
public function setPreference(int $uid, string $prefKey, $prefValue): bool
{
$db = XoopsDatabaseFactory::getDatabaseConnection();
// Check if preference exists
$result = $db->query(
"SELECT id FROM xoops_user_preferences
WHERE uid = ? AND pref_key = ?",
array($uid, $prefKey)
);
$serialized = serialize($prefValue);
if ($db->getRowCount($result) > 0) {
// Update existing preference
$success = $db->query(
"UPDATE xoops_user_preferences
SET pref_value = ?
WHERE uid = ? AND pref_key = ?",
array($serialized, $uid, $prefKey)
);
} else {
// Insert new preference
$success = $db->query(
"INSERT INTO xoops_user_preferences (uid, pref_key, pref_value)
VALUES (?, ?, ?)",
array($uid, $prefKey, $serialized)
);
}
if ($success) {
// Clear cache
$cacheKey = $this->prefixCache . $uid . '_' . $prefKey;
apcu_delete($cacheKey);
}
return (bool)$success;
}
/**
* Get all user preferences
*
* @param int $uid User ID
* @return array All preferences
*/
public function getAllPreferences(int $uid): array
{
$db = XoopsDatabaseFactory::getDatabaseConnection();
$result = $db->query(
"SELECT pref_key, pref_value FROM xoops_user_preferences WHERE uid = ?",
array($uid)
);
$prefs = [];
while ($row = $db->fetchArray($result)) {
$prefs[$row['pref_key']] = unserialize($row['pref_value']);
}
return $prefs;
}
/**
* Delete user preference
*
* @param int $uid User ID
* @param string $prefKey Preference key
* @return bool Success status
*/
public function deletePreference(int $uid, string $prefKey): bool
{
$db = XoopsDatabaseFactory::getDatabaseConnection();
$success = $db->query(
"DELETE FROM xoops_user_preferences WHERE uid = ? AND pref_key = ?",
array($uid, $prefKey)
);
if ($success) {
$cacheKey = $this->prefixCache . $uid . '_' . $prefKey;
apcu_delete($cacheKey);
}
return (bool)$success;
}
}
<?php
/**
* 常見用戶操作範例
*/
// Get current logged-in user
$xoopsUser = $GLOBALS['xoopsUser'];
if ($xoopsUser instanceof XoopsUser) {
$userId = $xoopsUser->getVar('uid');
$username = $xoopsUser->getVar('uname');
}
// Get user by ID
$userHandler = xoops_getHandler('user');
$user = $userHandler->getUser(1);
echo $user->getVar('uname');
// Get user by username
$user = $userHandler->getUserByName('admin');
if ($user) {
echo $user->getVar('email');
}
// Get user by email
$user = $userHandler->getUserByEmail('user@example.com');
// Get all users in a group
$users = $userHandler->getUsersByGroup(1);
foreach ($users as $user) {
echo $user->getVar('uname') . "\n";
}
// Create new user
$user = $userHandler->create();
$user->setVar('uname', 'newuser');
$user->setVar('email', 'newuser@example.com');
$user->setVar('pass', password_hash('password', PASSWORD_BCRYPT));
$user->setVar('user_regdate', time());
if ($userHandler->insertUser($user)) {
echo "User created: " . $user->getVar('uid');
}
// Delete user
$userHandler->deleteUser(123);
// Get user object from ID
$user = $userHandler->getUser(5);
$profile = [
'username' => $user->getVar('uname'),
'email' => $user->getVar('email'),
'regdate' => date('Y-m-d', $user->getVar('user_regdate')),
'avatar' => $user->getVar('user_avatar'),
];
  • 始終使用password_hash()PASSWORD_BCRYPT算法
  • 為bcrypt使用成本參數12
  • 絕不儲存純文字密碼
  • 實施密碼過期政策
  • 要求洩露帳戶更改密碼
<?php
// 會話配置
session_set_cookie_params([
'lifetime' => 0, // 會話cookie(瀏覽器關閉時刪除)
'path' => '/',
'domain' => '',
'secure' => true, // 僅HTTPS
'httponly' => true, // JavaScript無法訪問
'samesite' => 'Strict' // CSRF保護
]);
session_start();
// 登入後重新產生會話ID
session_regenerate_id(true);
// 驗證會話令牌
if (!isset($_SESSION['xoopsSessionToken'])) {
session_destroy();
redirect('login');
}
  • Group System.md
  • Permission System.md
  • Authentication.md
  • ../../Security/Security-Guidelines.md

#users #registration #authentication #profiles #password-security #sessions