Added user name history beginnings.

This commit is contained in:
flash 2025-02-09 01:32:25 +00:00
parent 7f7e644069
commit cc37b7cad3
6 changed files with 207 additions and 1 deletions

View file

@ -1 +1 @@
20250208
20250209

View file

@ -0,0 +1,28 @@
<?php
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class CreatedUserNameHistoryTable_20250209_005714 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute(<<<SQL
CREATE TABLE msz_users_names_history (
history_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
user_id INT(10) UNSIGNED NULL DEFAULT NULL,
history_name_before VARCHAR(255) NOT NULL COLLATE 'utf8mb4_unicode_520_ci',
history_name_after VARCHAR(255) NOT NULL COLLATE 'utf8mb4_unicode_520_ci',
history_private TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
history_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (history_id),
INDEX users_names_history_users_foreign (user_id),
INDEX users_names_history_name_before_index (history_name_before),
INDEX users_names_history_created_index (history_created),
INDEX users_names_history_private_index (history_private),
CONSTRAINT users_names_history_users_foreign
FOREIGN KEY (user_id)
REFERENCES msz_users (user_id)
ON UPDATE CASCADE
ON DELETE SET NULL
) COLLATE='utf8mb4_bin';
SQL);
}
}

View file

@ -26,6 +26,12 @@ $viewerId = $viewingAsGuest ? '0' : $viewerInfo->id;
try {
$userInfo = $msz->usersCtx->getUserInfo($userId, 'profile');
} catch(RuntimeException $ex) {
$userId = $msz->usersCtx->namesHistory->resolvePastUserName($userId);
if($userId !== null) {
header(sprintf('Location: %s', $msz->urls->format('user-profile', ['user' => $userId])));
return;
}
http_response_code(404);
Template::render('profile.index', [
'profile_is_guest' => $viewingAsGuest,

View file

@ -0,0 +1,31 @@
<?php
namespace Misuzu\Users;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class UserNameHistoryInfo {
public function __construct(
public private(set) string $id,
public private(set) string $userId,
public private(set) string $nameBefore,
public private(set) string $nameAfter,
public private(set) bool $private,
public private(set) int $createdTime
) {}
public static function fromResult(DbResult $result): UserPasswordInfo {
return new UserPasswordInfo(
id: $result->getString(0),
userId: $result->getString(1),
nameBefore: $result->getString(2),
nameAfter: $result->getString(3),
private: $result->getBoolean(4),
createdTime: $result->getInteger(5),
);
}
public CarbonImmutable $createdAt {
get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Misuzu\Users;
use RuntimeException;
use Index\Db\{DbConnection,DbStatementCache};
class UserNamesHistoryData {
private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
/** @return iterable<UserNameHistoryInfo> */
public function getUserNamesHistory(
UserInfo|string|null $userInfo = null,
?bool $private = null
): iterable {
$hasUserInfo = $userInfo !== null;
$args = 0;
$query = <<<SQL
SELECT history_id, user_id, history_name_before, history_name_after, history_private, UNIX_TIMESTAMP(history_created)
FROM msz_users_names_history
SQL;
if($hasUserInfo) {
++$args;
$query .= ' WHERE user_id = ?';
}
if($private !== null)
$query .= sprintf(' %s history_private %s 0', ++$args > 1 ? 'AND' : 'WHERE', $private ? '<>' : '=');
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
return $stmt->getResultIterator(UserNameHistoryInfo::fromResult(...));
}
public function getUserNameHistory(string $historyId): UserNameHistoryInfo {
$stmt = $this->cache->get(<<<SQL
SELECT history_id, user_id, history_name_before, history_name_after, history_private, UNIX_TIMESTAMP(history_created)
FROM msz_users_names_history
WHERE history_id = ?
SQL);
$stmt->nextParameter($historyId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('could not find that name history entry');
return UserNameHistoryInfo::fromResult($result);
}
public function deleteUserNameHistory(UserNameHistoryInfo|string $historyInfo): void {
$stmt = $this->cache->get(<<<SQL
DELETE FROM msz_users_names_history
WHERE history_id = ?
SQL);
$stmt->nextParameter($historyInfo instanceof UserNameHistoryInfo ? $historyInfo->id : $historyInfo);
$stmt->execute();
}
public function deleteUserNamesHistory(UserInfo|string $userInfo): void {
$stmt = $this->cache->get(<<<SQL
DELETE FROM msz_users_names_history
WHERE user_id = ?
SQL);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
}
public function setUserNameHistoryVisibility(
UserNameHistoryInfo|string $historyInfo,
bool $private
): void {
$stmt = $this->cache->get(<<<SQL
UPDATE msz_users_names_history
SET history_private = ?
WHERE history_id = ?
SQL);
$stmt->nextParameter($private ? 1 : 0);
$stmt->nextParameter($historyInfo instanceof UserNameHistoryInfo ? $historyInfo->id : $historyInfo);
$stmt->execute();
}
// this should probably let you specify a time period
public function setUserNamesHistoryVisibility(
UserInfo|string $userInfo,
bool $private
): void {
$stmt = $this->cache->get(<<<SQL
UPDATE msz_users_names_history
SET history_private = ?
WHERE user_id = ?
SQL);
$stmt->nextParameter($private ? 1 : 0);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
}
public function recordUserNameChange(
UserInfo|string $userInfo,
string $nameBefore,
string $nameAfter,
bool $private = false
): UserNameHistoryInfo {
$stmt = $this->cache->get(<<<SQL
INSERT INTO msz_users_names_history (
user_id, history_name_before, history_name_after, history_private
) VALUES (?, ?, ?, ?)
SQL);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($nameBefore);
$stmt->nextParameter($nameAfter);
$stmt->nextParameter($private ? 1 : 0);
$stmt->execute();
return $this->getUserNameHistory($stmt->lastInsertId);
}
public function resolvePastUserName(string $pastName): ?string {
$stmt = $this->cache->get(<<<SQL
SELECT IF(history_private, NULL, user_id)
FROM msz_users_names_history
WHERE user_id IS NOT NULL
AND history_name_before = ?
ORDER BY history_created DESC
LIMIT 1
SQL);
$stmt->nextParameter($pastName);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getStringOrNull(0) : null;
}
}

View file

@ -13,6 +13,7 @@ class UsersContext {
public private(set) UserPasswordsData $passwords;
public private(set) UserTotpsData $totps;
public private(set) UserBirthdatesData $birthdates;
public private(set) UserNamesHistoryData $namesHistory;
/** @var array<string, UserInfo> */
private array $userInfos = [];
@ -35,6 +36,7 @@ class UsersContext {
$this->passwords = new UserPasswordsData($dbConn);
$this->totps = new UserTotpsData($dbConn);
$this->birthdates = new UserBirthdatesData($dbConn);
$this->namesHistory = new UserNamesHistoryData($dbConn);
}
public function getUserInfo(string $value, int|string|null $select = null): UserInfo {