token support, api fixes and security, dark mode
This commit is contained in:
@ -16,11 +16,19 @@ use TheTempusProject\Houdini\Classes\Template;
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
use TheTempusProject\Hermes\Functions\Redirect;
|
||||
use TheTempusProject\Bedrock\Functions\Session;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
use TheTempusProject\Models\Token;
|
||||
|
||||
class ApiController extends Controller {
|
||||
public function __construct() {
|
||||
protected static $canAccessApplicationApi = false;
|
||||
protected static $canAccessUserApi = false;
|
||||
protected static $canAccessAuthenticationApi = false;
|
||||
protected static $authToken;
|
||||
|
||||
public function __construct( $secure = true ) {
|
||||
parent::__construct();
|
||||
if ( ! App::verifyApiRequest() ) {
|
||||
$this->verifyApiRequest();
|
||||
if ( $secure && ! $this->canUseApi() ) {
|
||||
Session::flash( 'error', 'You do not have permission to view this page.' );
|
||||
return Redirect::home();
|
||||
}
|
||||
@ -29,4 +37,100 @@ class ApiController extends Controller {
|
||||
Template::addHeader( 'Content-Type: application/json; charset=utf-8' );
|
||||
Template::setTemplate( 'api' );
|
||||
}
|
||||
|
||||
protected function canUseApi() {
|
||||
return ( $this->canUseUserApi() || $this->canUseAppApi() || $this->canUseAuthApi() );
|
||||
}
|
||||
|
||||
protected function canUseUserApi() {
|
||||
$apiEnabled = Config::getValue( 'api/apiAccessApp' );
|
||||
if ( empty( $apiEnabled ) ) {
|
||||
return false;
|
||||
}
|
||||
return self::$canAccessUserApi;
|
||||
}
|
||||
|
||||
protected function canUseAppApi() {
|
||||
$apiEnabled = Config::getValue( 'api/apiAccessPersonal' );
|
||||
if ( empty( $apiEnabled ) ) {
|
||||
return false;
|
||||
}
|
||||
return self::$canAccessApplicationApi;
|
||||
}
|
||||
|
||||
protected function canUseAuthApi() {
|
||||
return self::$canAccessAuthenticationApi;
|
||||
}
|
||||
|
||||
public function verifyApiRequest() {
|
||||
$tokens = new Token;
|
||||
$secret = null;
|
||||
|
||||
$bearer_token = $this->getBearerToken();
|
||||
if ( ! empty( $bearer_token ) ) {
|
||||
$token = $tokens->findByToken( $bearer_token );
|
||||
} else {
|
||||
$secret = $this->getSecretToken();
|
||||
if ( empty( $secret ) ) {
|
||||
return;
|
||||
}
|
||||
$token = $tokens->findBySecret( $secret );
|
||||
}
|
||||
if ( empty( $token ) ) {
|
||||
return;
|
||||
}
|
||||
self::$authToken = $token;
|
||||
if ( $token->expiresAt <= time() && empty( $secret ) ) {
|
||||
return;
|
||||
}
|
||||
if ( $token->expiresAt <= time() ) {
|
||||
self::$canAccessAuthenticationApi = true;
|
||||
return;
|
||||
}
|
||||
if ( $token->token_type == 'app' ) {
|
||||
self::$canAccessApplicationApi = true;
|
||||
return;
|
||||
}
|
||||
if ( $token->token_type == 'user' ) {
|
||||
self::$canAccessUserApi = true;
|
||||
return;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getSecretToken() {
|
||||
$headers = $this->getAuthorizationHeader();
|
||||
if ( ! empty( $headers ) ) {
|
||||
if ( preg_match( '/Secret\s(\S+)/', $headers, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getBearerToken() {
|
||||
$headers = $this->getAuthorizationHeader();
|
||||
if ( ! empty( $headers ) ) {
|
||||
if ( preg_match( '/Bearer\s(\S+)/', $headers, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAuthorizationHeader(){
|
||||
$headers = null;
|
||||
if ( isset( $_SERVER['Authorization'] ) ) {
|
||||
$headers = trim( $_SERVER["Authorization"] );
|
||||
} elseif ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
|
||||
$headers = trim( $_SERVER["HTTP_AUTHORIZATION"] );
|
||||
} elseif ( function_exists( 'apache_request_headers' ) ) {
|
||||
$requestHeaders = apache_request_headers();
|
||||
$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
|
||||
if ( isset( $requestHeaders['Authorization'] ) ) {
|
||||
$headers = trim( $requestHeaders['Authorization'] );
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,8 @@ class Forms extends Check {
|
||||
self::addHandler( 'newGroup', __CLASS__, 'newGroup' );
|
||||
self::addHandler( 'editGroup', __CLASS__, 'editGroup' );
|
||||
self::addHandler( 'install', __CLASS__, 'install' );
|
||||
self::addHandler( 'adminCreateToken', __CLASS__, 'adminCreateToken' );
|
||||
self::addHandler( 'apiLogin', __CLASS__, 'apiLogin' );
|
||||
self::addHandler( 'installStart', __CLASS__, 'install', [ 'start' ] );
|
||||
self::addHandler( 'installAgreement', __CLASS__, 'install', [ 'agreement' ] );
|
||||
self::addHandler( 'installCheck', __CLASS__, 'install', [ 'check' ] );
|
||||
@ -608,4 +610,40 @@ class Forms extends Check {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function adminCreateToken() {
|
||||
if ( !Input::exists( 'name' ) ) {
|
||||
self::addUserError( 'You must specify a name' );
|
||||
return false;
|
||||
}
|
||||
if ( !Input::exists( 'token_type' ) ) {
|
||||
self::addUserError( 'You must specify a token_type' );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function adminEditToken() {
|
||||
if ( !Input::exists( 'name' ) ) {
|
||||
self::addUserError( 'You must specify a name' );
|
||||
return false;
|
||||
}
|
||||
if ( !Input::exists( 'token_type' ) ) {
|
||||
self::addUserError( 'You must specify a token_type' );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function apiLogin() {
|
||||
if ( !self::checkUsername( Input::post( 'username' ) ) ) {
|
||||
self::addUserError( 'Invalid username.' );
|
||||
return false;
|
||||
}
|
||||
if ( !self::password( Input::post( 'password' ) ) ) {
|
||||
self::addUserError( 'Invalid password.' );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
90
app/controllers/admin/tokens.php
Normal file
90
app/controllers/admin/tokens.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* app/controllers/admin/tokens.php
|
||||
*
|
||||
* This is the admin app/user tokens controller.
|
||||
*
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Admin;
|
||||
|
||||
use TheTempusProject\Classes\Forms as TTPForms;
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Houdini\Classes\Issues;
|
||||
use TheTempusProject\Houdini\Classes\Navigation;
|
||||
use TheTempusProject\Houdini\Classes\Components;
|
||||
use TheTempusProject\Houdini\Classes\Forms;
|
||||
use TheTempusProject\Classes\AdminController;
|
||||
use TheTempusProject\Models\Token;
|
||||
use TheTempusProject\Bedrock\Functions\Input;
|
||||
use TheTempusProject\Bedrock\Functions\Check;
|
||||
use TheTempusProject\Hermes\Functions\Redirect;
|
||||
use TheTempusProject\Bedrock\Functions\Session;
|
||||
|
||||
class Tokens extends AdminController {
|
||||
public static $token;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
self::$title = 'Admin - Tokens';
|
||||
self::$token = new Token;
|
||||
$view = Navigation::activePageSelect( 'nav.admin', '/admin/tokens' );
|
||||
Components::set( 'ADMINNAV', $view );
|
||||
}
|
||||
|
||||
public function create() {
|
||||
if ( Input::exists( 'submit' ) ) {
|
||||
if ( !TTPForms::check( 'adminCreateToken' ) ) {
|
||||
Issues::add( 'error', [ 'There was an error with your token.' => Check::userErrors() ] );
|
||||
}
|
||||
if ( self::$token->create(
|
||||
Input::post( 'name' ),
|
||||
Input::post( 'notes' ),
|
||||
Input::post( 'token_type' )
|
||||
) ) {
|
||||
Session::flash( 'success', 'Token Created' );
|
||||
Redirect::to( 'admin/tokens' );
|
||||
}
|
||||
}
|
||||
Views::view( 'admin.tokens.create' );
|
||||
}
|
||||
|
||||
public function delete( $id = null ) {
|
||||
if ( self::$token->delete( [ $id ] ) ) {
|
||||
Session::flash( 'success', 'Token deleted.' );
|
||||
}
|
||||
Redirect::to( 'admin/tokens' );
|
||||
}
|
||||
|
||||
public function edit( $id = null ) {
|
||||
$token = self::$token->findById( $id );
|
||||
if ( Input::exists( 'submit' ) ) {
|
||||
if ( !TTPForms::check( 'adminEditToken' ) ) {
|
||||
Issues::add( 'error', [ 'There was an error with your token.' => Check::userErrors() ] );
|
||||
} else {
|
||||
if ( self::$token->update(
|
||||
$id,
|
||||
Input::post( 'name' ),
|
||||
Input::post( 'notes' ),
|
||||
Input::post( 'token_type' )
|
||||
) ) {
|
||||
Session::flash( 'success', 'Token Updated' );
|
||||
Redirect::to( 'admin/tokens' );
|
||||
}
|
||||
}
|
||||
}
|
||||
Forms::selectOption( $token->token_type );
|
||||
return Views::view( 'admin.tokens.edit', $token );
|
||||
}
|
||||
|
||||
public function index() {
|
||||
return Views::view( 'admin.tokens.list', self::$token->listPaginated() );
|
||||
}
|
||||
|
||||
public function view( $id = null ) {
|
||||
return Views::view( 'admin.tokens.view', self::$token->findById( $id ) );
|
||||
}
|
||||
}
|
38
app/controllers/api/auth.php
Normal file
38
app/controllers/api/auth.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* app/controllers/api/auth.php
|
||||
*
|
||||
* This is the api authentication controller.
|
||||
*
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Api;
|
||||
|
||||
use TheTempusProject\Models\User;
|
||||
use TheTempusProject\Classes\ApiController;
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Models\Token;
|
||||
|
||||
class Auth extends ApiController {
|
||||
public static $tokens;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
self::$tokens = new Token;
|
||||
}
|
||||
|
||||
public function refresh() {
|
||||
$token = self::$tokens->refresh( self::$authToken->ID );
|
||||
if ( empty( $token ) ) {
|
||||
$responseType = 'error';
|
||||
$response = 'IRDK';
|
||||
} else {
|
||||
$responseType = 'token';
|
||||
$response = $token;
|
||||
}
|
||||
Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]);
|
||||
}
|
||||
}
|
51
app/controllers/api/login.php
Normal file
51
app/controllers/api/login.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* app/controllers/api/auth.php
|
||||
*
|
||||
* This is the api authentication controller.
|
||||
*
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Api;
|
||||
|
||||
use TheTempusProject\Classes\ApiController;
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Models\Token;
|
||||
use TheTempusProject\Models\User;
|
||||
use TheTempusProject\Houdini\Classes\Template;
|
||||
use TheTempusProject\Classes\Forms;
|
||||
use TheTempusProject\Bedrock\Functions\Input;
|
||||
|
||||
class Login extends ApiController {
|
||||
public static $tokens;
|
||||
public static $user;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( false );
|
||||
self::$tokens = new Token;
|
||||
self::$user = new User;
|
||||
// Template::addHeader( 'Access-Control-Allow-Origin: *' );
|
||||
// Template::addHeader( 'Content-Type: application/json; charset=utf-8' );
|
||||
}
|
||||
|
||||
public function index() {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
if ( !Forms::check( 'apiLogin' ) ) {
|
||||
$responseType = 'error';
|
||||
$response = 'malformed input1';
|
||||
return Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]);
|
||||
}
|
||||
$user = self::$user->authorize( Input::post( 'username' ), Input::post( 'password' ) );
|
||||
if ( ! $user ) {
|
||||
$responseType = 'error';
|
||||
$response = 'bad credentials';
|
||||
return Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]);
|
||||
}
|
||||
$responseType = 'token';
|
||||
$token = self::$tokens->findOrCreateUserToken( $user->ID );
|
||||
return Views::view( 'api.response', ['response' => json_encode( [ $responseType => $token ], true )]);
|
||||
}
|
||||
}
|
@ -98,4 +98,22 @@ class Home extends Controller {
|
||||
// this should look up comments and blog posts with the hashtag in them
|
||||
Views::view( 'hashtags' );
|
||||
}
|
||||
|
||||
public function about() {
|
||||
self::$title = 'About - {SITENAME}';
|
||||
self::$pageDescription = '{SITENAME} Terms and Conditions of use. Please use {SITENAME} safely.';
|
||||
Views::view( 'switches' );
|
||||
}
|
||||
|
||||
public function contact() {
|
||||
self::$title = 'contact - {SITENAME}';
|
||||
self::$pageDescription = '{SITENAME} Terms and Conditions of use. Please use {SITENAME} safely.';
|
||||
Views::view( 'contact' );
|
||||
}
|
||||
|
||||
public function privacy() {
|
||||
self::$title = 'privacy - {SITENAME}';
|
||||
self::$pageDescription = '{SITENAME} Terms and Conditions of use. Please use {SITENAME} safely.';
|
||||
Views::view( 'privacy' );
|
||||
}
|
||||
}
|
||||
|
51
app/css/main-dark.css
Normal file
51
app/css/main-dark.css
Normal file
@ -0,0 +1,51 @@
|
||||
/* General body background and text color */
|
||||
body {
|
||||
background-color: #121212; /* Dark background */
|
||||
color: #e0e0e0; /* Light text */
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.navbar {
|
||||
background-color: #1f1f1f;
|
||||
border-color: #333;
|
||||
}
|
||||
.navbar a {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.navbar a:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.panel {
|
||||
background-color: #1f1f1f;
|
||||
border-color: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.panel-heading {
|
||||
background-color: #333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
background-color: #333;
|
||||
color: #ffffff;
|
||||
border-color: #444;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #444;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-control {
|
||||
background-color: #1f1f1f;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #555;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -666,4 +666,51 @@ ul.alert-dropdown {
|
||||
|
||||
.pagination {
|
||||
padding-left: 75px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.material-switch > input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.material-switch > label {
|
||||
cursor: pointer;
|
||||
height: 0px;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.material-switch > label::before {
|
||||
background: rgb(0, 0, 0);
|
||||
box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
content: '';
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
position:absolute;
|
||||
opacity: 0.3;
|
||||
transition: all 0.4s ease-in-out;
|
||||
width: 40px;
|
||||
}
|
||||
.material-switch > label::after {
|
||||
background: rgb(255, 255, 255);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
content: '';
|
||||
height: 24px;
|
||||
left: -4px;
|
||||
margin-top: -8px;
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
width: 24px;
|
||||
}
|
||||
.material-switch > input[type="checkbox"]:checked + label::before {
|
||||
background: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.material-switch > input[type="checkbox"]:checked + label::after {
|
||||
background: inherit;
|
||||
left: 20px;
|
||||
}
|
@ -88,4 +88,18 @@ function iv( $variable ) {
|
||||
echo '<pre>';
|
||||
echo var_export( $variable, true );
|
||||
echo '</pre>';
|
||||
}
|
||||
|
||||
function generateToken(): string {
|
||||
return bin2hex(random_bytes(32)); // Generates a 64-character hexadecimal token
|
||||
}
|
||||
|
||||
function generateRandomString( $length = 10 ) {
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$charactersLength = strlen( $characters );
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
return $randomString;
|
||||
}
|
@ -108,4 +108,24 @@ window.onload = function () {
|
||||
|
||||
// Update padding on window resize
|
||||
window.addEventListener('resize', updateFooterPadding);
|
||||
};
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const toggleButton = document.getElementById('dark-mode-toggle');
|
||||
const darkModeStylesheet = document.getElementById('dark-mode-stylesheet');
|
||||
|
||||
// Check if dark mode is saved in localStorage
|
||||
if (localStorage.getItem('darkMode') === 'enabled') {
|
||||
darkModeStylesheet.disabled = false;
|
||||
}
|
||||
|
||||
toggleButton.addEventListener('click', function () {
|
||||
if (darkModeStylesheet.disabled) {
|
||||
darkModeStylesheet.disabled = false;
|
||||
localStorage.setItem('darkMode', 'enabled');
|
||||
} else {
|
||||
darkModeStylesheet.disabled = true;
|
||||
localStorage.setItem('darkMode', 'disabled');
|
||||
}
|
||||
});
|
||||
});
|
@ -145,22 +145,6 @@ class Sessions extends DatabaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkToken( $apiToken, $create = false ) {
|
||||
$user = new User;
|
||||
if ( $apiToken === false ) {
|
||||
return false;
|
||||
}
|
||||
$result = $user->findByToken( $apiToken );
|
||||
if ( $result === false ) {
|
||||
Debug::info( 'sessions->checkToken - could not find user by token.' );
|
||||
return false;
|
||||
}
|
||||
if ( $create ) {
|
||||
return $this->newSession( null, false, false, $result->ID );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new session from the data provided. The
|
||||
* expiration time is optional and will be set to the
|
||||
|
198
app/models/token.php
Normal file
198
app/models/token.php
Normal file
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
/**
|
||||
* app/models/token.php
|
||||
*
|
||||
* This class is used for the manipulation of the tokens database table.
|
||||
*
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Models;
|
||||
|
||||
use TheTempusProject\Bedrock\Functions\Check;
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
use TheTempusProject\Classes\DatabaseModel;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
|
||||
class Token extends DatabaseModel {
|
||||
public $tableName = 'tokens';
|
||||
public $modelVersion = '1.0';
|
||||
public $configName = 'api';
|
||||
public $databaseMatrix = [
|
||||
[ 'name', 'varchar', '128' ],
|
||||
[ 'token_type', 'varchar', '8' ],
|
||||
[ 'notes', 'text', '' ],
|
||||
[ 'token', 'varchar', '64' ],
|
||||
[ 'secret', 'varchar', '256' ],
|
||||
[ 'createdAt', 'int', '10' ],
|
||||
[ 'createdBy', 'int', '10' ],
|
||||
[ 'expiresAt', 'int', '10' ],
|
||||
];
|
||||
public $permissionMatrix = [
|
||||
'addAppToken' => [
|
||||
'pretty' => 'Add Application Tokens',
|
||||
'default' => false,
|
||||
],
|
||||
'addAppToken' => [
|
||||
'pretty' => 'Add Personal Tokens',
|
||||
'default' => false,
|
||||
],
|
||||
];
|
||||
public $configMatrix = [
|
||||
'apiAccessApp' => [
|
||||
'type' => 'radio',
|
||||
'pretty' => 'Enable Api Access for Personal Tokens.',
|
||||
'default' => true,
|
||||
],
|
||||
'apiAccessPersonal' => [
|
||||
'type' => 'radio',
|
||||
'pretty' => 'Enable Api Access for Personal Tokens.',
|
||||
'default' => true,
|
||||
],
|
||||
'AppAccessTokenExpiration' => [
|
||||
'type' => 'text',
|
||||
'pretty' => 'How long before app tokens expire (in seconds)',
|
||||
'default' => 2592000,
|
||||
],
|
||||
'UserAccessTokenExpiration' => [
|
||||
'type' => 'text',
|
||||
'pretty' => 'How long before user tokens expire (in seconds)',
|
||||
'default' => 604800,
|
||||
],
|
||||
];
|
||||
|
||||
public function create( $name, $note, $token_type = 'app' ) {
|
||||
if ( 'app' == $token_type ) {
|
||||
$expiration = Config::getValue( 'api/AppAccessTokenExpiration' );
|
||||
if ( empty( $expiration ) ) {
|
||||
$expiration = $this->configMatrix['AppAccessTokenExpiration']['default'];
|
||||
}
|
||||
} else {
|
||||
$expiration = Config::getValue( 'api/UserAccessTokenExpiration' );
|
||||
if ( empty( $expiration ) ) {
|
||||
$expiration = $this->configMatrix['UserAccessTokenExpiration']['default'];
|
||||
}
|
||||
}
|
||||
$expireTime = time() + $expiration;
|
||||
|
||||
$fields = [
|
||||
'name' => $name,
|
||||
'notes' => $note,
|
||||
'token_type' => $token_type,
|
||||
'createdBy' => App::$activeUser->ID,
|
||||
'createdAt' => time(),
|
||||
'expiresAt' => $expireTime,
|
||||
'token' => generateToken(),
|
||||
'secret' => generateRandomString(256),
|
||||
];
|
||||
if ( self::$db->insert( $this->tableName, $fields ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function findOrCreateUserToken( $user_id ) {
|
||||
$test = $this->findUserToken( $user_id );
|
||||
if ( ! empty( $test ) ) {
|
||||
return $test->token;
|
||||
}
|
||||
|
||||
$expiration = Config::getValue( 'api/UserAccessTokenExpiration' );
|
||||
if ( empty( $expiration ) ) {
|
||||
$expiration = $this->configMatrix['UserAccessTokenExpiration']['default'];
|
||||
}
|
||||
$expireTime = time() + $expiration;
|
||||
$token = generateToken();
|
||||
$fields = [
|
||||
'name' => 'Browser Token',
|
||||
'notes' => 'findOrCreateUserToken',
|
||||
'token_type' => 'user',
|
||||
'createdBy' => $user_id,
|
||||
'createdAt' => time(),
|
||||
'expiresAt' => $expireTime,
|
||||
'token' => $token,
|
||||
'secret' => generateRandomString(256),
|
||||
];
|
||||
if ( self::$db->insert( $this->tableName, $fields ) ) {
|
||||
return $token;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update( $id, $name, $note, $token_type = 'app' ) {
|
||||
$fields = [
|
||||
'name' => $name,
|
||||
'notes' => $note,
|
||||
'token_type' => $token_type,
|
||||
];
|
||||
if ( self::$db->update( $this->tableName, $id, $fields ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function refresh( $id, $token_type = 'app' ) {
|
||||
if ( 'app' == $token_type ) {
|
||||
$expiration = Config::getValue( 'api/AppAccessTokenExpiration' );
|
||||
if ( empty( $expiration ) ) {
|
||||
$expiration = $this->configMatrix['AppAccessTokenExpiration']['default'];
|
||||
}
|
||||
} else {
|
||||
$expiration = Config::getValue( 'api/UserAccessTokenExpiration' );
|
||||
if ( empty( $expiration ) ) {
|
||||
$expiration = $this->configMatrix['UserAccessTokenExpiration']['default'];
|
||||
}
|
||||
}
|
||||
$expireTime = time() + $expiration;
|
||||
$token = generateToken();
|
||||
|
||||
$fields = [
|
||||
'expiresAt' => $expireTime,
|
||||
'token' => $token,
|
||||
];
|
||||
if ( self::$db->update( $this->tableName, $id, $fields ) ) {
|
||||
return $token;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function findByforwardedUrl( $url ) {
|
||||
if ( !Check::url( $url ) ) {
|
||||
Debug::warn( "Invalid forwarded_url: $url" );
|
||||
return false;
|
||||
}
|
||||
$routeData = self::$db->get( $this->tableName, [ 'forwarded_url', '=', $url ] );
|
||||
if ( !$routeData->count() ) {
|
||||
Debug::warn( "Could not find route by forwarded url: $url" );
|
||||
return false;
|
||||
}
|
||||
return $this->filter( $routeData->first() );
|
||||
}
|
||||
|
||||
public function findByToken( $token ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'token', '=', $token ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function findBySecret( $secret ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'secret', '=', $secret ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function findUserToken( $user_id ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'createdBy', '=', $user_id, 'AND', 'token_type', '=', 'user' ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
}
|
@ -43,7 +43,6 @@ class User extends DatabaseModel {
|
||||
[ 'name', 'varchar', '20' ],
|
||||
[ 'confirmationCode', 'varchar', '80' ],
|
||||
[ 'prefs', 'text', '' ],
|
||||
[ 'auth_token', 'text', '' ],
|
||||
];
|
||||
public $permissionMatrix = [
|
||||
'uploadImages' => [
|
||||
@ -694,33 +693,45 @@ class User extends DatabaseModel {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function findByToken( $token ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'auth_token', '=', $token ] );
|
||||
if ( ! $data->count() ) {
|
||||
public function authorize( $username, $password ) {
|
||||
if ( !isset( self::$log ) ) {
|
||||
self::$log = new Log;
|
||||
}
|
||||
if ( !$this->get( $username ) ) {
|
||||
self::$log->login( 0, "API: User not found: $username" );
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function addAccessToken( $id, $length = 64 ) {
|
||||
if ( ! Check::id( $id ) ) {
|
||||
// login attempts protection.
|
||||
$timeLimit = ( time() - 3600 );
|
||||
$limit = Config::getValue( 'main/loginLimit' );
|
||||
$user = $this->data();
|
||||
if ( $limit > 0 ) {
|
||||
$limitCheck = self::$db->get(
|
||||
'logs',
|
||||
[
|
||||
'source', '=', 'login',
|
||||
'AND',
|
||||
'userID', '=', $user->ID,
|
||||
'AND',
|
||||
'time', '>=', $timeLimit,
|
||||
'AND',
|
||||
'action', '!=', 'pass',
|
||||
]
|
||||
);
|
||||
if ( $limitCheck->count() >= $limit ) {
|
||||
self::$log->login( $user->ID, 'API: Too many failed attempts.' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ( !Check::password( $password ) ) {
|
||||
self::$log->login( $user->ID, 'API: Invalid Password.' );
|
||||
return false;
|
||||
}
|
||||
$fields = [ 'auth_token' => $this->generateRandomString( $length ) ];
|
||||
if ( !self::$db->update( $this->tableName, $id, $fields ) ) {
|
||||
Debug::error( "User: $id not updated." );
|
||||
if ( !Hash::check( $password, $user->password ) ) {
|
||||
self::$log->login( $user->ID, 'API: Wrong Password.' );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function generateRandomString( $length = 10 ) {
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$charactersLength = strlen( $characters );
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
return $randomString;
|
||||
self::$log->login( $this->data()->ID, 'API: pass' );
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
<div class="col-lg-3 text-center">
|
||||
<h3>Subscribe</h3>
|
||||
<ul>
|
||||
@ -12,4 +11,8 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="material-switch" style="margin-top: 25px; margin-bottom: 25px;">
|
||||
<input name="dark-mode-toggle" type="checkbox" id="dark-mode-toggle" style="margin-top: 25px; margin-bottom: 25px; text-align:left;"/>
|
||||
<label for="dark-mode-toggle" class="label-default" style="text-align:left;"></label>
|
||||
</div>
|
||||
</div>
|
@ -38,6 +38,7 @@ class DefaultLoader extends Loader {
|
||||
Components::set( 'FONT_AWESOME_URL', self::FONT_AWESOME_URL );
|
||||
}
|
||||
$this->addCss( '<link rel="stylesheet" href="{ROOT_URL}app/css/main.css">' );
|
||||
$this->addCss( '<link rel="stylesheet" href="{ROOT_URL}app/css/main-dark.css" id="dark-mode-stylesheet" disabled>' );
|
||||
$this->addJs( '<script language="JavaScript" crossorigin="anonymous" type="text/javascript" src="{ROOT_URL}app/js/main.js"></script>' );
|
||||
Components::setIfNull( 'LOGO', Config::getValue( 'main/logo' ) );
|
||||
Components::setIfNull( 'FOOTER_LEFT', Navigation::getMenuView( 'footer.left', 'FOOTER_LINKS', App::FOOTER_MENU_NAME, false ) );
|
||||
|
0
app/views/about.html
Normal file
0
app/views/about.html
Normal file
28
app/views/admin/tokens/create.html
Normal file
28
app/views/admin/tokens/create.html
Normal file
@ -0,0 +1,28 @@
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
<legend>Create Token</legend>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-lg-6 control-label">Name:</label>
|
||||
<div class="col-lg-2">
|
||||
<input class="form-control" type="text" name="name" id="name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes" class="col-lg-3 control-label">Notes</label>
|
||||
<div class="col-lg-6">
|
||||
<textarea class="form-control" name="notes" maxlength="2000" rows="10" cols="50" id="notes"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="token_type" class="col-lg-6 control-label">Token Type:</label>
|
||||
<div class="col-lg-2">
|
||||
<select name="token_type" id="token_type" class="">
|
||||
<option value='app' selected>Application</option>
|
||||
<option value='user'>User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<input type="hidden" name="token" value="{TOKEN}">
|
||||
<button name="submit" value="submit" type="submit" class="btn btn-lg btn-primary center-block">Create</button><br>
|
||||
</form>
|
29
app/views/admin/tokens/edit.html
Normal file
29
app/views/admin/tokens/edit.html
Normal file
@ -0,0 +1,29 @@
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
<legend>Edit Token</legend>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-lg-6 control-label">Name:</label>
|
||||
<div class="col-lg-2">
|
||||
<input class="form-control" type="text" name="name" id="name" value="{name}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes" class="col-lg-3 control-label">Notes</label>
|
||||
<div class="col-lg-6">
|
||||
<textarea class="form-control" name="notes" maxlength="2000" rows="10" cols="50" id="notes">{notes}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="token_type" class="col-lg-6 control-label">Token Type:</label>
|
||||
<div class="col-lg-2">
|
||||
<select name="token_type" id="token_type" class="">
|
||||
{OPTION=token_type}
|
||||
<option value='app'>Application</option>
|
||||
<option value='user'>User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<input type="hidden" name="token" value="{TOKEN}">
|
||||
<button name="submit" value="submit" type="submit" class="btn btn-lg btn-primary center-block">Save</button><br>
|
||||
</form>
|
30
app/views/admin/tokens/list.html
Normal file
30
app/views/admin/tokens/list.html
Normal file
@ -0,0 +1,30 @@
|
||||
<legend>Tokens</legend>
|
||||
{PAGINATION}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%">ID</th>
|
||||
<th style="width: 45%">Name</th>
|
||||
<th style="width: 25%">Type</th>
|
||||
<th style="width: 15%">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LOOP}
|
||||
<tr>
|
||||
<td align="center">{ID}</td>
|
||||
<td><a href='{ROOT_URL}admin/tokens/view/{ID}'>{name}</a></td>
|
||||
<td>{token_type}</td>
|
||||
<td><a href="{ROOT_URL}admin/tokens/delete/{ID}" class="btn btn-sm btn-danger" role="button"><i class="glyphicon glyphicon-trash"></i></a></td>
|
||||
</tr>
|
||||
{/LOOP}
|
||||
{ALT}
|
||||
<tr>
|
||||
<td align="center" colspan="6">
|
||||
No results to show.
|
||||
</td>
|
||||
</tr>
|
||||
{/ALT}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="{ROOT_URL}admin/tokens/create" class="btn btn-sm btn-primary" role="button">Create</a>
|
48
app/views/admin/tokens/view.html
Normal file
48
app/views/admin/tokens/view.html
Normal file
@ -0,0 +1,48 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-6 col-lg-6 col-xs-offset-0 col-sm-offset-0 col-md-offset-3 col-lg-offset-3 top-pad" >
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{name}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class=" col-md-9 col-lg-9 ">
|
||||
<table class="table table-user-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{token_type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created At</td>
|
||||
<td>{DTC}{createdAt}{/DTC}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Expires At</td>
|
||||
<td>{DTC}{expiresAt}{/DTC}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Token</td>
|
||||
<td>{token}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Secret</td>
|
||||
<td>{secret}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Notes</td>
|
||||
<td>{notes}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<a href="{ROOT_URL}admin/tokens/delete/{ID}" class="btn btn-sm btn-danger" role="button">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
0
app/views/contact.html
Normal file
0
app/views/contact.html
Normal file
@ -12,4 +12,8 @@
|
||||
<a href="{ROOT_URL}privacy">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="material-switch" style="margin-top: 25px; margin-bottom: 25px;">
|
||||
<input name="dark-mode-toggle" type="checkbox" id="dark-mode-toggle"/>
|
||||
<label for="dark-mode-toggle" class="label-default"></label>
|
||||
</div>
|
||||
</div>
|
0
app/views/privacy.html
Normal file
0
app/views/privacy.html
Normal file
52
app/views/switches.html
Normal file
52
app/views/switches.html
Normal file
@ -0,0 +1,52 @@
|
||||
<div class="col-xs-12 col-sm-6 col-md-4 col-sm-offset-3 col-md-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
<div class="panel-heading">Material Design Switch Demos</div>
|
||||
|
||||
<!-- List group -->
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
Bootstrap Switch Default
|
||||
<div class="material-switch pull-right">
|
||||
<input id="someSwitchOptionDefault" name="someSwitchOption001" type="checkbox"/>
|
||||
<label for="someSwitchOptionDefault" class="label-default"></label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Bootstrap Switch Primary
|
||||
<div class="material-switch pull-right">
|
||||
<input id="someSwitchOptionPrimary" name="someSwitchOption001" type="checkbox"/>
|
||||
<label for="someSwitchOptionPrimary" class="label-primary"></label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Bootstrap Switch Success
|
||||
<div class="material-switch pull-right">
|
||||
<input id="someSwitchOptionSuccess" name="someSwitchOption001" type="checkbox"/>
|
||||
<label for="someSwitchOptionSuccess" class="label-success"></label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Bootstrap Switch Info
|
||||
<div class="material-switch pull-right">
|
||||
<input id="someSwitchOptionInfo" name="someSwitchOption001" type="checkbox"/>
|
||||
<label for="someSwitchOptionInfo" class="label-info"></label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Bootstrap Switch Warning
|
||||
<div class="material-switch pull-right">
|
||||
<input id="someSwitchOptionWarning" name="someSwitchOption001" type="checkbox"/>
|
||||
<label for="someSwitchOptionWarning" class="label-warning"></label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Bootstrap Switch Danger
|
||||
<div class="material-switch pull-right">
|
||||
<input id="someSwitchOptionDanger" name="someSwitchOption001" type="checkbox"/>
|
||||
<label for="someSwitchOptionDanger" class="label-danger"></label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
Reference in New Issue
Block a user