From 708138db5f95f09812ef93ca5d12848c980f7c54 Mon Sep 17 00:00:00 2001
From: PavelBegunkov <asml.silence@gmail.com>
Date: Wed, 10 Aug 2016 13:49:32 +0300
Subject: [PATCH] #120 add auth tokens to db & model

---
 db/constraints/base.sql                       |   6 +
 db/migrations/V12_3__auth_token.sql           | 100 +++++++++++++++
 db/stored/functions.sql                       | 114 +++++++++++++----
 db/stored/procedures.sql                      |  20 +++
 db/structure/base.sql                         |  18 ++-
 .../application/classes/FileParser.php        |   2 +-
 .../application/classes/Model/Account.php     | 121 +++++++++++-------
 .../application/classes/Model/Student.php     |  23 ++++
 8 files changed, 330 insertions(+), 74 deletions(-)
 create mode 100644 db/migrations/V12_3__auth_token.sql

diff --git a/db/constraints/base.sql b/db/constraints/base.sql
index a06dc655c..ba1384cbd 100644
--- a/db/constraints/base.sql
+++ b/db/constraints/base.sql
@@ -143,3 +143,9 @@ ALTER TABLE `exam_period_options`
 ALTER TABLE `compound_disciplines`
   ADD CONSTRAINT `compound_disciplines_ibfk_1` FOREIGN KEY (`SpecializationID`) REFERENCES `specializations` (`ID`),
   ADD CONSTRAINT `compound_disciplines_ibfk_2` FOREIGN KEY (`GradeID`) REFERENCES `grades` (`ID`);
