diff --git a/Website/api/v4/classes/DatabaseObject.php b/Website/api/v4/classes/DatabaseObject.php index 5b5a654c2..6f64331c8 100644 --- a/Website/api/v4/classes/DatabaseObject.php +++ b/Website/api/v4/classes/DatabaseObject.php @@ -4,6 +4,7 @@ use BeaconCommon, BeaconRecordSet, BeaconUUID, Exception; abstract class DatabaseObject { + const kPermissionNone = 0; const kPermissionCreate = 1; const kPermissionRead = 2; const kPermissionUpdate = 4; diff --git a/Website/api/v4/classes/ProjectInvite.php b/Website/api/v4/classes/ProjectInvite.php new file mode 100644 index 000000000..a5defc7f9 --- /dev/null +++ b/Website/api/v4/classes/ProjectInvite.php @@ -0,0 +1,170 @@ +inviteCode = $row->Field('invite_code'); + $this->projectId = $row->Field('project_id'); + $this->projectPassword = BeaconEncryption::RSADecrypt(BeaconCommon::GetGlobal('Beacon_Private_Key'), base64_decode($row->Field('project_password'))); + $this->role = $row->Field('role'); + $this->creatorId = $row->Field('creator_id'); + $this->creationDate = floatval($row->Field('creation_date')); + $this->expirationDate = floatval($row->Field('expiration_date')); + } + + public static function BuildDatabaseSchema(): DatabaseSchema { + $schema = new DatabaseSchema('public', 'project_invites', [ + new DatabaseObjectProperty('inviteCode', ['primaryKey' => true, 'columnName' => 'invite_code', 'required' => false, 'editable' => DatabaseObjectProperty::kEditableNever]), + new DatabaseObjectProperty('projectId', ['columnName' => 'project_id']), + new DatabaseObjectProperty('projectPassword', ['columnName' => 'project_password']), + new DatabaseObjectProperty('role'), + new DatabaseObjectProperty('creatorId', ['columnName' => 'creator_id']), + new DatabaseObjectProperty('creationDate', ['columnName' => 'creation_date', 'accessor' => 'EXTRACT(EPOCH FROM %%TABLE%%.%%COLUMN%%)', 'setter' => 'TO_TIMESTAMP(%%PLACEHOLDER%%)']), + new DatabaseObjectProperty('expirationDate', ['columnName' => 'expiration_date', 'accessor' => 'EXTRACT(EPOCH FROM %%TABLE%%.%%COLUMN%%)', 'setter' => 'TO_TIMESTAMP(%%PLACEHOLDER%%)']), + ]); + return $schema; + } + + protected static function BuildSearchParameters(DatabaseSearchParameters $parameters, array $filters, bool $isNested): void { + $schema = static::DatabaseSchema(); + $table = $schema->Table(); + $parameters->AddFromFilter($schema, $filters, 'projectId'); + $parameters->orderBy = $schema->Accessor('creationDate') . ' DESC'; + $parameters->clauses[] = $schema->Table() . '.expiration_date >= CURRENT_TIMESTAMP'; + } + + public function jsonSerialize(): mixed { + return [ + 'inviteCode' => $this->inviteCode, + 'redeemUrl' => BeaconCommon::AbsoluteUrl('/invite/'. urlencode($this->inviteCode)), + 'projectId' => $this->projectId, + 'role' => $this->role, + 'creatorId' => $this->creatorId, + 'creationDate' => $this->creationDate, + 'expirationDate' => $this->expirationDate, + ]; + } + + protected static function InitializeProperties(array &$properties): void { + static::MutableDatabaseObjectInitializeProperties($properties); + $properties['inviteCode'] = BeaconCommon::GenerateRandomKey(8, '23456789ABCDEFGHJKMNPRSTUVWXYZ'); + $properties['creatorId'] = $properties['userId']; + $properties['creationDate'] = time(); + $properties['expirationDate'] = time() + 86400; + + if (isset($properties['projectPassword'])) { + $properties['projectPassword'] = base64_encode(BeaconEncryption::RSAEncrypt(BeaconEncryption::ExtractPublicKey(BeaconCommon::GetGlobal('Beacon_Private_Key')), base64_decode($properties['projectPassword']))); + } + } + + public function InviteCode(): string { + return $this->inviteCode; + } + + public function ProjectId(): string { + return $this->projectId; + } + + public function ProjectPassword(): string { + return $this->projectPassword; + } + + public function Role(): string { + return $this->role; + } + + public function CreatorId(): string { + return $this->creatorId; + } + + public function CreationDate(): float { + return $this->creationDate; + } + + public function ExpirationDate(): float { + return $this->expirationDate; + } + + public function IsExpired(): bool { + return $this->expirationDate < time(); + } + + protected static function UserIsAdmin(User $user, string $projectId): bool { + if (BeaconCommon::IsUUID($projectId) === false) { + return false; + } + + $database = BeaconCommon::Database(); + $rows = $database->Query('SELECT role FROM public.project_members WHERE user_id = $1 AND project_id = $2;', $user->UserId(), $projectId); + if ($rows->RecordCount() !== 1) { + return false; + } + $role = $rows->Field('role'); + + return $role === ProjectMember::kRoleOwner || $role === ProjectMember::kRoleAdmin; + } + + public static function GetNewObjectPermissionsForUser(User $user, ?array $newObjectProperties): int { + if (is_null($newObjectProperties) || isset($newObjectProperties['projectId']) === false) { + return DatabaseObject::kPermissionNone; + } + + $projectId = strval($newObjectProperties['projectId']); + $member = ProjectMember::Fetch($projectId, $user->UserId()); + if (is_null($member)) { + return DatabaseObject::kPermissionNone; + } + + try { + $desiredPermissions = ProjectMember::PermissionsForRole($newObjectProperties['role']); + } catch (Exception $err) { + return DatabaseObject::kPermissionNone; + } + + if ($desiredPermissions < $member->Permissions()) { + return DatabaseObject::kPermissionAll; + } else { + return DatabaseObject::kPermissionNone; + } + } + + public function GetPermissionsForUser(User $user): int { + $member = ProjectMember::Fetch($this->projectId, $user->UserId()); + if (is_null($member)) { + return DatabaseObject::kPermissionNone; + } + try { + $rolePermissions = ProjectMember::PermissionsForRole($this->role); + } catch (Exception $err) { + return DatabaseObject::kPermissionNone; + } + if ($rolePermissions < $member->Permissions()) { + return DatabaseObject::kPermissionAll; + } else { + return DatabaseObject::kPermissionNone; + } + } + + public static function CleanupExpired(): void { + $database = BeaconCommon::Database(); + $database->BeginTransaction(); + $database->Query('DELETE FROM public.project_invites WHERE expiration_date < CURRENT_TIMESTAMP;'); + $database->Commit(); + } +} + +?> diff --git a/Website/api/v4/classes/ProjectMember.php b/Website/api/v4/classes/ProjectMember.php index 1c82347ba..f39833dfe 100644 --- a/Website/api/v4/classes/ProjectMember.php +++ b/Website/api/v4/classes/ProjectMember.php @@ -9,6 +9,11 @@ class ProjectMember implements JsonSerializable { public const kRoleEditor = 'Editor'; public const kRoleGuest = 'Guest'; + public const kPermissionsOwner = 90; + public const kPermissionsAdmin = 80; + public const kPermissionsEditor = 70; + public const kPermissionsGuest = 10; + protected string $projectId; protected string $userId; protected string $username; @@ -107,6 +112,18 @@ public function Fingerprint(): ?string { return $this->fingerprint; } + public function IsOwner(): bool { + return $this->permissions >= self::kPermissionsOwner; + } + + public function IsAdmin(): bool { + return $this->permissions >= self::kPermissionsAdmin; + } + + public function IsEditor(): bool { + return $this->permissions >= self::kPermissionsEditor; + } + public function jsonSerialize(): mixed { return [ 'projectId' => $this->projectId, @@ -141,6 +158,21 @@ public static function GenerateFingerprint(string $userId, string $username, str return base64_encode(hash('sha3-256', implode('', $pieces), true)); } + + public static function PermissionsForRole(string $role): int { + switch ($role) { + case self::kRoleOwner: + return self::kPermissionsOwner; + case self::kRoleAdmin: + return self::kPermissionsAdmin; + case self::kRoleEditor: + return self::kPermissionsEditor; + case self::kRoleGuest: + return self::kPermissionsGuest; + default: + throw new Exception('Unknown role ' . $role); + } + } } ?> diff --git a/Website/api/v4/index.php b/Website/api/v4/index.php index 5d744f288..983476204 100644 --- a/Website/api/v4/index.php +++ b/Website/api/v4/index.php @@ -186,6 +186,7 @@ DatabaseObjectManager::RegisterRoutes('BeaconAPI\v4\Palworld\ConfigOption', 'palworld/configOptions', 'configOptionId'); DatabaseObjectManager::RegisterRoutes('BeaconAPI\v4\Palworld\GameVariable', 'palworld/gameVariables', 'key'); DatabaseObjectManager::RegisterRoutes('BeaconAPI\v4\SDTD\ConfigOption', '7dtd/configOptions', 'configOptionId'); +DatabaseObjectManager::RegisterRoutes('BeaconAPI\v4\ProjectInvite', 'projectInvites', 'inviteCode'); Core::HandleRequest(dirname(__FILE__) . '/requests'); diff --git a/Website/conf/www.conf b/Website/conf/www.conf index 3f8947970..bd992d5df 100644 --- a/Website/conf/www.conf +++ b/Website/conf/www.conf @@ -39,6 +39,7 @@ rewrite "^/help/math.php$" /help/ permanent; rewrite "^/help/spec.php$" /help/ permanent; rewrite "^/redeem/{0,1}$" /account/redeem permanent; rewrite "^/redeem/([a-zA-Z0-9]{9})/{0,1}$" /account/redeem/$1 permanent; +rewrite "^/invite/(.{8})/{0,1}$" /account/invite/$1 permanent; rewrite "^/object\.php/([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12})$" /games/ark/blueprint.php?objectId=$1 last; rewrite "^/object/([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12})$" /games/ark/blueprint.php?objectId=$1 last; @@ -87,6 +88,7 @@ rewrite "^/account/auth$" /account/auth/redeem.php last; rewrite "^/account/login/verify(\.php)?$" /account/auth/verify.php last; rewrite "^/download(/?((index)?\.php)?)?$" /download/index.php last; rewrite "^/tools/breeding(\.php)?$" /Games/Ark/Breeding permanent; +rewrite "^/account/invite/(.{8})/?$" /account/invite.php?code=$1 last; rewrite "(?i)^/Games/?$" /games/index.php last; diff --git a/Website/www/account/invite.php b/Website/www/account/invite.php new file mode 100644 index 000000000..51bf920ec --- /dev/null +++ b/Website/www/account/invite.php @@ -0,0 +1,126 @@ +User() : null; + +header('Cache-Control: no-cache'); + +$database = BeaconCommon::Database(); + +BeaconTemplate::AddStylesheet(BeaconCommon::AssetURI('account.css')); + +$process_step = 'start'; +if (isset($_REQUEST['process'])) { + $process_step = strtolower($_REQUEST['process']); +} + +?>
Project invite accepted!
You are now a member of the project.