Skylar Ittner 5 лет назад
Родитель 47540e57d2
Коммит 28b8563ea0

@ -0,0 +1,5 @@
{
"Edit": "Edit",
"cancel": "Cancel",
"Choose a user": "Choose a user"
}

@ -0,0 +1,127 @@
{
"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.",
"no admin permission": "You do not have permission to access this system.",
"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}!",
"settings": "Settings",
"options": "Options",
"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",
"users": "Users",
"more": "More",
"actions": "Actions",
"name": "Name",
"email": "Email",
"status": "Status",
"type": "Type",
"new user": "New User",
"total users": "Total Users",
"view users": "View Users",
"normal accounts": "Normal Accounts",
"locked accounts": "Locked Accounts",
"editing user": "Editing {user}",
"invalid userid": "Invalid user ID.",
"user saved": "User saved.",
"adding user": "Adding new user",
"placeholder name": "John Doe",
"placeholder username": "jdoe",
"placeholder email address": "jdoe@example.com",
"placeholder password": "swordfish",
"new password": "New Password",
"non-local account warning": "This account is not locally managed. Changes made here will not synchronize to the directory server and some attributes cannot be edited.",
"delete user": "Delete User",
"really delete user": "Are you sure you want to delete this user? This action cannot be reversed.",
"user deleted": "User account deleted.",
"user does not exist": "User does not exist.",
"logtime": "Date\/Time",
"logtype": "Event Type",
"ip address": "IP Address",
"other data": "Other",
"security log": "Security Log",
"event type reference": "Event Type Reference",
"clear log": "Clear Log",
"really clear log": "Are you sure you want to purge the security log? This action cannot be reversed.",
"log cleared": "Security log cleared.",
"removed n entries": "Removed {n} entries",
"security log entries": "Security Log Entries",
"view security log": "View Security Log",
"managers": "Managers",
"manager": "Manager",
"employee": "Employee",
"delete relationship": "Delete Relationship",
"really delete relationship": "Are you sure you want to remove this manager-employee relationship? This action cannot be reversed.",
"relationship deleted": "Relationship deleted.",
"edit relationship": "Edit Relationship",
"adding relationship": "Adding Relationship",
"relationship added": "Relationship added.",
"permissions": "Permissions",
"permission": "Permission",
"new permission": "New Permission",
"delete permission": "Delete Permission",
"adding permission": "Adding Permission",
"user": "User",
"permission does not exist": "Permission does not exist: {arg}",
"really delete permission": "Are you sure you want to revoke this permission?",
"permission added": "Permission assigned.",
"permission deleted": "Permission deleted.",
"remove 2fa": "Reset 2FA",
"action performed by": "Action performed by {user}",
"2fa removed": "2-factor authentication removed.",
"2fa": "2FA",
"show deleted": "Show deleted",
"editing deleted account": "You are editing an account marked as deleted. The account will be undeleted if you press Save.",
"manager assigned": "Manager relationships saved.",
"manager does not exist": "The selected manager username does not exist.",
"type to add a person": "Type to add a person",
"employees": "Employees",
"type to select a manager": "Type to select a manager",
"select a manager to view or edit employees": "Select a manager to view or edit the assigned employees.",
"report export": "Reports\/Export",
"report type": "Report type",
"format": "Format",
"generate report": "Generate report",
"choose an option": "Choose an option",
"csv file": "CSV text file",
"ods file": "ODS spreadsheet",
"html file": "HTML web page",
"uid": "User ID",
"manager name": "Manager",
"manager username": "Mgr. Username",
"employee name": "Employee",
"employee username": "Emp. Username",
"permission id": "Perm. ID",
"permissions assigned": "Permissions assigned.",
"type to select a user": "Type to select a user",
"type to add a permission": "Type to add a permission",
"Choose a permission": "Choose a permission",
"select a user to view or edit permissions": "Select a user to view or edit the assigned permissions.",
"group": "Group",
"groups": "Groups",
"group does not exist": "That group does not exist.",
"group members updated": "Group members updated.",
"group added": "Group added.",
"group deleted": "Group deleted.",
"group already exists": "A group with that name already exists.",
"save": "Save",
"next": "Next",
"add": "Add",
"delete": "Delete",
"new group": "New group",
"delete group": "Delete group",
"enter group name": "Group name",
"group management": "Group Management",
"group assignments": "Group Assignments",
"group id": "Group ID",
"group name": "Group Name"
}

