项目初始化

This commit is contained in:
2026-03-18 15:54:43 +08:00
commit dfcd762e23
601 changed files with 57883 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
<?php
namespace app\api\controller;
use ba\Date;
use ba\Captcha;
use ba\Random;
use app\common\model\User;
use app\common\facade\Token;
use app\common\model\UserScoreLog;
use app\common\model\UserMoneyLog;
use app\common\controller\Frontend;
use support\validation\Validator;
use support\validation\ValidationException;
use Webman\Http\Request;
use support\Response;
class Account extends Frontend
{
protected array $noNeedLogin = ['retrievePassword'];
protected array $noNeedPermission = ['verification', 'changeBind'];
public function overview(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$sevenDays = Date::unixTime('day', -6);
$score = $money = $days = [];
for ($i = 0; $i < 7; $i++) {
$days[$i] = date("Y-m-d", $sevenDays + ($i * 86400));
$tempToday0 = strtotime($days[$i]);
$tempToday24 = strtotime('+1 day', $tempToday0) - 1;
$score[$i] = UserScoreLog::where('user_id', $this->auth->id)
->where('create_time', 'BETWEEN', $tempToday0 . ',' . $tempToday24)
->sum('score');
$userMoneyTemp = UserMoneyLog::where('user_id', $this->auth->id)
->where('create_time', 'BETWEEN', $tempToday0 . ',' . $tempToday24)
->sum('money');
$money[$i] = bcdiv((string) $userMoneyTemp, '100', 2);
}
return $this->success('', [
'days' => $days,
'score' => $score,
'money' => $money,
]);
}
public function profile(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
if ($request->method() === 'POST') {
$model = $this->auth->getUser();
$data = $request->only(['avatar', 'username', 'nickname', 'gender', 'birthday', 'motto']);
$data['id'] = $this->auth->id;
if (!isset($data['birthday'])) {
$data['birthday'] = null;
}
try {
Validator::make($data, [
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/',
'nickname' => 'required|string|regex:/^[\x{4e00}-\x{9fa5}a-zA-Z0-9_-]+$/u',
'birthday' => 'nullable|date',
], [
'nickname.regex' => __('nicknameChsDash'),
])->validate();
} catch (ValidationException $e) {
return $this->error($e->getMessage());
}
$existUser = User::where('username', $data['username'])->where('id', '<>', $this->auth->id)->find();
if ($existUser) {
return $this->error(__('Username') . ' ' . __('already exists'));
}
$model->startTrans();
try {
$model->save($data);
$model->commit();
} catch (\Throwable $e) {
$model->rollback();
return $this->error($e->getMessage());
}
return $this->success(__('Data updated successfully~'));
}
return $this->success('', [
'accountVerificationType' => get_account_verification_type()
]);
}
public function verification(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$captcha = new Captcha();
$params = $request->only(['type', 'captcha']);
$key = ($params['type'] == 'email' ? $this->auth->email : $this->auth->mobile) . "user_{$params['type']}_verify";
if ($captcha->check($params['captcha'], $key)) {
$uuid = Random::uuid();
Token::set($uuid, $params['type'] . '-pass', $this->auth->id, 600);
return $this->success('', [
'type' => $params['type'],
'accountVerificationToken' => $uuid,
]);
}
return $this->error(__('Please enter the correct verification code'));
}
public function changeBind(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$captcha = new Captcha();
$params = $request->only(['type', 'captcha', 'email', 'mobile', 'accountVerificationToken', 'password']);
$user = $this->auth->getUser();
if ($user[$params['type']]) {
if (!Token::check($params['accountVerificationToken'], $params['type'] . '-pass', $user->id)) {
return $this->error(__('You need to verify your account before modifying the binding information'));
}
} elseif (!isset($params['password']) || !verify_password($params['password'], $user->password, ['salt' => $user->salt])) {
return $this->error(__('Password error'));
}
if ($captcha->check($params['captcha'], $params[$params['type']] . "user_change_{$params['type']}")) {
try {
if ($params['type'] == 'email') {
Validator::make($params, ['email' => 'required|email'])->validate();
if (User::where('email', $params['email'])->find()) {
return $this->error(__('Email') . ' ' . __('already exists'));
}
} else {
Validator::make($params, ['mobile' => 'required|regex:/^1[3-9]\d{9}$/'])->validate();
if (User::where('mobile', $params['mobile'])->find()) {
return $this->error(__('Mobile') . ' ' . __('already exists'));
}
}
} catch (ValidationException $e) {
return $this->error(__($e->getMessage()));
}
if ($params['type'] == 'email') {
$user->email = $params['email'];
} else {
$user->mobile = $params['mobile'];
}
Token::delete($params['accountVerificationToken']);
$user->save();
return $this->success();
}
return $this->error(__('Please enter the correct verification code'));
}
public function changePassword(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
if ($request->method() === 'POST') {
$model = $this->auth->getUser();
$params = $request->only(['oldPassword', 'newPassword']);
if (!verify_password($params['oldPassword'], $model->password, ['salt' => $model->salt])) {
return $this->error(__('Old password error'));
}
try {
Validator::make(
['password' => $params['newPassword']],
['password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/'],
['password.regex' => __('Please input correct password')]
)->validate();
} catch (ValidationException $e) {
return $this->error($e->getMessage());
}
$model->startTrans();
try {
$model->resetPassword($this->auth->id, $params['newPassword']);
$model->commit();
} catch (\Throwable $e) {
$model->rollback();
return $this->error($e->getMessage());
}
$this->auth->logout();
return $this->success(__('Password has been changed, please login again~'));
}
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
}
public function integral(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$limit = $request->get('limit', $request->post('limit', 15));
$res = UserScoreLog::where('user_id', $this->auth->id)
->order('create_time', 'desc')
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
]);
}
public function balance(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$limit = $request->get('limit', $request->post('limit', 15));
$res = UserMoneyLog::where('user_id', $this->auth->id)
->order('create_time', 'desc')
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
]);
}
public function retrievePassword(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$params = $request->only(['type', 'account', 'captcha', 'password']);
try {
Validator::make($params, [
'type' => 'required|in:email,mobile',
'account' => 'required|string',
'captcha' => 'required|string',
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
], [
'password.regex' => __('Please input correct password'),
])->validate();
} catch (ValidationException $e) {
return $this->error($e->getMessage());
}
if ($params['type'] == 'email') {
$user = User::where('email', $params['account'])->find();
} else {
$user = User::where('mobile', $params['account'])->find();
}
if (!$user) {
return $this->error(__('Account does not exist~'));
}
$captchaObj = new Captcha();
if (!$captchaObj->check($params['captcha'], $params['account'] . 'user_retrieve_pwd')) {
return $this->error(__('Please enter the correct verification code'));
}
if ($user->resetPassword($user->id, $params['password'])) {
return $this->success(__('Password has been changed~'));
}
return $this->error(__('Failed to modify password, please try again later~'));
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace app\api\controller;
use app\common\controller\Frontend;
use Webman\Http\Request;
use support\Response;
class Ajax extends Frontend
{
protected array $noNeedLogin = ['area', 'buildSuffixSvg'];
protected array $noNeedPermission = ['upload'];
public function upload(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$file = $request->file('file');
if (!$file) {
return $this->error(__('No files were uploaded'));
}
$file = new \app\common\library\upload\WebmanUploadedFile($file);
$driver = $request->get('driver', $request->post('driver', 'local'));
$topic = $request->get('topic', $request->post('topic', 'default'));
try {
$upload = new \app\common\library\Upload();
$attachment = $upload
->setFile($file)
->setDriver($driver)
->setTopic($topic)
->upload(null, 0, $this->auth->id);
unset($attachment['create_time'], $attachment['quote']);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
return $this->success(__('File uploaded successfully'), [
'file' => $attachment ?? []
]);
}
public function area(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
return $this->success('', get_area($request));
}
public function buildSuffixSvg(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$suffix = $request->get('suffix', $request->post('suffix', 'file'));
$background = $request->get('background', $request->post('background'));
$content = build_suffix_svg((string) $suffix, (string) $background);
return response($content, 200, [
'Content-Length' => strlen($content),
'Content-Type' => 'image/svg+xml'
]);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace app\api\controller;
use ba\Random;
use ba\Captcha;
use ba\ClickCaptcha;
use app\common\controller\Api;
use app\common\facade\Token;
use app\admin\library\Auth as AdminAuth;
use app\common\library\Auth as UserAuth;
use Webman\Http\Request;
use support\Response;
class Common extends Api
{
public function captcha(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) return $response;
$captchaId = $request->get('id', $request->post('id', ''));
$config = [
'codeSet' => '123456789',
'fontSize' => 22,
'useCurve' => false,
'useNoise' => true,
'length' => 4,
'bg' => [255, 255, 255],
];
$captcha = new Captcha($config);
return $captcha->entry($captchaId);
}
public function clickCaptcha(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) return $response;
$id = $request->get('id', $request->post('id', ''));
$captcha = new ClickCaptcha();
return $this->success('', $captcha->creat($id));
}
public function checkClickCaptcha(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) return $response;
$id = $request->post('id', '');
$info = $request->post('info', '');
$unset = filter_var($request->post('unset', false), FILTER_VALIDATE_BOOLEAN);
$captcha = new ClickCaptcha();
if ($captcha->check($id, $info, $unset)) return $this->success();
return $this->error();
}
public function refreshToken(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) return $response;
$refreshToken = $request->post('refreshToken', '');
$refreshToken = Token::get($refreshToken);
if (!$refreshToken || $refreshToken['expire_time'] < time()) {
return $this->error(__('Login expired, please login again.'));
}
$newToken = Random::uuid();
if ($refreshToken['type'] == AdminAuth::TOKEN_TYPE . '-refresh') {
Token::set($newToken, AdminAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)config('buildadmin.admin_token_keep_time', 259200));
}
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE . '-refresh') {
Token::set($newToken, UserAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)config('buildadmin.user_token_keep_time', 259200));
}
return $this->success('', [
'type' => $refreshToken['type'],
'token' => $newToken
]);
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace app\api\controller;
use ba\Captcha;
use ba\ClickCaptcha;
use app\common\model\User;
use app\common\library\Email;
use app\common\controller\Frontend;
use support\validation\Validator;
use support\validation\ValidationException;
use Webman\Http\Request;
use support\Response;
use PHPMailer\PHPMailer\Exception as PHPMailerException;
class Ems extends Frontend
{
protected array $noNeedLogin = ['send'];
public function send(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$params = $request->post(['email', 'event', 'captchaId', 'captchaInfo']);
$mail = new Email();
if (!$mail->configured) {
return $this->error(__('Mail sending service unavailable'));
}
try {
Validator::make($params, [
'email' => 'required|email',
'event' => 'required|string',
'captchaId' => 'required|string',
'captchaInfo' => 'required|string',
], [
'email.required' => 'email format error',
'event.required' => 'Parameter error',
'captchaId.required' => 'Captcha error',
'captchaInfo.required' => 'Captcha error',
])->validate();
} catch (ValidationException $e) {
return $this->error(__($e->getMessage()));
}
$captchaObj = new Captcha();
$clickCaptcha = new ClickCaptcha();
if (!$clickCaptcha->check($params['captchaId'], $params['captchaInfo'])) {
return $this->error(__('Captcha error'));
}
$captcha = $captchaObj->getCaptchaData($params['email'] . $params['event']);
if ($captcha && time() - $captcha['create_time'] < 60) {
return $this->error(__('Frequent email sending'));
}
$userInfo = User::where('email', $params['email'])->find();
if ($params['event'] == 'user_register' && $userInfo) {
return $this->error(__('Email has been registered, please log in directly'));
}
if ($params['event'] == 'user_change_email' && $userInfo) {
return $this->error(__('The email has been occupied'));
}
if (in_array($params['event'], ['user_retrieve_pwd', 'user_email_verify']) && !$userInfo) {
return $this->error(__('Email not registered'));
}
if ($params['event'] == 'user_email_verify') {
if (!$this->auth->isLogin()) {
return $this->error(__('Please login first'));
}
if ($this->auth->email != $params['email']) {
return $this->error(__('Please use the account registration email to send the verification code'));
}
$password = $request->post('password');
if (!verify_password($password, $this->auth->password, ['salt' => $this->auth->salt])) {
return $this->error(__('Password error'));
}
}
$code = $captchaObj->create($params['email'] . $params['event']);
$subject = __($params['event']) . '-' . get_sys_config('site_name');
$body = __('Your verification code is: %s', [$code]);
try {
$mail->isSMTP();
$mail->addAddress($params['email']);
$mail->isHTML();
$mail->setSubject($subject);
$mail->Body = $body;
$mail->send();
} catch (PHPMailerException) {
return $this->error($mail->ErrorInfo);
}
return $this->success(__('Mail sent successfully~'));
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace app\api\controller;
use ba\Tree;
use support\think\Db;
use app\common\controller\Frontend;
use app\common\library\token\TokenExpirationException;
use Webman\Http\Request;
class Index extends Frontend
{
protected array $noNeedLogin = ['index'];
public function index(Request $request)
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$menus = [];
if ($this->auth->isLogin()) {
$rules = [];
$userMenus = $this->auth->getMenus();
foreach ($userMenus as $item) {
if ($item['type'] == 'menu_dir') {
$menus[] = $item;
} elseif ($item['type'] != 'menu') {
$rules[] = $item;
}
}
$rules = array_values($rules);
} else {
$requiredLogin = filter_var($request->get('requiredLogin', false), FILTER_VALIDATE_BOOLEAN);
if ($requiredLogin) {
try {
$token = get_auth_token(['ba', 'user', 'token'], $request);
$this->auth->init($token);
} catch (TokenExpirationException) {
return $this->error(__('Token expiration'), [], 409);
}
return $this->error(__('Please login first'), [
'type' => $this->auth::NEED_LOGIN
], $this->auth::LOGIN_RESPONSE_CODE);
}
$rules = Db::name('user_rule')
->where('status', 1)
->where('no_login_valid', 1)
->where('type', 'in', ['route', 'nav', 'button'])
->order('weigh', 'desc')
->select()
->toArray();
$rules = Tree::instance()->assembleChild($rules);
}
return $this->success('', [
'site' => [
'siteName' => get_sys_config('site_name'),
'version' => get_sys_config('version'),
'cdnUrl' => full_url(),
'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types']),
'recordNumber' => get_sys_config('record_number'),
'cdnUrlParams' => config('buildadmin.cdn_url_params'),
],
'openMemberCenter' => config('buildadmin.open_member_center'),
'userInfo' => $this->auth->getUserInfo(),
'rules' => $rules,
'menus' => $menus,
]);
}
}

