Browse Source

Create from template@0c85643847

tags/v1.0
Skylar Ittner 2 years ago
commit
b80d8ffa9b

+ 4
- 0
.gitignore View File

@@ -0,0 +1,4 @@
vendor
settings.php
nbproject/private
*.sync-conflict*

+ 36
- 0
LICENSE.md View File

@@ -0,0 +1,36 @@
Copyright (C) 2017 Netsyms Technologies. All rights reserved.

**Definitions:**
* "the code", "the software", or "code" refers to the application (or any portion
thereof, in any form) this license is included with, excluding third-party libraries.
* "any form" means source code, binaries, or any other representation of the code.
* "explicit written permission" refers to a written, signed, non-transferable
statement from Netsyms Technologies granting you additional rights to the code.

**You are allowed to:**
* use the code for personal and non-commercial purposes.
* modify the code for personal and non-commercial purposes.

**You are NOT allowed to:**
* redistribute the code in any form.
* sell the code in any form.
* use the code for any projects you redistribute or sell, unless you have
explicit written permission.
* use the code for any projects that are not both personal and non-commercial.
* use the code for commercial, business, or non-profit purposes without
explicit written permission.
* do anything else not permitted in this license.

**You must:**
* retain any and all copyright notices in the code.
* retain any and all license notices in the code.
* ask for clarification from Netsyms Technologies if any portion of this license
is unclear or ambiguous.

**Disclaimer:**
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 4
- 0
README.md View File

@@ -0,0 +1,4 @@
QwikClock
=========

QwikClock is an employee time tracking app.

+ 32
- 0
action.php View File

@@ -0,0 +1,32 @@
<?php

/**
* Make things happen when buttons are pressed and forms submitted.
*/

require_once __DIR__ . "/required.php";

dieifnotloggedin();

/**
* Redirects back to the page ID in $_POST/$_GET['source'] with the given message ID.
* The message will be displayed by the app.
* @param string $msg message ID (see lang/messages.php)
* @param string $arg If set, replaces "{arg}" in the message string when displayed to the user.
*/
function returnToSender($msg, $arg = "") {
global $VARS;
if ($arg == "") {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=" . $msg);
} else {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=$arg");
}
die();
}

switch ($VARS['action']) {
case "signout":
session_destroy();
header('Location: index.php');
die("Logged out.");
}

+ 36
- 0
api.php View File

@@ -0,0 +1,36 @@
<?php

/**
* Simple JSON API to allow other apps to access data from this app.
*
* Requests can be sent via either GET or POST requests. POST is recommended
* as it has a lower chance of being logged on the server, exposing unencrypted
* user passwords.
*/
require __DIR__ . '/required.php';
require_once __DIR__ . '/lib/login.php';
require_once __DIR__ . '/lib/userinfo.php';
header("Content-Type: application/json");

$username = $VARS['username'];
$password = $VARS['password'];
if (user_exists($username) !== true || authenticate_user($username, $password, $errmsg) !== true) {
header("HTTP/1.1 403 Unauthorized");
die("\"403 Unauthorized\"");
}
$userinfo = getUserByUsername($username);

// query max results
$max = 20;
if (preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) {
$max = (int) $VARS['max'];
}

switch ($VARS['action']) {
case "ping":
$out = ["status" => "OK", "maxresults" => $max, "pong" => true];
exit(json_encode($out));
default:
header("HTTP/1.1 400 Bad Request");
die("\"400 Bad Request\"");
}

+ 190
- 0
app.php View File

@@ -0,0 +1,190 @@
<?php
require_once __DIR__ . "/required.php";

if ($_SESSION['loggedin'] != true) {
header('Location: index.php');
die("Session expired. Log in again to continue.");
}

require_once __DIR__ . "/pages.php";

