Merge branch 'master' of https://git.hofmannnet.myhome-server.de/Kilian/PHP-Course
This commit is contained in:
commit
7d92daeccd
@ -33,7 +33,7 @@ class Login extends Api
|
|||||||
case "NotFound":
|
case "NotFound":
|
||||||
throw ApiError::notFound("user");
|
throw ApiError::notFound("user");
|
||||||
case "Invalid":
|
case "Invalid":
|
||||||
throw ApiError::unauthorized("Invalid username or password");
|
throw ApiError::notAllowed("Invalid username or password");
|
||||||
default:
|
default:
|
||||||
throw $err;
|
throw $err;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class Posts extends Api
|
|||||||
$post = Post::getByID($id);
|
$post = Post::getByID($id);
|
||||||
|
|
||||||
// Throw 400 if we aren't admin but trying to edit another users post.
|
// Throw 400 if we aren't admin but trying to edit another users post.
|
||||||
if (!$self->getIsAdmin() && $post->getUser()->getID() !== $self->getID()) throw ApiError::unauthorized("Not allowed");
|
if (!$self->getIsAdmin() && $post->getUser()->getID() !== $self->getID()) throw ApiError::notAllowed("Not allowed");
|
||||||
|
|
||||||
// Try update.
|
// Try update.
|
||||||
Response::json($post->update($content));
|
Response::json($post->update($content));
|
||||||
|
|||||||
38
exam/api/Refresh/Refresh.php
Normal file
38
exam/api/Refresh/Refresh.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Api\Refresh;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Khofmann\Api\Api;
|
||||||
|
use Khofmann\ApiError\ApiError;
|
||||||
|
use Khofmann\Input\Input;
|
||||||
|
use Khofmann\Response\Response;
|
||||||
|
use Khofmann\Models\User\User;
|
||||||
|
use Khofmann\Request\Request;
|
||||||
|
|
||||||
|
class Refresh extends Api
|
||||||
|
{
|
||||||
|
public function post(): void
|
||||||
|
{
|
||||||
|
// Fetch all required inputs.
|
||||||
|
// Throw 400 error if a required one is missing.
|
||||||
|
$token = Request::token();
|
||||||
|
$refreshToken = Input::post("refreshToken");
|
||||||
|
if (empty($refreshToken)) throw ApiError::missingField(["refreshToken"]);
|
||||||
|
|
||||||
|
// Try and log in user.
|
||||||
|
// Throw errors according to situation.
|
||||||
|
try {
|
||||||
|
Response::json(User::refresh($token, $refreshToken));
|
||||||
|
} catch (Exception $err) {
|
||||||
|
switch ($err->getMessage()) {
|
||||||
|
case "Failed":
|
||||||
|
throw ApiError::failed("Refresh failed");
|
||||||
|
case "NotFound":
|
||||||
|
throw ApiError::unauthorized("Not authorized");
|
||||||
|
default:
|
||||||
|
throw $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -310,6 +310,45 @@ paths:
|
|||||||
value: { "code": "NotFound", "entity": "post" }
|
value: { "code": "NotFound", "entity": "post" }
|
||||||
tags:
|
tags:
|
||||||
- Post
|
- Post
|
||||||
|
/refresh:
|
||||||
|
post:
|
||||||
|
summary: Refresh
|
||||||
|
description: Token refresh.
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RefreshRequest"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/LoginResponse"
|
||||||
|
400:
|
||||||
|
description: Missing fields.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MissingFieldResponse"
|
||||||
|
examples:
|
||||||
|
Missing fields:
|
||||||
|
value: { "code": "MissingField", "fields": ["refreshToken"] }
|
||||||
|
500:
|
||||||
|
description: Failed.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/FailedResponse"
|
||||||
|
examples:
|
||||||
|
Failed:
|
||||||
|
value: { "code": "Failed", "message": "Refresh failed" }
|
||||||
|
tags:
|
||||||
|
- Refresh
|
||||||
/register:
|
/register:
|
||||||
post:
|
post:
|
||||||
summary: Register
|
summary: Register
|
||||||
@ -678,8 +717,7 @@ components:
|
|||||||
UnauthorizedResponse:
|
UnauthorizedResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
code:
|
code: type:NotAllowed
|
||||||
type: Unauthorized
|
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
FailedResponse:
|
FailedResponse:
|
||||||
@ -731,6 +769,8 @@ components:
|
|||||||
$ref: "#/components/schemas/UserResponse"
|
$ref: "#/components/schemas/UserResponse"
|
||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
|
refreshToken:
|
||||||
|
type: string
|
||||||
UserResponse:
|
UserResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -855,6 +895,14 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
content:
|
content:
|
||||||
type: string
|
type: string
|
||||||
|
RefreshRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- refreshToken
|
||||||
|
properties:
|
||||||
|
refreshToken:
|
||||||
|
type: string
|
||||||
|
format: uuid4
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
BasicAuth:
|
BasicAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
@ -864,4 +912,5 @@ tags:
|
|||||||
- name: Login/Logout
|
- name: Login/Logout
|
||||||
- name: Post
|
- name: Post
|
||||||
- name: Register
|
- name: Register
|
||||||
|
- name: Refresh
|
||||||
- name: User
|
- name: User
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -27,7 +27,7 @@ class ApiError extends Exception
|
|||||||
]), 400);
|
]), 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function unauthorized(string $message)
|
public static function notAllowed(string $message)
|
||||||
{
|
{
|
||||||
return new ApiError(json_encode([
|
return new ApiError(json_encode([
|
||||||
"code" => "NotAllowed",
|
"code" => "NotAllowed",
|
||||||
@ -35,6 +35,14 @@ class ApiError extends Exception
|
|||||||
]), 401);
|
]), 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function unauthorized(string $message)
|
||||||
|
{
|
||||||
|
return new ApiError(json_encode([
|
||||||
|
"code" => "Unauthorized",
|
||||||
|
"message" => $message,
|
||||||
|
]), 401);
|
||||||
|
}
|
||||||
|
|
||||||
public static function notFound(string $entity)
|
public static function notFound(string $entity)
|
||||||
{
|
{
|
||||||
return new ApiError(json_encode([
|
return new ApiError(json_encode([
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class AdminAuth implements IMiddleware
|
|||||||
->header("Access-Control-Allow-Methods: *")
|
->header("Access-Control-Allow-Methods: *")
|
||||||
->header("Access-Control-Allow-Headers: *")
|
->header("Access-Control-Allow-Headers: *")
|
||||||
->httpCode(401)
|
->httpCode(401)
|
||||||
->json(["code" => "Unauthorized", "message" => "Not Authorized"]);
|
->json(["code" => "NotAllowed", "message" => "Not Authorized"]);
|
||||||
}
|
}
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
// No user with this token exists
|
// No user with this token exists
|
||||||
|
|||||||
@ -61,4 +61,14 @@ class Config
|
|||||||
{
|
{
|
||||||
return Config::getInstance()->database;
|
return Config::getInstance()->database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getTokenExpiry(): string
|
||||||
|
{
|
||||||
|
return Config::getInstance()->app["tokenExpiry"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRefreshTokenExpiry(): string
|
||||||
|
{
|
||||||
|
return Config::getInstance()->app["refreshTokenExpiry"];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,8 @@ class User implements JsonSerializable
|
|||||||
FROM
|
FROM
|
||||||
egb_benutzer AS b
|
egb_benutzer AS b
|
||||||
WHERE
|
WHERE
|
||||||
token = :TOKEN"
|
token = :TOKEN AND
|
||||||
|
tokenExpiry > NOW()"
|
||||||
);
|
);
|
||||||
$stmt->bindValue(":TOKEN", $token);
|
$stmt->bindValue(":TOKEN", $token);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
@ -154,18 +155,29 @@ class User implements JsonSerializable
|
|||||||
$stmt->bindValue(":ID", $user->getID());
|
$stmt->bindValue(":ID", $user->getID());
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
// Generate token
|
// Generate tokens only if expired or missing
|
||||||
$stmt = $db->prepare("UPDATE egb_benutzer SET token = UUID() WHERE id = :ID");
|
if (empty($data["token"]) || new DateTime($data["tokenExpiry"]) <= new DateTime()) {
|
||||||
$stmt->bindValue(":ID", $user->getID());
|
$stmt = $db->prepare(
|
||||||
$stmt->execute();
|
"UPDATE
|
||||||
|
egb_benutzer
|
||||||
|
SET
|
||||||
|
token = UUID(),
|
||||||
|
tokenExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getTokenExpiry() . "),
|
||||||
|
refreshToken = UUID(),
|
||||||
|
refreshExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getRefreshTokenExpiry() . ")
|
||||||
|
WHERE id = :ID"
|
||||||
|
);
|
||||||
|
$stmt->bindValue(":ID", $user->getID());
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
// Get token
|
// Get token
|
||||||
$stmt = $db->prepare("SELECT token FROM egb_benutzer WHERE id = :ID");
|
$stmt = $db->prepare("SELECT token, refreshToken FROM egb_benutzer WHERE id = :ID");
|
||||||
$stmt->bindValue(":ID", $user->getID());
|
$stmt->bindValue(":ID", $user->getID());
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$token = $stmt->fetch(PDO::FETCH_COLUMN, 0);
|
[$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
|
||||||
// Return user and token
|
// Return user and tokens
|
||||||
if ($token) {
|
if ($token) {
|
||||||
return ["user" => $user, "token" => $token];
|
return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
|
||||||
}
|
}
|
||||||
// Token generation failed
|
// Token generation failed
|
||||||
throw new Exception("Failed");
|
throw new Exception("Failed");
|
||||||
@ -261,6 +273,58 @@ class User implements JsonSerializable
|
|||||||
return ["pages" => intdiv($count, $limit + 1), "data" => $list];
|
return ["pages" => intdiv($count, $limit + 1), "data" => $list];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function refresh(string $token, string $refreshToken)
|
||||||
|
{
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT
|
||||||
|
b.id, b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel, b.tokenExpiry,
|
||||||
|
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
|
||||||
|
FROM
|
||||||
|
egb_benutzer AS b
|
||||||
|
WHERE
|
||||||
|
token = :TOKEN AND
|
||||||
|
refreshToken = :REFRESH AND
|
||||||
|
refreshExpiry > NOW()"
|
||||||
|
);
|
||||||
|
$stmt->bindValue(":TOKEN", $token);
|
||||||
|
$stmt->bindValue(":REFRESH", $refreshToken);
|
||||||
|
$stmt->execute();
|
||||||
|
$data = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$data) throw new Exception("NotFound");
|
||||||
|
|
||||||
|
$user = new User($data["id"], $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
|
||||||
|
|
||||||
|
// Update tokens if expired
|
||||||
|
if (new DateTime($data["tokenExpiry"]) <= new DateTime()) {
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE
|
||||||
|
egb_benutzer
|
||||||
|
SET
|
||||||
|
token = UUID(),
|
||||||
|
tokenExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getTokenExpiry() . "),
|
||||||
|
refreshToken = UUID(),
|
||||||
|
refreshExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getRefreshTokenExpiry() . ")
|
||||||
|
WHERE id = :ID"
|
||||||
|
);
|
||||||
|
$stmt->bindValue(":ID", $user->getID());
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token
|
||||||
|
$stmt = $db->prepare("SELECT token, refreshToken FROM egb_benutzer WHERE id = :ID");
|
||||||
|
$stmt->bindValue(":ID", $user->getID());
|
||||||
|
$stmt->execute();
|
||||||
|
[$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
|
||||||
|
// Return user and tokens
|
||||||
|
if ($token) {
|
||||||
|
return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
|
||||||
|
}
|
||||||
|
// Token generation failed
|
||||||
|
throw new Exception("Failed");
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Members
|
* Members
|
||||||
*/
|
*/
|
||||||
@ -268,7 +332,16 @@ class User implements JsonSerializable
|
|||||||
public function logOut(): bool
|
public function logOut(): bool
|
||||||
{
|
{
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
$stmt = $db->prepare("UPDATE egb_benutzer SET token = NULL WHERE id = :ID");
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE
|
||||||
|
egb_benutzer
|
||||||
|
SET
|
||||||
|
token = NULL,
|
||||||
|
tokenExpiry = NULL,
|
||||||
|
refreshToken = NULL,
|
||||||
|
refreshExpiry = NULL
|
||||||
|
WHERE id = :ID"
|
||||||
|
);
|
||||||
$stmt->bindValue(":ID", $this->id);
|
$stmt->bindValue(":ID", $this->id);
|
||||||
return $stmt->execute();
|
return $stmt->execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,5 +4,7 @@ return [
|
|||||||
"basePath" => "/phpCourse/exam/",
|
"basePath" => "/phpCourse/exam/",
|
||||||
"storagePath" => "/phpCourse/exam/storage/",
|
"storagePath" => "/phpCourse/exam/storage/",
|
||||||
"baseFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/",
|
"baseFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/",
|
||||||
"storageFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/storage/"
|
"storageFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/storage/",
|
||||||
|
"tokenExpiry" => "5 MINUTE",
|
||||||
|
"refreshTokenExpiry" => "30 MINUTE"
|
||||||
];
|
];
|
||||||
|
|||||||
@ -35,6 +35,8 @@ SimpleRouter::post("/login", [Api\Login\Login::class, "post"]);
|
|||||||
// Register and confirm
|
// Register and confirm
|
||||||
SimpleRouter::post("/register", [Api\Register\Register::class, "post"]);
|
SimpleRouter::post("/register", [Api\Register\Register::class, "post"]);
|
||||||
SimpleRouter::patch("/register", [Api\Register\Register::class, "patch"]);
|
SimpleRouter::patch("/register", [Api\Register\Register::class, "patch"]);
|
||||||
|
// Refresh (open, but still needs token)
|
||||||
|
SimpleRouter::post("/refresh", [Api\Refresh\Refresh::class, "post"]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Optional Auth
|
* Optional Auth
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user