+
+--
+-- Ограничения внешнего ключа таблицы `auth_tokens`
+--
+ALTER TABLE `auth_tokens`
+  ADD CONSTRAINT `auth_tokens_ibfk_1` FOREIGN KEY (`AccountID`) REFERENCES `accounts` (`ID`);
diff --git a/db/migrations/V12_3__auth_token.sql b/db/migrations/V12_3__auth_token.sql
new file mode 100644
index 000000000..0ae0625eb
--- /dev/null
+++ b/db/migrations/V12_3__auth_token.sql
@@ -0,0 +1,100 @@
+START TRANSACTION;
+
+CREATE TABLE `auth_tokens` (
+  `Token` char(40) charset ascii NOT NULL,
+  `AccountID` int(11) NOT NULL,
+  `Created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `Accessed` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `Mask` int(11) NOT NULL DEFAULT 0,
+  UNIQUE KEY `Token` (`Token`),
+  KEY `AccountID` (`AccountID`)
+);
+
+ALTER TABLE `auth_tokens`
+  ADD CONSTRAINT `auth_tokens_ibfk_1` FOREIGN KEY (`AccountID`) REFERENCES `accounts` (`ID`);
+
+
+DELIMITER //
+
+-- returns:
+-- -1 : invalid token
+-- >0 : accountID
+DROP FUNCTION IF EXISTS SignInByToken//
+CREATE FUNCTION SignInByToken (pToken char(40) charset ascii) RETURNS int(11) # account id
+NO SQL
+BEGIN
+    DECLARE vAccountID INT DEFAULT -1;
+
+    SELECT auth_tokens.AccountID INTO vAccountID
+        FROM auth_tokens
+        WHERE auth_tokens.Token = pToken
+        LIMIT 1;
+
+    IF vAccountID = -1 THEN
+        RETURN -1; -- token not found
+    END IF;
+
+    UPDATE auth_tokens
+        SET Accessed = CURRENT_TIMESTAMP
+        WHERE auth_tokens.Token = pToken
+        LIMIT 1;
+
+    INSERT INTO logs_signin(AccountID) VALUES (vAccountID);
+    RETURN vAccountID;
+END //
+
+DROP FUNCTION IF EXISTS DeleteAuthToken//
+CREATE FUNCTION DeleteAuthToken(pToken char(40) CHARSET ascii) RETURNS int(11)
+NO SQL
+BEGIN
+    DELETE FROM auth_tokens
+        WHERE auth_tokens.Token = pToken;
+    RETURN ROW_COUNT()-1;
+END//
+
+DROP FUNCTION IF EXISTS CreateAuthToken//
+CREATE FUNCTION CreateAuthToken(
+    pAccountID int(11),
+    pRightMask int(11)
+) RETURNS char(40) charset ascii
+NO SQL
+BEGIN
+    DECLARE vCounter int(11) DEFAULT 666;
+    DECLARE vCreated boolean DEFAULT FALSE;
+    DECLARE vSeed int(11) DEFAULT FLOOR(4294967296 * RAND(CURRENT_TIMESTAMP ^ LAST_INSERT_ID() ^ (pAccountID << 10)));
+    DECLARE vToken char(40) charset ascii DEFAULT SHA1(vSeed);
+
+    WHILE NOT vCreated AND vCounter > 0 DO
+        DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET vCreated = FALSE;
+        SET vToken = SHA1(RAND());
+        INSERT INTO auth_tokens(Token, AccountID, Mask) VALUES (vToken, pAccountID, pRightMask);
+
+        SET vCreated = TRUE;
+        SET vCounter = vCounter - 1;
+    END WHILE;
+
+    RETURN IF(vCreated, vToken, '');
+END//
+
+-- pAccountID == 0 - get tokens of all users
+DROP PROCEDURE IF EXISTS GetAuthTokens//
+CREATE PROCEDURE GetAuthTokens(
+    IN pAccountID int(11))
+BEGIN
+    -- accountID, Created, Accessed, Mask
+    IF pAccountID = 0 THEN
+        SELECT auth_tokens.*
+            FROM auth_tokens;
+    ELSE
+        SELECT auth_tokens.*
+            FROM auth_tokens
+            WHERE auth_tokens.AccountID = pAccountID;
+    END IF;
+END//
+
+
+
+
+DELIMITER ;
+
+COMMIT ;
diff --git a/db/stored/functions.sql b/db/stored/functions.sql
index a642ed2ff..d638079e5 100644
--- a/db/stored/functions.sql
+++ b/db/stored/functions.sql
@@ -429,32 +429,6 @@ BEGIN
 END //
 
 
-
-DROP FUNCTION IF EXISTS SignIn//
-CREATE FUNCTION SignIn (
-    pLoginOrMail VARCHAR(255) CHARSET utf8,
-    pPassword  VARCHAR(64) CHARSET utf8
-) RETURNS int(11) # account id
-NO SQL
-BEGIN
-    DECLARE vAccountID INT DEFAULT -1;
-
-    #check account existence
-    SELECT accounts.ID INTO vAccountID FROM accounts
-        WHERE   accounts.Password = SHA1(pPassword) AND
-                (accounts.Login = pLoginOrMail OR accounts.EMail = pLoginOrMail)
-        LIMIT 1;
-    IF vAccountID <= 0 THEN
-        RETURN -1;
-    END IF;
-
-    INSERT INTO logs_signin (AccountID) VALUES (vAccountID);
-    RETURN vAccountID;
-END //
-
-
-
-
 # -------------------------------------------------------------------------------------------
 # Label: teachers
 # -------------------------------------------------------------------------------------------
@@ -2259,4 +2233,92 @@ BEGIN
     RETURN ROW_COUNT()-1;
 END//
 
+
+
+# -------------------------------------------------------------------------------------------
+# Label: authorization
+# -------------------------------------------------------------------------------------------
+
+DROP FUNCTION IF EXISTS SignIn//
+CREATE FUNCTION SignIn (
+    pLoginOrMail VARCHAR(255) CHARSET utf8,
+    pPassword  VARCHAR(64) CHARSET utf8
+) RETURNS int(11) # account id
+NO SQL
+BEGIN
+    DECLARE vAccountID INT DEFAULT -1;
+
+    #check account existence
+    SELECT accounts.ID INTO vAccountID FROM accounts
+        WHERE   accounts.Password = SHA1(pPassword) AND
+                (accounts.Login = pLoginOrMail OR accounts.EMail = pLoginOrMail)
+        LIMIT 1;
+    IF vAccountID <= 0 THEN
+        RETURN -1;
+    END IF;
+
+    INSERT INTO logs_signin (AccountID) VALUES (vAccountID);
+    RETURN vAccountID;
+END //
+
+-- returns:
+-- -1 : invalid token
+-- >0 : accountID
+DROP FUNCTION IF EXISTS SignInByToken//
+CREATE FUNCTION SignInByToken (pToken char(40) charset ascii) RETURNS int(11) # account id
+NO SQL
+BEGIN
+    DECLARE vAccountID INT DEFAULT -1;
+
+    SELECT auth_tokens.AccountID INTO vAccountID
+        FROM auth_tokens
+        WHERE auth_tokens.Token = pToken
+        LIMIT 1;
+
+    IF vAccountID = -1 THEN
+        RETURN -1; -- token not found
+    END IF;
+
+    UPDATE auth_tokens
+        SET Accessed = CURRENT_TIMESTAMP
+        WHERE auth_tokens.Token = pToken
+        LIMIT 1;
+
+    INSERT INTO logs_signin(AccountID) VALUES (vAccountID);
+    RETURN vAccountID;
+END //
+
+DROP FUNCTION IF EXISTS DeleteAuthToken//
+CREATE FUNCTION DeleteAuthToken(pToken char(40) CHARSET ascii) RETURNS int(11)
+NO SQL
+BEGIN
+    DELETE FROM auth_tokens
+        WHERE auth_tokens.Token = pToken;
+    RETURN ROW_COUNT()-1;
+END//
+
+DROP FUNCTION IF EXISTS CreateAuthToken//
+CREATE FUNCTION CreateAuthToken(
+    pAccountID int(11),
+    pRightMask int(11)
+) RETURNS char(40) charset ascii
+NO SQL
+BEGIN
+    DECLARE vCounter int(11) DEFAULT 666;
+    DECLARE vCreated boolean DEFAULT FALSE;
+    DECLARE vSeed int(11) DEFAULT FLOOR(4294967296 * RAND(CURRENT_TIMESTAMP ^ LAST_INSERT_ID() ^ (pAccountID << 10)));
+    DECLARE vToken char(40) charset ascii DEFAULT SHA1(vSeed);
+
+    WHILE NOT vCreated AND vCounter > 0 DO
+        DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET vCreated = FALSE;
+        SET vToken = SHA1(RAND());
+        INSERT INTO auth_tokens(Token, AccountID, Mask) VALUES (vToken, pAccountID, pRightMask);
+
+        SET vCreated = TRUE;
+        SET vCounter = vCounter - 1;
+    END WHILE;
+
+    RETURN IF(vCreated, vToken, '');
+END//
+
 DELIMITER ;
diff --git a/db/stored/procedures.sql b/db/stored/procedures.sql
index 0cf44e2ed..ede44a093 100644
--- a/db/stored/procedures.sql
+++ b/db/stored/procedures.sql
@@ -1459,4 +1459,24 @@ BEGIN
             requests.Description != '';
 END//
 
+# -------------------------------------------------------------------------------------------
+# Label: authorization
+# -------------------------------------------------------------------------------------------
+
+-- pAccountID == 0 - get tokens of all users
+DROP PROCEDURE IF EXISTS GetAuthTokens//
+CREATE PROCEDURE GetAuthTokens(
+    IN pAccountID int(11))
+BEGIN
+    -- accountID, Created, Accessed, Mask
+    IF pAccountID = 0 THEN
+        SELECT auth_tokens.*
+            FROM auth_tokens;
+    ELSE
+        SELECT auth_tokens.*
+            FROM auth_tokens
+            WHERE auth_tokens.AccountID = pAccountID;
+    END IF;
+END//
+
 DELIMITER ;
diff --git a/db/structure/base.sql b/db/structure/base.sql
index a6bf2b6bc..45e998558 100644
--- a/db/structure/base.sql
+++ b/db/structure/base.sql
@@ -402,7 +402,6 @@ CREATE TABLE IF NOT EXISTS `teachers` (
 -- Структура таблицы `user_roles`
 --
 
-# TODO: just name
 CREATE TABLE IF NOT EXISTS `user_roles` (
   `ID` int(11) NOT NULL AUTO_INCREMENT,
   `Type` enum('student','teacher') NOT NULL,
@@ -469,3 +468,20 @@ CREATE TABLE IF NOT EXISTS `exam_period_options` (
   KEY `StudentID_2` (`StudentID`),
   KEY `StudentID_3` (`StudentID`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+
+
+-- --------------------------------------------------------
+
+--
+-- Структура таблицы `auth_tokens`
+--
+
+CREATE TABLE IF NOT EXISTS `auth_tokens` (
+  `Token` char(40) charset ascii NOT NULL,
+  `AccountID` int(11) NOT NULL,
+  `Created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `Accessed` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `Mask` int(11) NOT NULL DEFAULT 0,
+  UNIQUE KEY `Token` (`Token`),
+  KEY `AccountID` (`AccountID`)
+);
diff --git a/~dev_rating/application/classes/FileParser.php b/~dev_rating/application/classes/FileParser.php
index 7f7c1d910..e881ca733 100644
--- a/~dev_rating/application/classes/FileParser.php
+++ b/~dev_rating/application/classes/FileParser.php
@@ -35,7 +35,7 @@ class FileParser
 
             $degree = self::getDegreeType($degree);
 