View File

@@ -0,0 +1,676 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use Throwable;
use ba\Random;
use ba\Version;
use ba\Terminal;
use ba\Filesystem;
use app\common\controller\Api;
use app\admin\model\Admin as AdminModel;
use app\admin\model\User as UserModel;
use support\Response;
use Webman\Http\Request;
/**
* 安装控制器
*/
class Install extends Api
{
public const X64 = 'x64';
public const X86 = 'x86';
protected bool $useSystemSettings = false;
/**
* 环境检查状态
*/
static string $ok = 'ok';
static string $fail = 'fail';
static string $warn = 'warn';
/**
* 安装锁文件名称
*/
static string $lockFileName = 'install.lock';
/**
* 配置文件
*/
static string $dbConfigFileName = 'thinkorm.php';
static string $buildConfigFileName = 'buildadmin.php';
/**
* 自动构建的前端文件的 outDir 相对于根目录
*/
static string $distDir = 'web' . DIRECTORY_SEPARATOR . 'dist';
/**
* 需要的依赖版本
*/
static array $needDependentVersion = [
'php' => '8.2.0',
'npm' => '9.8.1',
'cnpm' => '7.1.0',
'node' => '20.14.0',
'yarn' => '1.2.0',
'pnpm' => '6.32.13',
];
/**
* 安装完成标记
*/
static string $InstallationCompletionMark = 'install-end';
/**
* 命令执行窗口exec 为 SSE 长连接,不会返回)
* @throws Throwable
*/
public function terminal(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
}
(new Terminal())->exec(false);
return $this->success(); // unreachable: exec() blocks with SSE stream
}
public function changePackageManager(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
}
$newPackageManager = $request->post('manager', config('terminal.npm_package_manager'));
if (Terminal::changeTerminalConfig()) {
return $this->success('', [
'manager' => $newPackageManager
]);
}
return $this->error(__('Failed to switch package manager. Please modify the configuration file manually:%s', ['config/terminal.php']));
}
/**
* 环境基础检查
*/
public function envBaseCheck(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]), []);
}
if (($_ENV['DATABASE_TYPE'] ?? getenv('DATABASE_TYPE'))) {
return $this->error(__('The .env file with database configuration was detected. Please clean up and try again!'));
}
// php版本-start
$phpVersion = phpversion();
$phpBit = PHP_INT_SIZE == 8 ? self::X64 : self::X86;
$phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion);
if (!$phpVersionCompare) {
$phpVersionLink = [
[
'name' => __('need') . ' >= ' . self::$needDependentVersion['php'],
'type' => 'text'
],
[
'name' => __('How to solve?'),
'title' => __('Click to see how to solve it'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/preparePHP.html'
]
];
} elseif ($phpBit != self::X64) {
$phpVersionLink = [
[
'name' => __('need') . ' x64 PHP',
'type' => 'text'
],
[
'name' => __('How to solve?'),
'title' => __('Click to see how to solve it'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/preparePHP.html'
]
];
}
// php版本-end
// 配置文件-start分别检测目录和文件便于定位问题
$configDir = rtrim(config_path(), '/\\');
$dbConfigFile = $configDir . DIRECTORY_SEPARATOR . self::$dbConfigFileName;
$configDirWritable = Filesystem::pathIsWritable($configDir);
$dbConfigWritable = Filesystem::pathIsWritable($dbConfigFile);
$configIsWritable = $configDirWritable && $dbConfigWritable;
if (!$configIsWritable) {
$configIsWritableLink = [
[
'name' => __('View reason'),
'title' => __('Click to view the reason'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html'
]
];
}
// 配置文件-end
// public-start
$publicIsWritable = Filesystem::pathIsWritable(public_path());
if (!$publicIsWritable) {
$publicIsWritableLink = [
[
'name' => __('View reason'),
'title' => __('Click to view the reason'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html'
]
];
}
// public-end
// PDO-start
$phpPdo = extension_loaded("PDO") && extension_loaded('pdo_mysql');
if (!$phpPdo) {
$phpPdoLink = [
[
'name' => __('PDO extensions need to be installed'),
'type' => 'text'
],
[
'name' => __('How to solve?'),
'title' => __('Click to see how to solve it'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/missingExtension.html'
]
];
}
// PDO-end
// GD2和freeType-start
$phpGd2 = extension_loaded('gd') && function_exists('imagettftext');
if (!$phpGd2) {
$phpGd2Link = [
[
'name' => __('The gd extension and freeType library need to be installed'),
'type' => 'text'
],
[
'name' => __('How to solve?'),
'title' => __('Click to see how to solve it'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/gdFail.html'
]
];
}
// GD2和freeType-end
// proc_open
$phpProc = function_exists('proc_open') && function_exists('proc_close') && function_exists('proc_get_status');
if (!$phpProc) {
$phpProcLink = [
[
'name' => __('View reason'),
'title' => __('proc_open or proc_close functions in PHP Ini is disabled'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/disablement.html'
],
[
'name' => __('How to modify'),
'title' => __('Click to view how to modify'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/disablement.html'
],
[
'name' => __('Security assurance?'),
'title' => __('Using the installation service correctly will not cause any potential security problems. Click to view the details'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/senior.html'
],
];
}
// proc_open-end
return $this->success('', [
'php_version' => [
'describe' => $phpVersion . " ($phpBit)",
'state' => $phpVersionCompare && $phpBit == self::X64 ? self::$ok : self::$fail,
'link' => $phpVersionLink ?? [],
],
'config_is_writable' => [
'describe' => $configIsWritable
? self::writableStateDescribe(true)
: (self::writableStateDescribe(false) . ' [' . $configDir . ']'),
'state' => $configIsWritable ? self::$ok : self::$fail,
'link' => $configIsWritableLink ?? []
],
'public_is_writable' => [
'describe' => self::writableStateDescribe($publicIsWritable),
'state' => $publicIsWritable ? self::$ok : self::$fail,
'link' => $publicIsWritableLink ?? []
],
'php_pdo' => [
'describe' => $phpPdo ? __('already installed') : __('Not installed'),
'state' => $phpPdo ? self::$ok : self::$fail,
'link' => $phpPdoLink ?? []
],
'php_gd2' => [
'describe' => $phpGd2 ? __('already installed') : __('Not installed'),
'state' => $phpGd2 ? self::$ok : self::$fail,
'link' => $phpGd2Link ?? []
],
'php_proc' => [
'describe' => $phpProc ? __('Allow execution') : __('disabled'),
'state' => $phpProc ? self::$ok : self::$warn,
'link' => $phpProcLink ?? []
],
]);
}
/**
* npm环境检查
*/
public function envNpmCheck(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error('', [], 2);
}
$packageManager = $request->post('manager', 'none');
// npm
$npmVersion = Version::getVersion('npm');
$npmVersionCompare = Version::compare(self::$needDependentVersion['npm'], $npmVersion);
if (!$npmVersionCompare || !$npmVersion) {
$npmVersionLink = [
[
'name' => __('need') . ' >= ' . self::$needDependentVersion['npm'],
'type' => 'text'
],
[
'name' => __('How to solve?'),
'title' => __('Click to see how to solve it'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/prepareNpm.html'
]
];
}
// 包管理器
$pmVersionLink = [];
$pmVersion = __('nothing');
$pmVersionCompare = false;
if (in_array($packageManager, ['npm', 'cnpm', 'pnpm', 'yarn'])) {
$pmVersion = Version::getVersion($packageManager);
$pmVersionCompare = Version::compare(self::$needDependentVersion[$packageManager], $pmVersion);
if (!$pmVersion) {
$pmVersionLink[] = [
'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager],
'type' => 'text'
];
if ($npmVersionCompare) {
$pmVersionLink[] = [
'name' => __('Click Install %s', [$packageManager]),
'title' => '',
'type' => 'install-package-manager'
];
} else {
$pmVersionLink[] = [
'name' => __('Please install NPM first'),
'type' => 'text'
];
}
} elseif (!$pmVersionCompare) {
$pmVersionLink[] = [
'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager],
'type' => 'text'
];
$pmVersionLink[] = [
'name' => __('Please upgrade %s version', [$packageManager]),
'type' => 'text'
];
}
} elseif ($packageManager == 'ni') {
$pmVersionCompare = true;
}
// nodejs
$nodejsVersion = Version::getVersion('node');
$nodejsVersionCompare = Version::compare(self::$needDependentVersion['node'], $nodejsVersion);
if (!$nodejsVersionCompare || !$nodejsVersion) {
$nodejsVersionLink = [
[
'name' => __('need') . ' >= ' . self::$needDependentVersion['node'],
'type' => 'text'
],
[
'name' => __('How to solve?'),
'title' => __('Click to see how to solve it'),
'type' => 'faq',
'url' => 'https://doc.buildadmin.com/guide/install/prepareNodeJs.html'
]
];
}
return $this->success('', [
'npm_version' => [
'describe' => $npmVersion ?: __('Acquisition failed'),
'state' => $npmVersionCompare ? self::$ok : self::$warn,
'link' => $npmVersionLink ?? [],
],
'nodejs_version' => [
'describe' => $nodejsVersion ?: __('Acquisition failed'),
'state' => $nodejsVersionCompare ? self::$ok : self::$warn,
'link' => $nodejsVersionLink ?? []
],
'npm_package_manager' => [
'describe' => $pmVersion ?: __('Acquisition failed'),
'state' => $pmVersionCompare ? self::$ok : self::$warn,
'link' => $pmVersionLink ?? [],
]
]);
}
/**
* 测试数据库连接
*/
public function testDatabase(Request $request): Response
{
$this->setRequest($request);
$database = [
'hostname' => $request->post('hostname'),
'username' => $request->post('username'),
'password' => $request->post('password'),
'hostport' => $request->post('hostport'),
'database' => '',
];
$conn = $this->connectDb($database);
if ($conn['code'] == 0) {
return $this->error($conn['msg']);
}
return $this->success('', [
'databases' => $conn['databases']
]);
}
/**
* 系统基础配置
* post请求=开始安装
*/
public function baseConfig(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
}
$envOk = $this->commandExecutionCheck();
$rootPath = str_replace('\\', '/', root_path());
$migrateCommand = 'php vendor/bin/phinx migrate';
if ($request->isGet()) {
return $this->success('', [
'rootPath' => $rootPath,
'executionWebCommand' => $envOk,
'migrateCommand' => $migrateCommand,
]);
}
$connectData = $databaseParam = $request->only(['hostname', 'username', 'password', 'hostport', 'database', 'prefix']);
// 数据库配置测试
$connectData['database'] = '';
$connect = $this->connectDb($connectData, true);
if ($connect['code'] == 0) {
return $this->error($connect['msg']);
}
// 建立数据库
if (!in_array($databaseParam['database'], $connect['databases'])) {
$sql = "CREATE DATABASE IF NOT EXISTS `{$databaseParam['database']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
$connect['pdo']->exec($sql);
}
// 写入数据库配置文件thinkorm.php 使用 $env('database.xxx', 'default') 格式)
$dbConfigFile = config_path(self::$dbConfigFileName);
$dbConfigContent = @file_get_contents($dbConfigFile);
if ($dbConfigContent === false || $dbConfigContent === '') {
return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
}
$callback = function ($matches) use ($databaseParam) {
$key = $matches[1];
$value = (string) ($databaseParam[$key] ?? '');
return "\$env('database.{$key}', '" . addslashes($value) . "')";
};
$dbConfigText = preg_replace_callback("/\\\$env\('database\.(hostname|database|username|password|hostport|prefix)',\s*'[^']*'\)/", $callback, $dbConfigContent);
$result = @file_put_contents($dbConfigFile, $dbConfigText);
if (!$result) {
return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
}
// 写入 dafuweng-webman/.env-example
$envFile = root_path() . '.env-example';
$envFileContent = @file_get_contents($envFile);
if ($envFileContent) {
$databasePos = stripos($envFileContent, '[DATABASE]');
if ($databasePos !== false) {
$envFileContent = substr($envFileContent, 0, $databasePos);
}
$envFileContent .= "\n" . '[DATABASE]' . "\n";
$envFileContent .= 'TYPE = mysql' . "\n";
$envFileContent .= 'HOSTNAME = ' . $databaseParam['hostname'] . "\n";
$envFileContent .= 'DATABASE = ' . $databaseParam['database'] . "\n";
$envFileContent .= 'USERNAME = ' . $databaseParam['username'] . "\n";
$envFileContent .= 'PASSWORD = ' . $databaseParam['password'] . "\n";
$envFileContent .= 'HOSTPORT = ' . $databaseParam['hostport'] . "\n";
$envFileContent .= 'PREFIX = ' . ($databaseParam['prefix'] ?? '') . "\n";
$envFileContent .= 'CHARSET = utf8mb4' . "\n";
$envFileContent .= 'DEBUG = true' . "\n";
$result = @file_put_contents($envFile, $envFileContent);
if (!$result) {
return $this->error(__('File has no write permission:%s', ['/' . $envFile]));
}
}
// 设置新的Token随机密钥key
$oldTokenKey = config('buildadmin.token.key');
$newTokenKey = Random::build('alnum', 32);
$buildConfigFile = config_path(self::$buildConfigFileName);
$buildConfigContent = @file_get_contents($buildConfigFile);
if ($buildConfigContent === false || $buildConfigContent === '') {
return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName]));
}
$buildConfigContent = preg_replace("/'key'(\s+)=>(\s+)'$oldTokenKey'/", "'key'\$1=>\$2'$newTokenKey'", $buildConfigContent);
$result = @file_put_contents($buildConfigFile, $buildConfigContent);
if (!$result) {
return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName]));
}
// 建立安装锁文件
$result = @file_put_contents(public_path(self::$lockFileName), date('Y-m-d H:i:s'));
if (!$result) {
return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName]));
}
return $this->success('', [
'rootPath' => $rootPath,
'executionWebCommand' => $envOk,
'migrateCommand' => $migrateCommand,
]);
}
protected function isInstallComplete(): bool
{
if (is_file(public_path(self::$lockFileName))) {
$contents = @file_get_contents(public_path(self::$lockFileName));
if ($contents == self::$InstallationCompletionMark) {
return true;
}
}
return false;
}
/**
* 标记命令执行完毕
* @throws Throwable
*/
public function commandExecComplete(Request $request): Response
{
$this->setRequest($request);
if ($this->isInstallComplete()) {
return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %s file first', ['public/' . self::$lockFileName]));
}
$param = $request->only(['type', 'adminname', 'adminpassword', 'sitename']);
if ($param['type'] == 'web') {
$result = @file_put_contents(public_path(self::$lockFileName), self::$InstallationCompletionMark);
if (!$result) {
return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName]));
}
} else {
// 管理员配置入库
$adminModel = new AdminModel();
$defaultAdmin = $adminModel->where('username', 'admin')->find();
$defaultAdmin->username = $param['adminname'];
$defaultAdmin->nickname = ucfirst($param['adminname']);
$defaultAdmin->save();
if (isset($param['adminpassword']) && $param['adminpassword']) {
$adminModel->resetPassword($defaultAdmin->id, $param['adminpassword']);
}
// 默认用户密码修改
$user = new UserModel();
$user->resetPassword(1, Random::build());
// 修改站点名称
if (class_exists(\app\admin\model\Config::class)) {
\app\admin\model\Config::where('name', 'site_name')->update([
'value' => $param['sitename']
]);
}
}
return $this->success();
}
/**
* 获取命令执行检查的结果
* @return bool 是否拥有执行命令的条件
*/
private function commandExecutionCheck(): bool
{
$pm = config('terminal.npm_package_manager');
if ($pm == 'none') {
return false;
}
$check['phpPopen'] = function_exists('proc_open') && function_exists('proc_close');
$check['npmVersionCompare'] = Version::compare(self::$needDependentVersion['npm'], Version::getVersion('npm'));
$check['pmVersionCompare'] = Version::compare(self::$needDependentVersion[$pm], Version::getVersion($pm));
$check['nodejsVersionCompare'] = Version::compare(self::$needDependentVersion['node'], Version::getVersion('node'));
$envOk = true;
foreach ($check as $value) {
if (!$value) {
$envOk = false;
break;
}
}
return $envOk;
}
/**
* 安装指引
*/
public function manualInstall(Request $request): Response
{
$this->setRequest($request);
return $this->success('', [
'webPath' => str_replace('\\', '/', root_path() . 'web')
]);
}
public function mvDist(Request $request): Response
{
$this->setRequest($request);
if (!is_file(root_path() . self::$distDir . DIRECTORY_SEPARATOR . 'index.html')) {
return $this->error(__('No built front-end file found, please rebuild manually!'));
}
if (Terminal::mvDist()) {
return $this->success();
}
return $this->error(__('Failed to move the front-end file, please move it manually!'));
}
/**
* 目录是否可写
* @param $writable
* @return string
*/
private static function writableStateDescribe($writable): string
{
return $writable ? __('Writable') : __('No write permission');
}
/**
* 数据库连接-获取数据表列表(使用 raw PDO
* @param array $database hostname, hostport, username, password, database
* @param bool $returnPdo
* @return array
*/
private function connectDb(array $database, bool $returnPdo = false): array
{
$host = $database['hostname'] ?? '127.0.0.1';
$port = $database['hostport'] ?? '3306';
$user = $database['username'] ?? '';
$pass = $database['password'] ?? '';
$db = $database['database'] ?? '';
$dsn = "mysql:host={$host};port={$port};charset=utf8mb4";
if ($db) {
$dsn .= ";dbname={$db}";
}
try {
$pdo = new \PDO($dsn, $user, $pass, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
]);
$pdo->query("SELECT 1")->fetchAll(\PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
$errorMsg = mb_convert_encoding($e->getMessage() ?: 'unknown', 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');
$template = __('Database connection failed:%s');
return [
'code' => 0,
'msg' => strpos($template, '%s') !== false ? sprintf($template, $errorMsg) : $template . $errorMsg,
];
}
$databases = [];
$databasesExclude = ['information_schema', 'mysql', 'performance_schema', 'sys'];
$stmt = $pdo->query("SHOW DATABASES");
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$stmt->closeCursor();
foreach ($rows as $row) {
$dbName = $row['Database'] ?? $row['database'] ?? '';
if ($dbName && !in_array($dbName, $databasesExclude)) {
$databases[] = $dbName;
}
}
return [
'code' => 1,
'msg' => '',
'databases' => $databases,
'pdo' => $returnPdo ? $pdo : null,
];
}
}