$pageid = "home";
if (!is_empty($_GET['page'])) {
$pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg);
if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
$pageid = $pg;
} else {
$pageid = "404";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title><?php echo SITE_TITLE; ?></title>

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/font-awesome.min.css" rel="stylesheet">
<link href="static/css/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<?php
// custom page styles
if (isset(PAGES[$pageid]['styles'])) {
foreach (PAGES[$pageid]['styles'] as $style) {
echo "<link href=\"$style\" rel=\"stylesheet\">\n";
}
}
?>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<?php
if ((SHOW_ICON == "both" || SHOW_ICON == "app") && ICON_POSITION != "menu") {
if (MENU_BAR_STYLE != "fixed") {
?>
<img class="img-responsive banner-image" src="static/img/logo.png" />
<?php
}
}
?>
</div>
</div>
<nav class="navbar navbar-inverse navbar-<?php echo MENU_BAR_STYLE; ?>-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<?php
if (SHOW_ICON == "both" || SHOW_ICON == "app") {
if (MENU_BAR_STYLE == "fixed" || ICON_POSITION == "menu") {
$src = "static/img/logo.png";
if ($pageid != "home") {
$src = "static/img/up-arrow-white.png";
}
?>
<a class="navbar-brand" href="app.php">
<img style="height: 35px; padding-bottom: 12px; padding-left: 5px;" src="<?php echo $src; ?>" />
</a>
<?php
}
}
?>
<a class="navbar-brand" href="app.php">
<?php
echo SITE_TITLE;
?>
</a>
</div>

<div class="collapse navbar-collapse" id="navbar-collapse">
<ul class="nav navbar-nav">
<?php
foreach (PAGES as $id => $pg) {
if ($pg['navbar'] === TRUE) {
if ($pageid == $id) {
?>
<li class="active">
<?php
} else {
?>
<li>
<?php } ?>
<a href="app.php?page=<?php echo $id; ?>">
<?php
if (isset($pg['icon'])) {
?>
<i class="fa fa-<?php echo $pg['icon']; ?> fa-fw"></i>
<?php } ?>
<?php lang($pg['title']) ?>
</a>
</li>
<?php
}
}
?>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="<?php echo PORTAL_URL; ?>"><i class="fa fa-user fa-fw"></i> <?php echo $_SESSION['realname'] ?></a></li>
<li><a href="action.php?action=signout"><i class="fa fa-sign-out fa-fw"></i> <?php lang("sign out") ?></a></li>
</ul>
</div>
</div>
</nav>
<?php
if (MENU_BAR_STYLE == "fixed") {
?>
<div style="height: 75px;"></div>
<?php
}
?>
<?php
// Alert messages
if (!is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (is_empty($_GET['arg'])) {
$alertmsg = lang(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = lang2(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
}
echo <<<END
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div class="alert alert-dismissible alert-$alerttype">
<button type="button" class="close">&times;</button>
<i class="fa fa-$alerticon"></i> $alertmsg
</div>
</div>
</div>
END;
}
?>
<div>
<?php
include_once __DIR__ . '/pages/' . $pageid . ".php";
?>
</div>
<div class="footer">
<?php echo LICENSE_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.2.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/app.js"></script>
<?php
// custom page scripts
if (isset(PAGES[$pageid]['scripts'])) {
foreach (PAGES[$pageid]['scripts'] as $script) {
echo "<script src=\"$script\"></script>\n";
}
}
?>
</body>
</html>

+ 16
- 0
composer.json View File

@@ -0,0 +1,16 @@
{
"name": "netsyms/business-app-template",
"description": "Template for a webapp integrated with a Portal server for authentication.",
"type": "project",
"require": {
"catfan/medoo": "^1.2",
"guzzlehttp/guzzle": "^6.2"
},
"license": "MIT",
"authors": [
{
"name": "Skylar Ittner",
"email": "admin@netsyms.com"
}
]
}

+ 305
- 0
composer.lock View File

@@ -0,0 +1,305 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "1c8b61c5d506ae016285b99b20040cf0",
"packages": [
{
"name": "catfan/medoo",
"version": "v1.4.3",
"source": {
"type": "git",
"url": "https://github.com/catfan/Medoo.git",
"reference": "00b28046376880e1a7bd3026fa06b4e261bce3fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/catfan/Medoo/zipball/00b28046376880e1a7bd3026fa06b4e261bce3fd",
"reference": "00b28046376880e1a7bd3026fa06b4e261bce3fd",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=5.4"
},
"suggest": {
"ext-pdo_dblib": "For MSSQL or Sybase database on Linux/UNIX platform",
"ext-pdo_mysql": "For MySQL or MariaDB database",
"ext-pdo_oci": "For Oracle database",
"ext-pdo_oci8": "For Oracle version 8 database",
"ext-pdo_pqsql": "For PostgreSQL database",
"ext-pdo_sqlite": "For SQLite database",
"ext-pdo_sqlsrv": "For MSSQL database on Windows platform"
},
"type": "framework",
"autoload": {
"psr-4": {
"Medoo\\": "/src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Angel Lai",
"email": "angel@catfan.me"
}
],
"description": "The lightest PHP database framework to accelerate development",
"homepage": "https://medoo.in",
"keywords": [
"database",
"lightweight",
"mariadb",
"mssql",
"mysql",
"oracle",
"php framework",
"postgresql",
"sql",
"sqlite"
],
"time": "2017-05-22T04:39:48+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.2.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d6c6cc55186db87b7dc5009827429ba4e9dc006",
"reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006",
"shasum": ""
},
"require": {
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.4",
"php": ">=5.5"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.0",
"psr/log": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
],
"time": "2017-02-28T22:50:30+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"time": "2016-12-20T10:07:11+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"request",
"response",
"stream",
"uri",
"url"
],
"time": "2017-03-20T17:10:46+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"time": "2016-08-06T14:39:51+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

+ 156
- 0
index.php View File

@@ -0,0 +1,156 @@
<?php
require_once __DIR__ . "/required.php";

require_once __DIR__ . "/lib/login.php";

// if we're logged in, we don't need to be here.
if ($_SESSION['loggedin']) {
header('Location: app.php');
}

/* Authenticate user */
$userpass_ok = false;
$multiauth = false;
if (checkLoginServer()) {
if ($VARS['progress'] == "1") {
if (!RECAPTCHA_ENABLED || (RECAPTCHA_ENABLED && verifyReCaptcha($VARS['g-recaptcha-response']))) {
$errmsg = "";
if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
switch (get_account_status($VARS['username'])) {
case "LOCKED_OR_DISABLED":
$alert = lang("account locked", false);
break;
case "TERMINATED":
$alert = lang("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = lang("password expired", false);
case "NORMAL":
$userpass_ok = true;
break;
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
$userpass_ok = true;
break;
}
if ($userpass_ok) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if (userHasTOTP($VARS['username'])) {
$multiauth = true;
} else {
doLoginUser($VARS['username'], $VARS['password']);
header('Location: app.php');
die("Logged in, go to app.php");
}
}
} else {
if (!is_empty($errmsg)) {
$alert = lang2("login server error", ['arg' => $errmsg], false);
} else {
$alert = lang("login incorrect", false);
}
}
} else {
$alert = lang("captcha error", false);
}
} else if ($VARS['progress'] == "2") {
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
}
if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
if (doLoginUser($VARS['username'])) {
header('Location: app.php');
die("Logged in, go to app.php");
} else {
$alert = lang("login server user data error", false);
}
} else {
$alert = lang("2fa incorrect", false);
}
}
} else {
$alert = lang("login server unavailable", false);
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title><?php echo SITE_TITLE; ?></title>

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/font-awesome.min.css" rel="stylesheet">
<link href="static/css/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<?php if (RECAPTCHA_ENABLED) { ?>
<script src='https://www.google.com/recaptcha/api.js'></script>
<?php } ?>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div>
<?php
if (SHOW_ICON == "both" || SHOW_ICON == "index") {
?>
<img class="img-responsive banner-image" src="static/img/logo.png" />
<?php } ?>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><?php lang("sign in"); ?></h3>
</div>
<div class="panel-body">
<form action="" method="POST">
<?php
if (!is_empty($alert)) {
?>
<div class="alert alert-danger">
<i class="fa fa-fw fa-exclamation-triangle"></i> <?php echo $alert; ?>
</div>
<?php
}

