码桶

发现社区成员的开源项目

geekssl /

wstats

公开
main
wstats/app/Controllers/ProfileController.php
ProfileController.php5.2 KB
<?php
/**
 * 个人中心:个人资料查看/修改、修改密码、我的操作日志
 *
 * 设计要点:
 *  - 邮箱**只读**:它是登录凭证且唯一,变更涉及邮箱验证与通知,暂不开放(界面明确标注);
 *  - 修改密码成功后吊销全部旧会话并下发新 token(与找回密码一致的安全语义),
 *    当前设备的会话换新令牌继续有效,其它设备立即失效;
 *  - 所有变更动作写入 user_op_logs(表缺失时降级,见 Support\OpLog)。
 */
declare(strict_types=1);

namespace Wstat\Controllers;

use Wstat\Http\Request;
use Wstat\Support\Auth;
use Wstat\Support\Db;
use Wstat\Support\OpLog;
use Wstat\Support\Settings;

class ProfileController
{
    /** GET /api/profile —— 当前用户资料 */
    public function show(Request $req): void
    {
        $u = Auth::requireUser($req);
        wstat_json(self::data((int) $u['id']));
    }

    /** PATCH /api/profile —— 修改昵称 / 界面语言 */
    public function update(Request $req): void
    {
        $u = Auth::requireUser($req);
        $id = (int) $u['id'];

        $upd = [];
        $changed = [];
        if ($req->input('nickname') !== null) {
            $nick = trim((string) $req->input('nickname'));
            if (mb_strlen($nick) > 30) {
                wstat_err('昵称过长(最多 30 字)', 422);
            }
            $upd['nickname'] = $nick;
            $changed[] = '昵称';
        }
        if ($req->input('lang') !== null) {
            $lang = (string) $req->input('lang');
            if (!in_array($lang, ['zh-CN', 'en-US'], true)) {
                wstat_err('界面语言仅支持 zh-CN / en-US', 422);
            }
            $upd['lang'] = $lang;
            $changed[] = '界面语言';
        }
        if ($upd === []) {
            wstat_err('没有需要修改的字段', 422);
        }
        $upd['updated_at'] = time();

        $sets = implode(',', array_map(static fn (string $k): string => '`' . $k . '`=?', array_keys($upd)));
        Db::execute(
            'UPDATE users SET ' . $sets . ' WHERE id=?',
            array_merge(array_values($upd), [$id])
        );
        OpLog::log($id, 'profile_update', '修改了 ' . implode('、', $changed));
        wstat_json(self::data($id));
    }

    /**
     * POST /api/profile/password —— 修改密码。
     * body: {old_password, new_password}
     * 成功返回新 token(当前会话无缝续期);其它设备全部吊销。
     */
    public function password(Request $req): void
    {
        $u = Auth::requireUser($req);
        $id = (int) $u['id'];
        $old = (string) $req->input('old_password', '');
        $new = (string) $req->input('new_password', '');
        $min = (int) wstat_config('security.pwd_min');

        $row = Db::first('SELECT password FROM users WHERE id=? LIMIT 1', [$id]);
        if ($row === null) {
            wstat_err('账号不存在', 404, 404);
        }
        if (!password_verify($old, (string) $row['password'])) {
            wstat_err('当前密码不正确', 422, 4224);
        }
        if (strlen($new) < $min || strlen($new) > 72) {
            wstat_err('新密码长度需 ' . $min . '-72 位', 422);
        }
        if ($new === $old) {
            wstat_err('新密码不能与当前密码相同', 422);
        }

        Db::execute(
            'UPDATE users SET password=?, updated_at=? WHERE id=?',
            [password_hash($new, PASSWORD_DEFAULT), time(), $id]
        );
        $revoked = Auth::revokeAll($id);
        $token = Auth::issue($id);
        OpLog::log($id, 'password_change', '修改密码(吊销旧会话 ' . $revoked . ' 个,当前设备已换新令牌)');
        wstat_json(['ok' => true, 'token' => $token, 'revoked' => $revoked]);
    }

    /** GET /api/profile/logs?page=&page_size= —— 我的操作日志 */
    public function logs(Request $req): void
    {
        $u = Auth::requireUser($req);
        $page = max(1, (int) $req->input('page', 1));
        $size = (int) $req->input('page_size', 20);
        wstat_json(OpLog::page((int) $u['id'], $page, $size));
    }

    /** 资料数组(不含 password);附带名下/被授权站点计数 */
    private static function data(int $userId): array
    {
        $row = Db::first(
            'SELECT id,email,nickname,lang,status,created_at,updated_at FROM users WHERE id=? LIMIT 1',
            [$userId]
        );
        if ($row === null) {
            wstat_err('账号不存在', 404, 404);
        }
        $row['is_admin'] = Settings::isAdmin($userId);
        $row['sites_owned'] = (int) Db::value('SELECT COUNT(*) FROM sites WHERE user_id=?', [$userId]);
        $row['sites_shared'] = 0;
        try {
            if ((int) Db::value(
                'SELECT COUNT(*) FROM information_schema.TABLES
                 WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=?',
                ['site_members']
            ) > 0) {
                $row['sites_shared'] = (int) Db::value(
                    'SELECT COUNT(*) FROM site_members WHERE user_id=?',
                    [$userId]
                );
            }
        } catch (\Throwable $e) {
            $row['sites_shared'] = 0;
        }
        return $row;
    }
}