feat: 자동 로그인 구현
This commit is contained in:
@@ -77,12 +77,12 @@ ENGINE=Aria;
|
||||
CREATE TABLE `login_token` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`base_token` INT(11) NOT NULL,
|
||||
`base_token` VARCHAR(20) NOT NULL COLLATE 'utf8mb4_general_ci',
|
||||
`reg_ip` VARCHAR(40) NOT NULL COLLATE 'utf8mb4_general_ci',
|
||||
`reg_date` DATETIME NOT NULL,
|
||||
`expire_date` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX `by_token` (`user_id`, `base_token`),
|
||||
UNIQUE INDEX `by_token` (`base_token`),
|
||||
INDEX `by_date` (`user_id`, `expire_date`)
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
|
||||
+116
-2
@@ -11,9 +11,12 @@ import { unwrap_any } from '../util/unwrap_any';
|
||||
import { sha512 } from 'js-sha512';
|
||||
import { unwrap } from '../util/unwrap';
|
||||
import { InvalidResponse } from '../defs';
|
||||
import internal from 'stream';
|
||||
import { delay } from '../util/delay';
|
||||
|
||||
type LoginResponse = {
|
||||
result: true,
|
||||
nextToken: [number, string] | undefined,
|
||||
} | {
|
||||
result: false,
|
||||
reqOTP: boolean,
|
||||
@@ -29,6 +32,19 @@ type OTPResponse = {
|
||||
reason: string,
|
||||
}
|
||||
|
||||
type AutoLoginNonceResponse = {
|
||||
result: true,
|
||||
loginNonce: string,
|
||||
} | InvalidResponse;
|
||||
|
||||
type AutoLoginResponse = {
|
||||
result: true,
|
||||
nextToken: [number, string] | undefined,
|
||||
} | {
|
||||
result: false,
|
||||
silent: boolean,
|
||||
reason: string,
|
||||
}
|
||||
declare global {
|
||||
interface Window {
|
||||
getOAuthToken: (mode: string, scope_list: string[]) => void;
|
||||
@@ -41,6 +57,92 @@ declare const kakao_oauth_redirect_uri: string;
|
||||
|
||||
let oauthMode: string | null = null;
|
||||
|
||||
const TOKEN_VERSION = 1;
|
||||
const LOGIN_TOKEN_KEY = 'sammo_login_token';
|
||||
function regNextToken(tokenInfo: [number, string]) {
|
||||
localStorage.setItem(LOGIN_TOKEN_KEY, JSON.stringify([TOKEN_VERSION, tokenInfo, Date.now()]));
|
||||
}
|
||||
|
||||
function getToken(): [number, string] | undefined {
|
||||
const trialToken = localStorage.getItem(LOGIN_TOKEN_KEY);
|
||||
if (!trialToken) {
|
||||
return;
|
||||
}
|
||||
const tokenItems = JSON.parse(trialToken);
|
||||
if (tokenItems[0] != TOKEN_VERSION) {
|
||||
console.log(tokenItems);
|
||||
resetToken();
|
||||
return;
|
||||
}
|
||||
const [, token,] = tokenItems;
|
||||
return token;
|
||||
}
|
||||
|
||||
function resetToken(){
|
||||
localStorage.removeItem(LOGIN_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async function tryAutoLogin() {
|
||||
try {
|
||||
const tokenInfo = getToken();
|
||||
if (!tokenInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [tokenID, token] = tokenInfo;
|
||||
|
||||
const result = await axios.post<AutoLoginNonceResponse>('api.php', {
|
||||
path: ["Login", "ReqNonce"]
|
||||
});
|
||||
|
||||
if(!result){
|
||||
//api 에러.
|
||||
return;
|
||||
}
|
||||
|
||||
if(!result.data.result){
|
||||
resetToken();
|
||||
return;
|
||||
}
|
||||
|
||||
const nonce = result.data.loginNonce;
|
||||
|
||||
const hashedToken = sha512(token + nonce);
|
||||
const _loginResult = await axios.post<AutoLoginResponse>('api.php', {
|
||||
path: ["Login", "LoginByToken"],
|
||||
args: {
|
||||
'hashedToken':hashedToken,
|
||||
'token_id':tokenID,
|
||||
}
|
||||
});
|
||||
|
||||
if(!_loginResult){
|
||||
return;
|
||||
}
|
||||
|
||||
const loginResult = _loginResult.data;
|
||||
if(!loginResult.result){
|
||||
if(!loginResult.silent){
|
||||
alert(loginResult.reason);
|
||||
}
|
||||
console.error(loginResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if(loginResult.nextToken){
|
||||
regNextToken(loginResult.nextToken);
|
||||
}
|
||||
window.location.href = "./";
|
||||
|
||||
}
|
||||
catch(e){
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function getOAuthToken(mode: string, scope_list?: string[] | string) {
|
||||
if (mode === undefined) {
|
||||
mode = 'login';
|
||||
@@ -115,6 +217,9 @@ async function doLoginUsingOAuth() {
|
||||
}
|
||||
|
||||
if (result.result) {
|
||||
if (result.nextToken) {
|
||||
regNextToken(result.nextToken);
|
||||
}
|
||||
window.location.href = "./";
|
||||
return;
|
||||
}
|
||||
@@ -156,9 +261,16 @@ function postOAuthResult(mode: string) {
|
||||
window.postOAuthResult = postOAuthResult;
|
||||
|
||||
|
||||
$(function ($) {
|
||||
$(async function ($) {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
//로그인 먼저 해볼 것
|
||||
if(getToken()){
|
||||
void tryAutoLogin();
|
||||
await delay(100);
|
||||
}
|
||||
|
||||
|
||||
type LoginFormType = {
|
||||
username: string,
|
||||
password: string,
|
||||
@@ -215,6 +327,9 @@ $(function ($) {
|
||||
|
||||
|
||||
if (result.result) {
|
||||
if (result.nextToken) {
|
||||
regNextToken(result.nextToken);
|
||||
}
|
||||
window.location.href = "./";
|
||||
return;
|
||||
}
|
||||
@@ -276,7 +391,6 @@ $(function ($) {
|
||||
getOAuthToken('login', ['account_email', 'talk_message']);
|
||||
})
|
||||
|
||||
|
||||
//TODO: 모바일에서 크기 비례 지도 다시 띄우기?
|
||||
/*
|
||||
if (document.body.clientWidth < 700) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
require(__DIR__ . '/../vendor/autoload.php');
|
||||
@@ -8,6 +9,7 @@ require('lib.join.php');
|
||||
//TODO: KakaoOauth를 API로 정식으로 이동
|
||||
|
||||
WebUtil::requireAJAX();
|
||||
|
||||
use \kakao\Kakao_REST_API_Helper as Kakao_REST_API_Helper;
|
||||
|
||||
$RootDB = RootDB::db();
|
||||
@@ -122,7 +124,9 @@ if(!$email){
|
||||
|
||||
|
||||
$userInfo = $RootDB->queryFirstRow(
|
||||
'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl`, oauth_info, token_valid_until from member where email=%s',$email);
|
||||
'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl`, oauth_info, token_valid_until from member where email=%s',
|
||||
$email
|
||||
);
|
||||
|
||||
if (!$userInfo) {
|
||||
$restAPI->unlink();
|
||||
@@ -145,8 +149,7 @@ if($userInfo['delete_after']){
|
||||
'reqOTP' => false,
|
||||
'reason' => "기간 만기로 삭제되었습니다. 재 가입을 시도해주세요.",
|
||||
]);
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
$restAPI->unlink();
|
||||
$session->access_token = null;
|
||||
Json::die([
|
||||
@@ -155,7 +158,6 @@ if($userInfo['delete_after']){
|
||||
'reason' => "삭제 요청된 계정입니다.[{$userInfo['delete_after']}]"
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$oauthInfo = Json::decode($userInfo['oauth_info']) ?? [];
|
||||
@@ -181,7 +183,7 @@ if(!$tokenValidUntil || $tokenValidUntil < $now){
|
||||
'reason' => '인증 코드를 보내는데 실패했습니다.'
|
||||
]);
|
||||
}
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], Json::decode($userInfo['acl']??'{}'));
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], null, Json::decode($userInfo['acl'] ?? '{}'));
|
||||
Json::die([
|
||||
'result' => false,
|
||||
'reqOTP' => true,
|
||||
@@ -199,9 +201,45 @@ $RootDB->insert('member_log',[
|
||||
])
|
||||
]);
|
||||
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, $userInfo['token_valid_until'], Json::decode($userInfo['acl']??'{}'));
|
||||
|
||||
//TODO: 이동하면서 이부분 정리
|
||||
$nowDate = TimeUtil::now();
|
||||
$RootDB->delete(
|
||||
'login_token',
|
||||
'user_id = %i AND expire_date < %s',
|
||||
$userID,
|
||||
$nowDate
|
||||
);
|
||||
$RootDB->query(
|
||||
'DELETE invalid FROM login_token AS invalid
|
||||
JOIN
|
||||
( SELECT id
|
||||
FROM login_token
|
||||
WHERE user_id = %i
|
||||
ORDER BY id DESC
|
||||
LIMIT 8,1
|
||||
) AS valid
|
||||
ON invalid.id < valid.id WHERE user_id = %i',
|
||||
$userID,
|
||||
$userID
|
||||
);
|
||||
$token = Util::randomStr(20);
|
||||
$RootDB->insert('login_token', [
|
||||
'user_id' => $userInfo['no'],
|
||||
'base_token' => $token,
|
||||
'reg_ip' => Util::get_client_ip(true),
|
||||
'reg_date' => $nowDate,
|
||||
'expire_date' => TimeUtil::nowAddDays(7)
|
||||
]);
|
||||
$tokenID = $RootDB->insertId();
|
||||
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, $userInfo['token_valid_until'], $tokenID, Json::decode($userInfo['acl'] ?? '{}'));
|
||||
|
||||
|
||||
|
||||
Json::die([
|
||||
'result' => true,
|
||||
'reqOTP' => false,
|
||||
'nextToken' => [$tokenID, $token],
|
||||
'reason' => '로그인 되었습니다.'
|
||||
]);
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace sammo\API\Login;
|
||||
|
||||
use DateTime;
|
||||
use sammo\Session;
|
||||
use DateTimeInterface;
|
||||
use sammo\BaseAPI;
|
||||
@@ -34,6 +35,47 @@ class LoginByID extends \sammo\BaseAPI
|
||||
return BaseAPI::NO_SESSION;
|
||||
}
|
||||
|
||||
public function scrubToken(int $userID)
|
||||
{
|
||||
$RootDB = RootDB::db();
|
||||
$nowDate = TimeUtil::now();
|
||||
$RootDB->delete(
|
||||
'login_token',
|
||||
'user_id = %i AND expire_date < %s',
|
||||
$userID,
|
||||
$nowDate
|
||||
);
|
||||
$RootDB->query(
|
||||
'DELETE invalid FROM login_token AS invalid
|
||||
JOIN
|
||||
( SELECT id
|
||||
FROM login_token
|
||||
WHERE user_id = %i
|
||||
ORDER BY id DESC
|
||||
LIMIT 8,1
|
||||
) AS valid
|
||||
ON invalid.id < valid.id WHERE user_id = %i',
|
||||
$userID,
|
||||
$userID
|
||||
);
|
||||
}
|
||||
|
||||
public function addToken(int $userID)
|
||||
{
|
||||
$RootDB = RootDB::db();
|
||||
$nowDate = TimeUtil::now();
|
||||
$token = Util::randomStr(20);
|
||||
$RootDB->insert('login_token', [
|
||||
'user_id' => $userID,
|
||||
'base_token' => $token,
|
||||
'reg_ip' => Util::get_client_ip(true),
|
||||
'reg_date' => $nowDate,
|
||||
'expire_date' => TimeUtil::nowAddDays(7)
|
||||
]);
|
||||
$tokenID = $RootDB->insertId();
|
||||
return [$tokenID, $token];
|
||||
}
|
||||
|
||||
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
|
||||
{
|
||||
$RootDB = RootDB::db();
|
||||
@@ -82,16 +124,6 @@ class LoginByID extends \sammo\BaseAPI
|
||||
}
|
||||
}
|
||||
|
||||
$RootDB->insert('member_log', [
|
||||
'member_no' => $userInfo['no'],
|
||||
'action_type' => 'login',
|
||||
'action' => Json::encode([
|
||||
'ip' => Util::get_client_ip(true),
|
||||
'type' => 'plain'
|
||||
])
|
||||
]);
|
||||
|
||||
|
||||
$RootDB->insert('member_log', [
|
||||
'member_no' => $userInfo['no'],
|
||||
'action_type' => 'login',
|
||||
@@ -104,7 +136,7 @@ class LoginByID extends \sammo\BaseAPI
|
||||
if ($userInfo['oauth_type'] == 'KAKAO') {
|
||||
$oauthFailResult = KakaoUtil::kakaoOAuthCheck($userInfo);
|
||||
if ($oauthFailResult !== null) {
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], Json::decode($userInfo['acl'] ?? '{}'));
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], null, Json::decode($userInfo['acl'] ?? '{}'));
|
||||
[$oauthReqOTP, $oauthFailReason] = $oauthFailResult;
|
||||
return [
|
||||
'result' => false,
|
||||
@@ -114,11 +146,16 @@ class LoginByID extends \sammo\BaseAPI
|
||||
}
|
||||
}
|
||||
|
||||
$this->scrubToken($userInfo['no']);
|
||||
$nextToken = $this->addToken($userInfo['no']);
|
||||
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, $userInfo['token_valid_until'], $nextToken[0], Json::decode($userInfo['acl'] ?? '{}'));
|
||||
|
||||
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, $userInfo['token_valid_until'], Json::decode($userInfo['acl'] ?? '{}'));
|
||||
return [
|
||||
'result' => true,
|
||||
'reqOTP' => false,
|
||||
'nextToken' => $nextToken,
|
||||
'reason' => '로그인 되었습니다.'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,18 +12,139 @@ use sammo\TimeUtil;
|
||||
use sammo\Util;
|
||||
use sammo\Validator;
|
||||
|
||||
class LoginByToken extends \sammo\BaseAPI{
|
||||
class LoginByToken extends LoginByID
|
||||
{
|
||||
|
||||
public function getRequiredSessionMode(): int {
|
||||
return \sammo\BaseAPI::NO_SESSION;
|
||||
public function getRequiredSessionMode(): int
|
||||
{
|
||||
return \sammo\BaseAPI::NO_SESSION; //XXX: Token 때문에 엄밀히는 NO_SESSION이 아님
|
||||
}
|
||||
|
||||
public function validateArgs(): ?string {
|
||||
public function validateArgs(): ?string
|
||||
{
|
||||
$v = new Validator($this->args);
|
||||
$v
|
||||
->rule('required', [
|
||||
'token_id',
|
||||
'hashedToken'
|
||||
]);
|
||||
|
||||
if (!$v->validate()) {
|
||||
return $v->errorStr();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag) {
|
||||
return '미구현';
|
||||
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
|
||||
{
|
||||
$loginNonce = $session->loginNonce ?? null;
|
||||
$loginNonceExpired = $session->loginNonceExpired ?? null;
|
||||
if (!is_string($loginNonce) || !is_string($loginNonceExpired)) {
|
||||
return '자동 로그인: 절차 오류';
|
||||
}
|
||||
|
||||
$RootDB = RootDB::db();
|
||||
if ($session->isLoggedIn()) {
|
||||
$session->logout();
|
||||
}
|
||||
|
||||
$token_id = $this->args['token_id'];
|
||||
|
||||
$token_info = $RootDB->queryFirstRow('SELECT * FROM login_token WHERE id = %i', $token_id);
|
||||
if (!$token_info) {
|
||||
return [
|
||||
'result' => false,
|
||||
'silent' => true,
|
||||
'reason' => 'failed'
|
||||
];
|
||||
}
|
||||
|
||||
$hashedToken = $this->args['hashedToken'];
|
||||
$tokenAnswer = hash('sha512', "{$token_info['base_token']}{$loginNonce}");
|
||||
if (strtolower($hashedToken) != strtolower($tokenAnswer)) {
|
||||
return [
|
||||
'result' => false,
|
||||
'silent' => true,
|
||||
'reason' => 'failed'
|
||||
];
|
||||
}
|
||||
|
||||
$userInfo = $RootDB->queryFirstRow(
|
||||
'SELECT `no`, `id`, `name`, `grade`, `delete_after`, `acl`, oauth_id, oauth_type, oauth_info, token_valid_until ' .
|
||||
'from member where NO = %i',
|
||||
$token_info['user_id']
|
||||
);
|
||||
if(!$userInfo){
|
||||
return '자동 로그인: 올바른 계정이 아닙니다.';
|
||||
}
|
||||
|
||||
$canLogin = $RootDB->queryFirstField('SELECT `LOGIN` FROM `system` WHERE `NO` = 1');
|
||||
if ($canLogin != 'Y' && $userInfo['grade'] < 5) {
|
||||
return '자동 로그인: 현재는 로그인이 금지되어있습니다!';
|
||||
}
|
||||
|
||||
$nowDate = TimeUtil::now();
|
||||
if ($userInfo['delete_after']) {
|
||||
if ($userInfo['delete_after'] < $nowDate) {
|
||||
$RootDB->delete('member', 'no=%i', $userInfo['no']);
|
||||
return [
|
||||
'result' => false,
|
||||
'silent' => true,
|
||||
'reason' => "기간 만기"
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'result' => false,
|
||||
'silent' => true,
|
||||
'reason' => "삭제 요청된 계정[{$userInfo['delete_after']}]"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$RootDB->insert('member_log', [
|
||||
'member_no' => $userInfo['no'],
|
||||
'action_type' => 'login',
|
||||
'action' => Json::encode([
|
||||
'ip' => Util::get_client_ip(true),
|
||||
'type' => 'auto'
|
||||
])
|
||||
]);
|
||||
|
||||
if ($userInfo['oauth_type'] == 'KAKAO') {
|
||||
$oauthFailResult = KakaoUtil::kakaoOAuthCheck($userInfo);
|
||||
if ($oauthFailResult !== null) {
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], true, $userInfo['token_valid_until'], null, Json::decode($userInfo['acl'] ?? '{}'));
|
||||
[$oauthReqOTP, $oauthFailReason] = $oauthFailResult;
|
||||
$RootDB->delete(
|
||||
'login_token',
|
||||
'id = %i', $token_id
|
||||
);
|
||||
return $oauthFailReason;
|
||||
}
|
||||
}
|
||||
|
||||
$session->login($userInfo['no'], $userInfo['id'], $userInfo['grade'], false, $userInfo['token_valid_until'], $token_id, Json::decode($userInfo['acl'] ?? '{}'));
|
||||
$this->scrubToken($userInfo['no']);
|
||||
|
||||
$nextDate = TimeUtil::nowAddDays(2);
|
||||
if($nextDate < $token_info['expire_date']){
|
||||
return [
|
||||
'result' => true,
|
||||
'silent' => true,
|
||||
'reason' => 'success'
|
||||
];
|
||||
}
|
||||
|
||||
$RootDB->delete(
|
||||
'login_token',
|
||||
'id = %i', $token_id
|
||||
);
|
||||
$nextToken = $this->addToken($token_id);
|
||||
return [
|
||||
'result' => true,
|
||||
'silent' => true,
|
||||
'nextToken' => $nextToken,
|
||||
'reason' => 'success'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace sammo\API\Login;
|
||||
|
||||
use sammo\Session;
|
||||
use DateTimeInterface;
|
||||
use sammo\BaseAPI;
|
||||
use sammo\Json;
|
||||
use sammo\KakaoUtil;
|
||||
use sammo\RootDB;
|
||||
use sammo\TimeUtil;
|
||||
use sammo\Util;
|
||||
use sammo\Validator;
|
||||
|
||||
class ReqNonce extends BaseAPI{
|
||||
|
||||
public function getRequiredSessionMode(): int {
|
||||
return \sammo\BaseAPI::NO_SESSION;
|
||||
}
|
||||
|
||||
public function validateArgs(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag) {
|
||||
$loginNonce = Util::randomStr(16);
|
||||
$loginNonceExpired = TimeUtil::nowAddSeconds(2);
|
||||
$session->loginNonce = $loginNonce;
|
||||
$session->loginNonceExpired = $loginNonceExpired;
|
||||
return [
|
||||
'result'=>true,
|
||||
'loginNonce'=>$loginNonce,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace sammo;
|
||||
* @property string $ip IP
|
||||
* @property bool $reqOTP 인증 코드 필요
|
||||
* @property array $acl 권한
|
||||
* @property int|null $tokenID 토큰ID
|
||||
* @property string $tokenValidUntil 로그인 토큰 길이
|
||||
*
|
||||
* @property int $generalID 장수 번호 (게임 로그인 필요)
|
||||
@@ -170,7 +171,7 @@ class Session
|
||||
return Util::array_get($_SESSION[$name]);
|
||||
}
|
||||
|
||||
public function login(int $userID, string $userName, int $grade, bool $reqOTP, ?string $tokenValidUntil, array $acl): Session
|
||||
public function login(int $userID, string $userName, int $grade, bool $reqOTP, ?string $tokenValidUntil, ?int $tokenID, array $acl): Session
|
||||
{
|
||||
$this->set('userID', $userID);
|
||||
$this->set('userName', $userName);
|
||||
@@ -180,6 +181,7 @@ class Session
|
||||
$this->set('acl', $acl);
|
||||
$this->set('reqOTP', $reqOTP);
|
||||
$this->set('tokenValidUntil', $tokenValidUntil);
|
||||
$this->set('tokenID', $tokenID);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -198,6 +200,10 @@ class Session
|
||||
$this->logoutGame();
|
||||
}
|
||||
|
||||
if($this->tokenID??null){
|
||||
RootDB::db()->delete('login_token', 'id = %i', $this->tokenID);
|
||||
}
|
||||
|
||||
$this->set('userID', null);
|
||||
$this->set('userName', null);
|
||||
$this->set('userGrade', null);
|
||||
|
||||
Reference in New Issue
Block a user