-            $attempt = Model_Account::createStudentEx(
+            $attempt = Model_Student::createStudentEx(
                 $lastName, $firstName, $secondName, $gradeNum, $groupNum, $degree, $spec, $facultyID
             );
 
diff --git a/~dev_rating/application/classes/Model/Account.php b/~dev_rating/application/classes/Model/Account.php
index aba9b2938..22bd45375 100644
--- a/~dev_rating/application/classes/Model/Account.php
+++ b/~dev_rating/application/classes/Model/Account.php
@@ -14,43 +14,6 @@ class Model_Account extends Model
         return $result;
     }
 
-    public static function checkAuth($login, $password) {
-        $sql = 'SELECT `SignIn`(:login, :pass) AS `ID`';
-        return DB::query(Database::SELECT, $sql)
-            ->param(':login', $login)
-            ->param(':pass', $password)
-            ->execute()->get('ID');
-    }
-
-
-    public static function generateActivationCode() {
-        $activationCode = Text::random('ABDCEFGHJKLMNPQRSTUVWXYZ123456789', 10);
-        return UTF8::strtoupper($activationCode);
-    }
-
-    public static function createStudentEx($lastName, $firstName, $secondName, $gradeNum, $groupNum, $degree, $specialization, $facultyID, $semesterID = null) {
-        $code = self::generateActivationCode();
-
-        if (!$semesterID) $semesterID = Model_System::$SemesterID;
-
-        $sql = 'SELECT `CreateStudentEx`(:last, :first, :second, :grade, :group, :degree, :spec, :faculty, :code, :semester) AS `UserID`;';
-        $response = DB::query(Database::SELECT, $sql)
-            ->parameters([
-                ':last'     => trim($lastName),
-                ':first'    => trim($firstName),
-                ':second'   => trim($secondName),
-                ':grade'    => $gradeNum,
-                ':group'    => $groupNum,
-                ':degree'   => $degree,
-                ':spec'     => $specialization,
-                ':faculty'  => $facultyID,
-                ':code'     => $code,
-                ':semester' => $semesterID
-            ])->execute()->get('UserID');
-
-        return $response == -1 ? -1 : $code;
-    }
-
 
     public static function with($id, $semesterID = null) {
         if ($id <= 0) throw new LogicException(Error::ID_IS_INCORRECT);
@@ -107,6 +70,16 @@ class Model_Account extends Model
             ->execute()->get('Num');
     }
 