if ($multiauth != true) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php lang("username"); ?>" required="required" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php lang("password"); ?>" required="required" /><br />
<?php if (RECAPTCHA_ENABLED) { ?>
<div class="g-recaptcha" data-sitekey="<?php echo RECAPTCHA_SITE_KEY; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php lang("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php lang("authcode"); ?>" required="required" autocomplete="off" autofocus /><br />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
}
?>
<button type="submit" class="btn btn-primary">
<?php lang("continue"); ?>
</button>
</form>
</div>
</div>
</div>
</div>
<div class="footer">
<?php echo LICENSE_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.2.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
</body>
</html>

+ 28
- 0
lang/en_us.php View File

@@ -0,0 +1,28 @@
<?php

define("STRINGS", [
"sign in" => "Sign In",
"username" => "Username",
"password" => "Password",
"continue" => "Continue",
"authcode" => "Authentication code",
"2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login server unavailable" => "Login server unavailable. Try again later or contact technical support.",
"account locked" => "This account has been disabled. Contact technical support.",
"password expired" => "You must change your password before continuing.",
"account terminated" => "Account terminated. Access denied.",
"account state error" => "Your account state is not stable. Log out, restart your browser, and try again.",
"welcome user" => "Welcome, {user}!",
"sign out" => "Sign out",
"settings" => "Settings",
"options" => "Options",
"404 error" => "404 Error",
"page not found" => "Page not found.",
"invalid parameters" => "Invalid request parameters.",
"login server error" => "The login server returned an error: {arg}",
"login server user data error" => "The login server refused to provide account information. Try again or contact technical support.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). Try again.",
"home" => "Home",
]);

