ユーザーの申し込み
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,5 +2,7 @@ data/*.pem
|
||||
data/*.txt
|
||||
data/user/*.json
|
||||
log/*.txt
|
||||
public/static/user
|
||||
!public/static/user/.kara
|
||||
config/config.php
|
||||
public/static/*.pem
|
||||
|
||||
0
public/static/user/.kara
Normal file
0
public/static/user/.kara
Normal file
@@ -39,6 +39,10 @@ if (ACTIVITYPUB_ENABLED) {
|
||||
if (AUTH_ENABLED) {
|
||||
$routes[] = Route::add('POST', 'login', User::class.'@login');
|
||||
$routes[] = Route::add('GET', 'login', User::class.'@login');
|
||||
if (AUTH_REGISTER_ENABLED) {
|
||||
$routes[] = Route::add('POST', 'register', User::class.'@register');
|
||||
$routes[] = Route::add('GET', 'register', User::class.'@register');
|
||||
}
|
||||
$routes[] = Route::add('GET', 'logout', User::class.'@logout');
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class User {
|
||||
$description = 'サイトにサインイン';
|
||||
|
||||
$tmpl->assign('pagetit', $pagetit);
|
||||
$tmpl->assign('curPage', 'about');
|
||||
$tmpl->assign('curPage', 'auth');
|
||||
$tmpl->assign('custCss', false);
|
||||
$tmpl->assign('menu', $this->getMenu());
|
||||
$tmpl->assign('description', $description);
|
||||
@@ -58,7 +58,7 @@ class User {
|
||||
|
||||
public function logout(array $params): void {
|
||||
try {
|
||||
$auth = new Auth();
|
||||
$auth = new Auth;
|
||||
$user = $auth->getLoggedInUser();
|
||||
if (!$user) {
|
||||
header('Location: /');
|
||||
@@ -72,4 +72,62 @@ class User {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function register(array $params): void {
|
||||
if (!AUTH_REGISTER_ENABLED) return;
|
||||
try {
|
||||
$auth = new Auth;
|
||||
$user = $auth->getLoggedInUser();
|
||||
if ($user) {
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
|
||||
$doRegister = $_SERVER['REQUEST_METHOD'] === 'POST';
|
||||
$error = '';
|
||||
$nyuU = '';
|
||||
$nyuE = '';
|
||||
|
||||
if ($doRegister) {
|
||||
$a = [];
|
||||
if (count($_POST) === 4) {
|
||||
$i = 0;
|
||||
foreach ($_POST as $p) {
|
||||
$a[(int)$i] = $p;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
$auth = new Auth;
|
||||
$res = $auth->mkUser($a[0], $a[1], $a[2], $a[3]);
|
||||
if (!$res->isSuccess) {
|
||||
$error = $res->message;
|
||||
$nyuU = $a[0];
|
||||
$nyuE = $a[3];
|
||||
} else {
|
||||
$auth = new Auth($a[0]);
|
||||
$auth->setToken($a[0], $a[1]);
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$tmpl = new Template('/');
|
||||
$pagetit = '登録';
|
||||
$description = 'サイトに登録';
|
||||
|
||||
$tmpl->assign('pagetit', $pagetit);
|
||||
$tmpl->assign('curPage', 'auth');
|
||||
$tmpl->assign('custCss', false);
|
||||
$tmpl->assign('menu', $this->getMenu());
|
||||
$tmpl->assign('description', $description);
|
||||
$tmpl->assign('error', $error);
|
||||
$tmpl->assign('nyuU', $nyuU);
|
||||
$tmpl->assign('nyuE', $nyuE);
|
||||
|
||||
$tmpl->render('register');
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,13 @@ class Auth {
|
||||
/**
|
||||
* 性別: -1 = 不明, 0 = 男性, 1 = 女性
|
||||
* ロール: -1 = BAN, 0 = ユーザー, 1 = スタッフ
|
||||
* トークン: string token, int expDate
|
||||
*
|
||||
* パスワードとメールアドレスはArgon2IDでハッシュする
|
||||
*/
|
||||
private \stdClass $user;
|
||||
private \stdClass $pubUser;
|
||||
private ?string $token;
|
||||
|
||||
protected string $dataDir = ROOT."/data/user/";
|
||||
protected string $assDir = ROOT."/public/static/user/";
|
||||
protected int $minPassLen = 20;
|
||||
protected int $tokenDuration = 31536000; // 1年間
|
||||
protected string|int|null $algo = PASSWORD_ARGON2ID;
|
||||
@@ -36,25 +34,6 @@ class Auth {
|
||||
return $this->pubUser;
|
||||
}
|
||||
|
||||
private function verifyLogin(string $username, string $password): \Result {
|
||||
$userData = $this->getUserData();
|
||||
if ($username !== $userData->username || !password_verify($password, $userData->password)) {
|
||||
return \Result::error('エラー:ユーザー名又はパスワードが一致していません。');
|
||||
}
|
||||
|
||||
if (password_needs_rehash($userData->password, $algo)) {
|
||||
$userData->password = password_hash($password, $algo);
|
||||
$path = $this->dataDir.$this->id.'.'.$userData->username.'.json';
|
||||
$json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if (file_put_contents($path, $json) === false) {
|
||||
return \Result::error('エラー:ユーザーデータの保存に失敗。');
|
||||
}
|
||||
}
|
||||
|
||||
return \Result::success();
|
||||
}
|
||||
|
||||
public function setToken(string $username, string $password): \Result {
|
||||
$userData = $this->getUserData();
|
||||
if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = [];
|
||||
@@ -156,7 +135,7 @@ class Auth {
|
||||
$userList = scandir($this->dataDir);
|
||||
|
||||
foreach ($userList as $list) {
|
||||
if ($list === '.' || $list === '..') continue;
|
||||
if ($list === '.kara' || $list === '.' || $list === '..') continue;
|
||||
$file = str_replace('.json', '', $list);
|
||||
$user = explode('.', $file)[1];
|
||||
if ($username === $user) return \Result::Success("ユーザー「{$username}」は既に存在します。");
|
||||
@@ -165,8 +144,86 @@ class Auth {
|
||||
return \Result::Error("エラー:ユーザー「{$username}」は存在しません。");
|
||||
}
|
||||
|
||||
public function mkUser(?string $username = null, ?string $password = null, ?string $passwordVerify = null, ?string $email = null): \Result {
|
||||
if (!AUTH_REGISTER_ENABLED) return \Result::Error('ユーザー登録は無効です。');
|
||||
$resUsr = $this->verifyUsername($username);
|
||||
$resPwd = $this->verifyPassword($password, $passwordVerify);
|
||||
$resEml = $this->verifyEmail($email);
|
||||
|
||||
$err = '';
|
||||
|
||||
if (!$resUsr->isSuccess) $err .= $resUsr->message."<br />";
|
||||
if (!$resPwd->isSuccess) $err .= $resPwd->message."<br />";
|
||||
if (!$resEml->isSuccess) $err .= $resEml->message."<br />";
|
||||
|
||||
if ($err != '') return \Result::Error($err);
|
||||
$err = '';
|
||||
|
||||
if (!mkdir($this->assDir.$username, 0755)) {
|
||||
return \Result::Error('エラー:ユーザーのアイコンディレクトリの作成に失敗。');
|
||||
}
|
||||
|
||||
$file = scandir($this->dataDir);
|
||||
$lastId = 0;
|
||||
if ($file) {
|
||||
foreach ($file as $f) {
|
||||
if ($f === '.kara' || $f === '.' || $f === '..') continue;
|
||||
$base = substr($f, 0, -5); // .jsonの削除
|
||||
$dot = strpos($base, '.');
|
||||
if (!$dot) continue;
|
||||
$id = substr($base, 0, $dot);
|
||||
if (ctype_digit($id)) {
|
||||
if ((int)$id > $lastId) $lastId = $id;
|
||||
}
|
||||
}
|
||||
$lastId++;
|
||||
}
|
||||
|
||||
$user = new \stdClass;
|
||||
$user->id = $lastId;
|
||||
$user->username = $username;
|
||||
$user->password = password_hash($password, $algo);
|
||||
$user->avatar = '';
|
||||
$user->email = $email;
|
||||
$user->regDate = time();
|
||||
$user->namecolor = '';
|
||||
$user->displayname = '';
|
||||
$user->gender = -1;
|
||||
$user->role = 0;
|
||||
$user->tokens = [];
|
||||
|
||||
$path = "{$this->dataDir}{$lastId}.{$username}.json";
|
||||
$json = json_encode($user, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if (file_put_contents($path, $json) === false) {
|
||||
rmdir($this->assDir.$username);
|
||||
return \Result::Error('エラー:ユーザーデータの保存に失敗。');
|
||||
}
|
||||
|
||||
return \Result::Success();
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
private function verifyLogin(string $username, string $password): \Result {
|
||||
$userData = $this->getUserData();
|
||||
if ($username !== $userData->username || !password_verify($password, $userData->password)) {
|
||||
return \Result::Error('エラー:ユーザー名又はパスワードが一致していません。');
|
||||
}
|
||||
|
||||
if (password_needs_rehash($userData->password, $algo)) {
|
||||
$userData->password = password_hash($password, $algo);
|
||||
$path = $this->dataDir.$this->id.'.'.$userData->username.'.json';
|
||||
$json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if (file_put_contents($path, $json) === false) {
|
||||
return \Result::Error('エラー:ユーザーデータの保存に失敗。');
|
||||
}
|
||||
}
|
||||
|
||||
return \Result::Success();
|
||||
}
|
||||
|
||||
private function purgeOldTokens(\stdClass $userData): \stdClass|\Result {
|
||||
// 有効期限切れたトークンの削除
|
||||
$out = $userData;
|
||||
@@ -178,7 +235,7 @@ class Auth {
|
||||
$json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if (file_put_contents($path, $json) === false) {
|
||||
return \Result::error('エラー:ユーザーデータの保存に失敗。');
|
||||
return \Result::Error('エラー:ユーザーデータの保存に失敗。');
|
||||
}
|
||||
|
||||
return $out;
|
||||
@@ -191,7 +248,7 @@ class Auth {
|
||||
$id = 0;
|
||||
|
||||
foreach ($file as $f) {
|
||||
if ($f === '.' || $f === '..') continue;
|
||||
if ($f === '.kara' || $f === '.' || $f === '..') continue;
|
||||
|
||||
$path = "{$this->dataDir}{$f}";
|
||||
if (!is_file($path)) continue;
|
||||
@@ -215,7 +272,7 @@ class Auth {
|
||||
$matches = [];
|
||||
|
||||
foreach ($file as $f) {
|
||||
if ($f === '.' || $f === '..') continue;
|
||||
if ($f === '.kara' || $f === '.' || $f === '..') continue;
|
||||
if (preg_match('/^'.$this->id.'\.(.*?)\.json$/', $f, $matches)) {
|
||||
$userFile = $matches[0];
|
||||
break;
|
||||
@@ -239,41 +296,82 @@ class Auth {
|
||||
return json_decode($lines);
|
||||
}
|
||||
|
||||
private function isEmailExist(?string $email): \Result {
|
||||
if (null === $password) return \Result::Error('エラー:パスワードをご入力下さい。');
|
||||
private function isEmailExist(?string $email = null): \Result {
|
||||
$userList = scandir($this->dataDir);
|
||||
$matches = [];
|
||||
|
||||
foreach ($userList as $f) {
|
||||
if ($f === '.kara' || $f === '.' || $f === '..') continue;
|
||||
$path = "{$this->dataDir}{$f}";
|
||||
if (!is_file($path)) continue;
|
||||
$content = file_get_contents($path);
|
||||
$file = str_replace('.json', '', $f);
|
||||
if (str_contains($content, '"email": "'.$email.'",')) {
|
||||
$matches[] = $email;
|
||||
}
|
||||
if (count($matches) > 0) return \Result::Error("ユーザー「{$email}」は既に存在します。");
|
||||
}
|
||||
|
||||
return \Result::Success('エラー:メールアドレスが存在しません。');
|
||||
}
|
||||
|
||||
private function verifyEmail(?string $email = null): \Result {
|
||||
if (null === $email || '' === $email) return \Result::Error('エラー:メールアドレスをご入力下さい。');
|
||||
if (strpos($email, '@') === false) return \Result::Error('エラー:メールアドレスは不正です。');
|
||||
|
||||
$domain = explode('@', $email)[1];
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return \Result::Error('エラー:メールアドレスは不正です。');
|
||||
}
|
||||
|
||||
if (!checkdnsrr($domain, 'MX')) {
|
||||
return \Result::Error('エラー:メールアドレスは不正です。');
|
||||
}
|
||||
|
||||
$res = $this->isEmailExist($email);
|
||||
if (!$res->isSuccess) return \Result::Error($res->message);
|
||||
|
||||
return \Result::Success();
|
||||
}
|
||||
|
||||
private function verifyUsername(?string $username): \Result {
|
||||
if (null === $username) return \Result::Error('エラー:ユーザー名をご入力下さい。');
|
||||
if (null === $username || '' === $username) return \Result::Error('エラー:ユーザー名をご入力下さい。');
|
||||
if (strlen($username) < 6) return \Result::Error('エラー:ユーザー名は6文字以上をご入力下さい。');
|
||||
|
||||
$accepted = 'A-Za-z0-9';
|
||||
$res = $this->isUserExist($username);
|
||||
if ($res->isSuccess) return \Result::Error($res->message);
|
||||
|
||||
if (preg_match('/[^'.preg_quote($accepted, '/').']/', $password)) {
|
||||
$accepted = 'A-Za-z0-9';
|
||||
if (preg_match("/[^{$accepted}]/", $username)) {
|
||||
return \Result::Error('エラー:ユーザー名に不正な文字が含みます。');
|
||||
}
|
||||
|
||||
return \Result::Success();
|
||||
}
|
||||
|
||||
private function verifyPassword(?string $password): \Result {
|
||||
private function verifyPassword(?string $password = null, ?string $passwordVerify = null): \Result {
|
||||
if (null === $password || '' === $password) return \Result::Error('エラー:パスワードをご入力下さい。');
|
||||
if ($password !== $passwordVerify) return \Result::Error('エラー:パスワードは一致していません。');
|
||||
|
||||
$res = $this->checkPasswordStandards($password);
|
||||
if (!$res->isSuccess) {
|
||||
return \Result::Error($res->message);
|
||||
}
|
||||
|
||||
return \Result::Success();
|
||||
}
|
||||
|
||||
private function checkPasswordStandards(?string $password): \Result {
|
||||
if (null === $password) return \Result::Error('エラー:パスワードをご入力下さい。');
|
||||
if (strlen($password) < $this->minPassLen) return \Result::Error("エラー:パスワード数は{$this->minPassLen}以上をご入力下さい。");
|
||||
private function checkPasswordStandards(?string $password = null): \Result {
|
||||
if (strlen($password) < $this->minPassLen) return \Result::Error("エラー:パスワードは{$this->minPassLen}以上をご入力下さい。");
|
||||
|
||||
$specChar = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~]/';
|
||||
$accepted = 'A-Za-z0-9'.$specChar;
|
||||
|
||||
if (preg_match('/[^'.preg_quote($accepted, '/').']/', $password)) {
|
||||
if (!\countmatch($password)) {
|
||||
return \Result::Error('エラー:パスワードに不正な文字が含みます。');
|
||||
}
|
||||
|
||||
$countUpper = preg_match_all('/[A-Z]/', $password) < 2;
|
||||
$countLower = preg_match_all('/[a-z]/', $password) < 2;
|
||||
$countDigit = preg_match_all('/[0-9]/', $password) < 2;
|
||||
$countSymbol = preg_match_all('/['.$specChar.']/', $password) < 2;
|
||||
$countSymbol = \count_special_chars($password) < 2;
|
||||
|
||||
if ($countUpper || $countLower || $countDigit || $countSymbol) {
|
||||
return \Result::Error('エラー:パスワードは2つ以上の大文字、2つ以上の小文字、2つ以上の数字、及び2つ以上の特別文字を含む事が必須です。');
|
||||
|
||||
56
util.php
56
util.php
@@ -151,7 +151,7 @@ if (AUTH_ENABLED) {
|
||||
$gender = 'color: '.($userData->gender === 0 ? $male : ($userData->gender === 1 ? $female : $ungender)).';';
|
||||
$style = $userData->namecolor ?: ($userData->role >= 0 ? $gender : $ban);
|
||||
|
||||
$showname = $userData->displayname ?? $userData->username;
|
||||
$showname = $userData->displayname ?: $userData->username;
|
||||
|
||||
$color = "<span style=\"{$style}\">{$showname}</span>";
|
||||
if ($userData->role === 1) $color .= "<span style=\"font-size: x-small; background: #10c074; border: 1px solid #fcfcfc; border-radius: 10px; padding: 0 0.5em;\">✓</span>";
|
||||
@@ -160,4 +160,58 @@ if (AUTH_ENABLED) {
|
||||
|
||||
return $color.$suffix;
|
||||
}
|
||||
}
|
||||
|
||||
function count_special_chars(string $str): int {
|
||||
$count = 0;
|
||||
$len = strlen($str);
|
||||
|
||||
for ($i = 0; $i < $len; ++$i) {
|
||||
if (str_contains('!', $str[$i])) ++$count;
|
||||
if (str_contains('"', $str[$i])) ++$count;
|
||||
if (str_contains('#', $str[$i])) ++$count;
|
||||
if (str_contains('$', $str[$i])) ++$count;
|
||||
if (str_contains('%', $str[$i])) ++$count;
|
||||
if (str_contains('&', $str[$i])) ++$count;
|
||||
if (str_contains('\'', $str[$i])) ++$count;
|
||||
if (str_contains('(', $str[$i])) ++$count;
|
||||
if (str_contains(')', $str[$i])) ++$count;
|
||||
if (str_contains('-', $str[$i])) ++$count;
|
||||
if (str_contains('=', $str[$i])) ++$count;
|
||||
if (str_contains('^', $str[$i])) ++$count;
|
||||
if (str_contains('~', $str[$i])) ++$count;
|
||||
if (str_contains('\\', $str[$i])) ++$count;
|
||||
if (str_contains('|', $str[$i])) ++$count;
|
||||
if (str_contains('[', $str[$i])) ++$count;
|
||||
if (str_contains(']', $str[$i])) ++$count;
|
||||
if (str_contains(':', $str[$i])) ++$count;
|
||||
if (str_contains('@', $str[$i])) ++$count;
|
||||
if (str_contains('`', $str[$i])) ++$count;
|
||||
if (str_contains('*', $str[$i])) ++$count;
|
||||
if (str_contains('{', $str[$i])) ++$count;
|
||||
if (str_contains('}', $str[$i])) ++$count;
|
||||
if (str_contains(';', $str[$i])) ++$count;
|
||||
if (str_contains('+', $str[$i])) ++$count;
|
||||
if (str_contains(',', $str[$i])) ++$count;
|
||||
if (str_contains('<', $str[$i])) ++$count;
|
||||
if (str_contains('.', $str[$i])) ++$count;
|
||||
if (str_contains('>', $str[$i])) ++$count;
|
||||
if (str_contains('/', $str[$i])) ++$count;
|
||||
if (str_contains('?', $str[$i])) ++$count;
|
||||
if (str_contains('_', $str[$i])) ++$count;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
function countmatch(string $str): bool {
|
||||
$len = strlen($str);
|
||||
|
||||
$numUpper = preg_match_all('/[A-Z]/', $str) ?? 0;
|
||||
$numLower = preg_match_all('/[a-z]/', $str) ?? 0;
|
||||
$numDigit = preg_match_all('/[0-9]/', $str) ?? 0;
|
||||
$numSymbol = count_special_chars($str);
|
||||
$sum = $numUpper + $numLower + $numDigit + $numSymbol;
|
||||
|
||||
return $len == $sum;
|
||||
}
|
||||
47
view/register.maron
Normal file
47
view/register.maron
Normal file
@@ -0,0 +1,47 @@
|
||||
{@ include(common/header) @}
|
||||
<h1 class="paragraph">登録</h1>
|
||||
<p class="paragraph">
|
||||
登録して下さい。
|
||||
</p>
|
||||
|
||||
{@ if ($error) @}
|
||||
<p>
|
||||
<div class="errormes">
|
||||
{{{ $error }}}
|
||||
</div>
|
||||
</p>
|
||||
{@ endif @}
|
||||
|
||||
<p>
|
||||
<form action="/register" method="POST">
|
||||
{$ $username = randstr() $}
|
||||
{$ $password = randstr() $}
|
||||
{$ $passwordVerify = randstr() $}
|
||||
{$ $email = randstr() $}
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label for="{{ $username }}">ユーザー名:</label></td>
|
||||
<td><input type="text" id="{{ $username }}" name="{{ $username }}" value="{{ $nyuU }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="{{ $password }}">パスワード:</label></td>
|
||||
<td><input type="password" id="{{ $password }}" name="{{ $password }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="{{ $passwordVerify }}">パスワード(確認):</label></td>
|
||||
<td><input type="password" id="{{ $passwordVerify }}" name="{{ $passwordVerify }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="{{ $email }}">メールアドレス:</label></td>
|
||||
<td><input type="email" id="{{ $email }}" name="{{ $email }}" value="{{ $nyuE }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><button>登録</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</p>
|
||||
{@ include(common/footer) @}
|
||||
Reference in New Issue
Block a user