+
+    // =====================================
+    //      sign up
+    // =====================================
+
+    public static function generateActivationCode() {
+        $activationCode = Text::random('ABDCEFGHJKLMNPQRSTUVWXYZ123456789', 10);
+        return UTF8::strtoupper($activationCode);
+    }
+
     public static function isActivationCodeValid($code) {
         $sql = "SELECT `CheckAccountExistence`(:acode, 'code') AS Num";
         $res = DB::query(Database::SELECT, $sql)
@@ -114,6 +87,41 @@ class Model_Account extends Model
             ->execute()->get('Num');
         return $res == 1;
     }
+    
+    public static function activateAccount($login, $password, $email, $code) {
+        $sql = 'SELECT `ActivateAccount`(:code, :login, :email, :pass) AS `Num`';
+        return DB::query(Database::SELECT, $sql)
+            ->parameters([
+                ':code'  => $code,
+                ':login' => $login,
+                ':email' => $email,
+                ':pass'  => $password,
+            ])->execute()->get('Num');
+    }
+
+    // =====================================
+    //      authentication
+    // =====================================
+
+    public static function checkAuth($login, $password) {
+        $sql = 'SELECT `SignIn`(:login, :pass) AS `ID`';
+        return DB::query(Database::SELECT, $sql)
+            ->param(':login', $login)
+            ->param(':pass', $password)
+            ->execute()->get('ID');
+    }
+
+    public static function checkAuthToken($token) {
+        $sql = 'SELECT `SignInByToken`(:token) AS `ID`';
+        return DB::query(Database::SELECT, $sql)
+            ->param(':token', $token)
+            ->execute()->get('ID');
+    }
+
+
+    // =====================================
+    //      password recovery
+    // =====================================
 
     public static function createRecoveryToken($email, $token) {
         $sql = 'SELECT `CreateRecoveryToken`(:email, :token) AS UserName';
@@ -143,19 +151,40 @@ class Model_Account extends Model
             ->execute()->get('Num');
     }
 