+ 16
- 0
lang/messages.php View File

@@ -0,0 +1,16 @@
<?php

define("MESSAGES", [
"invalid_parameters" => [
"string" => "invalid parameters",
"type" => "danger"
],
"account_state_error" => [
"string" => "account state error",
"type" => "danger"
],
"404_error" => [
"string" => "page not found",
"type" => "info"
]
]);

+ 344
- 0
lib/login.php View File

@@ -0,0 +1,344 @@
<?php

/**
* Authentication and account functions. Connects to a Portal instance.
*/

/**
* Check the login server API for sanity
* @return boolean true if OK, else false
*/
function checkLoginServer() {
try {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "ping"
]
]);

if ($response->getStatusCode() != 200) {
return false;
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return false;
}
} catch (Exception $e) {
return false;
}
}

////////////////////////////////////////////////////////////////////////////////
// Account handling //
////////////////////////////////////////////////////////////////////////////////

/**
* Checks the given credentials against the API.
* @param string $username
* @param string $password
* @return boolean True if OK, else false
*/
function authenticate_user($username, $password, &$errmsg) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "auth",
'username' => $username,
'password' => $password
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
$errmsg = $resp['msg'];
return false;
}
}

/**
* Check if a username exists.
* @param String $username
*/
function user_exists($username) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userexists",
'username' => $username
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
return true;
} else {
return false;
}
}

/**
* Check if a UID exists.
* @param String $uid
*/
function uid_exists($uid) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userexists",
'uid' => $uid
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
return true;
} else {
return false;
}
}

/**
* Get the account status: NORMAL, TERMINATED, LOCKED_OR_DISABLED,
* CHANGE_PASSWORD, or ALERT_ON_ACCESS
* @param string $username
* @return string
*/
function get_account_status($username) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "acctstatus",
'username' => $username
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['account'];
} else {
return false;
}
}

/**
* Check if the given username has the given permission (or admin access)
* @param string $username
* @param string $permcode
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function account_has_permission($username, $permcode) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "permission",
'username' => $username,
'code' => $permcode
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['has_permission'];
} else {
return false;
}
}

////////////////////////////////////////////////////////////////////////////////
// Login handling //
////////////////////////////////////////////////////////////////////////////////

/**
* Setup $_SESSION values with user data and set loggedin flag to true
* @param string $username
*/
function doLoginUser($username) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'username' => $username
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
var_dump($resp);
if ($resp['status'] == "OK") {
$userinfo = $resp['data'];
$_SESSION['username'] = $username;
$_SESSION['uid'] = $userinfo['uid'];
$_SESSION['email'] = $userinfo['email'];
$_SESSION['realname'] = $userinfo['name'];
$_SESSION['loggedin'] = true;
return true;
} else {
return false;
}
}

function simLogin($username, $password) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "login",
'username' => $username,
'password' => $password
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return $resp['msg'];
}
}