151
app/api/controller/User.php Normal file
View File

@@ -0,0 +1,151 @@
<?php
namespace app\api\controller;
use ba\Captcha;
use ba\ClickCaptcha;
use app\common\controller\Frontend;
use app\common\facade\Token;
use support\validation\Validator;
use support\validation\ValidationException;
use Webman\Http\Request;
use support\Response;
class User extends Frontend
{
protected array $noNeedLogin = ['checkIn', 'logout'];
public function checkIn(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
$openMemberCenter = config('buildadmin.open_member_center');
if (!$openMemberCenter) {
return $this->error(__('Member center disabled'));
}
if ($this->auth->isLogin()) {
return $this->success(__('You have already logged in. There is no need to log in again~'), [
'type' => $this->auth::LOGGED_IN
], $this->auth::LOGIN_RESPONSE_CODE);
}
$userLoginCaptchaSwitch = config('buildadmin.user_login_captcha');
if ($request->method() === 'POST') {
$params = $request->post();
$params = array_merge($params, [
'tab' => $params['tab'] ?? '',
'email' => $params['email'] ?? '',
'mobile' => $params['mobile'] ?? '',
'username' => $params['username'] ?? '',
'password' => $params['password'] ?? '',
'keep' => $params['keep'] ?? false,
'captcha' => $params['captcha'] ?? '',
'captchaId' => $params['captchaId'] ?? '',
'captchaInfo' => $params['captchaInfo'] ?? '',
'registerType' => $params['registerType'] ?? '',
]);
if (!in_array($params['tab'], ['login', 'register'])) {
return $this->error(__('Unknown operation'));
}
try {
$rules = $params['tab'] === 'login' ? $this->getLoginRules($userLoginCaptchaSwitch) : $this->getRegisterRules();
Validator::make($params, $rules[0], $rules[1] ?? [], $rules[2] ?? [])->validate();
} catch (ValidationException $e) {
return $this->error($e->getMessage());
}
if ($params['tab'] === 'login') {
if ($userLoginCaptchaSwitch) {
$captchaObj = new ClickCaptcha();
if (!$captchaObj->check($params['captchaId'], $params['captchaInfo'])) {
return $this->error(__('Captcha error'));
}
}
$res = $this->auth->login($params['username'], $params['password'], !empty($params['keep']));
} else {
$captchaObj = new Captcha();
if (!$captchaObj->check($params['captcha'], $params[$params['registerType']] . 'user_register')) {
return $this->error(__('Please enter the correct verification code'));
}
$res = $this->auth->register($params['username'], $params['password'], $params['mobile'], $params['email']);
}
if ($res === true) {
return $this->success(__('Login succeeded!'), [
'userInfo' => $this->auth->getUserInfo(),
'routePath' => '/user'
]);
}
$msg = $this->auth->getError();
return $this->error($msg ?: __('Check in failed, please try again or contact the website administrator~'));
}
return $this->success('', [
'userLoginCaptchaSwitch' => $userLoginCaptchaSwitch,
'accountVerificationType' => get_account_verification_type()
]);
}
private function getLoginRules(bool $captchaSwitch): array
{
$rules = [
'username' => 'required|string',
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
];
$messages = [
'password.regex' => __('Please input correct password'),
];
if ($captchaSwitch) {
$rules['captchaId'] = 'required|string';
$rules['captchaInfo'] = 'required|string';
}
return [$rules, $messages, []];
}
private function getRegisterRules(): array
{
return [
[
'username' => 'required|string|regex:/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/|unique:user,username',
'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
'registerType' => 'required|in:email,mobile',
'email' => 'required_if:registerType,email|email|unique:user,email',
'mobile' => 'required_if:registerType,mobile|regex:/^1[3-9]\d{9}$/|unique:user,mobile',
'captcha' => 'required|string',
],
[
'username.regex' => __('Please input correct username'),
'password.regex' => __('Please input correct password'),
],
[
'username' => __('Username'),
'email' => __('Email'),
'mobile' => __('Mobile'),
'password' => __('Password'),
'captcha' => __('captcha'),
'registerType' => __('Register type'),
]
];
}
public function logout(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) return $response;
if ($request->method() === 'POST') {
$refreshToken = $request->post('refreshToken', '');
if ($refreshToken) {
Token::delete((string) $refreshToken);
}
$this->auth->logout();
return $this->success();
}
return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
}
}