@ -0,0 +1,72 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class Log {
/**
*
* @global $database
* @param int/LogType $type Either an integer (as defined by the constants in class LogType) or a LogType object.
* @param int/User $user Either a UID number or a User object.
* @param string $data Extra data to include in the log, in addition to the timestamp, log type, user, and IP address.
*/
public static function insert($type, $user, string $data = "") {
global $database;
// find IP address
$ip = IPUtils::getClientIP();
if (gettype($type) == "object" && is_a($type, "LogType")) {
$type = $type->getType();
}
if (is_a($user, "User")) {
$uid = $user->getUID();
} else if (gettype($user) == "integer") {
$uid = $user;
} else {
$uid = null;
}
$database->insert("authlog", ['logtime' => date("Y-m-d H:i:s"), 'logtype' => $type, 'uid' => $uid, 'ip' => $ip, 'otherdata' => $data]);
}
}
class LogType {
const LOGIN_OK = 1;
const LOGIN_FAILED = 2;
const PASSWORD_CHANGED = 3;
const API_LOGIN_OK = 4;
const API_LOGIN_FAILED = 5;
const BAD_2FA = 6;
const API_BAD_2FA = 7;
const BAD_CAPTCHA = 8;
const ADDED_2FA = 9;
const REMOVED_2FA = 10;
const LOGOUT = 11;
const API_AUTH_OK = 12;
const API_AUTH_FAILED = 13;
const API_BAD_KEY = 14;
const LOG_CLEARED = 15;
const USER_REMOVED = 16;
const USER_ADDED = 17;
const USER_EDITED = 18;
const MOBILE_LOGIN_OK = 19;
const MOBILE_LOGIN_FAILED = 20;
const MOBILE_BAD_KEY = 21;
private $type;
function __construct(int $type) {
$this->type = $type;
}
public function getType(): int {
return $type;
}
}

@ -0,0 +1,137 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
use League\Csv\Writer;
use League\Csv\HTMLConverter;
use odsPhpGenerator\ods;
use odsPhpGenerator\odsTable;
use odsPhpGenerator\odsTableRow;
use odsPhpGenerator\odsTableColumn;
use odsPhpGenerator\odsTableCellString;
use odsPhpGenerator\odsStyleTableColumn;
use odsPhpGenerator\odsStyleTableCell;
class Report {
private $title = "";
private $header = [];
private $data = [];
public function __construct(string $title = "", array $header = [], array $data = []) {
$this->title = $title;
$this->header = $header;
$this->data = $data;
}
public function setHeader(array $header) {
$this->header = $header;
}
public function addDataRow(array $columns) {
$this->data[] = $columns;
}
public function getHeader(): array {
return $this->header;
}
public function getData(): array {
return $this->data;
}
public function output(string $format) {
switch ($format) {
case "ods":
$this->toODS();
break;
case "html":
$this->toHTML();
break;
case "csv":
default:
$this->toCSV();
break;
}
}
private function toODS() {
$ods = new ods();
$styleColumn = new odsStyleTableColumn();
$styleColumn->setUseOptimalColumnWidth(true);
$headerstyle = new odsStyleTableCell();
$headerstyle->setFontWeight("bold");
$table = new odsTable($this->title);
for ($i = 0; $i < count($this->header); $i++) {
$table->addTableColumn(new odsTableColumn($styleColumn));
}
$row = new odsTableRow();
foreach ($this->header as $cell) {
$row->addCell(new odsTableCellString($cell, $headerstyle));
}
$table->addRow($row);
foreach ($this->data as $cols) {
$row = new odsTableRow();
foreach ($cols as $cell) {
$row->addCell(new odsTableCellString($cell));
}
$table->addRow($row);
}
$ods->addTable($table);
// The @ is a workaround to silence the tempnam notice,
// which breaks the file. This is apparently the intended behavior:
// https://bugs.php.net/bug.php?id=69489
@$ods->downloadOdsFile($this->title . "_" . date("Y-m-d_Hi") . ".ods");
}
private function toHTML() {
global $SECURE_NONCE;
$data = array_merge([$this->header], $this->data);
// HTML exporter doesn't like null values
for ($i = 0; $i < count($data); $i++) {
for ($j = 0; $j < count($data[$i]); $j++) {
if (is_null($data[$i][$j])) {
$data[$i][$j] = '';
}
}
}
header('Content-type: text/html');
$converter = new HTMLConverter();
$out = "<!DOCTYPE html>\n"
. "<meta charset=\"utf-8\">\n"
. "<meta name=\"viewport\" content=\"width=device-width\">\n"
. "<title>" . $this->title . "_" . date("Y-m-d_Hi") . "</title>\n"
. <<<STYLE
<style nonce="$SECURE_NONCE">
.table-csv-data {
border-collapse: collapse;
}
.table-csv-data tr:first-child {
font-weight: bold;
}
.table-csv-data tr td {
border: 1px solid black;
}
</style>
STYLE
. $converter->convert($data);
echo $out;
}
private function toCSV() {
$csv = Writer::createFromString('');
$data = array_merge([$this->header], $this->data);
$csv->insertAll($data);
header('Content-type: text/csv');
header('Content-Disposition: attachment; filename="' . $this->title . "_" . date("Y-m-d_Hi") . ".csv" . '"');
echo $csv;
}
}