function verifyReCaptcha($code) {
try {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', "https://www.google.com/recaptcha/api/siteverify", [
'form_params' => [
'secret' => RECAPTCHA_SECRET_KEY,
'response' => $code
]
]);

if ($response->getStatusCode() != 200) {
return false;
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['success'] === true) {
return true;
}
return false;
} catch (Exception $e) {
return false;
}
}

////////////////////////////////////////////////////////////////////////////////
// 2-factor authentication //
////////////////////////////////////////////////////////////////////////////////

/**
* Check if a user has TOTP setup
* @param string $username
* @return boolean true if TOTP secret exists, else false
*/
function userHasTOTP($username) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "hastotp",
'username' => $username
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['otp'];
} else {
return false;
}
}

/**
* Verify a TOTP multiauth code
* @global $database
* @param string $username
* @param int $code
* @return boolean true if it's legit, else false
*/
function verifyTOTP($username, $code) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "verifytotp",
'username' => $username,
'code' => $code
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['valid'];
} else {
return false;
}
}

+ 122
- 0
lib/userinfo.php View File

@@ -0,0 +1,122 @@
<?php
/**
* Get user info for the given username.
* @param int $u username
* @return [string] Array of [uid, username, name]
*/
function getUserByUsername($u) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'username' => $u
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['data'];
} else {
// this shouldn't happen, but in case it does just fake it.
return ["name" => $u, "username" => $u, "uid" => $u];
}
}

/**
* Get user info for the given UID.
* @param int $u user ID
* @return [string] Array of [uid, username, name]
*/
function getUserByID($u) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'uid' => $u
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['data'];
} else {
// this shouldn't happen, but in case it does just fake it.
return ["name" => $u, "username" => $u, "uid" => $u];
}
}

/**
* Check if the first UID is a manager of the second UID.
* @param int $m Manager UID
* @param int $e Employee UID
* @return boolean
*/
function isManagerOf($m, $e) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "ismanagerof",
'manager' => $m,
'employee' => $e,
'uid' => 1
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['managerof'];
} else {
// this shouldn't happen, but in case it does just fake it.
return ["name" => $u, "username" => $u, "uid" => $u];
}
}

/**
* Get an array of UIDs the given UID is a manager of.
* @param int $manageruid The UID of the manager to find employees for.
* @return [int]
*/
function getManagedUIDs($manageruid) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "getmanaged",
'uid' => $manageruid
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['employees'];
} else {
return [];
}
}

+ 8
- 0
nbproject/project.properties View File

@@ -0,0 +1,8 @@
auxiliary.org-netbeans-modules-html-editor-lib.default-html-public-id=-//W3C//DTD HTML 4.01 Transitional//EN
include.path=${php.global.include.path}
php.version=PHP_70
source.encoding=UTF-8
src.dir=.
tags.asp=false
tags.short=false
web.root=.

+ 9
- 0
nbproject/project.xml View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.php.project</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/php-project/1">
<name>QwikClock</name>
</data>
</configuration>
</project>

+ 13
- 0
pages.php View File

@@ -0,0 +1,13 @@
<?php

// List of pages and metadata
define("PAGES", [
"home" => [
"title" => "home",
"navbar" => true,
"icon" => "home"
],
"404" => [
"title" => "404 error"
]
]);

+ 5
- 0
pages/404.php View File

@@ -0,0 +1,5 @@
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div class="alert alert-warning"><b><?php lang("404 error");?></b><br /> <?php lang("page not found"); ?></div>
</div>
</div>

+ 1
- 0
pages/home.php View File

@@ -0,0 +1 @@
<h1>Hello World</h1>

+ 191
- 0
required.php View File

@@ -0,0 +1,191 @@
<?php

/**
* This file contains global settings and utility functions.
*/
ob_start(); // allow sending headers after content
// Unicode, solves almost all stupid encoding problems
header('Content-Type: text/html; charset=utf-8');

// l33t $ecurity h4x
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
$session_length = 60 * 60; // 1 hour
session_set_cookie_params($session_length, "/", null, false, true);

session_start(); // stick some cookies in it
// renew session cookie
setcookie(session_name(), session_id(), time() + $session_length);
//
// Composer
require __DIR__ . '/vendor/autoload.php';

