token support, api fixes and security, dark mode

This commit is contained in:
Joey Kimsey
2024-12-07 01:58:27 -05:00
parent b93d0259e4
commit 485d85cb0a
26 changed files with 934 additions and 85 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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 ) );
}
}

View 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 )]);
}
}

View 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 )]);
}
}

View File

@ -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
View 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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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');
}
});
});

View File

@ -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
View 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();
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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
View File

View 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>

View 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>

View 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>

View 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
View File

View 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
View File

52
app/views/switches.html Normal file
View 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>