- C1: verify.php now filters license='owner' — invite tokens no longer accepted - C1: onboarding API rejects non-owner sessions - C2: Existing-user invite requires explicit acceptance via accept_invite.php - C2: New accept_invite.php page and API engine added - C2: inviteUser() generates token + expiry for existing users; resendInvite() handles active users - C3: session_regenerate_id(true) before writing invite session keys on both invite pages - C4: invited_onboarding API wraps activation in transaction with SELECT FOR UPDATE; rowCount check added; SQLSTATE 23000 caught cleanly - C5: inviteUser() and resendInvite() two-table writes wrapped in transactions - M2: removeUser() wrapped in transaction with FOR UPDATE; clears default_company on active user removal - M4: Logged-in user guard added to invited_onboarding.php and accept_invite.php - M5: manage_users.php uses $server_url instead of HTTP_HOST for invite URLs - M6: Username regex enforces 3-32 chars; reserved name blocklist added - N5: searchUsers() changed from LIKE fuzzy search to exact email match only - N7: resendInvite() rate-limited to once per 60s via invite_resent_at column - Schema: company_map_user gains invite_expires_at and invite_resent_at columns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
6.1 KiB
PHP
141 lines
6.1 KiB
PHP
<?php
|
|
session_start();
|
|
require_once '../../../assets/utils/db_auth.php';
|
|
require_once '../../../assets/utils/classes/UserManager.php';
|
|
|
|
if ($user_role !== 'owner') { http_response_code(403); exit(json_encode(['success' => 0, 'message' => 'Only the owner can modify this setting.'])); }
|
|
|
|
$action = $data['action'] ?? '';
|
|
$um = new UserManager($pdo1, $company_id, $user_id);
|
|
|
|
try {
|
|
|
|
// All valid app_access values: every registered app key + 'all'
|
|
$all_app_keys = array_keys($app_registry);
|
|
$all_valid_apps = array_merge($all_app_keys, ['all']);
|
|
|
|
// What the current owner's license permits assigning to others
|
|
$owner_access = $_SESSION['login_app_access'] ?? 'wms';
|
|
$owner_allowed_access = $owner_access === 'all' ? $all_valid_apps : [$owner_access];
|
|
|
|
if ($action === 'create') {
|
|
$email = strtolower(trim($data['invite_email'] ?? ''));
|
|
$role = trim($data['invite_role'] ?? '');
|
|
$app_access = trim($data['invite_app_access'] ?? '');
|
|
|
|
if (!in_array($app_access, $owner_allowed_access, true)) {
|
|
throw new Exception('App access selection exceeds your license.');
|
|
}
|
|
|
|
$result = $um->inviteUser($email, $role, $app_access);
|
|
|
|
// Both new and existing users require explicit acceptance via email
|
|
$invite_url = rtrim($server_url, '/') . ($result['new_user']
|
|
? '/login/invited_onboarding.php?token=' . $result['token']
|
|
: '/login/accept_invite.php?token=' . $result['token']);
|
|
|
|
$subject = $result['new_user']
|
|
? 'You have been invited to join the team'
|
|
: 'You have been invited to join a new company';
|
|
|
|
$body_intro = $result['new_user']
|
|
? 'You have been invited to join the team. Click the button below to set up your account:'
|
|
: 'You have been invited to join a new company. Click the button below to accept:';
|
|
|
|
require_once '../../../assets/utils/module/mailer.php';
|
|
$mailer = new mailer(['pdo1' => $pdo1]);
|
|
$mailer->send_email([
|
|
'company_id' => $company_id,
|
|
'to' => $result['email'],
|
|
'subject' => $subject,
|
|
'message' => implode("\n", [
|
|
$body_intro,
|
|
"",
|
|
"<a href=\"{$invite_url}\" style=\"display:inline-block;padding:12px 28px;background:#E66239;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600;\">Accept Invitation</a>",
|
|
"",
|
|
"Or copy and paste this link into your browser:",
|
|
"<a href=\"{$invite_url}\">{$invite_url}</a>",
|
|
"",
|
|
"This link will expire in 7 days.",
|
|
"",
|
|
"If you did not expect this invitation, you can ignore this email.",
|
|
]),
|
|
'channel_name' => 'WMS',
|
|
'key' => $pinkey,
|
|
]);
|
|
$answer['message'] = htmlspecialchars($result['email']) . ' has been invited. An email has been sent.';
|
|
|
|
$answer['success'] = 1;
|
|
|
|
} elseif ($action === 'update') {
|
|
$map_id = (int)($data['map_id'] ?? 0);
|
|
$role = trim($data['role'] ?? '');
|
|
$app_access = trim($data['app_access'] ?? '');
|
|
|
|
$um->updateRole($map_id, $role);
|
|
|
|
if ($app_access !== '') {
|
|
if (!in_array($app_access, $owner_allowed_access, true)) {
|
|
throw new Exception('App access selection exceeds your license.');
|
|
}
|
|
$um->updateAppAccess($map_id, $app_access);
|
|
}
|
|
|
|
$answer['success'] = 1;
|
|
$answer['message'] = 'Access updated successfully.';
|
|
|
|
} elseif ($action === 'resend') {
|
|
$map_id = (int)($data['map_id'] ?? 0);
|
|
$result = $um->resendInvite($map_id);
|
|
|
|
$invite_url = rtrim($server_url, '/') . ($result['is_new_user']
|
|
? '/login/invited_onboarding.php?token=' . $result['token']
|
|
: '/login/accept_invite.php?token=' . $result['token']);
|
|
|
|
$body_intro = $result['is_new_user']
|
|
? 'Your invitation link has been refreshed. Click below to set up your account:'
|
|
: 'Your invitation link has been refreshed. Click below to accept the invitation:';
|
|
|
|
require_once '../../../assets/utils/module/mailer.php';
|
|
$mailer = new mailer(['pdo1' => $pdo1]);
|
|
$mailer->send_email([
|
|
'company_id' => $company_id,
|
|
'to' => $result['email'],
|
|
'subject' => 'Your invitation link has been resent',
|
|
'message' => implode("\n", [
|
|
$body_intro,
|
|
"",
|
|
"<a href=\"{$invite_url}\" style=\"display:inline-block;padding:12px 28px;background:#E66239;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600;\">Accept Invitation</a>",
|
|
"",
|
|
"Or copy and paste this link into your browser:",
|
|
"<a href=\"{$invite_url}\">{$invite_url}</a>",
|
|
"",
|
|
"This link will expire in 7 days.",
|
|
]),
|
|
'channel_name' => 'WMS',
|
|
'key' => $pinkey,
|
|
]);
|
|
|
|
$answer['success'] = 1;
|
|
$answer['message'] = 'Invitation resent to ' . htmlspecialchars($result['email']) . '.';
|
|
|
|
} elseif ($action === 'delete') {
|
|
$map_id = (int)($data['map_id'] ?? 0);
|
|
|
|
$um->removeUser($map_id);
|
|
$answer['success'] = 1;
|
|
$answer['message'] = 'User has been removed from this company.';
|
|
|
|
} else {
|
|
$answer['message'] = 'Unknown action.';
|
|
http_response_code(400);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$answer['message'] = $e->getMessage();
|
|
http_response_code(400);
|
|
}
|
|
|
|
exit(json_encode($answer));
|
|
?>
|