// Settings file
require __DIR__ . '/settings.php';
// List of alert messages
require __DIR__ . '/lang/messages.php';
// text strings (i18n)
require __DIR__ . '/lang/' . LANGUAGE . ".php";

/**
* Kill off the running process and spit out an error message
* @param string $error error message
*/
function sendError($error) {
die("<!DOCTYPE html><html><head><title>Error</title></head><body><h1 style='color: red; font-family: sans-serif; font-size:100%;'>" . htmlspecialchars($error) . "</h1></body></html>");
}

date_default_timezone_set(TIMEZONE);

// Database settings
// Also inits database and stuff
use Medoo\Medoo;

$database;
try {
$database = new Medoo([
'database_type' => DB_TYPE,
'database_name' => DB_NAME,
'server' => DB_SERVER,
'username' => DB_USER,
'password' => DB_PASS,
'charset' => DB_CHARSET
]);
} catch (Exception $ex) {
//header('HTTP/1.1 500 Internal Server Error');
sendError("Database error. Try again later. $ex");
}


if (!DEBUG) {
error_reporting(0);
} else {
error_reporting(E_ALL);
ini_set('display_errors', 'On');
}


$VARS;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$VARS = $_POST;
define("GET", false);
} else {
$VARS = $_GET;
define("GET", true);
}

/**
* Checks if a string or whatever is empty.
* @param $str The thingy to check
* @return boolean True if it's empty or whatever.
*/
function is_empty($str) {
return (is_null($str) || !isset($str) || $str == '');
}

/**
* I18N string getter. If the key doesn't exist, outputs the key itself.
* @param string $key I18N string key
* @param boolean $echo whether to echo the result or return it (default echo)
*/
function lang($key, $echo = true) {
if (array_key_exists($key, STRINGS)) {
$str = STRINGS[$key];
} else {
$str = $key;
}

if ($echo) {
echo $str;
} else {
return $str;
}
}

/**
* I18N string getter (with builder). If the key doesn't exist, outputs the key itself.
* @param string $key I18N string key
* @param array $replace key-value array of replacements.
* If the string value is "hello {abc}" and you give ["abc" => "123"], the
* result will be "hello 123".
* @param boolean $echo whether to echo the result or return it (default echo)
*/
function lang2($key, $replace, $echo = true) {
if (array_key_exists($key, STRINGS)) {
$str = STRINGS[$key];
} else {
$str = $key;
}

foreach ($replace as $find => $repl) {
$str = str_replace("{" . $find . "}", $repl, $str);
}

if ($echo) {
echo $str;
} else {
return $str;
}
}

function dieifnotloggedin() {
if ($_SESSION['loggedin'] != true) {
sendError("Session expired. Please log out and log in again.");
}
}

/**
* Check if the previous database action had a problem.
* @param array $specials int=>string array with special response messages for SQL errors
*/
function checkDBError($specials = []) {
global $database;
$errors = $database->error();
if (!is_null($errors[1])) {
foreach ($specials as $code => $text) {
if ($errors[1] == $code) {
sendError($text);
}
}
sendError("A database error occurred:<br /><code>" . $errors[2] . "</code>");
}
}

/*
* http://stackoverflow.com/a/20075147/2534036
*/
if (!function_exists('base_url')) {

function base_url($atRoot = FALSE, $atCore = FALSE, $parse = FALSE) {
if (isset($_SERVER['HTTP_HOST'])) {
$http = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off' ? 'https' : 'http';
$hostname = $_SERVER['HTTP_HOST'];
$dir = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);

$core = preg_split('@/@', str_replace($_SERVER['DOCUMENT_ROOT'], '', realpath(dirname(__FILE__))), NULL, PREG_SPLIT_NO_EMPTY);
$core = $core[0];

$tmplt = $atRoot ? ($atCore ? "%s://%s/%s/" : "%s://%s/") : ($atCore ? "%s://%s/%s/" : "%s://%s%s");
$end = $atRoot ? ($atCore ? $core : $hostname) : ($atCore ? $core : $dir);
$base_url = sprintf($tmplt, $http, $hostname, $end);
} else
$base_url = 'http://localhost/';

if ($parse) {
$base_url = parse_url($base_url);
if (isset($base_url['path']))
if ($base_url['path'] == '/')
$base_url['path'] = '';
}

return $base_url;
}

}