@ -0,0 +1,312 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
use Base32\Base32;
use OTPHP\TOTP;
class User {
private $uid = null;
private $username;
private $passhash;
private $email;
private $realname;
private $authsecret;
private $has2fa = false;
private $exists = false;
public function __construct(int $uid, string $username = "") {
global $database;
if ($database->has('accounts', ['AND' => ['uid' => $uid, 'deleted' => false]])) {
$this->uid = $uid;
$user = $database->get('accounts', ['username', 'password', 'email', 'realname', 'authsecret'], ['uid' => $uid]);
$this->username = $user['username'];
$this->passhash = $user['password'];
$this->email = $user['email'];
$this->realname = $user['realname'];
$this->authsecret = $user['authsecret'];
$this->has2fa = !empty($user['authsecret']);
$this->exists = true;
} else {
$this->uid = $uid;
$this->username = $username;
}
}
public static function byUsername(string $username): User {
global $database;
$username = strtolower($username);
if ($database->has('accounts', ['AND' => ['username' => $username, 'deleted' => false]])) {
$uid = $database->get('accounts', 'uid', ['username' => $username]);
return new self($uid * 1);
}
return new self(-1, $username);
}
/**
* Add a user to the system. /!\ Assumes input is OK /!\
* @param string $username Username, saved in lowercase.
* @param string $password Password, will be hashed before saving.
* @param string $realname User's real legal name
* @param string $email User's email address.
* @param string $phone1 Phone number #1
* @param string $phone2 Phone number #2
* @param int $type Account type
* @return int The new user's ID number in the database.
*/
public static function add(string $username, string $password, string $realname, string $email = null, string $phone1 = "", string $phone2 = "", int $type = 1): int {
global $database;
$database->insert('accounts', [
'username' => strtolower($username),
'password' => (is_null($password) ? null : password_hash($password, PASSWORD_BCRYPT)),
'realname' => $realname,
'email' => $email,
'phone1' => $phone1,
'phone2' => $phone2,
'acctstatus' => 1,
'accttype' => $type
]);
return $database->id();
}
public function exists(): bool {
return $this->exists;
}
public function has2fa(): bool {
return $this->has2fa;
}
function getUsername() {
return $this->username;
}
function getUID() {
return $this->uid;
}
function getEmail() {
return $this->email;
}
function getName() {
return $this->realname;
}
/**
* Check the given plaintext password against the stored hash.
* @param string $password
* @return bool
*/
function checkPassword(string $password): bool {
return password_verify($password, $this->passhash);
}
/**
* Change the user's password.
* @global $database $database
* @param string $old The current password
* @param string $new The new password
* @param string $new2 New password again
* @throws PasswordMatchException
* @throws PasswordMismatchException
* @throws IncorrectPasswordException
* @throws WeakPasswordException
*/
function changePassword(string $old, string $new, string $new2) {
global $database, $SETTINGS;
if ($old == $new) {
throw new PasswordMatchException();
}
if ($new != $new2) {
throw new PasswordMismatchException();
}
if (!$this->checkPassword($old)) {
throw new IncorrectPasswordException();
}
require_once __DIR__ . "/worst_passwords.php";
$passrank = checkWorst500List($new);
if ($passrank !== FALSE) {
throw new WeakPasswordException();
}
if (strlen($new) < $SETTINGS['min_password_length']) {
throw new WeakPasswordException();
}
$database->update('accounts', ['password' => password_hash($new, PASSWORD_DEFAULT), 'acctstatus' => 1], ['uid' => $this->uid]);
Log::insert(LogType::PASSWORD_CHANGED, $this);
return true;
}
function check2fa(string $code): bool {
if (!$this->has2fa) {
return true;
}
$totp = new TOTP(null, $this->authsecret);
$time = time();
if ($totp->verify($code, $time)) {
return true;
}
if ($totp->verify($code, $time - 30)) {
return true;
}
if ($totp->verify($code, $time + 30)) {
return true;
}
return false;
}
/**
* Generate a TOTP secret for the given user.
* @return string OTP provisioning URI (for generating a QR code)
*/
function generate2fa(): string {
global $SETTINGS;
$secret = random_bytes(20);
$encoded_secret = Base32::encode($secret);
$totp = new TOTP((empty($this->email) ? $this->realname : $this->email), $encoded_secret);
$totp->setIssuer($SETTINGS['system_name']);
return $totp->getProvisioningUri();
}
/**
* Save a TOTP secret for the user.
* @global $database $database
* @param string $username
* @param string $secret
*/
function save2fa(string $secret) {
global $database;
$database->update('accounts', ['authsecret' => $secret], ['username' => $this->username]);
}
/**
* Check if the given username has the given permission (or admin access)
* @global $database $database
* @param string $code
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function hasPermission(string $code): bool {
global $database;
return $database->has('assigned_permissions', [
'[>]permissions' => [
'permid' => 'permid'
]
], ['AND' => ['OR' => ['permcode #code' => $code, 'permcode #admin' => 'ADMIN'], 'uid' => $this->uid]]) === TRUE;
}
/**
* Get the account status.
* @return \AccountStatus
*/
function getStatus(): AccountStatus {
global $database;
$statuscode = $database->get('accounts', 'acctstatus', ['uid' => $this->uid]);
return new AccountStatus($statuscode);
}
function sendAlertEmail(string $appname = null) {
global $SETTINGS;
if (is_null($appname)) {
$appname = $SETTINGS['site_title'];
}
if (empty(ADMIN_EMAIL) || filter_var(ADMIN_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_to_email";
}
if (empty(FROM_EMAIL) || filter_var(FROM_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_from_email";
}
$mail = new PHPMailer;
if ($SETTINGS['debug']) {
$mail->SMTPDebug = 2;
}
if ($SETTINGS['email']['use_smtp']) {
$mail->isSMTP();
$mail->Host = $SETTINGS['email']['host'];
$mail->SMTPAuth = $SETTINGS['email']['auth'];
$mail->Username = $SETTINGS['email']['user'];
$mail->Password = $SETTINGS['email']['password'];
$mail->SMTPSecure = $SETTINGS['email']['secure'];
$mail->Port = $SETTINGS['email']['port'];
if ($SETTINGS['email']['allow_invalid_certificate']) {
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
}
}
$mail->setFrom(FROM_EMAIL, 'Account Alerts');
$mail->addAddress(ADMIN_EMAIL, "System Admin");
$mail->isHTML(false);
$mail->Subject = $Strings->get("admin alert email subject", false);
$mail->Body = $Strings->build("admin alert email message", ["username" => $this->username, "datetime" => date("Y-m-d H:i:s"), "ipaddr" => IPUtils::getClientIP(), "appname" => $appname], false);
if (!$mail->send()) {
return $mail->ErrorInfo;
}
return true;
}
}
class AccountStatus {
const NORMAL = 1;
const LOCKED_OR_DISABLED = 2;
const CHANGE_PASSWORD = 3;
const TERMINATED = 4;
const ALERT_ON_ACCESS = 5;
private $status;
public function __construct(int $status) {
$this->status = $status;
}
/**
* Get the account status/state as an integer.
* @return int
*/
public function get(): int {
return $this->status;
}
/**
* Get the account status/state as a string representation.
* @return string
*/
public function getString(): string {
switch ($this->status) {
case self::NORMAL:
return "NORMAL";
case self::LOCKED_OR_DISABLED:
return "LOCKED_OR_DISABLED";
case self::CHANGE_PASSWORD:
return "CHANGE_PASSWORD";
case self::TERMINATED:
return "TERMINATED";
case self::ALERT_ON_ACCESS:
return "ALERT_ON_ACCESS";
default:
return "OTHER_" . $this->status;
}
}
}
Загрузка…
Отмена
Сохранить