Copy from BusinessAppTemplate @ 542bb80d85

master
Skylar Ittner 7 years ago
commit 9f13a67c7e

5
.gitignore vendored

@ -0,0 +1,5 @@
vendor
settings.php
nbproject/private
*.sync-conflict*
database.mwb.bak

@ -0,0 +1,9 @@
Copyright (C) 2017 Netsyms Technologies.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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 NETSYMS TECHNOLOGIES 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.
Except as contained in this notice, the name and other identifying marks of Netsyms Technologies shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Netsyms Technologies.

@ -0,0 +1,81 @@
Business App Template
=====================
This is an empty (but fully functional) PHP application. It is designed to
integrate with Portal, an account management web interface. Portal manages
user credentials and account data, and is accessed by this app via [a simple API](http://docs.netsyms.com/docs/Portal/API%20Documentation/).
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2aeadc6b65d545c4a4c2e77d286373fd)](https://www.codacy.com/app/Netsyms/BusinessAppTemplate?utm_source=github.com&utm_medium=referral&utm_content=Netsyms/BusinessAppTemplate&utm_campaign=Badge_Grade)
Program Structure
-----------------
### Folders
* lang
Translations and alert messages.
The language file that is loaded depends on the value of `LANGUAGE` in `settings.php`.
Translate the values (but not the keys) in `en_us.php` into other languages and save in appropriately named files to add languages.
* lib
A good place to put helper functions that you don't want "in the way".
* pages
What it looks like. If you go into `pages.php` and define a page with the name `foo`, there should be a `foo.php` in here.
The app checks before loading, so it will give a friendly 404 error if it doesn't find your page.
Woe to you if you delete `home.php` or `404.php`, as those are assumed to exist for fallback behavior.
* static
CSS, JS, fonts, images...
* vendor
If you don't know what this is about, or you don't have it, you need to read up on Composer. Right now.
### Files
* settings.template.php
App configuration. Copy to `settings.php` and customize. Documented with inline comments.
* required.php
The "duct tape" that holds the app together. Use `require_once __DIR__."/required.php"` at the top of every file.
It loads Composer dependencies, app settings, language data, and creates `$database` for accessing the database.
It also has some utility functions, including `dieifnotloggedin()`, `is_empty($var)`, and `lang('key')`.
Read through it to see what those functions do.
* action.php
A good place to post forms to. By default it only handles logging out, but is easily expanded.
* api.php
Similar to action.php, but designed for user/pass authenticated JSON responses.
* index.php
Login page and handler. Hands off to `app.php` after authenticating user.
It includes 2fa support, by the way.
* app.php
Main app page after login. Handles loading app pages and 404 errors.
Redirects to `index.php` if the user is not logged in.
Note: to show an alert message (success, error, whatever), set the GET argument `msg` to a message ID from `lang/messages.php`.
* pages.php
Define app pages/screens in an array. The page ID/array key is assumed to exist as a file `pages/{key}.php`, or it will 404.
__Optional parameters:__
`'navbar' => true` will show the page as a button in the app menu bar.
`'icon' => '...'` will show an icon from FontAwesome in the menu bar. Setting this to `home` will show the icon `fa-home`.
`'styles' => ["file.css"]` will inject the listed CSS files into the page header (after all other CSS, like Bootstrap).
`'scripts' => ["file.js"]` will inject the listed JavaScript files into the page footer (after jQuery and other builtin scripts).
* lang/messages.php
Array of alert messages.
`"string"` is the language string for the message, `"type"` is one of `success`, `info`, `warning`, or `danger` (i.e. Bootstrap alert classes).
Changing the type changes the icon and color of the alert box.
*lang/en_us.php
Language data for US English.
*lib/login.php
Functions for logging in users and stuff like that. Most functions transparently makes requests to the Portal API and return the results.
*lib/userinfo.php
Functions for getting user data, like real names and managed employees.
*static/css/app.css
Custom styles for the app. See the comments inside for instructions on theming the app.
License
-------
tl;dr: MIT license, but also don't use our name in ads and stuff.
Copyright (C) 2017 Netsyms Technologies.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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 NETSYMS TECHNOLOGIES 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.
Except as contained in this notice, the name and other identifying marks of Netsyms Technologies shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Netsyms Technologies.

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

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

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

@ -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
composer.lock generated

@ -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.2",
"source": {
"type": "git",
"url": "https://github.com/catfan/Medoo.git",
"reference": "1134cd6372c6ec9e3ffca81335fbf04adaec4422"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/catfan/Medoo/zipball/1134cd6372c6ec9e3ffca81335fbf04adaec4422",
"reference": "1134cd6372c6ec9e3ffca81335fbf04adaec4422",
"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-16T14:30:44+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": []
}

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

@ -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",
]);

@ -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"
]
]);

@ -0,0 +1,313 @@
<?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;
}
}
////////////////////////////////////////////////////////////////////////////////
// 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;
}
}

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

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

@ -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>ManagePanel</name>
</data>
</configuration>
</project>

@ -0,0 +1,13 @@
<?php
// List of pages and metadata
define("PAGES", [
"home" => [
"title" => "home",
"navbar" => true,
"icon" => "home"
],
"404" => [
"title" => "404 error"
]
]);

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

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

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

@ -0,0 +1,56 @@
<?php
// Whether to show debugging data in output.
// DO NOT SET TO TRUE IN PRODUCTION!!!
define("DEBUG", false);
// Portal database connection settings
// See http://medoo.in/api/new for info
define("DB_TYPE", "mysql");
define("DB_NAME", "sso");
define("DB_SERVER", "localhost");
define("DB_USER", "sso");
define("DB_PASS", "");
define("DB_CHARSET", "utf8");
// Name of the app.
define("SITE_TITLE", "ManagePanel");
// 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/api.php");
// URL of the Portal home page
define("PORTAL_URL", "http://localhost/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/managepanel');
// 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");
//////////////////////////////////////////////////////////////

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

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

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

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

After

Width:  |  Height:  |  Size: 3.0 KiB

@ -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) {
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save