function redirectIfNotLoggedIn() {
if ($_SESSION['loggedin'] !== TRUE) {
header('Location: ' . URL . '/index.php');
die();
}
}

+ 56
- 0
settings.template.php View File

@@ -0,0 +1,56 @@
<?php

// Whether to show debugging data in output.
// DO NOT SET TO TRUE IN PRODUCTION!!!
define("DEBUG", false);

// Database connection settings
// See http://medoo.in/api/new for info
define("DB_TYPE", "mysql");
define("DB_NAME", "qwikclock");
define("DB_SERVER", "localhost");
define("DB_USER", "qwikclock");
define("DB_PASS", "");
define("DB_CHARSET", "utf8");

// Name of the app.
define("SITE_TITLE", "QwikClock");

// Which pages to show the app icon on:
// index, app, both, none
define("SHOW_ICON", "both");
// Where to put the icon: top or menu
// Overridden to 'menu' if MENU_BAR_STYLE is 'fixed'.
define("ICON_POSITION", "menu");
// App menu bar style: fixed or static
define("MENU_BAR_STYLE", "fixed");

// URL of the Business Portal API endpoint
define("PORTAL_API", "http://localhost/portal/api.php");
// URL of the Portal home page
define("PORTAL_URL", "http://localhost/portal/home.php");
// Business Portal API Key
define("PORTAL_KEY", "123");

// For supported values, see http://php.net/manual/en/timezones.php
define("TIMEZONE", "America/Denver");

// Base URL for site links.
define('URL', 'http://localhost/qwikclock');

// Use reCAPTCHA on login screen
// https://www.google.com/recaptcha/
define("RECAPTCHA_ENABLED", FALSE);
define('RECAPTCHA_SITE_KEY', '');
define('RECAPTCHA_SECRET_KEY', '');

// See lang folder for language options
define('LANGUAGE', "en_us");

//////////////////////////////////////////////////////////////
// /!\ Warning: Changing these values may /!\ //
// /!\ violate the terms of your license agreement! /!\ //
//////////////////////////////////////////////////////////////
define("LICENSE_TEXT", "<b>Personal, Non-commercial Use Only</b>");
define("COPYRIGHT_NAME", "Netsyms Technologies");
//////////////////////////////////////////////////////////////

+ 58
- 0
static/css/app.css View File

@@ -0,0 +1,58 @@
.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 12px;
}

.navbar-brand {
font-size: 110%;
}

.footer {
margin-top: 10em;
text-align: center;
}

/*
==============================
THEMING
==============================

Changing the .navbar-inverse background should be enough on modern browsers.
If this app is to be used on IE < 9 (not supported), also set a background-color
to replace the rgba()s.

To use a material-color.css navbar theme, remove all the theming styles in this
file and add a .navbar-[color] class to the navbar in app.php.
*/

/* navbar background */
.navbar-inverse {
/* background-color: green; */
}

/* Selected page background */
.navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus {
background: rgba(0,0,0,.2);
}

.navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form {
border-color: rgba(0,0,0,.2);
}

.navbar-inverse .navbar-toggle .icon-bar {
background-color: white;
}

.navbar-inverse .navbar-brand {
color: white;
}

.navbar-inverse .navbar-nav > li > a {
color: white;
}

.navbar-inverse .navbar-link {
color: white;
}

+ 11
- 0
static/css/bootstrap.min.css
File diff suppressed because it is too large
View File


+ 4
- 0
static/css/font-awesome.min.css
File diff suppressed because it is too large
View File


+ 1951
- 0
static/css/material-color.css
File diff suppressed because it is too large
View File


+ 2
- 0
static/css/material-color.min.css
File diff suppressed because it is too large
View File


BIN
static/fonts/FontAwesome.otf View File


BIN
static/fonts/fontawesome-webfont.eot View File


+ 2671
- 0
static/fonts/fontawesome-webfont.svg
File diff suppressed because it is too large
View File


BIN
static/fonts/fontawesome-webfont.ttf View File


BIN
static/fonts/fontawesome-webfont.woff View File


BIN
static/fonts/fontawesome-webfont.woff2 View File


