Auth: convert auth views and verification email to Nova layout

This commit is contained in:
2026-02-21 07:37:08 +01:00
parent 93b009d42a
commit 795c7a835f
117 changed files with 5385 additions and 1291 deletions

View File

@@ -19,9 +19,13 @@ class AvatarUrl
return self::default();
}
$base = rtrim((string) config('cdn.avatar_url', 'https://file.skinbase.org'), '/');
$base = rtrim((string) config('cdn.avatar_url', 'https://files.skinbase.org'), '/');
return sprintf('%s/avatars/%d/%d.webp?v=%s', $base, $userId, $size, $avatarHash);
// Use hash-based path: avatars/ab/cd/{hash}/{size}.webp?v={hash}
$p1 = substr($avatarHash, 0, 2);
$p2 = substr($avatarHash, 2, 2);
return sprintf('%s/avatars/%s/%s/%s/%d.webp?v=%s', $base, $p1, $p2, $avatarHash, $size, $avatarHash);
}
public static function default(): string

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Support;
class ForumPostContent
{
public static function render(?string $raw): string
{
$content = (string) ($raw ?? '');
if ($content === '') {
return '';
}
$allowedTags = '<p><br><strong><em><b><i><u><ul><ol><li><blockquote><code><pre><a><img>';
$sanitized = strip_tags($content, $allowedTags);
$sanitized = preg_replace('/\son\w+\s*=\s*"[^"]*"/i', '', $sanitized) ?? $sanitized;
$sanitized = preg_replace('/\son\w+\s*=\s*\'[^\']*\'/i', '', $sanitized) ?? $sanitized;
$sanitized = preg_replace('/\s(href|src)\s*=\s*"\s*javascript:[^"]*"/i', ' $1="#"', $sanitized) ?? $sanitized;
$sanitized = preg_replace('/\s(href|src)\s*=\s*\'\s*javascript:[^\']*\'/i', ' $1="#"', $sanitized) ?? $sanitized;
$linked = preg_replace_callback(
'/(?<!["\'>])(https?:\/\/[^\s<]+)/i',
static function (array $matches): string {
$url = $matches[1] ?? '';
$escapedUrl = e($url);
return '<a href="' . $escapedUrl . '" target="_blank" rel="noopener noreferrer" class="text-sky-300 hover:text-sky-200 underline">' . $escapedUrl . '</a>';
},
$sanitized,
);
return (string) ($linked ?? $sanitized);
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace App\Support;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
final class UsernamePolicy
{
public static function min(): int
{
return (int) config('usernames.min', 3);
}
public static function max(): int
{
return (int) config('usernames.max', 20);
}
public static function regex(): string
{
return (string) config('usernames.regex', '/^[a-zA-Z0-9_-]+$/');
}
/**
* @return array<int, string>
*/
public static function reserved(): array
{
return array_values(array_unique(array_map(static fn (string $v): string => strtolower(trim($v)), (array) config('usernames.reserved', []))));
}
public static function normalize(string $value): string
{
return strtolower(trim($value));
}
public static function sanitizeLegacy(string $value): string
{
$value = Str::ascii($value);
$value = strtolower(trim($value));
$value = preg_replace('/[^a-z0-9_-]+/', '_', $value) ?? '';
$value = trim($value, '_-');
if ($value === '') {
return 'user';
}
return substr($value, 0, self::max());
}
public static function isReserved(string $username): bool
{
return in_array(self::normalize($username), self::reserved(), true);
}
public static function similarReserved(string $username): ?string
{
$normalized = self::normalize($username);
$reduced = self::reduceForSimilarity($normalized);
$threshold = (int) config('usernames.similarity_threshold', 2);
foreach (self::reserved() as $reserved) {
if (levenshtein($reduced, self::reduceForSimilarity($reserved)) <= $threshold) {
return $reserved;
}
}
return null;
}
public static function hasApprovedOverride(string $username, ?int $userId = null): bool
{
if (! Schema::hasTable('username_approval_requests')) {
return false;
}
$normalized = self::normalize($username);
return DB::table('username_approval_requests')
->where('requested_username', $normalized)
->where('status', 'approved')
->when($userId !== null, fn ($q) => $q->where(function ($sub) use ($userId) {
$sub->where('user_id', $userId)->orWhereNull('user_id');
}))
->exists();
}
public static function uniqueCandidate(string $base, ?int $ignoreUserId = null): string
{
$base = self::sanitizeLegacy($base);
if ($base === '' || self::isReserved($base) || self::similarReserved($base) !== null) {
$base = 'user';
}
$max = self::max();
$candidate = substr($base, 0, $max);
$suffix = 1;
while (self::exists($candidate, $ignoreUserId) || self::isReserved($candidate) || self::similarReserved($candidate) !== null) {
$suffixStr = (string) $suffix;
$prefixLen = max(1, $max - strlen($suffixStr));
$candidate = substr($base, 0, $prefixLen) . $suffixStr;
$suffix++;
}
return $candidate;
}
private static function exists(string $username, ?int $ignoreUserId = null): bool
{
$query = User::query()->whereRaw('LOWER(username) = ?', [strtolower($username)]);
if ($ignoreUserId !== null) {
$query->where('id', '!=', $ignoreUserId);
}
return $query->exists();
}
private static function reduceForSimilarity(string $value): string
{
return preg_replace('/[0-9_-]+/', '', strtolower($value)) ?? strtolower($value);
}
}