diff --git a/api.php b/api.php index ba8cec4..153eabc 100644 --- a/api.php +++ b/api.php @@ -8,7 +8,7 @@ $routes = [ exit(); }, - 'i11n' => function () { + 'i18n' => function () { global $path; return match ($path[2]) { 'languages' => [ diff --git a/common/account_utils.php b/common/account_utils.php index 8ee181f..d68de02 100644 --- a/common/account_utils.php +++ b/common/account_utils.php @@ -33,7 +33,7 @@ function format_bcid ($bcid): string $stripped_bcid = strtoupper($stripped_bcid); if (!validate_bcid($stripped_bcid)) { - throw new Exception('Invalid BCID.'); + return '999-9999'; } return substr($stripped_bcid, 0, 3).'-'.substr($stripped_bcid, -4, 4); @@ -46,10 +46,6 @@ function get_user_by_id($bcid) { function get_user_display_name($userId, $escape = true) { global $user; - if (!$_SESSION['auth']) { - return ''; - } - $target = array(); if ($userId == $user['id']) { $target = $user; @@ -73,6 +69,40 @@ function get_user_display_name($userId, $escape = true) { return $display_name; } +function get_user_avatar($userId) { + global $user; + + if (!$_SESSION['auth']) { + return 'https://cdn.id.byecorps.com/profile/default.png'; + } + + $target = array(); + if ($userId == $user['id']) { + $target = $user; + } else { + $target = get_user_by_id($userId); + } + + $avatar = db_execute('SELECT * FROM avatars JOIN neo_id.files f on f.id = avatars.file_id WHERE avatars.owner = ?', + [ $target['id'] ]); + + if ($avatar) { +// echo '
'; print_r($avatar); echo '
'; + return 'https://cdn.id.byecorps.com/' . $avatar['path']; + } + + return 'https://cdn.id.byecorps.com/profile/default.png'; +} + +function set_user_language(string $lang_code, string $id): void +{ + db_execute( + 'UPDATE accounts SET language = ? WHERE id = ?', + [$lang_code, $id] + ); + $_SESSION['lang'] = $lang_code; +} + function requires_auth($redirect = '/auth/login') { global $path_raw; @@ -84,3 +114,13 @@ function requires_auth($redirect = '/auth/login') { header('Location: '.$redirect.'?callback='.urlencode($path_raw)); exit(); } + +function requires_admin() { + global $user; + + if ($user['is_admin']) { + return true; + } + + return false; +} diff --git a/common/files.php b/common/files.php new file mode 100644 index 0000000..55f3804 --- /dev/null +++ b/common/files.php @@ -0,0 +1,158 @@ + '.gif', + 'image/jpeg' => '.jpg', + 'image/png' => '.png', + 'image/webp' => '.webp', + default => '' + }; +} + +/** + * + * Squishes an image into a 128x128 square, and converts it to JPEG + * if it is not a JPEG, PNG, WEBP or GIF. + * + * @param string $path + * @return array + */ +function turn_image_into_avatar(string $path): array +{ + $manager = new \Intervention\Image\ImageManager( + new \Intervention\Image\Drivers\Gd\Driver() + ); + + $image = $manager->read($path); + + // Get mimetype + $mime = mime_content_type($path); + $filetype = mime_to_extension($mime); + + $enc = $image->resize(width: 128, height: 128); + + if ($filetype=='') { + $enc = $image->encodeByMediaType('image/webp'); + $filetype = '.webp'; + } else { + $enc = $image->encodeByMediaType(); + } + + $image_data = (string) $enc; + + return [ + "data" => $image_data, + "mime" => $enc->mimetype(), + 'filetype' => $filetype + ]; +} + +function get_blurhash_for_image($image): string +{ + // Copied shamelessly from https://github.com/kornrunner/php-blurhash + $width = imagesx($image); + $height = imagesy($image); + + $pixels = []; + for ($y = 0; $y < $height; ++$y) { + $row = []; + for ($x = 0; $x < $width; ++$x) { + $index = imagecolorat($image, $x, $y); + $colors = imagecolorsforindex($image, $index); + + $row[] = [$colors['red'], $colors['green'], $colors['blue']]; + } + $pixels[] = $row; + } + + $components_x = 4; + $components_y = 3; + return Blurhash::encode($pixels, $components_x, $components_y); +} + +function upload_file($filename, $target, $owner=null) { + global $bunny_client; + + $bunny_client->upload($filename, $target); + + // Get file mime time + $mime = mime_content_type($filename); + + $blurhash = ''; + if (str_starts_with('image/', $mime)) { + $blurhash = get_blurhash_for_image(imagecreatefromstring(file_get_contents($filename))); + } + + db_execute( + 'INSERT INTO files (path, uploader, blurhash) VALUES (?, ?, ?)', + [$target, $owner, $blurhash] + ); +} + +function upload_raw_data($data, $target, $owner=null) { + $filename = '/tmp/'.uniqid(more_entropy: true); + file_put_contents($filename, $data); + + upload_file($filename, $target, $owner); + + unlink($filename); +} + +/** + * @throws \Bunny\Storage\Exception + * @throws ImagickException + */ +function upload_avatar($data, $user): string +{ + global $bunny_client; + + $img = turn_image_into_avatar($data['tmp_name']); + + $uuid = uniqid(prefix: 'avatar', more_entropy: true); + + $remote_file_name = 'avatars/'.$uuid.$img['filetype']; + + upload_raw_data($img['data'], $remote_file_name); + + $file = db_execute('SELECT id FROM files WHERE path = ? LIMIT 1', [$remote_file_name]); + + $existing_avatar = db_execute('SELECT * FROM avatars WHERE owner = ?', [$user['id']]); + + if (empty($existing_avatar)) { + db_execute( + 'INSERT INTO avatars (file_id, owner) VALUES (?, ?)', + [$file['id'], $user['id']] + ); + } else { + db_execute( + 'UPDATE avatars SET file_id = ? WHERE owner = ?', + [$file['id'], $user['id']] + ); + } + + return 'https://'.BUNNY_STORAGE_ZONE.'.b-cdn.net/'.$remote_file_name; +} + +/** + * @param $id int + * @return void + */ +function delete_file_by_id(int $id): void +{ + global $bunny_client; + + // Get remote path + $file = db_execute('select * from files where id = ?', [$id]); + + if (empty($file)) { + return; + } + + // Remove reference to the file in the database + db_execute('delete from files where id = ?', [$id]); + // Then remove the file + $bunny_client->delete($file['path']); +} diff --git a/common/misc.php b/common/misc.php new file mode 100644 index 0000000..4aaf39e --- /dev/null +++ b/common/misc.php @@ -0,0 +1,17 @@ + $text, 'type' => $type]; +} diff --git a/common/validation.php b/common/validation.php new file mode 100644 index 0000000..ef5ede1 --- /dev/null +++ b/common/validation.php @@ -0,0 +1,18 @@ += $password_min_length) { + return $password; + } + return false; +} diff --git a/composer.json b/composer.json index 5070b5d..efedc74 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,11 @@ { "require": { - "bunnycdn/storage": "^3.3" + "bunnycdn/storage": "^3.3", + "kornrunner/blurhash": "^1.2", + "lbuchs/webauthn": "^2.2", + "intervention/image": "^3.7", + "ext-gd": "*", + "ext-imagick": "*", + "ext-fileinfo": "*" } } diff --git a/composer.lock b/composer.lock index 3f13854..09abba4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "959cf05207f5b0c9a0bc557d5387056c", + "content-hash": "2630884c327f232fb79f8fcefae92fa4", "packages": [ { "name": "bunnycdn/storage", @@ -381,6 +381,235 @@ ], "time": "2023-12-03T20:05:35+00:00" }, + { + "name": "intervention/gif", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3", + "reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.1.0" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-03-26T17:23:47+00:00" + }, + { + "name": "intervention/image", + "version": "3.7.2", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "5451ff9f909c2fc836722e5ed6831b9f9a6db68c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/5451ff9f909c2fc836722e5ed6831b9f9a6db68c", + "reference": "5451ff9f909c2fc836722e5ed6831b9f9a6db68c", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.1", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP image manipulation", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.7.2" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-07-05T13:35:01+00:00" + }, + { + "name": "kornrunner/blurhash", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/kornrunner/php-blurhash.git", + "reference": "bc8a4596cb0a49874f0158696a382ab3933fefe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kornrunner/php-blurhash/zipball/bc8a4596cb0a49874f0158696a382ab3933fefe4", + "reference": "bc8a4596cb0a49874f0158696a382ab3933fefe4", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ocramius/package-versions": "^1.4|^2.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "kornrunner\\Blurhash\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Boris Momčilović", + "email": "boris.momcilovic@gmail.com" + } + ], + "description": "Pure PHP implementation of Blurhash", + "homepage": "https://github.com/kornrunner/php-blurhash", + "support": { + "issues": "https://github.com/kornrunner/php-blurhash/issues", + "source": "https://github.com/kornrunner/php-blurhash.git" + }, + "time": "2022-07-13T19:38:39+00:00" + }, + { + "name": "lbuchs/webauthn", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/lbuchs/WebAuthn.git", + "reference": "20adb4a240c3997bd8cac7dc4dde38ab0bea0ed1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lbuchs/WebAuthn/zipball/20adb4a240c3997bd8cac7dc4dde38ab0bea0ed1", + "reference": "20adb4a240c3997bd8cac7dc4dde38ab0bea0ed1", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "lbuchs\\WebAuthn\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukas Buchs", + "role": "Developer" + } + ], + "description": "A simple PHP WebAuthn (FIDO2) server library", + "homepage": "https://github.com/lbuchs/webauthn", + "keywords": [ + "Authentication", + "webauthn" + ], + "support": { + "issues": "https://github.com/lbuchs/WebAuthn/issues", + "source": "https://github.com/lbuchs/WebAuthn/tree/v2.2.0" + }, + "time": "2024-07-04T07:17:40+00:00" + }, { "name": "psr/http-client", "version": "1.0.3", diff --git a/index.php b/index.php index 3ca2614..e3b96ff 100644 --- a/index.php +++ b/index.php @@ -3,6 +3,7 @@ $DOC_ROOT = $_SERVER['DOCUMENT_ROOT']; require_once $DOC_ROOT . '/vendor/autoload.php'; +use Intervention\Image\ImageManager; // Includes try { @@ -19,11 +20,18 @@ try { echo "Critical error: " . $e->getMessage() . "
Please contact the developers."; } +$bunny_client = new \Bunny\Storage\Client(BUNNY_ACCESS_KEY, BUNNY_STORAGE_ZONE, \Bunny\Storage\Region::STOCKHOLM); + require_once 'strings/en.php'; // This ensures strings will fall back to English if one is missing. require_once 'common/strings.php'; -require_once 'common/account_utils.php'; +require_once 'common/validation.php'; require_once 'common/database.php'; +require_once 'common/account_utils.php'; +require_once 'common/files.php'; +require_once 'common/misc.php'; + +$flash = []; // Starts the session // TODO: write this to use the database to work across more than one server (e.g. don't use PHP sessions) @@ -42,6 +50,7 @@ $user = null; if ($_SESSION['auth']) { $user = get_user_by_id($_SESSION['id']); + $_SESSION['lang'] = $user['language']; } $uri_string = $_SERVER['REQUEST_URI']; // `/foo/bar?bar=foo&foo=bar` @@ -72,6 +81,7 @@ if (str_ends_with($path_raw, '/') && $path_raw != '/') { // If there's a 'lang' query param, change the language! if (array_key_exists('lang', $query)) { $_SESSION['lang'] = $query['lang']; + location($path_raw); } patch_lang($_SESSION['lang']); @@ -79,6 +89,18 @@ patch_lang($_SESSION['lang']); $routes = [ '' => function () { require 'views/home.php'; }, + 'admin' => function () { + global $path, $query, $DOC_ROOT, $flash; + + requires_auth(); + requires_admin(); + + switch ($path[2]) { + default: return 404; + case 'files': + require 'views/admin/files.php'; + } + }, 'api' => function () { global $path, $query; @@ -88,16 +110,20 @@ $routes = [ require 'api.php'; /* Handoff further routing to API script. */ }, 'auth' => function () { - global $path, $query; + global $path, $query, $flash; - if ($path[2] == 'signout') { - require 'views/signedout.php'; - } else if ($path[2] == 'signup') { - require 'views/signup.php'; - } else if ($path[2] == 'login') { - require 'views/login.php'; - } else { - return 404; + switch ($path[2]) { + case 'signout': + require 'views/signedout.php'; + break; + case 'signup': + require 'views/signup.php'; + break; + case 'login': + require 'views/login.php'; + break; + default: + return 404; } exit(); }, @@ -119,6 +145,13 @@ $routes = [ if (isset($path[3])) { return 404; } + + if ($path[2] == 'edit') { + requires_auth(); + require 'views/profile_edit.php'; + return 200; + } + $profile_owner = $path[2]; $profile_owner = get_user_by_id($profile_owner); } else { @@ -129,7 +162,20 @@ $routes = [ return 200; }, 'settings' => function () { - require 'views/settings.php'; + global $path, $flash, $user; + if (isset($path[2])) { + switch ($path[2]) { + default: return 404; + case 'security': + require 'views/settings_security.php'; + break; + case 'region': + require 'views/settings_region.php'; + break; + } + } else { + require 'views/settings.php'; + } } ]; diff --git a/scripts/langauge_switcher.js b/scripts/langauge_switcher.js index 185eaab..efcb0b6 100644 --- a/scripts/langauge_switcher.js +++ b/scripts/langauge_switcher.js @@ -2,7 +2,7 @@ const select = document.createElement('select'); const script = document.scripts[document.scripts.length - 1]; -const langs_req = fetch('/api/i11n/languages') +const langs_req = fetch('/api/i18n/languages') .then(async data => { return await data.json(); }) diff --git a/scripts/passkey_helper.js b/scripts/passkey_helper.js new file mode 100644 index 0000000..e69de29 diff --git a/scripts/settings_list_assister.js b/scripts/settings_list_assister.js new file mode 100644 index 0000000..5c73b67 --- /dev/null +++ b/scripts/settings_list_assister.js @@ -0,0 +1,7 @@ + +const linksOfCurrentPage = document.querySelectorAll('a[href="'+window.location.pathname+'"]'); +console.log(window.location.pathname) +for (let i = 0; i < linksOfCurrentPage.length; i++) { + console.log(linksOfCurrentPage[i]) + linksOfCurrentPage[i].classList.add('selected'); +} diff --git a/strings b/strings index d9131af..66e3af2 160000 --- a/strings +++ b/strings @@ -1 +1 @@ -Subproject commit d9131afc20afc0bb4205990bb34f8c455ae81f8d +Subproject commit 66e3af2bab97bc80b247cf43944bcd5d6506719f diff --git a/styles/base.css b/styles/base.css index c1e27df..674081e 100644 --- a/styles/base.css +++ b/styles/base.css @@ -1,5 +1,7 @@ @import "colours.css"; +@import 'ui.css'; +@import 'extra_icons.css'; :root { color-scheme: light dark; @@ -12,6 +14,8 @@ body { display: flex; flex-direction: column; + background: var(--page-bg); + min-height: 100vh; } @@ -45,6 +49,20 @@ main { margin: 16px; } +.hero { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + justify-content: center; + padding: 1rem 1rem; + gap: 1rem; +} + +.hero .logo { + height: 128px; +} + footer { flex: 0; @@ -126,3 +144,11 @@ body > .errorbox { .center { text-align: center; } + +.align-vertically { + vertical-align: middle; +} + +.spacer { + padding: 16px; +} diff --git a/styles/colours.css b/styles/colours.css index 9509045..8894190 100644 --- a/styles/colours.css +++ b/styles/colours.css @@ -7,13 +7,17 @@ /* ByeCorps ID colour scheme. Use on this site. */ --black-bean: #330f0a; --dark-slate-gray: #394f49; + --dark-slate-gray-dim: #536E65; --fern-green: #65743a; --flax: #efdd8d; + --flax-dim: #BFB26E; --mindaro: #f4fdaf; + --white: #ffffff; --gray-0: #f8f9fa; --gray-1: #f1f3f5; --grey-5: #adb5bd; + --gray-8: #343a40; --gray-9: #212529; --red-2: #ffc9c9; @@ -21,20 +25,35 @@ --red-7: #f03e3e; --red-9: #c92a2a; + --ff-bg-black: #1a1a1a; + + --page-bg: var(--white); + --link-fg: var(--dark-slate-gray); --non-color-link-fg: var(--gray-9); --hover-bg: var(--gray-1); + --selected-bg: var(--gray-0); + + --input-bg: var(--gray-1); + + --button-primary-bg: var(--flax); + --button-primary-hover-bg: var(--flax-dim); --error-fg: var(--red-9); } @media screen and (prefers-color-scheme: dark) { :root { + --page-bg: var(--ff-bg-black); --link-fg: var(--flax); --non-color-link-fg: var(--gray-0); - --hover-bg: var(--gray-9); + --hover-bg: var(--gray-8); + --selected-bg: var(--gray-9); --error-fg: var(--red-3); + --input-bg: var(--gray-9); + --button-primary-bg: var(--dark-slate-gray); + --button-primary-hover-bg: var(--dark-slate-gray-dim); } } diff --git a/styles/dashboard.css b/styles/dashboard.css index c9dd6b1..b3b2ad5 100644 --- a/styles/dashboard.css +++ b/styles/dashboard.css @@ -16,16 +16,21 @@ ul > li > a { text-decoration: none; } +ul > li:has(.selected) { + background: var(--selected-bg); +} + ul > li:hover { - background: var(--hover-bg); + background: var(--hover-bg) !important; } .grid { display: grid; grid-gap: 1rem; - grid-template-columns: 1fr 2fr; + grid-template-columns: minmax(400px, 1fr) 3fr; } + .id-card { align-self: center; background: var(--hover-bg); @@ -36,3 +41,40 @@ ul > li:hover { .id-card > img { border-radius: 0.5rem; } + +.settingsthingy h3 { + margin-bottom: 0; +} + +.grid.halfandhalf { + grid-template-columns: 1fr 1fr; +} + +.language-selector { + display: grid; + gap: 1rem; + margin-bottom: 1rem; +} + +.language-selector label { + padding: 1rem; + border-radius: 1rem; + cursor: pointer; + background: var(--input-bg); +} + +.language-selector label:hover { + background: var(--selected-bg); +} + +@media screen and (max-width: 880px) { + .grid { + grid-template-columns: 1fr; + } +} + +@media screen and (max-width: 700px) { + .grid.halfandhalf { + grid-template-columns: 1fr; + } +} diff --git a/styles/extra_icons.css b/styles/extra_icons.css new file mode 100644 index 0000000..1308956 --- /dev/null +++ b/styles/extra_icons.css @@ -0,0 +1,44 @@ + +.fluent--person-passkey-48-filled { + display: inline-block; + width: 1em; + height: 1em; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23000' d='M24 4c-5.523 0-10 4.477-10 10s4.477 10 10 10s10-4.477 10-10S29.523 4 24 4M12.25 28A4.25 4.25 0 0 0 8 32.249V33c0 3.755 1.942 6.567 4.92 8.38C15.85 43.163 19.786 44 24 44c3.716 0 7.216-.65 10-2.027v-7.489A9 9 0 0 1 30.055 28zm19.82 0A7 7 0 1 1 41 33.71V34l2.293 2.293a1 1 0 0 1 0 1.414L41 40l2.322 2.322a1 1 0 0 1 .03 1.384l-3.646 3.968a1 1 0 0 1-1.444.03l-1.97-1.969a1 1 0 0 1-.292-.707V33.326A7.01 7.01 0 0 1 32.07 28M41 26a2 2 0 1 0-4 0a2 2 0 0 0 4 0'/%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.fluent--person-passkey-32-filled { + display: inline-block; + width: 1em; + height: 1em; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath fill='%23000' d='M16 16a7 7 0 1 0 0-14a7 7 0 0 0 0 14m-8.5 2A3.5 3.5 0 0 0 4 21.5v.5c0 2.393 1.523 4.417 3.685 5.793C9.859 29.177 12.802 30 16 30c2.718 0 5.25-.594 7.285-1.622v-5.067A5.78 5.78 0 0 1 20.735 18zm17.07 9.626v-5.06a4.5 4.5 0 1 1 3.215.248V23l1.474 1.474a.643.643 0 0 1 0 .91l-1.474 1.473l1.493 1.493a.643.643 0 0 1 .019.89l-2.344 2.55a.643.643 0 0 1-.928.02l-1.266-1.266a.64.64 0 0 1-.188-.454zm3.055-10.25a1.125 1.125 0 1 0-2.25 0a1.125 1.125 0 0 0 2.25 0'/%3E%3C/svg%3E"); + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.icon-48 { + font-size: 48px; +} + +.icon-32 { + font-size: 32px; +} + +.icon-24 { + font-size: 24px; +} + +.icon-16 { + font-size: 16px; +} diff --git a/styles/login_form.css b/styles/login_form.css new file mode 100644 index 0000000..18851f0 --- /dev/null +++ b/styles/login_form.css @@ -0,0 +1,23 @@ + +.login-form { + display: flex; + flex-direction: column; + gap: 16px; + + max-width: 600px; + margin: auto; +} + +.login-form > * { + margin: 0; +} + +.login-form > *[type=submit] { + margin: 0 auto; +} + +.passkey { + max-width: 600px; + margin: auto; +} + diff --git a/styles/ui.css b/styles/ui.css new file mode 100644 index 0000000..76dac46 --- /dev/null +++ b/styles/ui.css @@ -0,0 +1,53 @@ + +form.mini-form { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.input { + display: flex; + flex-direction: column; + background: var(--input-bg); + padding: 0.5rem; + border-radius: 0.5rem; +} + +.input label { + font-size: 0.8rem; + float: right; +} + +.input input { + all: unset; +} + +.input:has([data-com-onepassword-filled='light']) { + background-color: hsl(210, 100%, 93%); +} + +.input:has([data-com-onepassword-filled='dark']) { + background-color: rgb(36, 107, 179); +} + +.input:has(:-internal-autofill-selected) { + background-color: light-dark(rgb(232, 240, 254), rgba(70, 90, 126, 0.4)); +} + +button, .button { + padding: 0.5rem; + margin: 0 0.25rem; + border-radius: 0.5rem; + border: rgba(136, 136, 136, 0.3) 1px solid; + font-family: "Montserrat", system-ui, sans-serif; + + cursor: pointer; +} + +button.primary, .button.primary { + background: var(--button-primary-bg); +} + +button.primary:hover, .button.primary:hover { + background: var(--button-primary-hover-bg); +} diff --git a/views/admin/files.php b/views/admin/files.php new file mode 100644 index 0000000..f3ff68e --- /dev/null +++ b/views/admin/files.php @@ -0,0 +1,59 @@ + + + + + + + [A] Files ~> ByeCorps ID + + + +
+

[ADMIN] Files

+

There are files.

+ + + +
+ + + diff --git a/views/dashboard.php b/views/dashboard.php index a378945..4bd65c8 100644 --- a/views/dashboard.php +++ b/views/dashboard.php @@ -11,7 +11,7 @@

- + <?= get_user_display_name($_SESSION['id']) ?>'s avatar
@@ -31,12 +31,6 @@
-
  • - -
    -
    -
    -
  • diff --git a/views/home.php b/views/home.php index 93f70e4..f99802e 100644 --- a/views/home.php +++ b/views/home.php @@ -8,7 +8,17 @@
    -

    ByeCorps ID

    +
    diff --git a/views/login.php b/views/login.php index 500bb98..b497655 100644 --- a/views/login.php +++ b/views/login.php @@ -10,6 +10,11 @@ if ($_SESSION['auth']) { } if ($_SERVER['REQUEST_METHOD'] == 'POST') { + // Validate email address + if (!validate_email($_POST['email'])) { + $error_body = get_string('error.invalidEmail'); + } + // Figure out if it's a user $user_to_log_in_as = db_execute('SELECT id, email, password FROM accounts WHERE email = ?', [$_POST['email']]); if (!$user_to_log_in_as) { @@ -28,6 +33,10 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { } exit(); } +} else { + if (key_exists('callback', $query)) { + $subtitle = get_string('auth.logInToContinue'); + } } skip: @@ -38,6 +47,7 @@ skip: + @@ -49,20 +59,39 @@ skip: } ?> -

    - +

    +

    Don't have one? Sign up.

    + '. $subtitle .'

    '; + } + ?> + -
    -

    -

    -

    -

    + ?> + +
    +
    +
    +
    + + +
    +
  • + +
    + +
    +

    +

    +

    +

    +
    - - diff --git a/views/partials/settings_list.php b/views/partials/settings_list.php new file mode 100644 index 0000000..13dad35 --- /dev/null +++ b/views/partials/settings_list.php @@ -0,0 +1,22 @@ + + + diff --git a/views/profile.php b/views/profile.php index 79ea029..5950231 100644 --- a/views/profile.php +++ b/views/profile.php @@ -18,14 +18,22 @@ if (is_null($user)) {
    -

    +
    + <?= get_user_display_name($profile_owner['id']) ?>'s avatar +
    + + +
    +
    diff --git a/views/profile_edit.php b/views/profile_edit.php new file mode 100644 index 0000000..d417279 --- /dev/null +++ b/views/profile_edit.php @@ -0,0 +1,46 @@ + + + + + + + + + + + +
    + +

    Editing profile

    + +
    +
    + Images + + + + +
    + + +
    + +
    + + + + + diff --git a/views/settings.php b/views/settings.php new file mode 100644 index 0000000..59a4eb7 --- /dev/null +++ b/views/settings.php @@ -0,0 +1,18 @@ + + + + + <?= get_string('page.settings'); ?> ~> ByeCorps ID + + + + + +
    +

    + +
    + + + + diff --git a/views/settings_region.php b/views/settings_region.php new file mode 100644 index 0000000..b88e235 --- /dev/null +++ b/views/settings_region.php @@ -0,0 +1,67 @@ + + + + + + + <?= get_string('page.settings'); ?> ~> ByeCorps ID + + + + + +
    +

    +
    + +
    +

    +

    Here you can set the language ByeCorps ID is displayed in.

    +
    +
    + + + '. $lang['name'] .' + '; + } + ?> +
    + +
    +
    +
    +
    + + + + diff --git a/views/settings_security.php b/views/settings_security.php new file mode 100644 index 0000000..7831657 --- /dev/null +++ b/views/settings_security.php @@ -0,0 +1,57 @@ + + + + + + + + <?= get_string('page.settings'); ?> ~> ByeCorps ID + + + + + +
    +

    +
    + +
    +

    +

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    +
    +

    +
    +
    +

    Passkeys allow you to log in to ByeCorps ID using your device instead of a password.

    + +
    +
    + +
    +
    +
    +
    +
    +
    + + + +