BIN
static/img/logo.png View File


+ 78
- 0
static/img/logo.svg View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
viewBox="0 0 512.00001 512.00001"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/skylar/Documents/Projects/Sources/WebAppTemplate/static/img/logo.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="-493.3276 : 245.89848 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="464.45088 : 245.89848 : 1"
inkscape:persp3d-origin="-14.438371 : 160.56515 : 1"
id="perspective4236" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="-135.9681"
inkscape:cy="352.66131"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-540.36216)">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.74509804"
id="rect4726"
width="512"
height="512"
x="0"
y="540.36218"
rx="50"
ry="50" />
<path
id="path4348"
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:9.87128067;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 132.93564,682.51771 213.96788,-43.14304 0,313.97496 -213.96788,-37.9643 z m 213.96788,-43.14304 0,313.97496 32.16084,-45.18373 0,-217.44396 z m -213.96788,43.14304 213.96788,-43.14304 32.16084,51.34727 -167.21823,22.47784 z m 78.91049,30.68207 167.21823,-22.47784 0,217.44396 -167.21823,-19.77968 z m -78.91049,-30.68207 0,232.86762 78.91049,-26.99911 0,-175.18644 z m 0,232.86762 213.96788,37.9643 32.16084,-45.18373 -167.21823,-19.77968 z"
inkscape:connector-curvature="0" />
</g>
</svg>

BIN
static/img/up-arrow-black.png View File


+ 94
- 0
static/img/up-arrow-black.svg View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64"
height="64"
viewBox="0 0 64.000001 63.999997"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="up-arrow-black.svg"
inkscape:export-filename="/home/skylar/Documents/Projects/Sources/SimpleInventory/static/img/up-arrow-black.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="66.826957"
inkscape:cy="-6.0149722"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-83.334319,-53.898981)">
<g
id="g4224"
transform="matrix(0.69842965,0,0,0.69842965,25.127004,31.746558)"
style="fill:#000000;fill-opacity:1">
<rect
ry="7.1589174"
rx="7.1589174"
y="72.534538"
x="85.33432"
height="10"
width="90"
id="rect4136"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
ry="7.1589174"
rx="7.1589174"
y="-12.636598"
x="112.29343"
height="10"
width="60"
id="rect4136-6-7"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
ry="7.1589174"
rx="7.1589174"
y="112.287"
x="2.6430364"
height="10"
width="60"
id="rect4136-6-7-5"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

BIN
static/img/up-arrow-white.png View File


+ 94
- 0
static/img/up-arrow-white.svg View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64"
height="64"
viewBox="0 0 64.000001 63.999997"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="up-arrow-white.svg"
inkscape:export-filename="/home/skylar/Documents/Projects/Sources/SimpleInventory/static/img/up-arrow-white.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="66.826957"
inkscape:cy="-6.0149722"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-83.334319,-53.898981)">
<g
id="g4224"
transform="matrix(0.69842965,0,0,0.69842965,25.127004,31.746558)"
style="fill:#ffffff;fill-opacity:1">
<rect
ry="7.1589174"
rx="7.1589174"
y="72.534538"
x="85.33432"
height="10"
width="90"
id="rect4136"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
ry="7.1589174"
rx="7.1589174"
y="-12.636598"
x="112.29343"
height="10"
width="60"
id="rect4136-6-7"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
ry="7.1589174"
rx="7.1589174"
y="112.287"
x="2.6430364"
height="10"
width="60"
id="rect4136-6-7-5"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

+ 23
- 0
static/js/app.js View File

@@ -0,0 +1,23 @@

$(document).ready(function () {
/* Fade out alerts */
$(".alert .close").click(function (e) {
$(this).parent().fadeOut("slow");
});
});


/*
* Remove feedback params from the URL so they don't stick around too long
*/
function getniceurl() {
var url = window.location.search;
url = url.substring(url.lastIndexOf("/") + 1);
url = url.replace(/&?msg=([^&]$|[^&]*)/i, "");
return url;
}
try {
window.history.replaceState("", "", getniceurl());
} catch (ex) {
}

+ 7
- 0
static/js/bootstrap.min.js
File diff suppressed because it is too large
View File


+ 4
- 0
static/js/jquery-3.2.1.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save