-    public static function activateAccount($login, $password, $email, $code) {
-        $sql = 'SELECT `ActivateAccount`(:code, :login, :email, :pass) AS `Num`';
+    // =====================================
+    //      auth tokens management
+    // =====================================
+
+    public static function deleteAuthToken($token) {
+        $sql = 'SELECT `deleteAuthToken`(:token) AS `res`';
         return DB::query(Database::SELECT, $sql)
-            ->parameters([
-                ':code'  => $code,
-                ':login' => $login,
-                ':email' => $email,
-                ':pass'  => $password,
-            ])->execute()->get('Num');
+            ->param(':token', $token)
+            ->execute()->get('res');
     }
 
-    public function getActivationCode() {
+    /**
+     * @param int $accountID
+     * @param int $mask bit mask with access rights
+     * @return string token, if it was created. Empty string otherwise
+     */
+    public static function createAuthToken($accountID, $mask = 0) {
+        $sql = 'SELECT `CreateAuthToken`(:user, :mask) AS `token`';
+        return DB::query(Database::SELECT, $sql)
+            ->parameters([
+                ':user' => (int) $accountID,
+                ':mask' => (int) $mask,
+            ])->execute()->get('token');
+    }
 
+    /**
+     * @param int $accountID - 0 to get auth tokens of all users
+     * @return mixed
+     */
+    public static function getAuthTokens($accountID = 0) {
+        $sql = 'CALL `GetAuthTokens`(:user)`';
+        return DB::query(Database::SELECT, $sql)
+            ->param(':user', (int) $accountID)
+            ->execute();
     }
 
 }
diff --git a/~dev_rating/application/classes/Model/Student.php b/~dev_rating/application/classes/Model/Student.php
index 2d41e7e23..dcc1fa262 100644
--- a/~dev_rating/application/classes/Model/Student.php
+++ b/~dev_rating/application/classes/Model/Student.php
@@ -51,6 +51,29 @@ class Model_Student extends Model_Container
             throw new InvalidArgumentException(Error::INVALID_PARAMETERS);
     }
 
+    public static function createStudentEx($lastName, $firstName, $secondName, $gradeNum, $groupNum, $degree, $specialization, $facultyID, $semesterID = null) {
+        $code = Model_Account::generateActivationCode();
+
+        if (!$semesterID) $semesterID = Model_System::$SemesterID;
+
+        $sql = 'SELECT `CreateStudentEx`(:last, :first, :second, :grade, :group, :degree, :spec, :faculty, :code, :semester) AS `UserID`;';
+        $response = DB::query(Database::SELECT, $sql)
+            ->parameters([
+                ':last'     => trim($lastName),
+                ':first'    => trim($firstName),
+                ':second'   => trim($secondName),
+                ':grade'    => $gradeNum,
+                ':group'    => $groupNum,
+                ':degree'   => $degree,
+                ':spec'     => $specialization,
+                ':faculty'  => $facultyID,
+                ':code'     => $code,
+                ':semester' => $semesterID
+            ])->execute()->get('UserID');
+
+        return $response == -1 ? -1 : $code;
+    }
+    
     /** @return Model_Discipline[] */
     public function getDisciplines($semesterID = null) {
         $semesterID = $semesterID ?: User::instance()->SemesterID;
-- 
GitLab