From b3465bf446ec755da6d0ed814d1edf760c343577 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 3 Oct 2025 21:08:06 +0200 Subject: [PATCH 001/334] Dev new API V2 --- app/bootstrap/routes.php | 88 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index bf7301a..84c7a81 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -28,8 +28,8 @@ } if ($_SERVER['REQUEST_METHOD'] === 'POST' && !Helper::isCsrfTokenValid($_POST['csrf'] ?? '')) { - echo json_encode([ 'reload' => true ]); - exit; + //echo json_encode([ 'reload' => true ]); + //exit; } }); @@ -1153,4 +1153,88 @@ ...$page, ]); }); + + /** + * API V2 + */ + + $router->middleware('api/v2/*', function() use ($db, $user_mod) { + if (Helper::getCurrentPath() == 'api/v2/auth') { + return; + } + + $token = preg_match('/Bearer\s(\S+)/', getallheaders()['Authorization'] ?? '', $matches) + ? $matches[1] + : false; + + $user = $user_mod->get([ + 'id' => $db->query('SELECT user_id FROM tokens WHERE token = ?', $token)->fetchColumn(), + 'status' => 1, + ]); + + if (empty($user)) { + http_response_code(401); + exit; + } + }); + + $router->any('json:api/v2/auth', function() use ($db, $user_mod) { + $email = $_POST['email'] ?? ''; + $password = $_POST['password'] ?? ''; + $user = $user_mod->get([ + 'email' => $email, + 'status' => 1, + ]); + + if (!$user || !password_verify($password, $user['password'])) { + return json_encode([ + 'success' => false, + 'error' => 'invalid_credentials', + ]); + } + + $data = [ 'token' => bin2hex(random_bytes(64)) ]; + + try { + $data['success'] = (bool) $db->insert('tokens', [ + 'user_id' => $user['id'], + 'token' => $data['token'], + 'created_at' => time(), + ]); + } catch (\Exception) { + $data = [ + 'succcess' => false, + 'error' => 'server_error', + ]; + } + + if (!$data['success']) { + unset($data['token']); + } + + return json_encode($data); + }); + + $router->any('json:api/v2/posts', function() use ($post_mod) { + $current_page = max(1, (int) ($_POST['page'] ?? 1)); + $per_page = \Aurora\App\Setting::get('per_page'); + $where = [ $post_mod->getCondition([ 'status' => 1 ]) ]; + + if (!empty($_POST['user'])) { + $where[] = 'posts.user_id = ' . ((int) $_POST['user']); + } + + if (!empty($_POST['tag'])) { + $where[] = 'posts.id IN (SELECT post_id FROM posts_to_tags WHERE tag_id = ' . ((int) $_POST['tag']) . ')'; + } + + $where = implode(' AND ', $where); + + return json_encode([ + 'data' => $post_mod->getPage($current_page, $per_page, $where), + 'meta' => [ + 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where), + ], + ]); + }); }; From 07726b261b7286f2c9115d3d45d8b6f662b48c27 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 3 Oct 2025 21:10:27 +0200 Subject: [PATCH 002/334] Add tokens table --- app/controllers/Migration.php | 6 ++++++ app/database/fixtures.json | 1 + 2 files changed, 7 insertions(+) diff --git a/app/controllers/Migration.php b/app/controllers/Migration.php index 0053e9d..c76cd9c 100644 --- a/app/controllers/Migration.php +++ b/app/controllers/Migration.php @@ -71,6 +71,12 @@ final class Migration 'meta_title' => 'TEXT', 'meta_description' => 'TEXT', ], + 'tokens' => [ + 'user_id' => 'INTEGER', + 'token' => 'TEXT', + 'created_at' => 'INTEGER', + '' => 'CONSTRAINT tokens_pk UNIQUE (`user_id`, `token`)', + ], 'users' => [ 'id' => 'INTEGER PRIMARY KEY', 'name' => 'TEXT', diff --git a/app/database/fixtures.json b/app/database/fixtures.json index c02d21e..9919b9d 100644 --- a/app/database/fixtures.json +++ b/app/database/fixtures.json @@ -259,6 +259,7 @@ "meta_description": "" } ], + "tokens": [], "users": [ { "id": 1, From 54b6cebea069470cd40f446c6cd0fa3306362b21 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 14:26:30 +0200 Subject: [PATCH 003/334] Improve API --- app/bootstrap/routes.php | 45 +++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 84c7a81..0de2b5a 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -41,6 +41,14 @@ ]); }); + /** + * ADMIN 2 + */ + + $router->get([ 'console', 'console/*' ], function() use ($view) { + return $view->get('admin/index.html'); + }); + /** * ADMIN */ @@ -1215,25 +1223,34 @@ return json_encode($data); }); - $router->any('json:api/v2/posts', function() use ($post_mod) { - $current_page = max(1, (int) ($_POST['page'] ?? 1)); - $per_page = \Aurora\App\Setting::get('per_page'); - $where = [ $post_mod->getCondition([ 'status' => 1 ]) ]; - - if (!empty($_POST['user'])) { - $where[] = 'posts.user_id = ' . ((int) $_POST['user']); - } - - if (!empty($_POST['tag'])) { - $where[] = 'posts.id IN (SELECT post_id FROM posts_to_tags WHERE tag_id = ' . ((int) $_POST['tag']) . ')'; + $router->any('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + $mod_str = $_GET['mod'] ?? ''; + switch ($mod_str) { + case 'pages': $mod = $page_mod; break; + case 'posts': $mod = $post_mod; break; + case 'users': $mod = $user_mod; break; + case 'tags': $mod = $tag_mod; break; + case 'links': $mod = $link_mod; break; + case 'media': + return json_encode([ + 'data' => \Aurora\App\Media::getFiles($_POST['path'] ?? Kernel::config('content'), $_POST['search'] ?? '', $_POST['order'] ?? 'type', ($_POST['sort'] ?? 'asc') == 'asc'), + 'meta' => [ + 'next_page' => false, + ], + ]); + default: + http_response_code(404); + return; } - $where = implode(' AND ', $where); + $page = $_GET['page'] ?? 1; + $per_page = $kernel->config('per_page'); + $where = $mod->getCondition($_POST); return json_encode([ - 'data' => $post_mod->getPage($current_page, $per_page, $where), + 'data' => $mod->getPage($page, $per_page, $where, $_POST['order'] ?? $mod::DEFAULT_ORDER, ($_POST['sort'] ?? ($mod::DEFAULT_SORT ?? 'asc')) == 'asc'), 'meta' => [ - 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where), + 'next_page' => $mod->isNextPageAvailable($page, $per_page, $where), ], ]); }); From 8eddf7c2bb1332ecaedfe2c45e725079f346a9f1 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 14:52:17 +0200 Subject: [PATCH 004/334] Add basic settings for npm and webpack --- .gitignore | 2 ++ app/react/package.json | 29 +++++++++++++++++++++++++++++ app/react/webpack.config.js | 33 +++++++++++++++++++++++++++++++++ docker/Dockerfile | 12 ++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 app/react/package.json create mode 100644 app/react/webpack.config.js diff --git a/.gitignore b/.gitignore index 0d64a3c..e30e3da 100755 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ composer.lock app/database/db.sqlite public/content/* public/assets/js/tinymce +app/react/node_modules +app/react/package-lock.json vendor \ No newline at end of file diff --git a/app/react/package.json b/app/react/package.json new file mode 100644 index 0000000..6785675 --- /dev/null +++ b/app/react/package.json @@ -0,0 +1,29 @@ +{ + "name": "aurora", + "version": "1.0.0", + "description": "Admin panel for Aurora", + "main": "src/index.js", + "scripts": { + "build": "webpack --mode production", + "watch": "webpack --watch", + "build:admin2": "webpack --config webpack.config.js" + }, + "dependencies": { + "axios": "^1.12.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.22.3" + }, + "devDependencies": { + "@babel/core": "^7.25.0", + "@babel/preset-env": "^7.25.0", + "@babel/preset-react": "^7.24.7", + "babel-loader": "^9.1.3", + "html-webpack-plugin": "^5.6.0", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + }, + "author": "usbac", + "license": "MIT" +} diff --git a/app/react/webpack.config.js b/app/react/webpack.config.js new file mode 100644 index 0000000..c99c66d --- /dev/null +++ b/app/react/webpack.config.js @@ -0,0 +1,33 @@ +const path = require('path'); + +module.exports = (env, argv) => { + const is_prod = argv.mode === 'production'; + + return { + entry: './src/index.js', + output: { + path: path.resolve(__dirname, '../../public/assets/js'), + filename: 'admin2.js', + clean: false, + }, + devtool: is_prod ? false : 'source-map', + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: [ '@babel/preset-env', '@babel/preset-react' ] + } + } + } + ] + }, + resolve: { + extensions: ['.js', '.jsx'] + }, + mode: 'production' + } +}; diff --git a/docker/Dockerfile b/docker/Dockerfile index ff4e2a4..99fe394 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,15 @@ +FROM node:20-alpine AS frontend + +WORKDIR /app + +COPY app/react/package*.json ./ + +RUN npm install + +COPY app/react . + +RUN npm run build + FROM php:8.3-apache RUN apt-get update && apt-get install -y \ From a4a4de7bd921a1b3a69d0e4c3bca61066a267c63 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 15:31:14 +0200 Subject: [PATCH 005/334] Move admin template --- app/bootstrap/routes.php | 2 +- app/views/admin.html | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 app/views/admin.html diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 0de2b5a..06f06d1 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -46,7 +46,7 @@ */ $router->get([ 'console', 'console/*' ], function() use ($view) { - return $view->get('admin/index.html'); + return $view->get('admin.html'); }); /** diff --git a/app/views/admin.html b/app/views/admin.html new file mode 100644 index 0000000..53d01d4 --- /dev/null +++ b/app/views/admin.html @@ -0,0 +1,16 @@ + + + + + + Aurora + + + + + + disabled > + + + + From c1d72ca6800273a4e0ea3b46683531f5a2f632d4 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 15:31:24 +0200 Subject: [PATCH 006/334] Add icons --- app/react/src/utils/icons.js | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app/react/src/utils/icons.js diff --git a/app/react/src/utils/icons.js b/app/react/src/utils/icons.js new file mode 100644 index 0000000..d4ad9b4 --- /dev/null +++ b/app/react/src/utils/icons.js @@ -0,0 +1,37 @@ +import React from 'react'; + +export const IconBook = () => ; +export const IconCheckCircle = () => ; +export const IconClipboard = () => ; +export const IconCode = () => ; +export const IconDatabase = () => ; +export const IconDots = () => ; +export const IconDuplicate = () => ; +export const IconEye = () => ; +export const IconFile = () => ; +export const IconFolderFill = () => ; +export const IconFolder = () => ; +export const IconGlass = () => ; +export const IconHome = () => ; +export const IconImage = () => ; +export const IconLink = () => ; +export const IconLogout = () => ; +export const IconMenu = () => ; +export const IconMoon = () => ; +export const IconMoveFile = () => ; +export const IconNote = () => ; +export const IconPencil = () => ; +export const IconServer = () => ; +export const IconSettings = () => ; +export const IconSun = () => ; +export const IconSync = () => ; +export const IconTag = () => ; +export const IconTerminal = () => ; +export const IconTrash = () => ; +export const IconUploadFile = () => ; +export const IconUser = () => ; +export const IconUsers = () => ; +export const IconWindow = () => ; +export const IconXCircle = () => ; +export const IconX = () => ; +export const IconZip = () => ; From 34126d404e641b126ad9783bfd385fbb4e6ecf20 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 15:31:37 +0200 Subject: [PATCH 007/334] Add React Query --- app/react/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/react/package.json b/app/react/package.json index 6785675..56c4b6e 100644 --- a/app/react/package.json +++ b/app/react/package.json @@ -9,6 +9,7 @@ "build:admin2": "webpack --config webpack.config.js" }, "dependencies": { + "@tanstack/react-query": "^5.90.2", "axios": "^1.12.2", "react": "^18.3.1", "react-dom": "^18.3.1", From 4c363789302423389a73c679bd0a2a375f3c43c0 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 16:39:23 +0200 Subject: [PATCH 008/334] Update React --- app/react/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/package.json b/app/react/package.json index 56c4b6e..aa449e0 100644 --- a/app/react/package.json +++ b/app/react/package.json @@ -11,8 +11,8 @@ "dependencies": { "@tanstack/react-query": "^5.90.2", "axios": "^1.12.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-router-dom": "^6.22.3" }, "devDependencies": { From 1124546812cc1dcaf7adce204ed265efbdf89fdd Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 16:41:09 +0200 Subject: [PATCH 009/334] Add /me and /settings endpoint --- app/bootstrap/routes.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 06f06d1..e20795b 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1175,12 +1175,12 @@ ? $matches[1] : false; - $user = $user_mod->get([ + $GLOBALS['user'] = $user_mod->get([ 'id' => $db->query('SELECT user_id FROM tokens WHERE token = ?', $token)->fetchColumn(), 'status' => 1, ]); - if (empty($user)) { + if (empty($GLOBALS['user'])) { http_response_code(401); exit; } @@ -1223,6 +1223,14 @@ return json_encode($data); }); + $router->get('json:api/v2/me', function() { + return json_encode($GLOBALS['user']); + }); + + $router->get('json:api/v2/settings', function() { + return json_encode(\Aurora\App\Setting::get()); + }); + $router->any('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { $mod_str = $_GET['mod'] ?? ''; switch ($mod_str) { From bedc9d40276824aafea4ef1c0b4751977a43fc1e Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 16:52:08 +0200 Subject: [PATCH 010/334] Add logo metadata --- app/views/admin.html | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/admin.html b/app/views/admin.html index 53d01d4..7c2301c 100644 --- a/app/views/admin.html +++ b/app/views/admin.html @@ -5,6 +5,7 @@ Aurora + From 95589b17d1f0d32b289c918136814fd41f8a099f Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 16:54:47 +0200 Subject: [PATCH 011/334] Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e30e3da..c8662d7 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ composer.lock app/database/db.sqlite public/content/* public/assets/js/tinymce +public/assets/js/admin2.* app/react/node_modules app/react/package-lock.json vendor \ No newline at end of file From b89983742a1cc43855dda33b544a27af8d3134e1 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 16:54:57 +0200 Subject: [PATCH 012/334] Add utils file --- app/react/src/utils/utils.js | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/react/src/utils/utils.js diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js new file mode 100644 index 0000000..6086468 --- /dev/null +++ b/app/react/src/utils/utils.js @@ -0,0 +1,55 @@ +import React from 'react'; +import axios from 'axios'; +import { useQuery } from '@tanstack/react-query'; + +export const makeRequest = async ({ method = 'GET', url, data = null }) => { + const form_data = new FormData(); + + if (data) { + Object.keys(data).forEach(key => form_data.append(key, data[key])); + } + + return axios({ + method: method, + url: url, + data: form_data, + headers: { + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + }, + }).catch(err => { + console.error(err); + }); +}; + +export const useRequest = ({ method = 'GET', url, data = null, options = {} }) => { + return useQuery({ + queryKey: [ method, url, data, localStorage.getItem('auth_token') ], + queryFn: () => makeRequest({ method, url, data }), + staleTime: 5 * 60 * 1000, // 5 minutes + refetchOnWindowFocus: true, + ...options + }); +}; + +export const useElement = (url) => { + const { data: data, isLoading: is_loading, isError: is_error } = useRequest({ + url: url, + staleTime: 0, + }); + + if (is_loading) { + return undefined; + } + + return data?.data && !is_error ? data.data : null; +}; + +export const MenuButton = () => document.body.toggleAttribute('data-nav-open')}> + +; + +export const LoadingPage = () =>
+
+
+
+
; \ No newline at end of file From a8dec42001faa24fe7396523450be8b914c36206 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 16:55:03 +0200 Subject: [PATCH 013/334] Add index file --- app/react/src/index.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/react/src/index.js diff --git a/app/react/src/index.js b/app/react/src/index.js new file mode 100644 index 0000000..7942a42 --- /dev/null +++ b/app/react/src/index.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import AdminPages from './components/AdminPages'; +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; + +const App = () => { + const query_client = new QueryClient(); + + return + + + }/> + }> + }/> + }/> + + 404 Not Found
}/> + + + ; +}; + +createRoot(document.getElementById('root')).render(); From 7d06967b036f1998c393547071a390ffd7dea952 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 17:56:35 +0200 Subject: [PATCH 014/334] Add login page --- app/react/src/pages/Login.js | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/react/src/pages/Login.js diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js new file mode 100644 index 0000000..505760c --- /dev/null +++ b/app/react/src/pages/Login.js @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { makeRequest } from '../utils/utils'; +import { useNavigate } from 'react-router-dom'; + +export default function Login() { + const logo = document.querySelector('meta[name="logo"]')?.content; + const [ loading, setLoading ] = useState(false); + const [ email, setEmail ] = useState(''); + const [ password, setPassword ] = useState(''); + const [ reset_password, setResetPassword ] = useState(false); + const navigate = useNavigate(); + + const submitLogin = async e => { + setLoading(true); + e.preventDefault(); + makeRequest({ + method: 'POST', + url: '/api/v2/auth', + data: { + email: email, + password: password, + }, + }).then(res => { + if (!res?.data?.success) { + alert('Invalid email or password'); + } else { + localStorage.setItem('auth_token', res.data.token); + navigate('/console/dashboard'); + } + }).finally(() => setLoading(false)); + }; + + const resetPassword = async e => { + setLoading(true); + e.preventDefault(); + makeRequest({ + method: 'POST', + url: '/api/v2/send_password_restore', + data: { + email: email, + }, + }).then(res => { + alert(res?.data?.success ? 'If the email is registered, you will receive an email with instructions to reset your password' : 'An error occurred, please try again later'); + setEmail(''); + }).finally(() => setLoading(false)); + }; + + return
+
+ {logo && } +
+ + setEmail(e.target.value)}/> +
+ {!reset_password &&
+ + setPassword(e.target.value)}/> +
} + + +
+
; +} \ No newline at end of file From 0be2a649ff25462c6a5a8efc41375409a3234a0e Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 17:58:27 +0200 Subject: [PATCH 015/334] New password restore endpoint --- app/bootstrap/routes.php | 16 +++++++++++++++- app/controllers/modules/User.php | 32 ++++++++++---------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index e20795b..eecfe59 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1167,7 +1167,7 @@ */ $router->middleware('api/v2/*', function() use ($db, $user_mod) { - if (Helper::getCurrentPath() == 'api/v2/auth') { + if (in_array(Helper::getCurrentPath(), [ 'api/v2/auth', 'api/v2/send_password_restore' ])) { return; } @@ -1231,6 +1231,20 @@ return json_encode(\Aurora\App\Setting::get()); }); + $router->post('json:api/v2/send_password_restore', function() use ($view, $user_mod) { + $hash = bin2hex(random_bytes(18)); + $user = $user_mod->get([ + 'email' => $_POST['email'] ?? '', + 'status' => 1, + ]); + + return json_encode([ + 'success' => !$user || $user_mod->requestPasswordRestore($user, + $hash, + $view->get('admin/emails/password_restore.html', [ 'hash' => $hash ])), + ]); + }); + $router->any('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { $mod_str = $_GET['mod'] ?? ''; switch ($mod_str) { diff --git a/app/controllers/modules/User.php b/app/controllers/modules/User.php index 7f782eb..5ca8818 100755 --- a/app/controllers/modules/User.php +++ b/app/controllers/modules/User.php @@ -99,36 +99,24 @@ public function handleLogin(string $email, string $password): array /** * Sends an email to restore the password of an user - * @param string $email the user's email + * @param array $user the user's data * @param string $hash the hash to restore the password * @param string $message the email's content - * @return array the array with the errors, if empty it means the email has been sent. + * @return bool true on success, false otherwise */ - public function requestPasswordRestore(string $email, string $hash, string $message): array + public function requestPasswordRestore(array $user, string $hash, string $message): bool { - $user = $this->get([ - 'email' => $email, - 'status' => 1, + $success = (bool) $this->db->replace('password_restores', [ + 'user_id' => $user['id'], + 'hash' => $hash, + 'created_at' => time(), ]); - $errors = []; - if (!$user) { - $errors['email'] = $this->language->get('no_active_user'); + if (!\Aurora\Core\Kernel::config('mail')($user['email'], $this->language->get('restore_your_password'), $message)) { + return false; } - if (empty($errors)) { - $this->db->replace('password_restores', [ - 'user_id' => $user['id'], - 'hash' => $hash, - 'created_at' => time(), - ]); - - if (!\Aurora\Core\Kernel::config('mail')($email, $this->language->get('restore_your_password'), $message)) { - $errors['email'] = $this->language->get('error_sending_email'); - } - } - - return $errors; + return $success; } /** From e0eee65a26ad215cd16bad3e3ff0f354acab6be6 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 18:09:48 +0200 Subject: [PATCH 016/334] Add AdminPages component and update dark theme code --- app/react/src/components/AdminPages.js | 71 ++++++++++++++++++++++++++ app/views/admin.html | 1 - public/assets/css/admin/main.css | 5 -- 3 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 app/react/src/components/AdminPages.js diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js new file mode 100644 index 0000000..f70f854 --- /dev/null +++ b/app/react/src/components/AdminPages.js @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; +import { Navigate, Outlet } from 'react-router-dom'; +import { useElement } from '../utils/utils'; + +export default function AdminPages() { + const user = useElement('/api/v2/me'); + const settings = useElement('/api/v2/settings'); + const dark_theme_element = document.getElementById('css-dark'); + const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); + + const toggleTheme = () => { + const is_light_enabled = dark_theme_element.toggleAttribute('disabled'); + setTheme(is_light_enabled ? 'light' : 'dark'); + document.cookie = 'theme=' + (is_light_enabled ? 'light' : 'dark') + ';path=/'; + }; + + if (user === null) { + return ; + } + + return ; +}; \ No newline at end of file diff --git a/app/views/admin.html b/app/views/admin.html index 7c2301c..b13e997 100644 --- a/app/views/admin.html +++ b/app/views/admin.html @@ -7,7 +7,6 @@ - disabled > diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 1d091a4..fc3be39 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -877,11 +877,6 @@ textarea.code { margin: 0 auto 0 10px; } -#toggle-theme[data-theme="dark"] svg:first-of-type, -#toggle-theme:not([data-theme="dark"]) svg:last-of-type { - display: none; -} - .admin > .content > *:first-child { display: flex; align-items: center; From 6bd94f97ef6a4279df3f0d870e491083dfea2a6c Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 18:15:24 +0200 Subject: [PATCH 017/334] Redirect to dashboard if user is logged in and update routes --- app/react/src/index.js | 3 +-- app/react/src/pages/Login.js | 12 +++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index 7942a42..8408692 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -12,10 +12,9 @@ const App = () => { return - }/> + }/> }> }/> - }/> 404 Not Found}/> diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index 505760c..cadc1d2 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -1,8 +1,9 @@ import React, { useState } from 'react'; -import { makeRequest } from '../utils/utils'; +import { makeRequest, useElement } from '../utils/utils'; import { useNavigate } from 'react-router-dom'; export default function Login() { + const user = useElement('/api/v2/me'); const logo = document.querySelector('meta[name="logo"]')?.content; const [ loading, setLoading ] = useState(false); const [ email, setEmail ] = useState(''); @@ -45,6 +46,15 @@ export default function Login() { }).finally(() => setLoading(false)); }; + if (user === undefined) { + return <>; + } + + if (user) { + navigate('/console/dashboard'); + return null; + } + return
{logo && } From 1fbdd5e6b3efab7740ed11ab9c7229c3c6bdc9da Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 18:29:31 +0200 Subject: [PATCH 018/334] Add logout --- app/react/src/components/AdminPages.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index f70f854..6908104 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -1,13 +1,14 @@ import React, { useState } from 'react'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; -import { Navigate, Outlet } from 'react-router-dom'; +import { Navigate, Outlet, useNavigate } from 'react-router-dom'; import { useElement } from '../utils/utils'; export default function AdminPages() { + const dark_theme_element = document.getElementById('css-dark'); const user = useElement('/api/v2/me'); const settings = useElement('/api/v2/settings'); - const dark_theme_element = document.getElementById('css-dark'); const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); + const navigate = useNavigate(); const toggleTheme = () => { const is_light_enabled = dark_theme_element.toggleAttribute('disabled'); @@ -15,8 +16,13 @@ export default function AdminPages() { document.cookie = 'theme=' + (is_light_enabled ? 'light' : 'dark') + ';path=/'; }; + const logout = () => { + localStorage.removeItem('auth_token'); + navigate('/console', { replace: true }); + }; + if (user === null) { - return ; + return ; } return
@@ -61,9 +67,9 @@ export default function AdminPages() {
{theme == 'light' ? : }
- +
- +
From 700c986378fe58cffae5cc9cf7c8e7fd5c535c7f Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 18:42:32 +0200 Subject: [PATCH 019/334] Update links --- app/react/src/components/AdminPages.js | 42 +++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index 6908104..983072e 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; -import { Navigate, Outlet, useNavigate } from 'react-router-dom'; +import { Link, Navigate, Outlet, useNavigate } from 'react-router-dom'; import { useElement } from '../utils/utils'; export default function AdminPages() { @@ -28,46 +28,46 @@ export default function AdminPages() { return
; +}; + +export const formatDate = (timestamp, timezone, locale) => { + return new Intl.DateTimeFormat(locale, { + timeZone: timezone, + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(timestamp * 1000)); +}; + +export const formatSize = (bytes) => { + if (bytes === 0) { + return '0B'; + } + + const factor = Math.floor((bytes.toString().length - 1) / 3); + const size = bytes / Math.pow(1024, factor); + + return `${size.toFixed(2)}${[ 'B', 'kB', 'MB', 'GB', 'TB' ][factor] ?? ''}`; +}; + +export const getContentUrl = (path = '') => { + const content_path = document.querySelector('meta[name="content_path"]')?.content || '/'; + return '/' + content_path + '/' + path.replace(/^\/+|\/+$/g, ''); +}; + +export const ImageDialog = ({ onSave, onClose }) => { + const user = useElement('/api/v2/me'); + const settings = useElement('/api/v2/settings'); + const [ path, setPath ] = useState(''); + const { data: files_req, isLoading: is_loading, refetch: refetch_files } = useRequest({ + method: 'GET', + url: `/api/v2/media?images=1&path=${path}`, + }); + const folders = path.split('/'); + const input_ref = useRef(null); + + const uploadFile = async (e) => { + makeRequest({ + method: 'POST', + url: `/api/v2/media/upload?path=${path}`, + data: { + file: e.target.files[0], + }, + }).finally(() => { + refetch_files(); + input_ref.current.value = ''; + }); + }; + + const ListingContent = () => { + const files = files_req ? files_req.data?.data : []; + + if (is_loading) { + return ; + } + + return <> +
+
+
Information
+
Last modification
+
+ {files.map(file => { + const file_path = getContentUrl(file.path); + return
{ + if (file.is_file) { + onSave(file.path); + onClose(); + } else { + setPath(file.path); + } + }} + > +
+ {file.is_file + ? e.stopPropagation()}> + + + :
+ +
} + {file.name} +
+
+ {file.is_file &&

{formatSize(file.size)}

} +

{file.mime}

+
+
{formatDate(file.time, settings.timezone, settings.language)}
+
; + })} + {files.length == 0 && No items} + ; + }; + + return createPortal(
+
+
+
+

Image picker

+ onClose()}> +
+
+
+ + + +
+
+
+
+ +
+
+
+ {folders.map((folder, i) => <> +
setPath(folders.slice(0, i + 1).join('/'))}>{i == 0 ? : folder}
+ / + )} +
+
+
+
, document.querySelector('body')); }; \ No newline at end of file From 06dba4e360c9031ed4d64b19646a70091627b3c8 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 4 Oct 2025 23:52:59 +0200 Subject: [PATCH 032/334] Dev of new settings page --- app/react/src/pages/Settings.js | 99 +++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 2135782..e92c925 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -1,13 +1,80 @@ import React, { useEffect, useState } from 'react'; -import { LoadingPage, MenuButton } from '../utils/utils'; +import { getContentUrl, ImageDialog, Input, LoadingPage, MenuButton, Switch } from '../utils/utils'; import { IconCode, IconDatabase, IconNote, IconServer, IconSettings, IconSync, IconTerminal } from '../utils/icons'; import { useLocation, useOutletContext } from 'react-router-dom'; +const General = ({ data, setData }) => { + const [ open_image_dialog, setOpenImageDialog ] = useState(false); + + return
+
+
+ + setOpenImageDialog(true)}/> + {open_image_dialog && setData({ ...data, logo: path })} onClose={() => setOpenImageDialog(false)}/>} +
+
+ + setData({ ...data, title: e.target.value })} charCount={true}/> +
+
+
+ + setData({ ...data, blog_url: e.target.value })}/> +
+
+ + setData({ ...data, rss: e.target.value })}/> +
+
+
+
+ + +
+
+ + setData({ ...data, per_page: e.target.value })}/> +
+
+
+
+ + System language + +
+
+ + Must follow a valid ICU date format + setData({ ...data, date_format: e.target.value })}/> +
+
+
+
+ + +
+
+ + setData({ ...data, maintenance: e.target.checked })}/> +
+
+
+
; +}; + export default function Settings() { const version = document.querySelector('meta[name="version"]')?.content; const location = useLocation(); const [ hash, setHash ] = useState(location.hash); const { user, settings } = useOutletContext(); + const [ data, setData ] = useState(undefined); useEffect(() => { const onHashChange = () => setHash(window.location.hash); @@ -15,11 +82,20 @@ export default function Settings() { return () => window.removeEventListener('hashchange', onHashChange); }, []); - if (!settings) { + useEffect(() => { + setData(settings); + }, [ settings ]); + + const save = e => { + e.preventDefault(); + console.log(data); + }; + + if (!data) { return ; } - return (
+ return (
@@ -33,17 +109,20 @@ export default function Settings() { + {settings && <> + {hash == '#general' && } + }
From ff95e51ecf5126f05ae47bac88032eab67f76118 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 5 Oct 2025 04:17:43 +0200 Subject: [PATCH 033/334] Add settings post endpoint --- app/bootstrap/routes.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index c9b02c2..04aa245 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1290,6 +1290,29 @@ return json_encode([ 'success' => $success ]); }); + $router->post('json:api/v2/settings', function() use ($db) { + if (!\Aurora\App\Permission::can('edit_settings')) { + http_response_code(403); + exit; + } + + try { + $db->connection->beginTransaction(); + + foreach ($_POST as $key => $val) { + $db->replace('settings', [ 'key' => $key, 'value' => $val ]); + } + + $success = $db->connection->commit(); + } catch (\PDOException $e) { + $db->connection->rollBack(); + error_log($e->getMessage()); + $success = false; + } + + return json_encode([ 'success' => $success ]); + }); + $router->get('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { $mod_str = $_GET['mod'] ?? ''; switch ($mod_str) { From 1eebb013600833a048031e6d83a821f241de68a0 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 5 Oct 2025 04:17:57 +0200 Subject: [PATCH 034/334] Fix form data in makeRequest --- app/react/src/utils/utils.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 9e768b4..108c7d7 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -8,7 +8,15 @@ export const makeRequest = async ({ method = 'GET', url, data = null }) => { const form_data = new FormData(); if (data) { - Object.keys(data).forEach(key => form_data.append(key, data[key])); + Object.keys(data).forEach(key => { + let val = data[key]; + + if (typeof val === 'boolean') { + val = val ? '1' : '0'; + } + + form_data.append(key, val); + }); } return axios({ From b4f3f6fb7704afdab137fcc7e16cf83c2d22f5d1 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 5 Oct 2025 19:15:51 +0200 Subject: [PATCH 035/334] Minor fixes --- app/bootstrap/routes.php | 2 +- app/controllers/modules/Post.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 04aa245..5b6263a 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -19,7 +19,7 @@ $_SESSION['user'] = $db->query('SELECT * FROM users WHERE id = ? AND status', $_SESSION['user']['id'])->fetch(); } - if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'admin') && !Helper::isValidId($_SESSION['user']['id'] ?? false)) { + if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'admin') && !str_starts_with(Helper::getCurrentPath(), 'api') && !Helper::isValidId($_SESSION['user']['id'] ?? false)) { echo $view->get("$theme_dir/information.html", [ 'description' => $lang->get('under_maintenance'), 'subdescription' => $lang->get('come_back_soon'), diff --git a/app/controllers/modules/Post.php b/app/controllers/modules/Post.php index 91fe2dc..19d0951 100755 --- a/app/controllers/modules/Post.php +++ b/app/controllers/modules/Post.php @@ -115,11 +115,16 @@ public function getCondition(array $filters): string $where = []; if (isset($filters['status']) && $filters['status'] !== '') { - $where[] = match (strval($filters['status'])) { + $val = match (strval($filters['status'])) { '1' => 'posts.status AND posts.published_at <= ' . time(), '0' => 'posts.status = 0', 'scheduled' => 'posts.status AND posts.published_at > ' . time(), + default => '', }; + + if ($val) { + $where[] = $val; + } } if (isset($filters['user']) && $filters['user'] !== '') { From 464393aadc343ea7c88847c417600d51d8dff5bd Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 5 Oct 2025 19:16:09 +0200 Subject: [PATCH 036/334] Add textarea component --- app/react/src/utils/utils.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 108c7d7..17255c2 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -73,6 +73,15 @@ export const Input = (props) => { ; }; +export const Textarea = (props) => { + const char_count = props.value?.length || 0; + + return <> + + {props.charCount && {char_count} character{char_count !== 1 ? 's' : ''}} + +}; + export const Switch = (props) => { const ref = useRef(null); From 0f392bef5fe101a6b1e1cdc99d9e4bdad4f11d91 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 5 Oct 2025 19:16:24 +0200 Subject: [PATCH 037/334] Dev of meta section in settings page --- app/react/src/pages/Settings.js | 39 +++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index e92c925..7109c03 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { getContentUrl, ImageDialog, Input, LoadingPage, MenuButton, Switch } from '../utils/utils'; +import { getContentUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea } from '../utils/utils'; import { IconCode, IconDatabase, IconNote, IconServer, IconSettings, IconSync, IconTerminal } from '../utils/icons'; import { useLocation, useOutletContext } from 'react-router-dom'; @@ -62,19 +62,43 @@ const General = ({ data, setData }) => {
- setData({ ...data, maintenance: e.target.checked })}/> + setData({ ...data, maintenance: e.target.checked })}/>
; }; +const Meta = ({ data, setData }) => { + return
+
+
+ + setData({ ...data, meta_title: e.target.value })} charCount/> +
+
+ + +
+ + +
+
+
} +
; +}; + export default function Settings() { const version = document.querySelector('meta[name="version"]')?.content; const location = useLocation(); @@ -211,6 +284,7 @@ export default function Settings() { {hash == '#general' && } {hash == '#meta' && } {hash == '#data' && } + {hash == '#advanced' && } }
From c13c9841cb79aef437e5c5e35c09e08453407b5c Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 01:11:59 +0200 Subject: [PATCH 048/334] Dev info and code sections --- app/bootstrap/routes.php | 17 +++++++ app/react/src/pages/Settings.js | 84 ++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 4942406..63225a8 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1389,6 +1389,23 @@ return json_encode([ 'success' => $success ]); }); + $router->get('json:api/v2/server', function() use ($db) { + if (!\Aurora\App\Permission::can('edit_settings')) { + http_response_code(403); + exit; + } + + return json_encode([ + 'os' => php_uname('s') . ' ' . php_uname('r'), + 'php_version' => phpversion(), + 'db_dsn' => $db->dsn, + 'root_folder' => rtrim(\Aurora\Core\Helper::getPath(), '/'), + 'date' => date('Y-m-d H:i:s'), + 'memory_limit' => \Aurora\Core\Helper::getPhpSize(ini_get('memory_limit')), + 'file_size_limit' => \Aurora\App\Media::getMaxUploadFileSize(), + ]); + }); + $router->get('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { $mod_str = $_GET['mod'] ?? ''; switch ($mod_str) { diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index adeb6e9..d0ec4c4 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { downloadFile, getContentUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea } from '../utils/utils'; +import { downloadFile, formatSize, getContentUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea } from '../utils/utils'; import { IconCode, IconDatabase, IconNote, IconServer, IconSettings, IconSync, IconTerminal } from '../utils/icons'; import { useLocation, useOutletContext } from 'react-router-dom'; @@ -222,6 +222,86 @@ const Advanced = ({ data, setData, user }) => {
; }; +const Info = () => { + const [ server, setServer ] = useState(undefined); + + useEffect(() => { + makeRequest({ + method: 'GET', + url: '/api/v2/server', + }).then(res => setServer(res?.data)); + }, []); + + if (!server) { + return null; + } + + return
+
+
+ + {server.os} +
+
+ + {server.php_version} +
+
+ + {server.db_dsn} +
+
+ + {server.host_name} +
+
+ + {server.root_folder} +
+
+ + {server.date} +
+
+ + {formatSize(server.memory_limit)} +
+
+ + The value is the lowest possible value between the post_max_size and the upload_max_filesize options of your PHP configuration. + {formatSize(server.file_size_limit)} +
+
+
; +}; + +const Code = ({ data, setData }) => { + return
+
+
+ + Code here will be injected into the header of all pages. + +
+
+ + Code here will be injected into the footer of all pages. + +
+
+ + Code here will be injected at the bottom of all post pages. Useful for things like adding a comment system. + +
+
+ + Code here will be injected into the editor of all pages. + +
+
+
; +}; + export default function Settings() { const version = document.querySelector('meta[name="version"]')?.content; const location = useLocation(); @@ -285,6 +365,8 @@ export default function Settings() { {hash == '#meta' && } {hash == '#data' && } {hash == '#advanced' && } + {hash == '#info' && } + {hash == '#code' && } }
From 116b64faedd3e63774a1a477f48a7100ee7b97aa Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 01:31:18 +0200 Subject: [PATCH 049/334] Unify sections code --- app/react/src/pages/Settings.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index d0ec4c4..7b6f347 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -309,6 +309,15 @@ export default function Settings() { const { user, settings } = useOutletContext(); const [ data, setData ] = useState(undefined); const [ loading, setLoading ] = useState(false); + const SECTIONS = [ + { id: 'general', name: 'General', icon: IconSettings, section: General }, + { id: 'meta', name: 'Meta', icon: IconNote, section: Meta }, + { id: 'data', name: 'Data', icon: IconDatabase, section: Data }, + { id: 'advanced', name: 'Advanced', icon: IconTerminal, section: Advanced }, + { id: 'info', name: 'Server Info', icon: IconServer, section: Info }, + { id: 'code', name: 'Code', icon: IconCode, section: Code }, + { id: 'update', name: 'Update', icon: IconSync, section: <> }, + ]; useEffect(() => { const onHashChange = () => setHash(window.location.hash); @@ -349,25 +358,12 @@ export default function Settings() {
- General - Meta - Data - Advanced - Server Info - Code - Update + {SECTIONS.map(section => {section.name})}

Version: {version}

- {settings && <> - {hash == '#general' && } - {hash == '#meta' && } - {hash == '#data' && } - {hash == '#advanced' && } - {hash == '#info' && } - {hash == '#code' && } - } + {settings && SECTIONS.map(section => (hash == ('#' + section.id) && ))}
From 2df361535936de6d431b451ae39c3fead13b6643 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 14:02:42 +0200 Subject: [PATCH 050/334] Hide Update section --- app/react/src/pages/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 7b6f347..21bc903 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -316,7 +316,7 @@ export default function Settings() { { id: 'advanced', name: 'Advanced', icon: IconTerminal, section: Advanced }, { id: 'info', name: 'Server Info', icon: IconServer, section: Info }, { id: 'code', name: 'Code', icon: IconCode, section: Code }, - { id: 'update', name: 'Update', icon: IconSync, section: <> }, + //{ id: 'update', name: 'Update', icon: IconSync, section: <> }, ]; useEffect(() => { From f5cad45979e05fe488cc3f9dc55decd0eadd9cd2 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 18:06:28 +0200 Subject: [PATCH 051/334] Avoid usage of react-query --- app/react/src/utils/utils.js | 71 ++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 9a539d9..2424c91 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -1,7 +1,6 @@ -import React, { useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { useQuery } from '@tanstack/react-query'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { IconFolderFill, IconHome, IconUploadFile, IconX } from './icons'; +import { createPortal } from 'react-dom'; import axios from 'axios'; export const makeRequest = async ({ method = 'GET', url, data = null, options = {} }) => { @@ -19,31 +18,57 @@ export const makeRequest = async ({ method = 'GET', url, data = null, options = }); } - return axios({ - method: method, - url: url, - data: form_data, - headers: { - Authorization: `Bearer ${localStorage.getItem('auth_token')}`, - }, - ...options, - }).catch(err => { + try { + const res = await axios({ + method, + url, + data: form_data, + headers: { + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + }, + ...options, + }); + + return res; + } catch (err) { console.error(err); - }); + throw err; + } }; -export const useRequest = ({ method = 'GET', url, data = null, options = {} }) => { - return useQuery({ - queryKey: [ url, method, data, localStorage.getItem('auth_token') ], - queryFn: () => makeRequest({ method, url, data }), - staleTime: 5 * 60 * 1000, // 5 minutes - refetchOnWindowFocus: true, - ...options - }); +export const useRequest = (params, dependencies = []) => { + const [ data, setData ] = useState(null); + const [ is_loading, setIsLoading ] = useState(true); + const [ is_error, setIsError ] = useState(false); + + const fetch = useCallback(async () => { + setIsLoading(true); + setIsError(false); + + try { + const res = await makeRequest(params); + setData(res); + } catch (err) { + setIsError(true); + } finally { + setIsLoading(false); + } + }, [ JSON.stringify(params) ]); + + useEffect(() => { + fetch(); + }, dependencies); + + return { + data: data, + is_loading: is_loading, + is_error: is_error, + refetch: fetch, + }; }; export const useElement = (url) => { - const { data: data, isLoading: is_loading, isError: is_error } = useRequest({ + const { data, is_loading, is_error } = useRequest({ url: url, staleTime: 0, }); @@ -123,7 +148,7 @@ export const ImageDialog = ({ onSave, onClose }) => { const user = useElement('/api/v2/me'); const settings = useElement('/api/v2/settings'); const [ path, setPath ] = useState(''); - const { data: files_req, isLoading: is_loading, refetch: refetch_files } = useRequest({ + const { data: files_req, is_loading, refetch: refetch_files } = useRequest({ method: 'GET', url: `/api/v2/media?images=1&path=${path}`, }); From 15e2dbc9642c4c7b1b707c0921fc529eeb2e2834 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 18:06:40 +0200 Subject: [PATCH 052/334] Update calls to useRequest --- app/react/src/pages/Dashboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/pages/Dashboard.js b/app/react/src/pages/Dashboard.js index 4f3df74..a072072 100644 --- a/app/react/src/pages/Dashboard.js +++ b/app/react/src/pages/Dashboard.js @@ -5,11 +5,11 @@ import { useOutletContext } from 'react-router-dom'; export default function Dashboard() { const { settings } = useOutletContext(); - const { data: links_req, isLoading: is_loading_links } = useRequest({ + const { data: links_req, is_loading: is_loading_links } = useRequest({ method: 'GET', url: '/api/v2/links', }); - const { data: posts_req, isLoading: is_loading_posts } = useRequest({ + const { data: posts_req, is_loading: is_loading_posts } = useRequest({ method: 'GET', url: '/api/v2/posts?limit=6&status=1&order=published_at&sort=desc', }); From ca191734cb0fa40aabedb1f4c11034d9bb19871b Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 18:32:10 +0200 Subject: [PATCH 053/334] Refactor --- app/react/src/utils/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 2424c91..9c9c034 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -69,8 +69,8 @@ export const useRequest = (params, dependencies = []) => { export const useElement = (url) => { const { data, is_loading, is_error } = useRequest({ + method: 'GET', url: url, - staleTime: 0, }); if (is_loading) { From ac6aae301cf440f70e94abc7d7b56f39c92960ba Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 22:56:23 +0200 Subject: [PATCH 054/334] Dev of table --- app/react/src/components/Table.js | 123 ++++++++++++++++++++++++++++++ app/react/src/index.js | 2 + app/react/src/pages/Links.js | 67 ++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 app/react/src/components/Table.js create mode 100644 app/react/src/pages/Links.js diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js new file mode 100644 index 0000000..3c1bbf0 --- /dev/null +++ b/app/react/src/components/Table.js @@ -0,0 +1,123 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { MenuButton, useRequest } from '../utils/utils'; +import { IconGlass } from '../utils/icons'; + +const DefaultHeader = ({ title, addLink = null }) => { + return <> +
+ +
+

{title}

+   + +
+
+ {addLink && + New} + ; +}; + +const getQueryString = (filters, search, page) => { + let values = {}; + + Object.keys(filters).map(key => { + let val = filters[key].options.find(opt => opt.selected)?.key; + if (val) { + values[key] = val; + } + }); + + if (search) { + values.search = search; + } + + if (page > 1) { + values.page = page; + } + + return (new URLSearchParams(values)).toString(); +}; + +export const Table = ({ + url, + title = '', + CustomHeader = null, + ExtraHeader = null, + addLink = false, + filters: initialFilters = [], + columns = [], +}) => { + const params = useMemo(() => new URLSearchParams(window.location.search), []); + const [ page, setPage ] = useState(params.get('page') ? parseInt(params.get('page')) : 1); + const [ search, setSearch ] = useState(params.get('search') || ''); + const [ filters, setFilters ] = useState(initialFilters); // TODO initialize from params + const [ query_string, setQueryString ] = useState(getQueryString(filters, search, page)); + const { data: page_req, is_loading, is_error, fetch } = useRequest({ + method: 'GET', + url: url + (query_string ? `?${query_string}` : ''), + data: {}, + }); + + useEffect(() => { + setQueryString(getQueryString(filters, search, page)); + }, [ filters ]); + + useEffect(() => { + fetch(); + }, [ query_string ]); + + const Filter = ({ id }) => { + const filter = filters[id]; + + return
+ {filter.title && } + +
; + }; + + if (is_loading) return

Cargando...

; + if (is_error) return

Error al cargar los datos.

; + + return <> +
+ {CustomHeader ? : } +
+ { + e.preventDefault(); + setPage(1); + let aux = getQueryString(filters, search, 1); + if (aux !== query_string) { + setQueryString(aux); + } else { + fetch(); + } + }}> + {Object.keys(filters).map(key => )} + setSearch(e.target.value)}/> + + + {ExtraHeader && } +
+
+
+ {columns.filter(c => c.condition === undefined || c.condition).map(c =>
{c.title}
)} +
+
+
+
+
+ + ; +}; diff --git a/app/react/src/index.js b/app/react/src/index.js index 7718d93..c958b5d 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -6,6 +6,7 @@ import AdminPages from './components/AdminPages'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Settings from './pages/Settings'; +import Links from './pages/Links'; const App = () => { const query_client = new QueryClient(); @@ -16,6 +17,7 @@ const App = () => { }/> }> }/> + }/> }/> 404 Not Found
}/> diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/Links.js new file mode 100644 index 0000000..416d2c8 --- /dev/null +++ b/app/react/src/pages/Links.js @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react'; +import { Table } from '../components/Table'; +import { useNavigate } from 'react-router-dom'; + +export default function Links() { + const navigate = useNavigate(); + return
+ navigate(`/console/links/edit?id=${link.id}`)} + filters={{ + status: { + title: 'Status', + options: [ + { key: '', title: 'All' }, + { key: '1', title: 'Active' }, + { key: '0', title: 'Inactive' }, + ], + }, + order: { + title: 'Sort by', + options: [ + { key: 'title', title: 'Title' }, + { key: 'url', title: 'URL' }, + { key: 'status', title: 'Status' }, + { key: 'order', title: 'Order' }, + ], + }, + sort: { + options: [ + { key: 'asc', title: 'Ascending' }, + { key: 'desc', title: 'Descending' }, + ], + }, + }} + columns={[ + { + title: '', + class: 'w100', + content: link => <>, + }, + { + title: 'URL', + class: 'w20', + content: link => <>, + }, + { + title: 'Status', + class: 'w20', + content: link => <>, + }, + { + title: 'Order', + class: 'w10 numeric', + content: link => <>, + }, + { + title: '', + class: 'w10 row-actions', + content: link => <>, + }, + ]} + /> + +} \ No newline at end of file From 2d1c6aa6937d5156f3b23274d6959b71e8938a88 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 22:56:29 +0200 Subject: [PATCH 055/334] Update useRequest --- app/react/src/utils/utils.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 9c9c034..e3d90cb 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -36,7 +36,7 @@ export const makeRequest = async ({ method = 'GET', url, data = null, options = } }; -export const useRequest = (params, dependencies = []) => { +export const useRequest = (params) => { const [ data, setData ] = useState(null); const [ is_loading, setIsLoading ] = useState(true); const [ is_error, setIsError ] = useState(false); @@ -55,15 +55,11 @@ export const useRequest = (params, dependencies = []) => { } }, [ JSON.stringify(params) ]); - useEffect(() => { - fetch(); - }, dependencies); - return { data: data, is_loading: is_loading, is_error: is_error, - refetch: fetch, + fetch: fetch, }; }; From d2ad08d720731130ac91bea996e64d65ae32a9a5 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 23:37:26 +0200 Subject: [PATCH 056/334] Improve mod endpoint --- app/bootstrap/routes.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 63225a8..64560ab 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1427,6 +1427,9 @@ return json_encode([ 'data' => $files, 'meta' => [ + 'current_page' => 1, + 'per_page' => false, + 'prev_page' => false, 'next_page' => false, ], ]); @@ -1435,13 +1438,16 @@ return; } - $page = $_GET['page'] ?? 1; + $page = (int) max($_GET['page'] ?? 1, 1); $per_page = $kernel->config('per_page'); $where = $mod->getCondition($_GET); return json_encode([ 'data' => $mod->getPage($page, $per_page, $where, $_GET['order'] ?? $mod::DEFAULT_ORDER, ($_GET['sort'] ?? ($mod::DEFAULT_SORT ?? 'asc')) == 'asc'), 'meta' => [ + 'current_page' => $page, + 'per_page' => $per_page, + 'prev_page' => $page > 1, 'next_page' => $mod->isNextPageAvailable($page, $per_page, $where), ], ]); From d981d3bc46d7aa20190d89737df4e065bcf3c13d Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 23:37:33 +0200 Subject: [PATCH 057/334] Update listing-row style --- public/assets/css/admin/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index ed2c3c3..a77f121 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -1144,6 +1144,7 @@ textarea.code { text-decoration: none; color: var(--main-color); gap: 16px; + cursor: pointer; } .listing-row div { From 1966ab768664be3a73526af2907763c59056314c Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 23:37:45 +0200 Subject: [PATCH 058/334] Dev of links endpoint --- app/react/src/pages/Links.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/Links.js index 416d2c8..3cdd9f9 100644 --- a/app/react/src/pages/Links.js +++ b/app/react/src/pages/Links.js @@ -9,7 +9,7 @@ export default function Links() { url="/api/v2/links" title="Links" addLink="/links/new" - onClick={link => navigate(`/console/links/edit?id=${link.id}`)} + rowOnClick={link => navigate(`/console/links/edit?id=${link.id}`)} filters={{ status: { title: 'Status', @@ -39,22 +39,22 @@ export default function Links() { { title: '', class: 'w100', - content: link => <>, + content: link =>

{link.title}

, }, { title: 'URL', class: 'w20', - content: link => <>, + content: link => link.url, }, { title: 'Status', class: 'w20', - content: link => <>, + content: link => {link.status == 1 ? 'Active' : 'Inactive'}, }, { title: 'Order', class: 'w10 numeric', - content: link => <>, + content: link => link.order, }, { title: '', From 26fe7e9afcd7ceb0e187d9c99664bfe5f6b96bbb Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 23:37:57 +0200 Subject: [PATCH 059/334] Dev of table --- app/react/src/components/Table.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index 3c1bbf0..7fc854f 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -45,12 +45,14 @@ export const Table = ({ addLink = false, filters: initialFilters = [], columns = [], + rowOnClick = null, }) => { const params = useMemo(() => new URLSearchParams(window.location.search), []); const [ page, setPage ] = useState(params.get('page') ? parseInt(params.get('page')) : 1); const [ search, setSearch ] = useState(params.get('search') || ''); const [ filters, setFilters ] = useState(initialFilters); // TODO initialize from params const [ query_string, setQueryString ] = useState(getQueryString(filters, search, page)); + const [ rows, setRows ] = useState([]); const { data: page_req, is_loading, is_error, fetch } = useRequest({ method: 'GET', url: url + (query_string ? `?${query_string}` : ''), @@ -65,6 +67,13 @@ export const Table = ({ fetch(); }, [ query_string ]); + useEffect(() => { + const page_rows = page_req?.data?.data || null; + if (page_rows) { + setRows(page == 1 ? page_rows : [ ...rows, ...page_rows ]); + } + }, [ page_req ]); + const Filter = ({ id }) => { const filter = filters[id]; @@ -112,12 +121,24 @@ export const Table = ({
- {columns.filter(c => c.condition === undefined || c.condition).map(c =>
{c.title}
)} + {columns.filter(c => c.condition === undefined || c.condition).map(c =>
{c.title}
)}
+ {rows.map((row, index) => ( +
rowOnClick ? rowOnClick(row, e) : null}> + {columns.filter(c => c.condition === undefined || c.condition).map(c =>
{c.content(row, index)}
)} +
+ ))}
- + ; }; From d493695d9986850a7f298f751b381deb7e9c9ed2 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 11 Oct 2025 23:45:34 +0200 Subject: [PATCH 060/334] Fix load more button --- app/react/src/components/Table.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index 7fc854f..59fbcad 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -132,13 +132,6 @@ export const Table = ({ ))} - + {page_req?.data?.meta?.next_page && } ; }; From dee0dad16e679982eb878bec36b614893b3601b5 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 01:58:48 +0200 Subject: [PATCH 061/334] Fix table --- app/react/src/components/Table.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index 59fbcad..0c6c34d 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -50,7 +50,8 @@ export const Table = ({ const params = useMemo(() => new URLSearchParams(window.location.search), []); const [ page, setPage ] = useState(params.get('page') ? parseInt(params.get('page')) : 1); const [ search, setSearch ] = useState(params.get('search') || ''); - const [ filters, setFilters ] = useState(initialFilters); // TODO initialize from params + const [ input_search, setInputSearch ] = useState(params.get('search') || ''); + const [ filters, setFilters ] = useState(initialFilters); const [ query_string, setQueryString ] = useState(getQueryString(filters, search, page)); const [ rows, setRows ] = useState([]); const { data: page_req, is_loading, is_error, fetch } = useRequest({ @@ -61,7 +62,7 @@ export const Table = ({ useEffect(() => { setQueryString(getQueryString(filters, search, page)); - }, [ filters ]); + }, [ filters, search, page ]); useEffect(() => { fetch(); @@ -87,6 +88,7 @@ export const Table = ({ }); setFilters({ ...filters, [id]: aux }); + setPage(1); }}> {Object.keys(filter.options).map(opt_key =>
+ New, + onClick: () => navigate('/console/links/new'), + }, + ]} rowOnClick={link => navigate(`/console/links/edit?id=${link.id}`)} filters={{ status: { @@ -35,6 +42,14 @@ export default function Links() { ], }, }} + options={[ + { + title: 'Delete', + class: 'danger', + condition: Boolean(user?.actions?.edit_links), + onClick: () => alert('Delete clicked'), + }, + ]} columns={[ { title: '', @@ -59,7 +74,21 @@ export default function Links() { { title: '', class: 'w10 row-actions', - content: link => <>, + content: link => <> /*
+ include('icons/dots.svg') ?> + +
*/, }, ]} /> From 4b4d183b3e361bbad80fa0ed15a45d3c1b6b93e7 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 14:09:22 +0200 Subject: [PATCH 070/334] Improve makeRequest --- app/react/src/utils/utils.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 0a9ed0a..fe4a00f 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -18,14 +18,20 @@ export const makeRequest = async ({ method = 'GET', url, data = null, options = }); } + let headers = { + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + }; + + if ([ 'DELETE', 'PUT' ].includes(method)) { + headers['Content-Type'] = 'application/json'; + } + try { const res = await axios({ method, url, + headers: headers, data: form_data, - headers: { - Authorization: `Bearer ${localStorage.getItem('auth_token')}`, - }, ...options, }); From 4e1a624501cdbf3cc3107f999e3d83b7357b6cfa Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 14:14:58 +0200 Subject: [PATCH 071/334] Add options to Links table --- app/react/src/pages/Links.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/Links.js index 5746deb..d141204 100644 --- a/app/react/src/pages/Links.js +++ b/app/react/src/pages/Links.js @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Table } from '../components/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; +import { makeRequest } from '../utils/utils'; export default function Links() { const { user } = useOutletContext(); @@ -47,7 +48,15 @@ export default function Links() { title: 'Delete', class: 'danger', condition: Boolean(user?.actions?.edit_links), - onClick: () => alert('Delete clicked'), + onClick: (links) => { + if (confirm('Are you sure you want to delete the selected links? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/links', + data: { id: links.map(l => l.id) }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, }, ]} columns={[ From 833e16c4cc3ba75d0e3c8c6c51600f8c6d991530 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 14:15:11 +0200 Subject: [PATCH 072/334] Add endpoint to remove items --- app/bootstrap/routes.php | 57 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 1c9c652..5898b3e 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1348,7 +1348,7 @@ return file_get_contents(\Aurora\Core\Helper::getPath(\Aurora\App\Setting::get('log_file'))); }); - $router->delete('json:api/v2/logs', function() use ($lang) { + $router->delete('json:api/v2/logs', function() { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -1406,9 +1406,60 @@ ]); }); - $router->get('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + $router->delete('json:api/v2/{mod}', function() use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + $_POST = json_decode(file_get_contents('php://input'), true); + $ids = array_map(fn($id) => (int) $id, is_array($_POST['id']) ? $_POST['id'] : explode(',', $_POST['id'])); $mod_str = $_GET['mod'] ?? ''; - switch ($mod_str) { + + if (!\Aurora\App\Permission::can("edit_$mod_str")) { + http_response_code(403); + exit; + } + + $success = match ($mod_str) { + 'pages' => $page_mod->remove($ids), + 'posts' => $post_mod->remove($ids), + 'tags' => $tag_mod->remove($ids), + 'links' => $link_mod->remove($ids), + 'users' => (function() use ($user_mod, $ids) { + $valid_ids = []; + + foreach ($user_mod->getPage(null, null, 'users.id IN (' . implode(',', $ids) . ')') as $user) { + if (\Aurora\App\Permission::edit_user($user) && $user['id'] != $GLOBALS['user']['id']) { + $valid_ids[] = $user['id']; + } + } + + $ids = $valid_ids; + return $user_mod->remove($ids); + })(), + 'media' => (function() { + $paths = json_decode($_POST['paths'] ?? '') ?? []; + $done = 0; + + try { + foreach ($paths as $path) { + $done += \Aurora\App\Media::remove($path); + } + + $success = $done == count($paths); + } catch (Exception) { + $success = false; + } + + return $success; + })(), + default => (function() { + http_response_code(404); + exit; + })(), + }; + + return json_encode([ 'success' => $success ]); + }); + + $router->get('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + switch ($_GET['mod'] ?? '') { case 'pages': $mod = $page_mod; break; case 'posts': $mod = $post_mod; break; case 'users': $mod = $user_mod; break; From 77d138ea892b15ae96c710d28190ec6f588f514f Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 16:19:35 +0200 Subject: [PATCH 073/334] Fix styles --- public/assets/css/admin/main.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index eea587f..3b8ba3b 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -1149,7 +1149,7 @@ textarea.code { overflow: hidden; } -.listing-row div:first-of-type { +.listing-row > div:first-of-type { gap: 10px; } @@ -1246,6 +1246,7 @@ textarea.code { .dropdown-menu > * { display: flex; + align-items: center; gap: 10px; padding: 8px; border-radius: var(--border-radius); From 16219980dfbaf4d4b8d0412ae5f8e565efc5e684 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 17:15:38 +0200 Subject: [PATCH 074/334] Add DropdownMenu --- app/react/src/utils/utils.js | 69 +++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index fe4a00f..e2f6e3f 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -123,6 +123,73 @@ export const Switch = (props) => { ; }; +export const DropdownMenu = ({ content, className, options = [] }) => { + const [ open, setOpen ] = useState(false); + const dropdown_ref = useRef(null); + const button_ref = useRef(null); + + useEffect(() => { + let updateActiveDropdown = () => { + const MARGIN = 4; + + if (!dropdown_ref.current || !button_ref.current) { + return; + } + + let btn_rect = button_ref.current.getBoundingClientRect(); + dropdown_ref.current.style.top = (btn_rect.top + btn_rect.height + MARGIN) + 'px'; + dropdown_ref.current.style.left = btn_rect.left + 'px'; + let dropdown_rect = dropdown_ref.current.getBoundingClientRect(); + + if ((dropdown_rect.x + dropdown_rect.width) >= (window.innerWidth - MARGIN)) { + dropdown_ref.current.style.left = ((btn_rect.x - dropdown_rect.width) + btn_rect.width) + 'px'; + } + + if (dropdown_rect.y + dropdown_rect.height >= (window.innerHeight - MARGIN)) { + dropdown_ref.current.style.top = (btn_rect.y - dropdown_rect.height - MARGIN) + 'px'; + } + }; + + let handleClick = e => { + if (!button_ref.current?.contains(e?.target)) { + setOpen(false); + } + }; + + document.addEventListener('scroll', updateActiveDropdown); + window.addEventListener('resize', updateActiveDropdown); + document.addEventListener('click', handleClick, true); + updateActiveDropdown(); + + return () => { + window.removeEventListener('scroll', updateActiveDropdown); + window.removeEventListener('resize', updateActiveDropdown); + document.removeEventListener('click', handleClick, true); + }; + }, [ open ]); + + return
{ + e.stopPropagation(); + if (!dropdown_ref?.current?.contains(e.target)) { + setOpen(!open); + } + }} + dropdown + > + {content} + +
; +}; + export const formatDate = (timestamp, timezone, locale) => { return new Intl.DateTimeFormat(locale, { timeZone: timezone, @@ -258,4 +325,4 @@ export const downloadFile = (data, filename) => { document.body.appendChild(link); link.click(); link.remove(); -}; \ No newline at end of file +}; From 6361bbb83148e7cfd9d84fd92bbb741f085ba2da Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 17:16:04 +0200 Subject: [PATCH 075/334] Dev of three dots button --- app/react/src/pages/Links.js | 36 ++++++++++++++++++++---------------- app/react/src/utils/icons.js | 1 + 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/Links.js index d141204..33b2b22 100644 --- a/app/react/src/pages/Links.js +++ b/app/react/src/pages/Links.js @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; import { Table } from '../components/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { makeRequest } from '../utils/utils'; +import { DropdownMenu, makeRequest } from '../utils/utils'; +import { IconEye, IconThreeDots, IconTrash } from '../utils/icons'; export default function Links() { const { user } = useOutletContext(); @@ -83,21 +84,24 @@ export default function Links() { { title: '', class: 'w10 row-actions', - content: link => <> /*
- include('icons/dots.svg') ?> - -
*/, + content: link => } + className="three-dots" + options={[ + { + onClick: () => window.open(link.url, '_blank').focus(), + content: <> View + }, + { + class: 'danger', + condition: Boolean(user?.actions?.edit_links), + onClick: () => { + + }, + content: <> Delete + }, + ]} + />, }, ]} /> diff --git a/app/react/src/utils/icons.js b/app/react/src/utils/icons.js index d4ad9b4..eb3be73 100644 --- a/app/react/src/utils/icons.js +++ b/app/react/src/utils/icons.js @@ -35,3 +35,4 @@ export const IconWindow = () => ; export const IconX = () => ; export const IconZip = () => ; +export const IconThreeDots = () => ; \ No newline at end of file From 56e8aaf46d6e67097c026da64f1284faec22b320 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 12 Oct 2025 17:18:46 +0200 Subject: [PATCH 076/334] Minor improvements --- app/react/src/components/Table.js | 2 +- app/react/src/pages/Links.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index cf463cc..727760d 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -173,7 +173,7 @@ export const Table = ({
- {columns.filter(c => c.condition === undefined || c.condition).map(c =>
{c.title}
)} + {columns.filter(c => c.condition === undefined || c.condition).map(c =>
{c.title ?? ''}
)}
diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/Links.js index 33b2b22..cbf06c3 100644 --- a/app/react/src/pages/Links.js +++ b/app/react/src/pages/Links.js @@ -62,7 +62,6 @@ export default function Links() { ]} columns={[ { - title: '', class: 'w100', content: link =>

{link.title}

, }, @@ -82,7 +81,6 @@ export default function Links() { content: link => link.order, }, { - title: '', class: 'w10 row-actions', content: link => } From d3d94818b930b59501c0840eb87948110f2b81f4 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 16 Oct 2025 00:07:45 +0200 Subject: [PATCH 077/334] Move check for loading state --- app/react/src/components/AdminPages.js | 4 ++-- app/react/src/pages/Dashboard.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index 983072e..006c108 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; import { Link, Navigate, Outlet, useNavigate } from 'react-router-dom'; -import { useElement } from '../utils/utils'; +import { LoadingPage, useElement } from '../utils/utils'; export default function AdminPages() { const dark_theme_element = document.getElementById('css-dark'); @@ -72,6 +72,6 @@ export default function AdminPages() {
- + {settings ? : } ; }; \ No newline at end of file diff --git a/app/react/src/pages/Dashboard.js b/app/react/src/pages/Dashboard.js index a072072..f6b7dcd 100644 --- a/app/react/src/pages/Dashboard.js +++ b/app/react/src/pages/Dashboard.js @@ -23,7 +23,7 @@ export default function Dashboard() { const total_users = 0; const total_inactive_users = 0; - if (is_loading_links || is_loading_posts || !settings) { + if (is_loading_links || is_loading_posts) { return ; } From 9dee88238bab20e3c727afea7971fb5a67c6bac6 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 16 Oct 2025 00:07:58 +0200 Subject: [PATCH 078/334] Add mod post endpoint --- app/bootstrap/routes.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 5898b3e..f96c44c 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1406,6 +1406,35 @@ ]); }); + $router->post('json:api/v2/{mod}', function() use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + switch ($_GET['mod']) { + case 'pages': $mod = $page_mod; break; + case 'posts': $mod = $post_mod; break; + case 'users': $mod = $user_mod; break; + case 'tags': $mod = $tag_mod; break; + case 'links': $mod = $link_mod; break; + default: + http_response_code(404); + return; + } + + $id = $_GET['id'] ?? ''; + $errors = $mod->checkFields($_POST, $id); + if (!empty($errors)) { + return json_encode([ + 'success' => false, + 'errors' => $errors, + ]); + } + + return json_encode([ + 'success' => Helper::isValidId($id) + ? $mod->save($id, $_POST) + : ($id = $mod->add($_POST)) !== false, + 'id' => $id, + ]); + }); + $router->delete('json:api/v2/{mod}', function() use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { $_POST = json_decode(file_get_contents('php://input'), true); $ids = array_map(fn($id) => (int) $id, is_array($_POST['id']) ? $_POST['id'] : explode(',', $_POST['id'])); From 3e0d4fea45405322a3642bb76f54394a2f021d8f Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 16 Oct 2025 00:09:44 +0200 Subject: [PATCH 079/334] Add id to getCondition methods of mods --- app/controllers/modules/Link.php | 4 ++++ app/controllers/modules/Page.php | 4 ++++ app/controllers/modules/Post.php | 4 ++++ app/controllers/modules/Tag.php | 4 ++++ app/controllers/modules/User.php | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/app/controllers/modules/Link.php b/app/controllers/modules/Link.php index 53d12dd..bdd5438 100755 --- a/app/controllers/modules/Link.php +++ b/app/controllers/modules/Link.php @@ -76,6 +76,10 @@ public function getCondition(array $filters): string { $where = []; + if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) { + $where[] = 'links.id = ' . ((int) $filters['id']); + } + if (isset($filters['status']) && $filters['status'] !== '') { $where[] = 'links.status = ' . ((int) $filters['status']); } diff --git a/app/controllers/modules/Page.php b/app/controllers/modules/Page.php index bc52644..1c9463e 100755 --- a/app/controllers/modules/Page.php +++ b/app/controllers/modules/Page.php @@ -79,6 +79,10 @@ public function getCondition(array $filters): string { $where = []; + if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) { + $where[] = 'pages.id = ' . ((int) $filters['id']); + } + if (isset($filters['status']) && $filters['status'] !== '') { $where[] = 'pages.status = ' . ((int) $filters['status']); } diff --git a/app/controllers/modules/Post.php b/app/controllers/modules/Post.php index 19d0951..2b8196e 100755 --- a/app/controllers/modules/Post.php +++ b/app/controllers/modules/Post.php @@ -114,6 +114,10 @@ public function getCondition(array $filters): string { $where = []; + if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) { + $where[] = 'posts.id = ' . ((int) $filters['id']); + } + if (isset($filters['status']) && $filters['status'] !== '') { $val = match (strval($filters['status'])) { '1' => 'posts.status AND posts.published_at <= ' . time(), diff --git a/app/controllers/modules/Tag.php b/app/controllers/modules/Tag.php index 18dbf8c..f745977 100755 --- a/app/controllers/modules/Tag.php +++ b/app/controllers/modules/Tag.php @@ -78,6 +78,10 @@ public function getCondition(array $filters): string { $where = []; + if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) { + $where[] = 'tags.id = ' . ((int) $filters['id']); + } + if (!empty($filters['search'])) { $search = $this->db->escape($filters['search']); $where[] = "(tags.name LIKE '%$search%' OR tags.slug LIKE '%$search%')"; diff --git a/app/controllers/modules/User.php b/app/controllers/modules/User.php index 5ca8818..702da44 100755 --- a/app/controllers/modules/User.php +++ b/app/controllers/modules/User.php @@ -217,6 +217,10 @@ public function getCondition(array $filters): string { $where = []; + if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) { + $where[] = 'users.id = ' . ((int) $filters['id']); + } + if (isset($filters['status']) && $filters['status'] !== '') { $where[] = 'users.status = ' . ((int) $filters['status']); } From b6abdbd5978cc84c753ebcdac23157cf98cd525b Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 16 Oct 2025 00:24:33 +0200 Subject: [PATCH 080/334] Dev of link page --- app/react/src/index.js | 2 + app/react/src/pages/Link.js | 122 +++++++++++++++++++++++++++++++++++ app/react/src/pages/Links.js | 2 +- 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 app/react/src/pages/Link.js diff --git a/app/react/src/index.js b/app/react/src/index.js index c958b5d..1d05801 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -7,6 +7,7 @@ import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Settings from './pages/Settings'; import Links from './pages/Links'; +import Link from './pages/Link'; const App = () => { const query_client = new QueryClient(); @@ -19,6 +20,7 @@ const App = () => { }/> }/> }/> + }/> 404 Not Found}/> diff --git a/app/react/src/pages/Link.js b/app/react/src/pages/Link.js new file mode 100644 index 0000000..4870f3b --- /dev/null +++ b/app/react/src/pages/Link.js @@ -0,0 +1,122 @@ +import React, { useEffect, useState } from 'react'; +import { Input, LoadingPage, makeRequest, MenuButton, Switch } from '../utils/utils'; +import { IconEye, IconTrash } from '../utils/icons'; +import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; + +export default function Link() { + const { user } = useOutletContext(); + const [ data, setData ] = useState(undefined); + const location = useLocation(); + const navigate = useNavigate(); + const params = new URLSearchParams(location.search); + const [ id, setId ] = useState(params.get('id')); + + useEffect(() => { + if (id) { + makeRequest({ + method: 'GET', + url: `/api/v2/links?id=${id}`, + }).then(res => setData(res?.data?.data[0] ?? null)); + } else { + setData({}); + } + }, []); + + const remove = () => { + if (confirm('Are you sure you want to delete the link? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/links', + data: { id: id }, + }).then(res => { + if (res?.data?.success) { + alert('Done'); + navigate('/console/links', { replace: true }); + } else { + alert('Error'); + } + }); + } + }; + + const submit = e => { + e.preventDefault(); + makeRequest({ + method: 'POST', + url: '/api/v2/links' + (id ? `?id=${id}` : ''), + data: data, + }).then(res => { + alert(res?.data?.success ? 'Done' : 'Error'); + if (res?.data?.id) { + navigate(`/console/links/edit?id=${res.data.id}`, { replace: true }); + setId(res.data.id); + } + }); + }; + + if (data === undefined) { + return ; + } + + if (!data) { + return <>Error; + } + + return ( +
+
+ +

Link

+
+
+ {id && <> + + + } + +
+
+
+
+
+ + setData({...data, title: e.target.value})} + charCount={true} + /> +
+
+ + setData({...data, url: e.target.value})} + charCount={true} + /> +
+
+ + setData({...data, order: e.target.value})} + /> +
+
+ + setData({...data, status: e.target.checked })}/> +
+ {id &&
+ ID: {id} +
} +
+
+ ); +} \ No newline at end of file diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/Links.js index cbf06c3..d7597c9 100644 --- a/app/react/src/pages/Links.js +++ b/app/react/src/pages/Links.js @@ -15,7 +15,7 @@ export default function Links() { topOptions={[ { content: <>+ New, - onClick: () => navigate('/console/links/new'), + onClick: () => navigate('/console/links/edit'), }, ]} rowOnClick={link => navigate(`/console/links/edit?id=${link.id}`)} From f877c56309f680360d06dbe7109e499abca2b389 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 20:23:47 +0200 Subject: [PATCH 081/334] Fix dashboard --- app/bootstrap/routes.php | 12 +++++++++ app/react/src/pages/Dashboard.js | 42 ++++++++++++++------------------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index f96c44c..e15a81d 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1406,6 +1406,18 @@ ]); }); + $router->get('json:api/v2/stats', function() use ($db, $post_mod) { + return json_encode([ + 'total_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 1 ])), + 'total_scheduled_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 'scheduled' ])), + 'total_draft_posts' => $db->count('posts', '', 'status != 1'), + 'total_pages' => $db->count('pages', '', 'status = 1'), + 'total_draft_pages' => $db->count('pages', '', 'status != 1'), + 'total_users' => $db->count('users', '', 'status = 1'), + 'total_inactive_users' => $db->count('users', '', 'status != 1'), + ]); + }); + $router->post('json:api/v2/{mod}', function() use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { switch ($_GET['mod']) { case 'pages': $mod = $page_mod; break; diff --git a/app/react/src/pages/Dashboard.js b/app/react/src/pages/Dashboard.js index f6b7dcd..a9c44e1 100644 --- a/app/react/src/pages/Dashboard.js +++ b/app/react/src/pages/Dashboard.js @@ -1,29 +1,33 @@ -import React, { useState } from 'react'; +import React, { useEffect } from 'react'; import { LoadingPage, MenuButton, useRequest } from '../utils/utils'; import { IconBook, IconPencil, IconTag, IconUser } from '../utils/icons'; import { useOutletContext } from 'react-router-dom'; export default function Dashboard() { const { settings } = useOutletContext(); - const { data: links_req, is_loading: is_loading_links } = useRequest({ + const { data: links_req, is_loading: is_loading_links, fetch: fetch_links } = useRequest({ method: 'GET', url: '/api/v2/links', }); - const { data: posts_req, is_loading: is_loading_posts } = useRequest({ + const { data: posts_req, is_loading: is_loading_posts, fetch: fetch_posts } = useRequest({ method: 'GET', url: '/api/v2/posts?limit=6&status=1&order=published_at&sort=desc', }); + const { data: stats_req, is_loading: is_loading_stats, fetch: fetch_stats } = useRequest({ + method: 'GET', + url: '/api/v2/stats', + }); const links = links_req ? links_req.data?.data : null; const posts = posts_req ? posts_req.data?.data : null; - const total_posts = 0; - const total_scheduled_posts = 0; - const total_draft_posts = 0; - const total_pages = 0; - const total_draft_pages = 0; - const total_users = 0; - const total_inactive_users = 0; + const stats = stats_req ? stats_req.data : null; + + useEffect(() => { + fetch_links(); + fetch_posts(); + fetch_stats(); + }, []); - if (is_loading_links || is_loading_posts) { + if (is_loading_links || is_loading_posts || is_loading_stats) { return ; } @@ -74,25 +78,15 @@ export default function Dashboard() {
Posts - - {total_posts} Published, - {total_scheduled_posts} Scheduled, - {total_draft_posts} Draft - + {stats.total_posts} Published, {stats.total_scheduled_posts} Scheduled, {stats.total_draft_posts} Draft
Pages - - {total_pages} Published, - {total_draft_pages} Draft - + {stats.total_pages} Published, {stats.total_draft_pages} Draft
Users - - {total_users} Active, - {total_inactive_users} Inactive - + {stats.total_users} Active, {stats.total_inactive_users} Inactive
From e6c78b20d4553b1c5fffc4d0a81935b5eceb2c57 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 20:25:29 +0200 Subject: [PATCH 082/334] Fix delete link action --- app/react/src/pages/Links.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/Links.js index d7597c9..db09a99 100644 --- a/app/react/src/pages/Links.js +++ b/app/react/src/pages/Links.js @@ -94,7 +94,13 @@ export default function Links() { class: 'danger', condition: Boolean(user?.actions?.edit_links), onClick: () => { - + if (confirm('Are you sure you want to delete the link? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/links', + data: { id: link.id }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } }, content: <> Delete }, From 522ae9ac2b05b99d7176970ed8676ccb32e095e8 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 20:28:53 +0200 Subject: [PATCH 083/334] Refactor dropdown --- app/react/src/utils/utils.js | 3 +-- public/assets/css/admin/main.css | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index e2f6e3f..3db6cb7 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -170,14 +170,13 @@ export const DropdownMenu = ({ content, className, options = [] }) => { return
{ e.stopPropagation(); if (!dropdown_ref?.current?.contains(e.target)) { setOpen(!open); } }} - dropdown > {content}
+ New, + onClick: () => navigate('/console/tags/edit'), + }, + ]} + rowOnClick={tag => navigate(`/console/tags/edit?id=${tag.id}`)} + filters={{ + order: { + title: 'Sort by', + options: [ + { key: 'name', title: 'Name' }, + { key: 'slug', title: 'Slug' }, + { key: 'posts', title: 'No. posts' }, + ], + }, + sort: { + options: [ + { key: 'asc', title: 'Ascending' }, + { key: 'desc', title: 'Descending' }, + ], + }, + }} + options={[ + { + title: 'Delete', + class: 'danger', + condition: Boolean(user?.actions?.edit_tags), + onClick: (tags) => { + if (confirm('Are you sure you want to delete the selected tags? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/tags', + data: { id: tags.map(l => l.id) }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + }, + ]} + columns={[ + { + class: 'w100', + content: tag =>

{tag.name}

, + }, + { + title: 'Slug', + class: 'w30', + content: tag => tag.slug, + }, + { + title: 'No. posts', + class: 'w10 numeric', + content: tag => tag.posts, + }, + { + class: 'w10 row-actions', + content: tag => } + className="three-dots" + options={[ + { + onClick: () => window.open(`/${settings.blog_url}/tag/${tag.slug}`, '_blank').focus(), + content: <> View + }, + { + class: 'danger', + condition: Boolean(user?.actions?.edit_tags), + onClick: () => { + if (confirm('Are you sure you want to delete the tag? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/tags', + data: { id: tag.id }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + content: <> Delete + }, + ]} + />, + }, + ]} + /> + +} \ No newline at end of file From ba5e40451682f134f74bef754319e908650b3ed7 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 23:00:48 +0200 Subject: [PATCH 085/334] Move tables to folder --- app/react/src/index.js | 4 ++-- app/react/src/pages/{ => tables}/Links.js | 8 ++++---- app/react/src/pages/{ => tables}/Tags.js | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename app/react/src/pages/{ => tables}/Links.js (95%) rename app/react/src/pages/{ => tables}/Tags.js (95%) diff --git a/app/react/src/index.js b/app/react/src/index.js index 63e5823..607245b 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -6,9 +6,9 @@ import AdminPages from './components/AdminPages'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Settings from './pages/Settings'; -import Links from './pages/Links'; +import Links from './pages/tables/Links'; import Link from './pages/Link'; -import Tags from './pages/Tags'; +import Tags from './pages/tables/Tags'; const App = () => { const query_client = new QueryClient(); diff --git a/app/react/src/pages/Links.js b/app/react/src/pages/tables/Links.js similarity index 95% rename from app/react/src/pages/Links.js rename to app/react/src/pages/tables/Links.js index db09a99..e7f410a 100644 --- a/app/react/src/pages/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -1,8 +1,8 @@ -import React, { useEffect, useState } from 'react'; -import { Table } from '../components/Table'; +import React from 'react'; +import { Table } from '../../components/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, makeRequest } from '../utils/utils'; -import { IconEye, IconThreeDots, IconTrash } from '../utils/icons'; +import { DropdownMenu, makeRequest } from '../../utils/utils'; +import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; export default function Links() { const { user } = useOutletContext(); diff --git a/app/react/src/pages/Tags.js b/app/react/src/pages/tables/Tags.js similarity index 95% rename from app/react/src/pages/Tags.js rename to app/react/src/pages/tables/Tags.js index 723257c..ad76afa 100644 --- a/app/react/src/pages/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -1,8 +1,8 @@ import React from 'react'; -import { Table } from '../components/Table'; +import { Table } from '../../components/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, makeRequest } from '../utils/utils'; -import { IconEye, IconThreeDots, IconTrash } from '../utils/icons'; +import { DropdownMenu, makeRequest } from '../../utils/utils'; +import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; export default function Tags() { const { user } = useOutletContext(); From ddb282908b8abcee9f64c99edccaed4acfbba0f7 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 23:12:51 +0200 Subject: [PATCH 086/334] Add pages listing --- app/react/src/index.js | 2 + app/react/src/pages/tables/Pages.js | 118 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 app/react/src/pages/tables/Pages.js diff --git a/app/react/src/index.js b/app/react/src/index.js index 607245b..960a499 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -9,6 +9,7 @@ import Settings from './pages/Settings'; import Links from './pages/tables/Links'; import Link from './pages/Link'; import Tags from './pages/tables/Tags'; +import Pages from './pages/tables/Pages'; const App = () => { const query_client = new QueryClient(); @@ -19,6 +20,7 @@ const App = () => { }/> }> }/> + }/> }/> }/> }/> diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js new file mode 100644 index 0000000..89b3c6e --- /dev/null +++ b/app/react/src/pages/tables/Pages.js @@ -0,0 +1,118 @@ +import React from 'react'; +import { Table } from '../../components/Table'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { DropdownMenu, formatDate, makeRequest } from '../../utils/utils'; +import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; + +export default function Pages() { + const { user, settings } = useOutletContext(); + const navigate = useNavigate(); + + return
+
+ New, + onClick: () => navigate('/console/pages/edit'), + }, + ]} + rowOnClick={page => navigate(`/console/pages/edit?id=${page.id}`)} + filters={{ + status: { + title: 'Status', + options: [ + { key: '', title: 'All' }, + { key: '1', title: 'Published' }, + { key: '0', title: 'Draft' }, + ], + }, + order: { + title: 'Sort by', + options: [ + { key: 'title', title: 'Title' }, + { key: 'status', title: 'Status' }, + { key: 'slug', title: 'Slug' }, + { key: 'edited', title: 'Edited' }, + { key: 'views', title: 'No. views' }, + ], + }, + sort: { + options: [ + { key: 'asc', title: 'Ascending' }, + { key: 'desc', title: 'Descending' }, + ], + }, + }} + options={[ + { + title: 'Delete', + class: 'danger', + condition: Boolean(user?.actions?.edit_pages), + onClick: (pages) => { + if (confirm('Are you sure you want to delete the selected pages? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/pages', + data: { id: pages.map(l => l.id) }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + }, + ]} + columns={[ + { + class: 'w100', + content: page =>

+ {page.title} + {!page.status && Draft} +

, + }, + { + title: 'Slug', + class: 'w20', + content: page => '/' + page.slug, + }, + { + title: 'Edited', + class: 'w20', + content: page => formatDate(page.edited_at), + }, + { + title: 'No. views', + class: 'w10 numeric', + condition: Boolean(settings.views_count), + content: page => page.views, + }, + { + class: 'w10 row-actions', + content: page => } + className="three-dots" + options={[ + { + onClick: () => window.open(`/${page.slug}`, '_blank').focus(), + content: <> View + }, + { + class: 'danger', + condition: Boolean(user?.actions?.edit_pages), + onClick: () => { + if (confirm('Are you sure you want to delete the page? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/pages', + data: { id: page.id }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + content: <> Delete + }, + ]} + />, + }, + ]} + /> + +} \ No newline at end of file From fe26f31ea541995a8ac2797b3de373fbdc19bc2a Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 23:38:48 +0200 Subject: [PATCH 087/334] Minor table fixes --- app/react/src/components/Table.js | 2 +- app/react/src/pages/tables/Links.js | 1 + app/react/src/pages/tables/Pages.js | 1 + app/react/src/pages/tables/Tags.js | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index 727760d..aa20bb5 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -12,7 +12,7 @@ const Header = ({ title, totalItems, selectedItems = 0, options = [] }) => { {selectedItems > 0 && {selectedItems} selected} - {options.map((opt, i) => )} + {options.filter(opt => opt.condition === undefined || opt.condition).map((opt, i) => )} ; }; diff --git a/app/react/src/pages/tables/Links.js b/app/react/src/pages/tables/Links.js index e7f410a..d706c09 100644 --- a/app/react/src/pages/tables/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -15,6 +15,7 @@ export default function Links() { topOptions={[ { content: <>+ New, + condition: Boolean(user?.actions?.edit_links), onClick: () => navigate('/console/links/edit'), }, ]} diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js index 89b3c6e..a13d437 100644 --- a/app/react/src/pages/tables/Pages.js +++ b/app/react/src/pages/tables/Pages.js @@ -15,6 +15,7 @@ export default function Pages() { topOptions={[ { content: <>+ New, + condition: Boolean(user?.actions?.edit_pages), onClick: () => navigate('/console/pages/edit'), }, ]} diff --git a/app/react/src/pages/tables/Tags.js b/app/react/src/pages/tables/Tags.js index ad76afa..56c1bb1 100644 --- a/app/react/src/pages/tables/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -15,6 +15,7 @@ export default function Tags() { topOptions={[ { content: <>+ New, + condition: Boolean(user?.actions?.edit_tags), onClick: () => navigate('/console/tags/edit'), }, ]} From b3c79ac7ec95bd107949c8253b836a0b96c1d5bf Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 23:46:03 +0200 Subject: [PATCH 088/334] Add posts listing --- app/react/src/index.js | 2 + app/react/src/pages/tables/Posts.js | 159 ++++++++++++++++++++++++++++ public/assets/css/admin/main.css | 1 + 3 files changed, 162 insertions(+) create mode 100644 app/react/src/pages/tables/Posts.js diff --git a/app/react/src/index.js b/app/react/src/index.js index 960a499..bd5897e 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -10,6 +10,7 @@ import Links from './pages/tables/Links'; import Link from './pages/Link'; import Tags from './pages/tables/Tags'; import Pages from './pages/tables/Pages'; +import Posts from './pages/tables/Posts'; const App = () => { const query_client = new QueryClient(); @@ -21,6 +22,7 @@ const App = () => { }> }/> }/> + }/> }/> }/> }/> diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js new file mode 100644 index 0000000..2a37fe5 --- /dev/null +++ b/app/react/src/pages/tables/Posts.js @@ -0,0 +1,159 @@ +import React, { useEffect, useMemo } from 'react'; +import { Table } from '../../components/Table'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { DropdownMenu, formatDate, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; +import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; + +export default function Posts() { + const { user, settings } = useOutletContext(); + const { data: users_req, is_loading: is_loading_users, fetch: fetch_users } = useRequest({ + method: 'GET', + url: '/api/v2/users', + data: { + order: 'name', + sort: 'asc', + }, + }); + const navigate = useNavigate(); + const users_options = useMemo(() => { + let users = users_req?.data?.data ?? {}; + + return [ + { key: '', title: 'All' }, + ...Object.keys(users).map(key => ({ key: users[key].id, title: users[key].name })), + ]; + }, [ users_req ]); + + useEffect(() => { + fetch_users(); + }, []); + + if (is_loading_users) { + return ; + } + + return
+
+ New, + condition: Boolean(user?.actions?.edit_posts), + onClick: () => navigate('/console/posts/edit'), + }, + ]} + rowOnClick={post => navigate(`/console/posts/edit?id=${post.id}`)} + filters={{ + user: { + title: 'Author', + options: users_options, + }, + status: { + title: 'Status', + options: [ + { key: '', title: 'All' }, + { key: '1', title: 'Published' }, + { key: 'scheduled', title: 'Scheduled' }, + { key: '0', title: 'Draft' }, + ], + }, + order: { + title: 'Sort by', + options: [ + { key: 'title', title: 'Title' }, + { key: 'author', title: 'Author' }, + { key: 'date', title: 'Publish Date' }, + { key: 'views', title: 'No. views' }, + ], + }, + sort: { + options: [ + { key: 'asc', title: 'Ascending' }, + { key: 'desc', title: 'Descending' }, + ], + }, + }} + options={[ + { + title: 'Delete', + class: 'danger', + condition: Boolean(user?.actions?.edit_posts), + onClick: (posts) => { + if (confirm('Are you sure you want to delete the selected posts? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/posts', + data: { id: posts.map(l => l.id) }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + }, + ]} + columns={[ + { + class: 'w100 align-center', + content: post => <> + {post.image_alt +
+

+ {post.title} + {!post.status && Draft} + {post.status && post.published_at > Date.now() / 1000 && Scheduled} +

+

{Object.values(post.tags)?.join(', ') || ''}

+
+ , + }, + { + title: 'Author', + class: 'w20', + content: post => post.user_name || '', + }, + { + title: 'Publish Date', + class: 'w20', + content: post => formatDate(post.published_at), + }, + { + title: 'No. views', + class: 'w10 numeric', + condition: Boolean(settings.views_count), + content: post => post.views || '', + }, + { + class: 'w10 row-actions', + content: post => } + className="three-dots" + options={[ + { + onClick: () => window.open(`/${settings.blog_url}/${post.slug}`, '_blank').focus(), + content: <> View + }, + { + class: 'danger', + condition: Boolean(user?.actions?.edit_posts), + onClick: () => { + if (confirm('Are you sure you want to delete the post? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/posts', + data: { id: post.id }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + content: <> Delete + }, + ]} + />, + }, + ]} + /> + +} diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index bdc9506..0629f3b 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -1295,6 +1295,7 @@ textarea.code { flex: 1; } +.row-thumb, .listing-row.post > div > img, .listing-row.file > div > *:first-child > img, .listing-row.file > div > *:first-child > svg { From 9ed57dc5666ae6e85c8c69c3c9244ef29e4dd6c2 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 17 Oct 2025 23:57:19 +0200 Subject: [PATCH 089/334] Fix filter select in table --- app/react/src/components/Table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index aa20bb5..535bccb 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -119,7 +119,7 @@ export const Table = ({ let aux = { ...filter }; Object.keys(aux.options).map(opt_key => { - aux.options[opt_key].selected = aux.options[opt_key].key === e.target.value; + aux.options[opt_key].selected = String(aux.options[opt_key].key) === String(e.target.value); }); setFilters({ ...filters, [id]: aux }); From cd445155948a85e7898f0b4c26d9104022e12dce Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Oct 2025 12:47:45 +0200 Subject: [PATCH 090/334] Add roles endpoint --- app/bootstrap/routes.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index e15a81d..5b9ff1f 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -44,7 +44,7 @@ /** * ADMIN 2 */ - + $router->get([ 'console', 'console/*' ], function() use ($view) { return $view->get('admin.html'); }); @@ -1499,6 +1499,30 @@ return json_encode([ 'success' => $success ]); }); + $router->get('json:api/v2/roles', function() use ($db) { + $roles = []; + $permissions_data = $db->query('SELECT role_level, permission FROM roles_permissions ORDER BY role_level ASC, permission ASC')->fetchAll(); + foreach ($db->query('SELECT * FROM roles ORDER BY level ASC')->fetchAll() as $role) { + $role_permissions = []; + + foreach ($permissions_data as $permission) { + if ($role['level'] >= $permission['role_level']) { + $role_permissions[] = $permission['permission']; + } + } + + sort($role_permissions); + + $roles[] = [ + 'level' => (int) $role['level'], + 'slug' => $role['slug'], + 'permissions' => $role_permissions + ]; + } + + return json_encode($roles); + }); + $router->get('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { switch ($_GET['mod'] ?? '') { case 'pages': $mod = $page_mod; break; From 913d20019bdca1711ad446092aa15f65f686242c Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 19 Oct 2025 21:41:03 +0200 Subject: [PATCH 091/334] Add getRoleTitle function --- app/react/src/utils/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 3db6cb7..9a02a50 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -325,3 +325,13 @@ export const downloadFile = (data, filename) => { link.click(); link.remove(); }; + +export const getRoleTitle = (role_slug) => { + switch (role_slug) { + case 'contributor': return 'Contributor'; + case 'editor': return 'Editor'; + case 'admin': return 'Administrator'; + case 'owner': return 'Owner'; + default: return ''; + } +} \ No newline at end of file From f77ce1b760e312cae7e3df4df34077d4d121bb8b Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 19 Oct 2025 21:46:30 +0200 Subject: [PATCH 092/334] Update table --- app/react/src/components/Table.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index 535bccb..2c5a03a 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -41,7 +41,7 @@ export const Table = ({ url, title = '', topOptions = [], - filters: initialFilters = [], + filters: initialFilters = {}, columns = [], rowOnClick = null, options: initialOptions = [], @@ -52,7 +52,15 @@ export const Table = ({ const [ selected_rows, setSelectedRows ] = useState([]); const [ search, setSearch ] = useState(params.get('search') || ''); const [ input_search, setInputSearch ] = useState(params.get('search') || ''); - const [ filters, setFilters ] = useState(initialFilters); + const [ filters, setFilters ] = useState(() => { + let aux = { ...initialFilters }; + + Object.keys(aux).map(key => { + aux[key].options.map((opt, i) => opt.selected = i === 0); + }); + + return aux; + }); const [ query_string, setQueryString ] = useState(getQueryString(filters, search, page)); const [ rows, setRows ] = useState([]); const options = initialOptions.filter(opt => opt.condition === undefined || opt.condition); From 3f26ae8fe9205f4d011cdfc76adc3afb67371e28 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 19 Oct 2025 22:02:02 +0200 Subject: [PATCH 093/334] Update table when changing filters --- app/react/src/components/Table.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index 2c5a03a..b84b031 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -52,15 +52,7 @@ export const Table = ({ const [ selected_rows, setSelectedRows ] = useState([]); const [ search, setSearch ] = useState(params.get('search') || ''); const [ input_search, setInputSearch ] = useState(params.get('search') || ''); - const [ filters, setFilters ] = useState(() => { - let aux = { ...initialFilters }; - - Object.keys(aux).map(key => { - aux[key].options.map((opt, i) => opt.selected = i === 0); - }); - - return aux; - }); + const [ filters, setFilters ] = useState({}); const [ query_string, setQueryString ] = useState(getQueryString(filters, search, page)); const [ rows, setRows ] = useState([]); const options = initialOptions.filter(opt => opt.condition === undefined || opt.condition); @@ -69,6 +61,16 @@ export const Table = ({ url: url + (query_string ? `?${query_string}` : ''), }); + useEffect(() => { + let aux = { ...initialFilters }; + + Object.keys(aux).map(key => { + aux[key].options.map((opt, i) => opt.selected = i === 0); + }); + + setFilters(aux); + }, [ initialFilters ]); + useEffect(() => { setQueryString(getQueryString(filters, search, page)); }, [ filters, search, page ]); @@ -96,7 +98,7 @@ export const Table = ({ e.preventDefault(); setPage(1); setSearch(input_search); - + const aux = getQueryString(filters, input_search, 1); if (aux !== query_string) { setQueryString(aux); From 26e62cc80abcbdd19144055c59f35b9563e84703 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 19 Oct 2025 22:08:30 +0200 Subject: [PATCH 094/334] Add users listing --- app/react/src/index.js | 2 + app/react/src/pages/tables/Users.js | 166 ++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 app/react/src/pages/tables/Users.js diff --git a/app/react/src/index.js b/app/react/src/index.js index bd5897e..091bc00 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -11,6 +11,7 @@ import Link from './pages/Link'; import Tags from './pages/tables/Tags'; import Pages from './pages/tables/Pages'; import Posts from './pages/tables/Posts'; +import Users from './pages/tables/Users'; const App = () => { const query_client = new QueryClient(); @@ -23,6 +24,7 @@ const App = () => { }/> }/> }/> + }/> }/> }/> }/> diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js new file mode 100644 index 0000000..9aaa72c --- /dev/null +++ b/app/react/src/pages/tables/Users.js @@ -0,0 +1,166 @@ +import React, { useEffect, useMemo } from 'react'; +import { Table } from '../../components/Table'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { DropdownMenu, formatDate, getContentUrl, getRoleTitle, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; +import { IconEye, IconThreeDots, IconTrash, IconUsers } from '../../utils/icons'; + +export default function Users() { + const { user, settings } = useOutletContext(); + const navigate = useNavigate(); + const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ + method: 'GET', + url: '/api/v2/roles', + }); + const roles_options = useMemo(() => { + let roles = roles_req?.data ?? {}; + + return [ + { key: '', title: 'All' }, + ...Object.keys(roles).map(key => ({ key: roles[key].level, title: getRoleTitle(roles[key].slug) })), + ]; + }, [ roles_req ]); + + useEffect(() => { + fetch_roles(); + }, []); + + if (is_loading_roles) { + return ; + } + + return
+
+ New, + condition: Boolean(user?.actions?.edit_users), + onClick: () => navigate('/console/users/edit'), + }, + ]} + rowOnClick={item => navigate(`/console/users/edit?id=${item.id}`)} + filters={{ + status: { + title: 'Status', + options: [ + { key: '', title: 'All' }, + { key: '1', title: 'Active' }, + { key: '0', title: 'Inactive' }, + ], + }, + role: { + title: 'Role', + options: roles_options, + }, + order: { + title: 'Sort by', + options: [ + { key: 'name', title: 'Name' }, + { key: 'email', title: 'Email' }, + { key: 'status', title: 'Status' }, + { key: 'role', title: 'Role' }, + { key: 'last_active', title: 'Last Active' }, + { key: 'posts', title: 'No. posts' }, + ], + }, + sort: { + options: [ + { key: 'asc', title: 'Ascending' }, + { key: 'desc', title: 'Descending' }, + ], + }, + }} + options={[ + { + title: 'Delete', + class: 'danger', + condition: Boolean(user?.actions?.edit_users), + onClick: (users) => { + if (confirm('Are you sure you want to delete the selected users? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/users', + data: { id: users.map(u => u.id) }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + }, + ]} + columns={[ + { + class: 'w100 align-center', + content: item => (<> +
+ {item.name} +
+
+

+ {item.name} + {item.id == user?.id && (you)} + {item.status != 1 && Inactive} +

+

{item.email}

+
+ ), + }, + { + title: 'Role', + class: 'w20', + content: item => getRoleTitle(item.role_slug), + }, + { + title: 'Last Active', + class: 'w20', + content: item => formatDate(item.last_active), + }, + { + title: 'No. posts', + class: 'w10 numeric', + content: item => item.posts, + }, + { + class: 'w10 row-actions', + content: item => } + className="three-dots" + options={[ + { + onClick: () => window.open(`/${settings.blog_url}/author/${item.slug}`, '_blank').focus(), + content: <> View + }, + { + condition: item.id != user?.id && user.role > item.role, + onClick: () => { + if (confirm('Are you sure you want to impersonate this user?')) { + window.location.href = `/admin/users/impersonate?id=${item.id}`; + } + }, + content: <> Impersonate + }, + { + class: 'danger', + condition: item.id != user?.id && Boolean(user?.actions?.edit_users), + onClick: () => { + if (confirm(`Are you sure you want to delete ${item.name}? This action cannot be undone.`)) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/users', + data: { id: item.id }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + content: <> Delete + }, + ]} + />, + }, + ]} + /> + +} From 4225164bbb53647f36812a23cd1d10c81e633c31 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 19 Oct 2025 22:34:55 +0200 Subject: [PATCH 095/334] Improve user and settings context --- app/react/src/components/AdminPages.js | 6 +++--- app/react/src/pages/Login.js | 2 +- app/react/src/utils/utils.js | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index 006c108..cef1016 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -5,8 +5,8 @@ import { LoadingPage, useElement } from '../utils/utils'; export default function AdminPages() { const dark_theme_element = document.getElementById('css-dark'); - const user = useElement('/api/v2/me'); - const settings = useElement('/api/v2/settings'); + const [ user, fetch_user ] = useElement('/api/v2/me'); + const [ settings, fetch_settings ] = useElement('/api/v2/settings'); const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); const navigate = useNavigate(); @@ -72,6 +72,6 @@ export default function AdminPages() { - {settings ? : } + {settings ? : } ; }; \ No newline at end of file diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index cadc1d2..38b8a45 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -3,7 +3,7 @@ import { makeRequest, useElement } from '../utils/utils'; import { useNavigate } from 'react-router-dom'; export default function Login() { - const user = useElement('/api/v2/me'); + const [ user ] = useElement('/api/v2/me'); const logo = document.querySelector('meta[name="logo"]')?.content; const [ loading, setLoading ] = useState(false); const [ email, setEmail ] = useState(''); diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 9a02a50..fdf687a 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -83,7 +83,10 @@ export const useElement = (url) => { return undefined; } - return data?.data && !is_error ? data.data : null; + return [ + data?.data && !is_error ? data.data : null, + fetch, + ]; }; export const MenuButton = () => document.body.toggleAttribute('data-nav-open')}> @@ -217,8 +220,8 @@ export const getContentUrl = (path = '') => { }; export const ImageDialog = ({ onSave, onClose }) => { - const user = useElement('/api/v2/me'); - const settings = useElement('/api/v2/settings'); + const [ user ] = useElement('/api/v2/me'); + const [ settings ] = useElement('/api/v2/settings'); const [ path, setPath ] = useState(''); const { data: files_req, is_loading, refetch: refetch_files } = useRequest({ method: 'GET', From 79251e4a80822ce04a5482bfa188b16d01100dcb Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 19 Oct 2025 22:36:03 +0200 Subject: [PATCH 096/334] Fix useElement --- app/react/src/utils/utils.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index fdf687a..af751f0 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -79,12 +79,8 @@ export const useElement = (url) => { fetch(); }, []); - if (is_loading) { - return undefined; - } - return [ - data?.data && !is_error ? data.data : null, + is_loading ? undefined : (data?.data && !is_error ? data.data : null), fetch, ]; }; From 4bffa40f6985fb6b73d3f7033e66910193bd5e10 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 19 Oct 2025 22:38:11 +0200 Subject: [PATCH 097/334] Fix loading state in AdminPages --- app/react/src/components/AdminPages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index cef1016..bf1f0b2 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -72,6 +72,6 @@ export default function AdminPages() { - {settings ? : } + {user && settings ? : } ; }; \ No newline at end of file From a8ddea6f21dd163d4f1163d00caaef27f4c2db5b Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 20 Oct 2025 22:35:57 +0200 Subject: [PATCH 098/334] Fix user image in sidebar --- app/react/src/components/AdminPages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index bf1f0b2..ca35f0f 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -62,7 +62,7 @@ export default function AdminPages() {
- +
{theme == 'light' ? : } From 0ea9cd29e6d001a17e08a5f960c234a87266eb97 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 20 Oct 2025 22:36:08 +0200 Subject: [PATCH 099/334] Add missing import --- app/react/src/components/AdminPages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index ca35f0f..9becacc 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; import { Link, Navigate, Outlet, useNavigate } from 'react-router-dom'; -import { LoadingPage, useElement } from '../utils/utils'; +import { getContentUrl, LoadingPage, useElement } from '../utils/utils'; export default function AdminPages() { const dark_theme_element = document.getElementById('css-dark'); From cc7a6a3de695f0c150cb65752900ab08a1c9bc04 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 20 Oct 2025 22:36:22 +0200 Subject: [PATCH 100/334] Add impersonate functionality --- app/react/src/pages/tables/Users.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index 9aaa72c..4e9e706 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -5,7 +5,7 @@ import { DropdownMenu, formatDate, getContentUrl, getRoleTitle, LoadingPage, mak import { IconEye, IconThreeDots, IconTrash, IconUsers } from '../../utils/icons'; export default function Users() { - const { user, settings } = useOutletContext(); + const { user, settings, fetch_user } = useOutletContext(); const navigate = useNavigate(); const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ method: 'GET', @@ -138,7 +138,17 @@ export default function Users() { condition: item.id != user?.id && user.role > item.role, onClick: () => { if (confirm('Are you sure you want to impersonate this user?')) { - window.location.href = `/admin/users/impersonate?id=${item.id}`; + makeRequest({ + method: 'GET', + url: '/api/v2/users/impersonate?id=' + item.id, + }).then(res => { + if (!res?.data?.success) { + alert('Error'); + } else { + localStorage.setItem('auth_token', res.data.token); + fetch_user(); + } + }); } }, content: <> Impersonate From eb5bde7b4faeb525cc46b38eaf9a6dcf99285ff3 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 20 Oct 2025 22:36:40 +0200 Subject: [PATCH 101/334] Add impersonate endpoint --- app/bootstrap/routes.php | 60 ++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 5b9ff1f..fc3e285 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -1162,6 +1162,29 @@ ]); }); + $login = function($user_id) use ($db) { + $data = [ 'token' => bin2hex(random_bytes(64)) ]; + + try { + $data['success'] = (bool) $db->insert('tokens', [ + 'user_id' => $user_id, + 'token' => $data['token'], + 'created_at' => time(), + ]); + } catch (\Exception) { + $data = [ + 'success' => false, + 'error' => 'server_error', + ]; + } + + if (!$data['success']) { + unset($data['token']); + } + + return $data; + }; + /** * API V2 */ @@ -1188,7 +1211,7 @@ } }); - $router->any('json:api/v2/auth', function() use ($db, $user_mod) { + $router->any('json:api/v2/auth', function() use ($db, $user_mod, $login) { $email = $_POST['email'] ?? ''; $password = $_POST['password'] ?? ''; $user = $user_mod->get([ @@ -1203,26 +1226,7 @@ ]); } - $data = [ 'token' => bin2hex(random_bytes(64)) ]; - - try { - $data['success'] = (bool) $db->insert('tokens', [ - 'user_id' => $user['id'], - 'token' => $data['token'], - 'created_at' => time(), - ]); - } catch (\Exception) { - $data = [ - 'succcess' => false, - 'error' => 'server_error', - ]; - } - - if (!$data['success']) { - unset($data['token']); - } - - return json_encode($data); + return json_encode($login($user['id'])); }); $router->get('json:api/v2/me', function() { @@ -1263,6 +1267,20 @@ ]); }); + $router->get('json:api/v2/users/impersonate', function() use ($user_mod, $login) { + $user = $user_mod->get([ + 'id' => $_GET['id'] ?? 0, + 'status' => 1, + ]); + + if (!\Aurora\App\Permission::can('impersonate') || empty($user) || $user['role'] > $GLOBALS['user']['role']) { + http_response_code(403); + exit; + } + + return json_encode($login($user['id'])); + }); + $router->post('json:api/v2/media', function() { if (!\Aurora\App\Permission::can('edit_media')) { http_response_code(403); From 96fc7601a4003c85f9fdf98f3bb2d5cf9d03e3ce Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 20 Oct 2025 23:28:23 +0200 Subject: [PATCH 102/334] Add tag page --- app/react/src/index.js | 2 + app/react/src/pages/Tag.js | 139 +++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 app/react/src/pages/Tag.js diff --git a/app/react/src/index.js b/app/react/src/index.js index 091bc00..e6ec57b 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -12,6 +12,7 @@ import Tags from './pages/tables/Tags'; import Pages from './pages/tables/Pages'; import Posts from './pages/tables/Posts'; import Users from './pages/tables/Users'; +import Tag from './pages/Tag'; const App = () => { const query_client = new QueryClient(); @@ -29,6 +30,7 @@ const App = () => { }/> }/> }/> + }/> 404 Not Found
}/> diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js new file mode 100644 index 0000000..49b4abc --- /dev/null +++ b/app/react/src/pages/Tag.js @@ -0,0 +1,139 @@ +import React, { useEffect, useState } from 'react'; +import { Input, LoadingPage, makeRequest, MenuButton, Textarea } from '../utils/utils'; +import { IconEye, IconTrash } from '../utils/icons'; +import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; + +export default function Tag() { + const { user, settings } = useOutletContext(); + const [ data, setData ] = useState(undefined); + const location = useLocation(); + const navigate = useNavigate(); + const params = new URLSearchParams(location.search); + const [ id, setId ] = useState(params.get('id')); + + useEffect(() => { + if (id) { + makeRequest({ + method: 'GET', + url: `/api/v2/tags?id=${id}`, + }).then(res => setData(res?.data?.data[0] ?? null)); + } else { + setData({}); + } + }, []); + + const remove = () => { + if (confirm('Are you sure you want to delete the tag? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/tags', + data: { id: id }, + }).then(res => { + if (res?.data?.success) { + alert('Done'); + navigate('/console/tags', { replace: true }); + } else { + alert('Error'); + } + }); + } + }; + + const submit = e => { + e.preventDefault(); + makeRequest({ + method: 'POST', + url: '/api/v2/tags' + (id ? `?id=${id}` : ''), + data: data, + }).then(res => { + alert(res?.data?.success ? 'Done' : 'Error'); + if (res?.data?.id) { + navigate(`/console/tags/edit?id=${res.data.id}`, { replace: true }); + setId(res.data.id); + } + }); + }; + + if (data === undefined) { + return ; + } + + if (!data) { + return <>Error; + } + + return (
+
+
+ +

Link

+
+
+ {id && <> + + + } + +
+
+
+
+
+ + setData({...data, name: e.target.value})} + charCount={true} + /> +
+
+ + setData({...data, slug: e.target.value})} + charCount={true} + /> +
+
+ + -
-
-
-
-
- - - -
- -
- t('id') ?>: - - t('number_views') ?>: - -
- -
-
-
- -
- checked > - -
-
-
- -
- checked oninput="toggleEditor(!this.checked)"> - -
-
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
- -
- -sectionEnd() ?> - -sectionStart('extra') ?> - - -sectionEnd() ?> diff --git a/app/views/admin/partials/extra_headers/links.html b/app/views/admin/partials/extra_headers/links.html deleted file mode 100644 index 6b28c6e..0000000 --- a/app/views/admin/partials/extra_headers/links.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- - - -
- -
diff --git a/app/views/admin/partials/extra_headers/pages.html b/app/views/admin/partials/extra_headers/pages.html deleted file mode 100644 index 3cc46c3..0000000 --- a/app/views/admin/partials/extra_headers/pages.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- - - -
- -
diff --git a/app/views/admin/partials/extra_headers/posts.html b/app/views/admin/partials/extra_headers/posts.html deleted file mode 100644 index aa8eb4c..0000000 --- a/app/views/admin/partials/extra_headers/posts.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- - - -
- -
diff --git a/app/views/admin/partials/extra_headers/tags.html b/app/views/admin/partials/extra_headers/tags.html deleted file mode 100644 index 17ec383..0000000 --- a/app/views/admin/partials/extra_headers/tags.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- - - -
- -
diff --git a/app/views/admin/partials/extra_headers/users.html b/app/views/admin/partials/extra_headers/users.html deleted file mode 100644 index 006bae1..0000000 --- a/app/views/admin/partials/extra_headers/users.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- - - -
- -
diff --git a/app/views/admin/partials/head.html b/app/views/admin/partials/head.html deleted file mode 100755 index ca6b813..0000000 --- a/app/views/admin/partials/head.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - disabled > - - diff --git a/app/views/admin/partials/images_dialog.html b/app/views/admin/partials/images_dialog.html deleted file mode 100755 index 315112a..0000000 --- a/app/views/admin/partials/images_dialog.html +++ /dev/null @@ -1,78 +0,0 @@ -
-
-

t('image_picker') ?>

- - include('icons/x.svg') ?> - -
-
-
- - - - -
-
-
-
-
-
-
t('information') ?>
-
t('last_modification') ?>
-
- $file): ?> - -
- onclick="ImageDialog.close(); ImageDialog.setImage();" - - onclick="ImageDialog.setImagePage()" - - > - -
- -

- -

-
-
- dateFormat($file['time'])) ?> -
-
- - - t('no_items') ?> - -
-
-
- - $folder): ?> - - - include('icons/home.svg') : e($folder) ?> - / - - -
-
diff --git a/app/views/admin/partials/lists/links.html b/app/views/admin/partials/lists/links.html deleted file mode 100755 index 81f6b60..0000000 --- a/app/views/admin/partials/lists/links.html +++ /dev/null @@ -1,37 +0,0 @@ - - -
-

-
-
- -
-
- - t('active') ?> - - t('inactive') ?> - -
-
- -
-
-
- include('icons/dots.svg') ?> - -
-
-
- diff --git a/app/views/admin/partials/lists/pages.html b/app/views/admin/partials/lists/pages.html deleted file mode 100755 index caa2a8f..0000000 --- a/app/views/admin/partials/lists/pages.html +++ /dev/null @@ -1,40 +0,0 @@ - - -
-

- - - t('draft') ?> - -

-
-
- / -
-
- dateFormat($page['edited_at']) ?> -
- -
- -
- -
-
- include('icons/dots.svg') ?> - -
-
-
- diff --git a/app/views/admin/partials/lists/posts.html b/app/views/admin/partials/lists/posts.html deleted file mode 100755 index cc47e17..0000000 --- a/app/views/admin/partials/lists/posts.html +++ /dev/null @@ -1,46 +0,0 @@ - - -
- <?= e($post['image_alt'] ?? '') ?> style="visibility: hidden;" /> -
-

- - - t('draft') ?> - time()): ?> - t('scheduled') ?> - -

-

-
-
-
- -
-
- dateFormat($post['published_at'])) ?> -
- -
- -
- -
-
- include('icons/dots.svg') ?> - -
-
-
- diff --git a/app/views/admin/partials/lists/tags.html b/app/views/admin/partials/lists/tags.html deleted file mode 100755 index 10f6d3c..0000000 --- a/app/views/admin/partials/lists/tags.html +++ /dev/null @@ -1,30 +0,0 @@ - - -
-

-
-
- -
-
- -
-
-
- include('icons/dots.svg') ?> - -
-
-
- diff --git a/app/views/admin/partials/lists/users.html b/app/views/admin/partials/lists/users.html deleted file mode 100755 index 674d8b8..0000000 --- a/app/views/admin/partials/lists/users.html +++ /dev/null @@ -1,52 +0,0 @@ - - -
-
- <?= e($user['name'] ?? '') ?> style="visibility: hidden;" /> -
-
-

- - - (t('you') ?>) - - - t('inactive') ?> - -

-

-
-
-
- t($user['role_slug']) ?> -
-
- dateFormat($user['last_active'])) ?> -
-
- -
-
-
- include('icons/dots.svg') ?> - -
-
-
- diff --git a/app/views/admin/partials/menu_btn.html b/app/views/admin/partials/menu_btn.html deleted file mode 100755 index 76b75b3..0000000 --- a/app/views/admin/partials/menu_btn.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/views/admin/partials/snackbar.html b/app/views/admin/partials/snackbar.html deleted file mode 100644 index 77af6fa..0000000 --- a/app/views/admin/partials/snackbar.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
- include('icons/x-circle.svg') ?> -
-
- include('icons/check-circle.svg') ?> -
- -
- include('icons/x.svg') ?> -
-
diff --git a/app/views/admin/password_restore.html b/app/views/admin/password_restore.html deleted file mode 100755 index aac691c..0000000 --- a/app/views/admin/password_restore.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - <?= $this->t('restore_your_password') . ' - ' . e(setting('title')) ?> - include('admin/partials/head.html') ?> - - - include('admin/partials/snackbar.html') ?> -
-
- - -
-
- - -
- - - - - - - diff --git a/app/views/admin/post.html b/app/views/admin/post.html deleted file mode 100755 index ae489e4..0000000 --- a/app/views/admin/post.html +++ /dev/null @@ -1,172 +0,0 @@ -extend('admin/base.html') ?> - -sectionStart('title') ?> - t('post') ?> -sectionEnd() ?> - -sectionStart('content') ?> -
- -
-
- include('admin/partials/menu_btn.html') ?> -

t('post') ?>

-
-
- - - - - -
-
-
-
-
-
- - -
-
- -
-
-
-
- - <?= $this->t('post_image') ?> - -
-
- - - -
-
- - -
- -
- t('id') ?>: - - t('number_views') ?>: - -
- -
-
-
- - -
-
- - -
-
- -
- checked > - -
-
- -
- -
- - - - -
-
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
-
-
-
- -sectionEnd() ?> - -sectionStart('extra') ?> - - -sectionEnd() ?> diff --git a/app/views/admin/settings.html b/app/views/admin/settings.html deleted file mode 100755 index d63669e..0000000 --- a/app/views/admin/settings.html +++ /dev/null @@ -1,361 +0,0 @@ -extend('admin/base.html') ?> - -sectionStart('title') ?> - t('settings') ?> -sectionEnd() ?> - -sectionStart('content') ?> - -
-
-
- include('admin/partials/menu_btn.html') ?> -

t('settings') ?>

-
-
- -
-
-
- -
-
-
- - - -
-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - t('system_language_description', false) ?> - -
-
- - t('date_format_description', false) ?> - -
-
-
-
- - -
-
- -
- checked > - -
-
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-
- - t('session_lifetime_description') ?> - -
-
- - t('samesite_cookie_description') ?> - -
-
- -
- checked > - -
-
-
- -
- checked > - -
-
-
- - t('relative_system_root') ?> - -
-
- -
-
- - - -
- - - -
-
-
- -
-
-
-
- - -
-
- -
- - - -
- -
-
- -
- checked > - -
-
- -
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - t('file_size_upload_limit_description', false) ?> - -
-
-
-
-
-
- - t('site_header_description', false) ?> - -
-
- - t('site_footer_description', false) ?> - -
-
- - t('post_code_description', false) ?> - -
-
- - t('editor_code_description', false) ?> - -
-
-
-
-
-
- - t('update_description', false) ?> - - -
-
-
- -
-
-
-
- -sectionEnd() ?> - -sectionStart('extra') ?> - -sectionEnd() ?> diff --git a/app/views/admin/tag.html b/app/views/admin/tag.html deleted file mode 100755 index 5b0888b..0000000 --- a/app/views/admin/tag.html +++ /dev/null @@ -1,97 +0,0 @@ -extend('admin/base.html') ?> - -sectionStart('title') ?> - t('tag') ?> -sectionEnd() ?> - -sectionStart('content') ?> -
-
-
- include('admin/partials/menu_btn.html') ?> -

t('tag') ?>

-
-
- - - - - -
-
-
-
-
- - -
-
- - -
-
- - -
- -
- t('id') ?>: - t('number_posts') ?>: -
- -
-
-
- - -
-
- - -
-
- -
- -sectionEnd() ?> - -sectionStart('extra') ?> - -sectionEnd() ?> diff --git a/app/views/admin/user.html b/app/views/admin/user.html deleted file mode 100755 index 8c7544c..0000000 --- a/app/views/admin/user.html +++ /dev/null @@ -1,143 +0,0 @@ -extend('admin/base.html') ?> - - -t($current_user ? 'your_user' : 'user'); ?> - -sectionStart('title') ?> - -sectionEnd() ?> - -sectionStart('content') ?> -
- -
-
- include('admin/partials/menu_btn.html') ?> -

-
-
- - - - - - - - - - -
-
-
-
-
- - - - - -
- - -
-

t('id') ?>:

-

t('number_posts') ?>:

-

t('last_active') ?>: dateFormat($user['last_active'])) ?>

-
- -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- checked disabled > - -
-
-
-
-

t('password') ?>

-
- - -
-
- - -
-
-
- -
-
-
-
- -sectionEnd() ?> - -sectionStart('extra') ?> - -sectionEnd() ?> diff --git a/app/views/admin/emails/password_restore.html b/app/views/emails/password_restore.html similarity index 100% rename from app/views/admin/emails/password_restore.html rename to app/views/emails/password_restore.html diff --git a/public/assets/js/admin.js b/public/assets/js/admin.js deleted file mode 100755 index 0a8ce3c..0000000 --- a/public/assets/js/admin.js +++ /dev/null @@ -1,486 +0,0 @@ -const LOADING_ICON = ''; - -function get(query) { - return document.querySelectorAll(query)[0]; -} - -String.prototype.sprintf = function(...args) { - let str = this; - args.map(arg => str = str.replace('%s', arg)); - return str; -}; - -String.prototype.toSlug = function() { - return this.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-'); -}; - -Element.prototype.setLoading = function() { - this.dataset.originalHtml = this.innerHTML; - this.innerHTML = LOADING_ICON; - this.classList.add('loading'); -}; - -Element.prototype.resetState = function() { - this.classList.remove('loading'); - if (this.dataset.hasOwnProperty('originalHtml')) { - this.innerHTML = this.dataset.originalHtml; - delete this.dataset.originalHtml; - } -}; - -class Snackbar { - static #timeout = null; - - static show(msg = '', success = true) { - let snackbar = get('#snackbar'); - - if (snackbar.hasAttribute('show')) { - this.hide(); - setTimeout(() => this.show(msg, success), 200); - return; - } - - if (!success) { - snackbar.classList.add('error'); - } - - get('#snackbar > span').innerHTML = msg; - snackbar.dataset.show = true; - this.#timeout = setTimeout(() => this.hide(), 5000); - } - - static hide() { - let snackbar = get('#snackbar'); - clearTimeout(this.#timeout); - delete snackbar.dataset.show; - setTimeout(() => snackbar.classList.remove('error'), 200); - } -} - -class Form { - static #appendAfter = function(element_a, element_b) { - element_b.parentNode.insertBefore(element_a, element_b.nextSibling); - }; - - static #getData(form_id, initial_data = {}) { - let form_data = new FormData; - - Object.keys(initial_data).forEach(key => form_data.append(key, initial_data[key])); - - document.querySelectorAll(`#${form_id} *[name]`).forEach(el => { - let type = el.getAttribute('type'); - let key = el.getAttribute('name'); - let value = el.value; - - if (type == 'checkbox') { - if (el.hasAttribute('data-multiselect')) { - key += '[]'; - value = el.checked ? el.getAttribute('value') : undefined; - } else { - value = Number(el.checked); - } - } else if (type == 'file') { - value = el.files[0]; - } - - if (typeof value !== 'undefined') { - form_data.append(key, value); - } - }); - - return form_data; - } - - static #handleResponse(res, form_id) { - if (res?.reload) { - location.reload(); - } - - Object.keys(res?.errors ?? {}).forEach(key => { - let input = get(`#${form_id} *[name="${key}"]`); - if (!input) { - return; - } - - let err = document.createElement('span'); - err.classList.add('field-error'); - err.innerHTML = res.errors[key]; - this.#appendAfter(err, input); - }); - - if (res?.success) { - if (res?.msg !== null) { - Snackbar.show(res?.msg ? res.msg : LANG.done); - } - } else if (res?.errors?.hasOwnProperty(0)) { - Snackbar.show(res.errors[0], false); - } - - return res; - } - - static send(url, form_id = null, btn = null, extra_data = {}) { - let btn_el = btn ? btn : event.target; - - if (btn_el) { - if (btn_el.classList.contains('loading')) { - return; - } - - btn_el.setLoading(); - } - - if (form_id) { - document.querySelectorAll(`#${form_id} .field-error`).forEach(el => el.remove()); - } - - return fetch(url, { - method: 'POST', - body: this.#getData(form_id, extra_data), - }) - .then(res => { - if (res.redirected) { - window.location.href = res.url; - return {}; - } - - return res.json(); - }) - .then(res => this.#handleResponse(res, form_id)) - .catch(() => { - Snackbar.show(LANG.unexpected_error, false); - return {}; - }) - .then(res => { - if (btn_el) { - btn_el.resetState(); - } - - return res; - }); - } - - static initFileInput(el) { - el.querySelector('input[type="file"]').addEventListener('change', e => { - el.querySelector('input[type="text"]').value = e.target.files[0] ? e.target.files[0].name : ''; - }); - } - - static initCharCounters() { - document.querySelectorAll('*[data-char-count]').forEach(input => { - let count_el = document.createElement('span'); - count_el.classList.add('char-counter'); - - this.#appendAfter(count_el, input); - input.addEventListener('input', e => count_el.innerHTML = e.target.value.length + ' ' + LANG.characters); - input.dispatchEvent(new Event('input')); - }); - } -} - -class Listing { - static #select_mode = false; - static #next_page_url = ''; - static #next_page = 1; - static #prev_selected_row = null; - - static #getElementsBetween = function(element_a, element_b) { - if (element_a === element_b) { - return []; - } - - const elements = [ ...element_a.parentElement.children ]; - const result = []; - const is_a_first = elements.indexOf(element_b) > elements.indexOf(element_a); - const start = is_a_first ? element_a : element_b; - const end = is_a_first ? element_b : element_a; - let next = start.nextElementSibling; - - while (next && next !== end) { - result.push(next); - next = next.nextElementSibling; - } - - return result; - }; - - - static init() { - window.addEventListener('keydown', e => { - if (document.activeElement.tagName != 'INPUT' && (e.key == 's' || (e.key == 'Escape' && this.#select_mode))) { - this.toggleSelectMode(get('.batch-options-container > button')); - } - }); - } - - static setNextPageUrl(url) { - this.#next_page_url = url; - } - - static toggleSelectMode(btn_el) { - let listing = get('#main-listing'); - let batch_options = get('#batch-options'); - let selected_items = get('#selected-items'); - - if ('selectMode' in listing.dataset) { - delete listing.dataset.selectMode; - batch_options.style.visibility = 'hidden'; - this.getSelectedRows().map(el => this.toggleRow(el)); - this.#prev_selected_row = null; - selected_items.style.visibility = 'hidden'; - } else { - listing.dataset.selectMode = true; - batch_options.style.visibility = 'visible'; - batch_options.querySelectorAll('button').forEach(el => el.setAttribute('disabled', true)); - selected_items.style.visibility = 'visible'; - } - - this.#select_mode = !this.#select_mode; - btn_el.innerText = LANG[this.#select_mode ? 'done' : 'select']; - } - - static toggleRow(row, event = null) { - if (!this.#select_mode) { - return; - } - - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - - let selecting = !('selected' in row.dataset); - let selected_rows = event?.shiftKey && this.#prev_selected_row - ? [ row, this.#prev_selected_row, ...this.#getElementsBetween(this.#prev_selected_row, row) ] - : [ row ]; - - selected_rows.map(el => { - if (selecting) { - el.dataset.selected = true; - } else { - delete el.dataset.selected; - } - }); - - this.#prev_selected_row = row; - let rows_selected_count = this.getSelectedRows().length; - get('#batch-options').querySelectorAll('button').forEach(el => rows_selected_count > 0 - ? el.removeAttribute('disabled') - : el.setAttribute('disabled', true)); - get('#selected-items').innerText = rows_selected_count == 0 ? '' : (rows_selected_count + ' Selected'); - } - - static getSelectedRows() { - return [ ...document.querySelectorAll('.listing-row[data-selected="true"]') ]; - } - - static loadNextPage() { - if (!this.#next_page) { - return; - } - - let listing = get('#main-listing'); - let total_items = get('#total-items'); - let btn_load_more = get('#load-more'); - btn_load_more.setLoading(); - - if (this.#next_page == 1) { - this.getSelectedRows().map(el => this.toggleRow(el)); - listing.innerHTML = LOADING_ICON; - } - - fetch(`${this.#next_page_url}${window.location.search}&page=${this.#next_page}`) - .then(res => res.json()) - .then(res => { - if (this.#next_page == 1) { - listing.innerHTML = res.html - ? res.html - : '

' + LANG.no_results + '

'; - } else { - listing.insertAdjacentHTML('beforeend', res.html); - } - - if (!res.next_page) { - btn_load_more.classList.add('hidden'); - this.#next_page = false; - } else { - btn_load_more.classList.remove('hidden'); - this.#next_page++; - } - - if (total_items && res.hasOwnProperty('count')) { - total_items.innerHTML = res.count + ' ' + LANG[res.count == 1 ? 'item' : 'items']; - } - }) - .finally(() => btn_load_more.resetState()); - } - - static refresh() { - this.#next_page = 1; - this.loadNextPage(); - } - - static handleResponse(res) { - let open_dialog = get('.dialog.open'); - - if (open_dialog) { - Dialog.close(open_dialog); - } - - Dropdown.close(); - - if (res.success) { - this.refresh(); - } - } -} - -class Dropdown { - static #active_dropdown = null; - - static init() { - let updateActiveDropdown = () => { - const MARGIN = 4; - - if (!this.#active_dropdown) { - return; - } - - let btn_rect = this.#active_dropdown.original_btn.getBoundingClientRect(); - this.#active_dropdown.style.top = (btn_rect.top + btn_rect.height + MARGIN) + 'px'; - this.#active_dropdown.style.left = btn_rect.left + 'px'; - let dropdown_rect = this.#active_dropdown.getBoundingClientRect(); - - if ((dropdown_rect.x + dropdown_rect.width) >= (window.innerWidth - MARGIN)) { - this.#active_dropdown.style.left = ((btn_rect.x - dropdown_rect.width) + btn_rect.width) + 'px'; - } - - if (dropdown_rect.y + dropdown_rect.height >= (window.innerHeight - MARGIN)) { - this.#active_dropdown.style.top = (btn_rect.y - dropdown_rect.height - MARGIN) + 'px'; - } - } - - document.addEventListener('scroll', updateActiveDropdown); - window.addEventListener('resize', updateActiveDropdown); - document.addEventListener('click', e => { - let dropdown_btn = e?.target?.closest('*[dropdown]'); - let dropdown = dropdown_btn?.querySelector('.dropdown-menu'); - - if (this.#active_dropdown?.contains(e?.target)) { - return; - } - - this.close(); - if (this.#active_dropdown === dropdown) { - return; - } - - this.#active_dropdown = dropdown; - - if (this.#active_dropdown) { - this.#active_dropdown.dataset.active = true; - this.#active_dropdown.original_btn = dropdown_btn; - document.body.appendChild(this.#active_dropdown); - updateActiveDropdown(); - } - }, true); - } - - static close() { - if (this.#active_dropdown) { - delete this.#active_dropdown.dataset.active; - this.#active_dropdown.original_btn.appendChild(this.#active_dropdown); - } - } -} - -class Dialog { - static show(container) { - container.classList.add('open'); - } - - static close(container) { - container.classList.remove('open'); - } -} - -class ImageDialog { - static #input_el = null; - static #img_el = null; - static #dialog_container = null; - static #dialog_el = null; - static #content_path = ''; - static #current_path = ''; - - static init(dialog_container, input_el, img_el, content_path) { - this.#dialog_container = dialog_container; - this.#dialog_el = dialog_container.querySelector('div'); - this.#input_el = input_el; - this.#img_el = img_el; - this.#content_path = content_path; - this.#current_path = ''; - - img_el.addEventListener('click', () => { - this.#dialog_el.innerHTML = LOADING_ICON; - Dialog.show(this.#dialog_container); - this.setImagePage(content_path); - }); - - this.#dialog_el.addEventListener('dragover', event => { - event.preventDefault(); - }, false); - - this.#dialog_el.addEventListener('drop', event => { - event.preventDefault(); - - this.#dialog_el.innerHTML = LOADING_ICON; - let data = new FormData(); - data.append('csrf', csrf_token); - Array.from(event.dataTransfer.files).map(file => data.append('file[]', file)); - - fetch('/admin/media/upload?path=' + this.#current_path, { - method: 'POST', - body: data, - }) - .then(res => res.json()) - .then(res => { - this.setImagePage(this.#current_path); - if (res.errors && res.errors.hasOwnProperty(0)) { - alert(res.errors[0]); - } - }) - .catch(() => alert(LANG.unexpected_error)); - }); - } - - static setImagePage(path) { - this.#current_path = path; - fetch('/admin/image_dialog?path=' + path) - .catch(() => alert(LANG.unexpected_error)) - .then(async res => { - if (res.status != 200) { - alert(LANG.unexpected_error); - Dialog.close(this); - return; - } - - this.#dialog_el.innerHTML = await res.text(); - }); - } - - static setImage(path) { - this.#input_el.value = path; - this.#img_el.src = '/' + this.#content_path + '/' + path; - this.#img_el.classList.remove('empty-img'); - } - - static clearImage() { - this.#input_el.value = ''; - this.#img_el.src = '/public/assets/no-image.svg'; - this.#img_el.classList.add('empty-img'); - } - - static close() { - Dialog.close(this.#dialog_container); - } -} From cccc6c1cd5ca1f2c469e1ab476eb63b00741b8b9 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 31 Oct 2025 17:58:57 +0100 Subject: [PATCH 133/334] Fix onChange of slug inputs --- app/react/src/pages/Page.js | 6 +++--- app/react/src/pages/Post.js | 4 ++-- app/react/src/pages/Tag.js | 8 ++++---- app/react/src/pages/User.js | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/react/src/pages/Page.js b/app/react/src/pages/Page.js index dbd3be1..6ebb59a 100644 --- a/app/react/src/pages/Page.js +++ b/app/react/src/pages/Page.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Editor, getUrl, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest } from '../utils/utils'; +import { Editor, getSlug, getUrl, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; @@ -90,7 +90,7 @@ export default function Page() {
- setData({...data, title: e.target.value})} charCount={true}/> + setData({ ...data, title: e.target.value })} charCount={true}/>
@@ -101,7 +101,7 @@ export default function Page() {
- setData({...data, slug: e.target.value})} maxLength="255" charCount={true}/> + setData({ ...data, slug: getSlug(e.target.value) })} maxLength="255" charCount={true}/> {getUrl(data.slug)}
{id &&
diff --git a/app/react/src/pages/Post.js b/app/react/src/pages/Post.js index f13a204..c83e0e8 100644 --- a/app/react/src/pages/Post.js +++ b/app/react/src/pages/Post.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { DateTimeInput, Editor, getContentUrl, getUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest } from '../utils/utils'; +import { DateTimeInput, Editor, getContentUrl, getSlug, getUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; @@ -122,7 +122,7 @@ export default function Post() {
- setData({ ...data, slug: e.target.value })} maxlength="255" charCount={true}/> + setData({ ...data, slug: getSlug(e.target.value) })} maxlength="255" charCount={true}/> {getUrl(`/${settings.blog_url}/${data.slug}`)}
diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js index 17e9fdb..859c882 100644 --- a/app/react/src/pages/Tag.js +++ b/app/react/src/pages/Tag.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Input, LoadingPage, makeRequest, MenuButton, Textarea } from '../utils/utils'; +import { getSlug, Input, LoadingPage, makeRequest, MenuButton, Textarea } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; @@ -86,7 +86,7 @@ export default function Tag() { id="name" type="text" value={data.name} - onChange={e => setData({...data, name: e.target.value})} + onChange={e => setData({ ...data, name: e.target.value })} charCount={true} />
@@ -96,7 +96,7 @@ export default function Tag() { id="slug" type="text" value={data.slug} - onChange={e => setData({...data, slug: e.target.value})} + onChange={e => setData({ ...data, slug: getSlug(e.target.value) })} charCount={true} />
@@ -105,7 +105,7 @@ export default function Tag() {
-
- - Code here will be injected into the editor of all pages. - -
; }; diff --git a/bin/settings/Listing.php b/bin/settings/Listing.php index a0dd8ef..a871b65 100644 --- a/bin/settings/Listing.php +++ b/bin/settings/Listing.php @@ -24,7 +24,6 @@ protected function execute(InputInterface $input, OutputInterface $output) [ 'Blog url', $settings['blog_url'] ], [ 'Date format', $settings['date_format'] ], [ 'Description', $settings['description'] ], - [ 'Editor code', $settings['editor_code'] ], [ 'Error log filename', $settings['log_file'] ], [ 'Footer code', $settings['footer_code'] ], [ 'Header code', $settings['header_code'] ], diff --git a/bin/settings/Set.php b/bin/settings/Set.php index c9f75ec..1dc6466 100644 --- a/bin/settings/Set.php +++ b/bin/settings/Set.php @@ -25,7 +25,6 @@ protected function execute(InputInterface $input, OutputInterface $output) 'Blog url' => 'blog_url', 'Date format' => 'date_format', 'Description' => 'description', - 'Editor code' => 'editor_code', 'Error log filename' => 'log_file', 'Footer code' => 'footer_code', 'Header code' => 'header_code', From 58d995a67de2e58fc1aee9edf155f961d9bfeb13 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 31 Oct 2025 19:43:40 +0100 Subject: [PATCH 137/334] Refactor login --- app/react/src/pages/Login.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index 38b8a45..6753563 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -36,12 +36,12 @@ export default function Login() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/send_password_restore', - data: { - email: email, - }, + url: '/api/v2/password-reset/request', + data: { email: email }, }).then(res => { - alert(res?.data?.success ? 'If the email is registered, you will receive an email with instructions to reset your password' : 'An error occurred, please try again later'); + alert(res?.data?.success + ? 'If the email is registered, you will receive an email with instructions to reset your password' + : 'An error occurred, please try again later'); setEmail(''); }).finally(() => setLoading(false)); }; From 6f56d61cb859065ecf26c7709ab7a4e333deb55b Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 1 Nov 2025 03:11:40 +0100 Subject: [PATCH 138/334] Add getTable function --- app/controllers/ModuleBase.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/controllers/ModuleBase.php b/app/controllers/ModuleBase.php index e9ddb15..5218c2d 100644 --- a/app/controllers/ModuleBase.php +++ b/app/controllers/ModuleBase.php @@ -169,6 +169,15 @@ public function remove(array $ids): bool return $success; } + /** + * Returns the table name + * @return string the table name + */ + public function getTable(): string + { + return $this->table; + } + /** * Returns the row with additional data mapped into it * @param mixed $data the row From 6588f60fb5cdeece11b3aa50a3290f95220af07b Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 1 Nov 2025 03:17:16 +0100 Subject: [PATCH 139/334] Fix password restore --- app/bootstrap/routes.php | 87 +++++++++++++++----------- app/controllers/modules/User.php | 58 +---------------- app/react/src/index.js | 2 + app/react/src/pages/NewPassword.js | 47 ++++++++++++++ app/views/emails/password_restore.html | 2 +- 5 files changed, 100 insertions(+), 96 deletions(-) create mode 100644 app/react/src/pages/NewPassword.js diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index b9633de..6a2d6b7 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -14,6 +14,10 @@ $rss = \Aurora\App\Setting::get('rss'); $router = $kernel->router; + $router->get([ 'console', 'console/*' ], function() use ($view) { + return $view->get('admin.html'); + }); + /* MEDIA */ $router->get('admin/media', function() use ($view, $lang) { @@ -462,12 +466,8 @@ ]); }); - $router->middleware('*', function() use ($db, $view, $lang, $theme_dir) { - if (Helper::isValidId($_SESSION['user']['id'] ?? false)) { - $_SESSION['user'] = $db->query('SELECT * FROM users WHERE id = ? AND status', $_SESSION['user']['id'])->fetch(); - } - - if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'console') && !str_starts_with(Helper::getCurrentPath(), 'api') && !Helper::isValidId($_SESSION['user']['id'] ?? false)) { + $router->middleware('*', function() use ($view, $lang, $theme_dir) { + if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'console') && !str_starts_with(Helper::getCurrentPath(), 'api') && !Helper::isValidId($GLOBALS['user']['id'] ?? false)) { echo $view->get("$theme_dir/information.html", [ 'description' => $lang->get('under_maintenance'), 'subdescription' => $lang->get('come_back_soon'), @@ -476,36 +476,61 @@ } }); - $router->get([ 'console', 'console/*' ], function() use ($view) { - return $view->get('admin.html'); - }); - - $router->post('json:admin/send_password_restore', function() use ($view, $user_mod) { + $router->post('json:api/v2/password-reset/request', function($body) use ($db, $lang, $user_mod, $view) { $hash = bin2hex(random_bytes(18)); - $errors = $user_mod->requestPasswordRestore($_POST['email'], - $hash, - $view->get('admin/emails/password_restore.html', [ 'hash' => $hash ])); + $user = $user_mod->get([ + 'email' => $body['email'], + 'status' => 1, + ]); return json_encode([ - 'success' => empty($errors), - 'errors' => $errors, + 'success' => $user && (bool) $db->replace('password_restores', [ + 'user_id' => $user['id'], + 'hash' => $hash, + 'created_at' => time(), + ]) && \Aurora\Core\Kernel::config('mail')($user['email'], $lang->get('restore_your_password'), $view->get('emails/password_restore.html', [ 'hash' => $hash ])), ]); }); - $router->get('admin/new_password', function() use ($view) { - return $view->get('admin/password_restore.html', [ 'hash' => $_GET['hash'] ]); - }); + $router->post('json:api/v2/password-reset/confirm', function($body) use ($db, $user_mod, $login) { + $hash = $body['hash'] ?? ''; + $password = $body['password'] ?? ''; + $restore = $db->query('SELECT * FROM password_restores WHERE hash = ?', $hash)->fetch(); + + if (empty($restore) || $restore['created_at'] < strtotime('-2 hours')) { + return json_encode([ + 'success' => false, + 'error' => 'expired_restore', + ]); + } + + $error = $user_mod->checkPassword($password, $body['password_confirm'] ?? ''); + if (empty($error)) { + $user = $user_mod->get([ + 'id' => $restore['user_id'], + 'status' => 1, + ]); + + if (!$user) { + return json_encode([ + 'success' => false, + 'error' => 'no_active_user', + ]); + } + + $db->delete('password_restores', $hash, 'hash'); + $db->update($user_mod->getTable(), [ 'password' => $user_mod->getPassword($password) ], $user['id']); + return json_encode($login($user['id'])); + } - $router->post('json:admin/password_restore', function() use ($user_mod) { - $error = $user_mod->passwordRestore($_POST['hash'], $_POST['password'], $_POST['password_confirm']); return json_encode([ - 'success' => empty($error), - 'errors' => [ $error ], + 'success' => false, + 'error' => $error, ]); }); $router->middleware('api/v2/*', function() use ($db, $user_mod) { - if (in_array(Helper::getCurrentPath(), [ 'api/v2/auth', 'api/v2/send_password_restore' ])) { + if (in_array(Helper::getCurrentPath(), [ 'api/v2/auth', 'api/v2/password-reset/request', 'api/v2/password-reset/confirm' ])) { return; } @@ -568,20 +593,6 @@ ]); }); - $router->post('json:api/v2/send_password_restore', function($body) use ($view, $user_mod) { - $hash = bin2hex(random_bytes(18)); - $user = $user_mod->get([ - 'email' => $body['email'] ?? '', - 'status' => 1, - ]); - - return json_encode([ - 'success' => !$user || $user_mod->requestPasswordRestore($user, - $hash, - $view->get('admin/emails/password_restore.html', [ 'hash' => $hash ])), - ]); - }); - $router->get('json:api/v2/users/impersonate', function() use ($user_mod, $login) { $user = $user_mod->get([ 'id' => $_GET['id'] ?? 0, diff --git a/app/controllers/modules/User.php b/app/controllers/modules/User.php index 31344a9..6d99719 100755 --- a/app/controllers/modules/User.php +++ b/app/controllers/modules/User.php @@ -97,62 +97,6 @@ public function handleLogin(string $email, string $password): array return $errors; } - /** - * Sends an email to restore the password of an user - * @param array $user the user's data - * @param string $hash the hash to restore the password - * @param string $message the email's content - * @return bool true on success, false otherwise - */ - public function requestPasswordRestore(array $user, string $hash, string $message): bool - { - $success = (bool) $this->db->replace('password_restores', [ - 'user_id' => $user['id'], - 'hash' => $hash, - 'created_at' => time(), - ]); - - if (!\Aurora\Core\Kernel::config('mail')($user['email'], $this->language->get('restore_your_password'), $message)) { - return false; - } - - return $success; - } - - /** - * Restores an user's password - * @param string $hash the hash to restore the password - * @param string $password the new password - * @param string $password_confirm the confirmation of the new password - * @return string the error message, if empty it means the password has been successfully been restored - */ - public function passwordRestore(string $hash, string $password, string $password_confirm): string - { - $restore = $this->db->query('SELECT * FROM password_restores WHERE hash = ?', $hash)->fetch(); - - if (empty($restore) || $restore['created_at'] < strtotime('-2 hours')) { - return $this->language->get('error_expired_restore'); - } - - $error = $this->checkPassword($password, $password_confirm); - if (empty($error)) { - $user = $this->get([ - 'id' => $restore['user_id'], - 'status' => 1, - ]); - - if (!$user) { - return $this->language->get('no_active_user'); - } - - $this->db->delete('password_restores', $hash, 'hash'); - $this->db->update($this->table, [ 'password' => $this->getPassword($password) ], $user['id']); - $_SESSION['user'] = $user; - } - - return $error; - } - /** * Returns an array with all the user fields that contain an error * @param array $data the user fields @@ -264,7 +208,7 @@ public function getPassword(string $password): string * @param string $password_confirm the password confirmation * @return string the error message, if empty it means both passwords are equal and valid */ - private function checkPassword(string $password, string $password_confirm): string + public function checkPassword(string $password, string $password_confirm): string { if (mb_strlen($password) < 8) { return $this->language->get('bad_password'); diff --git a/app/react/src/index.js b/app/react/src/index.js index 57740e2..1bd7c8f 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import AdminPages from './components/AdminPages'; +import NewPassword from './pages/NewPassword'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Settings from './pages/Settings'; @@ -23,6 +24,7 @@ const App = () => { return + }/> }/> }> }/> diff --git a/app/react/src/pages/NewPassword.js b/app/react/src/pages/NewPassword.js new file mode 100644 index 0000000..0152282 --- /dev/null +++ b/app/react/src/pages/NewPassword.js @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { makeRequest } from '../utils/utils'; +import { useNavigate } from 'react-router-dom'; + +export default function NewPassword() { + const logo = document.querySelector('meta[name="logo"]')?.content; + const [ loading, setLoading ] = useState(false); + const [ password, setPassword ] = useState(''); + const [ password_confirm, setPasswordConfirm ] = useState(''); + const navigate = useNavigate(); + + const submit = async e => { + setLoading(true); + e.preventDefault(); + makeRequest({ + method: 'POST', + url: '/api/v2/password-reset/confirm', + data: { + hash: (new URLSearchParams(window.location.search)).get('hash'), + password: password, + password_confirm: password_confirm, + }, + }).then(res => { + if (!res?.data?.success) { + alert('Invalid email or password'); + } else { + localStorage.setItem('auth_token', res.data.token); + navigate('/console/dashboard'); + } + }).finally(() => setLoading(false)); + }; + + return
+
+ {logo && } +
+ + setPassword(e.target.value)}/> +
+
+ + setPasswordConfirm(e.target.value)}/> +
+ + +
; +} \ No newline at end of file diff --git a/app/views/emails/password_restore.html b/app/views/emails/password_restore.html index ddb0367..1032085 100644 --- a/app/views/emails/password_restore.html +++ b/app/views/emails/password_restore.html @@ -8,7 +8,7 @@ url()) ?> From 3775f5f18fbcef6debe46f8f6e90d7e01f1e94cc Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 1 Nov 2025 03:34:03 +0100 Subject: [PATCH 140/334] Refactor routes code --- app/bootstrap/routes.php | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 6a2d6b7..6b5e7c1 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -466,7 +466,18 @@ ]); }); - $router->middleware('*', function() use ($view, $lang, $theme_dir) { + $router->middleware('*', function() use ($db, $view, $lang, $theme_dir, $user_mod) { + $token = preg_match('/Bearer\s(\S+)/', getallheaders()['Authorization'] ?? '', $matches) + ? $matches[1] + : false; + + $GLOBALS['user'] = $user_mod->get([ + 'id' => $db->query('SELECT user_id FROM tokens WHERE token = ?', $token)->fetchColumn(), + 'status' => 1, + ]); + + \Aurora\App\Permission::set($db->query('SELECT permission, role_level FROM roles_permissions ORDER BY permission')->fetchAll(\PDO::FETCH_KEY_PAIR), $GLOBALS['user']['role'] ?? 0); + if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'console') && !str_starts_with(Helper::getCurrentPath(), 'api') && !Helper::isValidId($GLOBALS['user']['id'] ?? false)) { echo $view->get("$theme_dir/information.html", [ 'description' => $lang->get('under_maintenance'), @@ -529,23 +540,8 @@ ]); }); - $router->middleware('api/v2/*', function() use ($db, $user_mod) { - if (in_array(Helper::getCurrentPath(), [ 'api/v2/auth', 'api/v2/password-reset/request', 'api/v2/password-reset/confirm' ])) { - return; - } - - $token = preg_match('/Bearer\s(\S+)/', getallheaders()['Authorization'] ?? '', $matches) - ? $matches[1] - : false; - - $GLOBALS['user'] = $user_mod->get([ - 'id' => $db->query('SELECT user_id FROM tokens WHERE token = ?', $token)->fetchColumn(), - 'status' => 1, - ]); - - \Aurora\App\Permission::set($db->query('SELECT permission, role_level FROM roles_permissions ORDER BY permission')->fetchAll(\PDO::FETCH_KEY_PAIR), $GLOBALS['user']['role'] ?? 0); - - if (empty($GLOBALS['user'])) { + $router->middleware('api/v2/*', function() { + if (empty($GLOBALS['user']) && !in_array(Helper::getCurrentPath(), [ 'api/v2/auth', 'api/v2/password-reset/request', 'api/v2/password-reset/confirm' ])) { http_response_code(401); exit; } From 8561394a7ebdf1391063728a4831b37ca788515a Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 1 Nov 2025 18:55:10 +0100 Subject: [PATCH 141/334] Dev new media page --- app/react/src/index.js | 3 +- app/react/src/pages/tables/Media.js | 107 ++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 app/react/src/pages/tables/Media.js diff --git a/app/react/src/index.js b/app/react/src/index.js index 1bd7c8f..878c330 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -10,6 +10,7 @@ import Settings from './pages/Settings'; import Links from './pages/tables/Links'; import Link from './pages/Link'; import Tags from './pages/tables/Tags'; +import Media from './pages/tables/Media'; import Pages from './pages/tables/Pages'; import Posts from './pages/tables/Posts'; import Users from './pages/tables/Users'; @@ -31,7 +32,7 @@ const App = () => { }/> }/> }/> - {/* }/> */} + }/> }/> }/> }/> diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js new file mode 100644 index 0000000..c8df8d6 --- /dev/null +++ b/app/react/src/pages/tables/Media.js @@ -0,0 +1,107 @@ +import React from 'react'; +import { Table } from '../../components/Table'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { DropdownMenu, formatDate, formatSize, makeRequest } from '../../utils/utils'; +import { IconFile, IconFolderFill, IconThreeDots, IconTrash } from '../../utils/icons'; + +export default function Media() { + const { user } = useOutletContext(); + const navigate = useNavigate(); + + return
+
+ New, + condition: Boolean(user?.actions?.edit_media), + onClick: () => navigate('/console/media/edit'), + }, + ]} + rowOnClick={media => navigate(`/console/media/edit?id=${media.id}`)} + filters={{ + order: { + title: 'Sort by', + options: [ + { key: 'type', title: 'Type' }, + { key: 'name', title: 'Name' }, + { key: 'size', title: 'Size' }, + ], + }, + sort: { + options: [ + { key: 'asc', title: 'Ascending' }, + { key: 'desc', title: 'Descending' }, + ], + }, + }} + options={[ + { + title: 'Delete', + class: 'danger', + condition: Boolean(user?.actions?.edit_media), + onClick: (file) => { + if (confirm('Are you sure you want to delete the selected files? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/media', + data: { id: file.map(l => l.id) }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + }, + ]} + columns={[ + { + class: 'w100', + content: file => <> + {file.is_image && + + } + {!file.is_image && file.is_file && + + } + {!file.is_file && + + } + {file.name} + , + }, + { + title: 'Slug', + class: 'w20 file-info', + content: file =>

{file.is_file ? formatSize(file.size) : file.mime}

, + }, + { + title: '', + class: 'w20', + content: file =>

{formatDate(file.time)}

, + }, + { + class: 'w10 row-actions', + content: tag => } + className="three-dots" + options={[ + { + class: 'danger', + condition: Boolean(user?.actions?.edit_media), + onClick: () => { + if (confirm('Are you sure you want to delete the file? This action cannot be undone.')) { + makeRequest({ + method: 'DELETE', + url: '/api/v2/media', + data: { id: tag.id }, + }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); + } + }, + content: <> Delete + }, + ]} + />, + }, + ]} + /> + +} \ No newline at end of file From 9f3614dd812d8472a585127f475082d88399f04c Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 7 Nov 2025 21:12:55 +0100 Subject: [PATCH 142/334] Fix columns --- app/react/src/pages/tables/Media.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index c8df8d6..c5e9fb6 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -69,12 +69,12 @@ export default function Media() { , }, { - title: 'Slug', + title: 'Information', class: 'w20 file-info', content: file =>

{file.is_file ? formatSize(file.size) : file.mime}

, }, { - title: '', + title: 'Last Modification', class: 'w20', content: file =>

{formatDate(file.time)}

, }, From 5a11869652087116bfee2187a759660c945c49eb Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 7 Nov 2025 21:13:13 +0100 Subject: [PATCH 143/334] Remove extra lines --- app/react/src/pages/User.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index 36afd10..8f73269 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -164,5 +164,3 @@ export default function User() { ); } - - From 3d35d39a03a0262e0e54b90b9dc5ead721893ed3 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 7 Nov 2025 23:12:21 +0100 Subject: [PATCH 144/334] Add props parameters to icons --- app/react/src/utils/icons.js | 72 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/app/react/src/utils/icons.js b/app/react/src/utils/icons.js index eb3be73..dba93ff 100644 --- a/app/react/src/utils/icons.js +++ b/app/react/src/utils/icons.js @@ -1,38 +1,38 @@ import React from 'react'; -export const IconBook = () => ; -export const IconCheckCircle = () => ; -export const IconClipboard = () => ; -export const IconCode = () => ; -export const IconDatabase = () => ; -export const IconDots = () => ; -export const IconDuplicate = () => ; -export const IconEye = () => ; -export const IconFile = () => ; -export const IconFolderFill = () => ; -export const IconFolder = () => ; -export const IconGlass = () => ; -export const IconHome = () => ; -export const IconImage = () => ; -export const IconLink = () => ; -export const IconLogout = () => ; -export const IconMenu = () => ; -export const IconMoon = () => ; -export const IconMoveFile = () => ; -export const IconNote = () => ; -export const IconPencil = () => ; -export const IconServer = () => ; -export const IconSettings = () => ; -export const IconSun = () => ; -export const IconSync = () => ; -export const IconTag = () => ; -export const IconTerminal = () => ; -export const IconTrash = () => ; -export const IconUploadFile = () => ; -export const IconUser = () => ; -export const IconUsers = () => ; -export const IconWindow = () => ; -export const IconXCircle = () => ; -export const IconX = () => ; -export const IconZip = () => ; -export const IconThreeDots = () => ; \ No newline at end of file +export const IconBook = (props) => ; +export const IconCheckCircle = (props) => ; +export const IconClipboard = (props) => ; +export const IconCode = (props) => ; +export const IconDatabase = (props) => ; +export const IconDots = (props) => ; +export const IconDuplicate = (props) => ; +export const IconEye = (props) => ; +export const IconFile = (props) => ; +export const IconFolderFill = (props) => ; +export const IconFolder = (props) => ; +export const IconGlass = (props) => ; +export const IconHome = (props) => ; +export const IconImage = (props) => ; +export const IconLink = (props) => ; +export const IconLogout = (props) => ; +export const IconMenu = (props) => ; +export const IconMoon = (props) => ; +export const IconMoveFile = (props) => ; +export const IconNote = (props) => ; +export const IconPencil = (props) => ; +export const IconServer = (props) => ; +export const IconSettings = (props) => ; +export const IconSun = (props) => ; +export const IconSync = (props) => ; +export const IconTag = (props) => ; +export const IconTerminal = (props) => ; +export const IconTrash = (props) => ; +export const IconUploadFile = (props) => ; +export const IconUser = (props) => ; +export const IconUsers = (props) => ; +export const IconWindow = (props) => ; +export const IconXCircle = (props) => ; +export const IconX = (props) => ; +export const IconZip = (props) => ; +export const IconThreeDots = (props) => ; \ No newline at end of file From b50e5b9e0a6ab426e9cc7f2f93c542d9b7cdc4e9 Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 7 Nov 2025 23:35:35 +0100 Subject: [PATCH 145/334] Unify and fix file and folder icons styles --- app/react/src/pages/tables/Media.js | 6 ++--- app/react/src/utils/utils.js | 6 ++--- public/assets/css/admin/main.css | 40 ++++++++++------------------- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index c5e9fb6..b81e4e7 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -1,7 +1,7 @@ import React from 'react'; import { Table } from '../../components/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, formatDate, formatSize, makeRequest } from '../../utils/utils'; +import { DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; import { IconFile, IconFolderFill, IconThreeDots, IconTrash } from '../../utils/icons'; export default function Media() { @@ -54,10 +54,10 @@ export default function Media() { ]} columns={[ { - class: 'w100', + class: 'w100 align-center', content: file => <> {file.is_image && - + } {!file.is_image && file.is_file && diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 15ace8a..0edd750 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -281,7 +281,7 @@ export const ImageDialog = ({ onSave, onClose }) => { {files.map(file => { const file_path = getContentUrl(file.path); return
{ if (file.is_file) { onSave(file.path); @@ -294,10 +294,10 @@ export const ImageDialog = ({ onSave, onClose }) => { diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 38126bb..2bc9b08 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -1158,6 +1158,7 @@ textarea.code { text-overflow: ellipsis; overflow: hidden; white-space: break-spaces; + margin: 0; } .listing-row:not(.header):hover { @@ -1287,18 +1288,11 @@ textarea.code { aspect-ratio: 16 / 9; } -.listing-row.file > div > *:first-child { - display: flex; -} - .listing-row.post .main-data { flex: 1; } -.row-thumb, -.listing-row.post > div > img, -.listing-row.file > div > *:first-child > img, -.listing-row.file > div > *:first-child > svg { +.row-thumb { height: var(--image-size); width: auto; aspect-ratio: 16/9; @@ -1306,27 +1300,21 @@ textarea.code { border-radius: 4px; } -.listing-row.file > div > *:first-child > img, -.listing-row.file > div > *:first-child > svg { - aspect-ratio: 13/9; -} - -.listing-row.file > div > *:first-child > svg { - color: var(--second-color); -} - -.listing-row.file .custom-media svg { - --image-size: 32px; - padding: 9px 13px; -} - -.listing-row.file .custom-media.file svg { +.custom-media.file svg { + height: 50px; + width: auto; + box-sizing: border-box; + aspect-ratio: 16 / 9; + padding: 6px; fill: var(--main-color); } -.listing-row.file .custom-media.folder svg { - --image-size: 32px; - padding: 9px 13px; +.custom-media.folder svg { + height: 50px; + width: auto; + box-sizing: border-box; + aspect-ratio: 16 / 9; + padding: 6px; fill: #54aeff; } From dbb171ddefb303113aac72a5d90059cccef5393f Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 8 Nov 2025 00:08:07 +0100 Subject: [PATCH 146/334] Improve url code in table --- app/react/src/components/Table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/components/Table.js b/app/react/src/components/Table.js index b84b031..0493bf6 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/components/Table.js @@ -58,7 +58,7 @@ export const Table = ({ const options = initialOptions.filter(opt => opt.condition === undefined || opt.condition); const { data: page_req, is_loading, is_error, fetch } = useRequest({ method: 'GET', - url: url + (query_string ? `?${query_string}` : ''), + url: url + (query_string ? `${url.includes('?') ? '&' : '?'}${query_string}` : ''), }); useEffect(() => { @@ -77,7 +77,7 @@ export const Table = ({ useEffect(() => { fetch(); - }, [ query_string ]); + }, [ url, query_string ]); useEffect(() => { const page_rows = page_req?.data?.data || null; From 3cdedb2f502e56eb3cf72b0f85a5279e3c1e52e9 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 8 Nov 2025 00:08:14 +0100 Subject: [PATCH 147/334] Dev of media page --- app/react/src/pages/tables/Media.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index b81e4e7..cc1ce43 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Table } from '../../components/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; @@ -6,11 +6,12 @@ import { IconFile, IconFolderFill, IconThreeDots, IconTrash } from '../../utils/ export default function Media() { const { user } = useOutletContext(); + const [ current_path, setCurrentPath ] = useState(''); const navigate = useNavigate(); return
navigate('/console/media/edit'), }, ]} - rowOnClick={media => navigate(`/console/media/edit?id=${media.id}`)} + rowOnClick={file => setCurrentPath(file.path)} filters={{ order: { title: 'Sort by', @@ -62,9 +63,9 @@ export default function Media() { {!file.is_image && file.is_file && } - {!file.is_file && + {!file.is_file &&
- } +
} {file.name} , }, From e8356834a28c0ac96a1af05da75cb26a9f6b6835 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 8 Nov 2025 00:12:54 +0100 Subject: [PATCH 148/334] Fix onclick for rows in media --- app/react/src/pages/tables/Media.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index cc1ce43..3a75886 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -20,7 +20,6 @@ export default function Media() { onClick: () => navigate('/console/media/edit'), }, ]} - rowOnClick={file => setCurrentPath(file.path)} filters={{ order: { title: 'Sort by', @@ -57,13 +56,13 @@ export default function Media() { { class: 'w100 align-center', content: file => <> - {file.is_image && + {file.is_image && } - {!file.is_image && file.is_file && + {!file.is_image && file.is_file && } - {!file.is_file &&
+ {!file.is_file &&
setCurrentPath(file.path)} className="pointer custom-media folder">
} {file.name} From 917d560d50c8bf461f4202b2b723081360b66aa8 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 8 Nov 2025 19:47:33 +0100 Subject: [PATCH 149/334] Add path logic in media page --- app/react/src/pages/tables/Media.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index 3a75886..b62e511 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -1,17 +1,21 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Table } from '../../components/Table'; -import { useNavigate, useOutletContext } from 'react-router-dom'; +import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'; import { DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; import { IconFile, IconFolderFill, IconThreeDots, IconTrash } from '../../utils/icons'; export default function Media() { const { user } = useOutletContext(); - const [ current_path, setCurrentPath ] = useState(''); + const [ search_params, setSearchParams ] = useSearchParams(); const navigate = useNavigate(); - return
+ const setPath = (new_path) => { + setSearchParams({ ...search_params, path: new_path }); + }; + + return
} - {!file.is_file &&
setCurrentPath(file.path)} className="pointer custom-media folder"> + {!file.is_file &&
setPath(file.path)} className="pointer custom-media folder">
} {file.name} From 559f0f977e435124b529f42a5d746fd162699dd0 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 18:57:53 +0100 Subject: [PATCH 150/334] Add media path to media page and improve styles --- app/react/src/pages/tables/Media.js | 18 +++++++++++++++++- public/assets/css/admin/main.css | 29 +++++++++-------------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index b62e511..cfbdd1a 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -2,7 +2,22 @@ import React from 'react'; import { Table } from '../../components/Table'; import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'; import { DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; -import { IconFile, IconFolderFill, IconThreeDots, IconTrash } from '../../utils/icons'; +import { IconFile, IconFolderFill, IconHome, IconThreeDots, IconTrash } from '../../utils/icons'; + +const MediaPath = ({ path, setPath }) => { + const paths = path.split('/'); + + return
+ {paths.map((folder, i) => { + const folder_path = paths.slice(0, i + 1).join('/'); + + return <> +
setPath(folder_path)} className="pointer">{i == 0 ? : folder}
+ / + ; + })} +
; +}; export default function Media() { const { user } = useOutletContext(); @@ -14,6 +29,7 @@ export default function Media() { }; return
+
a, .media-paths > div { display: flex; justify-content: center; @@ -1404,17 +1396,14 @@ textarea.code { font-size: var(--font-size-bg); } -.media-paths > a:hover, .media-paths > div:hover { background-color: var(--third-color); } -.media-paths > a svg, .media-paths > div svg { fill: var(--main-color); } -.media-paths > a:last-of-type, .media-paths > div:last-of-type { font-weight: var(--font-bold); } From dc1392a2e447298532792cbdebac5092dd58ef48 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 18:58:05 +0100 Subject: [PATCH 151/334] Fix media path html --- app/react/src/utils/utils.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 0edd750..c74608b 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -330,13 +330,11 @@ export const ImageDialog = ({ onSave, onClose }) => {
-
-
- {folders.map((folder, i) => <> -
setPath(folders.slice(0, i + 1).join('/'))}>{i == 0 ? : folder}
- / - )} -
+
+ {folders.map((folder, i) => <> +
setPath(folders.slice(0, i + 1).join('/'))}>{i == 0 ? : folder}
+ / + )}
, document.querySelector('body')); From d7b18b80f8808af1f824cf19220abd5d47537833 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 19:18:06 +0100 Subject: [PATCH 152/334] Refactor options code --- app/react/src/pages/tables/Media.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index cfbdd1a..15b38b7 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -61,12 +61,12 @@ export default function Media() { title: 'Delete', class: 'danger', condition: Boolean(user?.actions?.edit_media), - onClick: (file) => { + onClick: (files) => { if (confirm('Are you sure you want to delete the selected files? This action cannot be undone.')) { makeRequest({ method: 'DELETE', url: '/api/v2/media', - data: { id: file.map(l => l.id) }, + data: { id: files.map(l => l.id) }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } }, From 6b16a963f7c7f6de190ecf9726931a6f502582de Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 19:21:44 +0100 Subject: [PATCH 153/334] Move CSS variable definition --- public/assets/css/admin/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 7ede598..827d623 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -29,6 +29,7 @@ --font-size-bg: 15px; --image-size: 50px; --nav-z-index: 10; + --content-spacing: 34px; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; font-size: var(--font-size); line-height: 1.3em; @@ -854,7 +855,6 @@ textarea.code { display: flex; flex-direction: column; gap: 20px; - --content-spacing: 34px; padding: var(--content-spacing); position: relative; background-color: var(--background-color); From 442baecab543029c301bce08c61c05ec5f992038 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 19:40:17 +0100 Subject: [PATCH 154/334] Fix variable name --- app/react/src/pages/tables/Media.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index 15b38b7..387cce1 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -100,7 +100,7 @@ export default function Media() { }, { class: 'w10 row-actions', - content: tag => } className="three-dots" options={[ @@ -112,7 +112,7 @@ export default function Media() { makeRequest({ method: 'DELETE', url: '/api/v2/media', - data: { id: tag.id }, + data: { id: file.id }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } }, From 5619ccaa05cfa3135a5a4ccb81a9a2243dc4dd70 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 19:56:42 +0100 Subject: [PATCH 155/334] Add edit file dialog --- app/bootstrap/routes.php | 44 ++++++++++------------ app/react/src/pages/tables/Media.js | 57 ++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 27 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 6b5e7c1..5c2f396 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -161,31 +161,6 @@ ]); }); - $router->post('json:admin/media/save', function() use ($lang) { - if (empty($_POST['name']) || str_contains($_POST['name'], '/')) { - return json_encode([ - 'success' => false, - 'errors' => [ 'name' => $lang->get('invalid_value') ] - ]); - } - - if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]); - } - - try { - $success = \Aurora\App\Media::rename($_POST['path'] ?? '', $_POST['name']); - } catch (Exception) { - $success = false; - } - - return json_encode([ - 'success' => $success, - 'errors' => $success ? [] : [ $lang->get('error_rename_item') ], - ]); - }); - $router->post('json:admin/media/move', function() use ($lang) { if (!\Aurora\App\Permission::can('edit_media')) { http_response_code(403); @@ -603,6 +578,25 @@ return json_encode($login($user['id'])); }); + $router->post('json:api/v2/media/rename', function($body) { + if (empty($body['name']) || str_contains($body['name'], '/')) { + return json_encode([ 'success' => false ]); + } + + if (!\Aurora\App\Permission::can('edit_media')) { + http_response_code(403); + exit; + } + + try { + $success = \Aurora\App\Media::rename($body['path'] ?? '', $body['name']); + } catch (Exception) { + $success = false; + } + + return json_encode([ 'success' => $success ]); + }); + $router->any('json:api/v2/media/upload_image', function() { $path = Kernel::config('content') . '/' . date('Y/m/'); \Aurora\App\Media::uploadFile($_FILES['file'], $path); diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index 387cce1..bbe8772 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Table } from '../../components/Table'; import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'; import { DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; -import { IconFile, IconFolderFill, IconHome, IconThreeDots, IconTrash } from '../../utils/icons'; +import { IconFile, IconFolderFill, IconHome, IconPencil, IconThreeDots, IconTrash, IconX } from '../../utils/icons'; +import { createPortal } from 'react-dom'; const MediaPath = ({ path, setPath }) => { const paths = path.split('/'); @@ -19,17 +20,61 @@ const MediaPath = ({ path, setPath }) => { ; }; +const DialogEditFile = ({ file, onClose }) => { + const [ name, setName ] = useState(file.name); + + const save = () => { + makeRequest({ + method: 'POST', + url: '/api/v2/media/rename', + data: { + name: name, + path: getContentUrl(file.path), + }, + }).then(res => { + alert(res?.data?.success ? 'Done' : 'Error renaming item. The name is invalid, the file does not comply with the server rules or the path is not writable.'); + onClose(); + }); + }; + + return createPortal(
+
+
+
+

Rename

+ + + +
+
+
+ + setName(e.target.value)}/> +
+
+ + +
+
+
, document.body); +}; + export default function Media() { const { user } = useOutletContext(); const [ search_params, setSearchParams ] = useSearchParams(); + const [ current_dialog, setCurrentDialog ] = useState(null); + const [ current_file, setCurrentFile ] = useState(null); const navigate = useNavigate(); const setPath = (new_path) => { setSearchParams({ ...search_params, path: new_path }); }; + const closeDialog = () => setCurrentDialog(null); + return
+ {current_dialog == 'edit_file' && }
} className="three-dots" options={[ + { + condition: Boolean(user?.actions?.edit_media), + onClick: () => { + setCurrentFile(file); + setCurrentDialog('edit_file'); + }, + content: <> Rename + }, { class: 'danger', condition: Boolean(user?.actions?.edit_media), From b72c27c387d2a10564f13bd63071a932f9434480 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 20:02:44 +0100 Subject: [PATCH 156/334] Refactor media page code --- app/react/src/pages/tables/Media.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index bbe8772..2c3d340 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -66,8 +66,11 @@ export default function Media() { const [ current_file, setCurrentFile ] = useState(null); const navigate = useNavigate(); - const setPath = (new_path) => { - setSearchParams({ ...search_params, path: new_path }); + const setPath = (new_path) => setSearchParams({ ...search_params, path: new_path }); + + const openDialog = (dialog, file) => { + setCurrentFile(file); + setCurrentDialog(dialog); }; const closeDialog = () => setCurrentDialog(null); @@ -151,10 +154,7 @@ export default function Media() { options={[ { condition: Boolean(user?.actions?.edit_media), - onClick: () => { - setCurrentFile(file); - setCurrentDialog('edit_file'); - }, + onClick: () => openDialog('edit_file', file), content: <> Rename }, { From d485b7afd45541980c63cd0494316dae23c3de71 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 20:28:55 +0100 Subject: [PATCH 157/334] Add duplicate file dialog --- app/bootstrap/routes.php | 47 ++++++++++++++--------------- app/react/src/pages/tables/Media.js | 47 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 5c2f396..f152490 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -188,31 +188,6 @@ ]); }); - $router->post('json:admin/media/duplicate', function() use ($lang) { - if (empty($_POST['name']) || str_contains($_POST['name'], '/')) { - return json_encode([ - 'success' => false, - 'errors' => [ 'name' => $lang->get('invalid_value') ] - ]); - } - - if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]); - } - - try { - $success = \Aurora\App\Media::duplicate($_POST['path'] ?? '', $_POST['name']); - } catch (Exception) { - $success = false; - } - - return json_encode([ - 'success' => $success, - 'errors' => $success ? [] : [ $lang->get('error_duplicate_item') ], - ]); - }); - $router->get('admin/settings/media_download', function() use ($lang) { $file_path = Helper::getPath('content.zip'); $path = $_GET['path'] ?? ''; @@ -578,6 +553,28 @@ return json_encode($login($user['id'])); }); + $router->post('json:api/v2/media/duplicate', function($body) { + if (empty($body['name']) || str_contains($body['name'], '/')) { + return json_encode([ + 'success' => false, + 'error' => 'invalid_value', + ]); + } + + if (!\Aurora\App\Permission::can('edit_media')) { + http_response_code(403); + exit; + } + + try { + $success = \Aurora\App\Media::duplicate($body['path'] ?? '', $body['name']); + } catch (Exception) { + $success = false; + } + + return json_encode([ 'success' => $success ]); + }); + $router->post('json:api/v2/media/rename', function($body) { if (empty($body['name']) || str_contains($body['name'], '/')) { return json_encode([ 'success' => false ]); diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index 2c3d340..7c3bdfb 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Table } from '../../components/Table'; import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'; import { DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; -import { IconFile, IconFolderFill, IconHome, IconPencil, IconThreeDots, IconTrash, IconX } from '../../utils/icons'; +import { IconDuplicate, IconFile, IconFolderFill, IconHome, IconPencil, IconThreeDots, IconTrash, IconX } from '../../utils/icons'; import { createPortal } from 'react-dom'; const MediaPath = ({ path, setPath }) => { @@ -59,6 +59,45 @@ const DialogEditFile = ({ file, onClose }) => { , document.body); }; +const DialogDuplicate = ({ file, onClose }) => { + const [ name, setName ] = useState(file.name); + + const save = () => { + makeRequest({ + method: 'POST', + url: '/api/v2/media/duplicate', + data: { + name: name, + path: getContentUrl(file.path), + }, + }).then(res => { + alert(res?.data?.success ? 'Done' : 'Error duplicating item. The name is invalid, the file does not comply with the server rules or the path is not writable.'); + onClose(); + }); + }; + + return createPortal(
+
+
+
+

Duplicate

+ + + +
+
+
+ + setName(e.target.value)}/> +
+
+ + +
+
+
, document.body); +}; + export default function Media() { const { user } = useOutletContext(); const [ search_params, setSearchParams ] = useSearchParams(); @@ -78,6 +117,7 @@ export default function Media() { return
{current_dialog == 'edit_file' && } + {current_dialog == 'duplicate_file' && }
} className="three-dots" options={[ + { + condition: Boolean(user?.actions?.edit_media), + onClick: () => openDialog('duplicate_file', file), + content: <> Duplicate + }, { condition: Boolean(user?.actions?.edit_media), onClick: () => openDialog('edit_file', file), From 447f552c9a8ca0c1575d87e1ee06609c133bcdd7 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 22 Dec 2025 20:29:03 +0100 Subject: [PATCH 158/334] Remove old media code --- app/views/admin/partials/media_header.html | 66 ---------------------- 1 file changed, 66 deletions(-) diff --git a/app/views/admin/partials/media_header.html b/app/views/admin/partials/media_header.html index 8f04302..2135b7c 100644 --- a/app/views/admin/partials/media_header.html +++ b/app/views/admin/partials/media_header.html @@ -27,26 +27,6 @@

t('media') ?>

-
-
-
-
-

t('rename') ?>

- - include('icons/x.svg') ?> - -
-
-
- - -
-
- - -
-
-
@@ -91,26 +71,6 @@

t('move') ?>

-
-
-
-
-

t('duplicate') ?>

- - include('icons/x.svg') ?> - -
-
-
- - -
-
- - -
-
-
From aac66596ee87c282fb1b869fbbb6dfe0c00b2bdd Mon Sep 17 00:00:00 2001 From: Usbac Date: Wed, 24 Dec 2025 08:46:45 +0100 Subject: [PATCH 175/334] Remove v2 from api urls --- app/bootstrap/routes.php | 60 +++++++++++++------------- app/react/src/components/AdminPages.js | 4 +- app/react/src/pages/Dashboard.js | 6 +-- app/react/src/pages/Link.js | 6 +-- app/react/src/pages/Login.js | 6 +-- app/react/src/pages/NewPassword.js | 2 +- app/react/src/pages/Page.js | 8 ++-- app/react/src/pages/Post.js | 10 ++--- app/react/src/pages/Settings.js | 14 +++--- app/react/src/pages/Tag.js | 6 +-- app/react/src/pages/User.js | 10 ++--- app/react/src/pages/tables/Links.js | 6 +-- app/react/src/pages/tables/Media.js | 20 ++++----- app/react/src/pages/tables/Pages.js | 6 +-- app/react/src/pages/tables/Posts.js | 8 ++-- app/react/src/pages/tables/Tags.js | 6 +-- app/react/src/pages/tables/Users.js | 10 ++--- app/react/src/utils/utils.js | 10 ++--- 18 files changed, 99 insertions(+), 99 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 580ab85..5e7c2cd 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -239,7 +239,7 @@ } }); - $router->post('json:api/v2/password-reset/request', function($body) use ($db, $lang, $user_mod, $view) { + $router->post('json:api/password-reset/request', function($body) use ($db, $lang, $user_mod, $view) { $hash = bin2hex(random_bytes(18)); $user = $user_mod->get([ 'email' => $body['email'], @@ -255,7 +255,7 @@ ]); }); - $router->post('json:api/v2/password-reset/confirm', function($body) use ($db, $user_mod, $login) { + $router->post('json:api/password-reset/confirm', function($body) use ($db, $user_mod, $login) { $hash = $body['hash'] ?? ''; $password = $body['password'] ?? ''; $restore = $db->query('SELECT * FROM password_restores WHERE hash = ?', $hash)->fetch(); @@ -292,14 +292,14 @@ ]); }); - $router->middleware('api/v2/*', function() { - if (empty($GLOBALS['user']) && !in_array(Helper::getCurrentPath(), [ 'api/v2/auth', 'api/v2/password-reset/request', 'api/v2/password-reset/confirm' ])) { + $router->middleware('api/*', function() { + if (empty($GLOBALS['user']) && !in_array(Helper::getCurrentPath(), [ 'api/auth', 'api/password-reset/request', 'api/password-reset/confirm' ])) { http_response_code(401); exit; } }); - $router->any('json:api/v2/auth', function($body) use ($user_mod, $login) { + $router->any('json:api/auth', function($body) use ($user_mod, $login) { $email = $body['email'] ?? ''; $password = $body['password'] ?? ''; $user = $user_mod->get([ @@ -317,7 +317,7 @@ return json_encode($login($user['id'])); }); - $router->get('json:api/v2/me', function() { + $router->get('json:api/me', function() { $user = $GLOBALS['user']; foreach (\Aurora\App\Permission::getPermissions() as $action) { $user['actions'][$action] = \Aurora\App\Permission::can($action); @@ -326,7 +326,7 @@ return json_encode($user); }); - $router->get('json:api/v2/settings', function() use ($db, $lang) { + $router->get('json:api/settings', function() use ($db, $lang) { $themes_dir = Helper::getPath(Kernel::config('views') . '/themes'); return json_encode([ @@ -341,7 +341,7 @@ ]); }); - $router->get('json:api/v2/users/impersonate', function() use ($user_mod, $login) { + $router->get('json:api/users/impersonate', function() use ($user_mod, $login) { $user = $user_mod->get([ 'id' => $_GET['id'] ?? 0, 'status' => 1, @@ -355,7 +355,7 @@ return json_encode($login($user['id'])); }); - $router->post('json:api/v2/media/create_folder', function($body) { + $router->post('json:api/media/create_folder', function($body) { if (!\Aurora\App\Permission::can('edit_media')) { http_response_code(403); exit; @@ -370,7 +370,7 @@ return json_encode([ 'success' => $success ]); }); - $router->post('json:api/v2/media/duplicate', function($body) { + $router->post('json:api/media/duplicate', function($body) { if (empty($body['name']) || str_contains($body['name'], '/')) { return json_encode([ 'success' => false, @@ -392,7 +392,7 @@ return json_encode([ 'success' => $success ]); }); - $router->post('json:api/v2/media/rename', function($body) { + $router->post('json:api/media/rename', function($body) { if (empty($body['name']) || str_contains($body['name'], '/')) { return json_encode([ 'success' => false ]); } @@ -411,7 +411,7 @@ return json_encode([ 'success' => $success ]); }); - $router->post('json:api/v2/media/move', function($body) { + $router->post('json:api/media/move', function($body) { if (!\Aurora\App\Permission::can('edit_media')) { http_response_code(403); exit; @@ -426,7 +426,7 @@ return json_encode([ 'success' => $success ]); }); - $router->post('json:api/v2/media/upload', function() { + $router->post('json:api/media/upload', function() { if (!\Aurora\App\Permission::can('edit_media')) { http_response_code(403); exit; @@ -455,7 +455,7 @@ return json_encode([ 'success' => $success ]); }); - $router->get('api/v2/media/download', function() { + $router->get('api/media/download', function() { $file_path = Helper::getPath('content.zip'); $path = Helper::getPath(Kernel::config('content') . '/' . ltrim($_GET['path'] ?? '', '/')); @@ -482,7 +482,7 @@ Helper::downloadFile($file_path, 'media.zip', 'application/zip'); }); - $router->get('json:api/v2/media/folders', function() { + $router->get('json:api/media/folders', function() { $folders = [ Kernel::config('content') => '/' ]; $content_dir = Helper::getPath(Kernel::config('content')); @@ -500,14 +500,14 @@ return json_encode(array_values($folders)); }); - $router->any('json:api/v2/media/upload_image', function() { + $router->any('json:api/media/upload_image', function() { $path = Kernel::config('content') . '/' . date('Y/m/'); \Aurora\App\Media::uploadFile($_FILES['file'], $path); return json_encode([ 'location' => "/$path/" . $_FILES['file']['name'] ]); }); - $router->post('json:api/v2/media', function() { + $router->post('json:api/media', function() { if (!\Aurora\App\Permission::can('edit_media')) { http_response_code(403); exit; @@ -536,7 +536,7 @@ return json_encode([ 'success' => $success ]); }); - $router->get('json:api/v2/db', function() use ($db) { + $router->get('json:api/db', function() use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -551,7 +551,7 @@ ]); }); - $router->post('json:api/v2/db', function() use ($db, $lang) { + $router->post('json:api/db', function() use ($db, $lang) { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -583,7 +583,7 @@ return json_encode($data); }); - $router->get('api/v2/logs', function() { + $router->get('api/logs', function() { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -592,7 +592,7 @@ return file_get_contents(\Aurora\Core\Helper::getPath(\Aurora\App\Setting::get('log_file'))); }); - $router->delete('json:api/v2/logs', function() { + $router->delete('json:api/logs', function() { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -601,7 +601,7 @@ return json_encode([ 'success' => unlink(Helper::getPath(\Aurora\App\Setting::get('log_file'))) ]); }); - $router->get('json:api/v2/reset_views_count', function() use ($db) { + $router->get('json:api/reset_views_count', function() use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -610,7 +610,7 @@ return json_encode([ 'success' => $db->delete('views') ]); }); - $router->post('json:api/v2/settings', function($body) use ($db) { + $router->post('json:api/settings', function($body) use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -633,7 +633,7 @@ return json_encode([ 'success' => $success ]); }); - $router->get('json:api/v2/server', function() use ($db) { + $router->get('json:api/server', function() use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; @@ -650,7 +650,7 @@ ]); }); - $router->get('json:api/v2/stats', function() use ($db, $post_mod) { + $router->get('json:api/stats', function() use ($db, $post_mod) { return json_encode([ 'total_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 1 ])), 'total_scheduled_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 'scheduled' ])), @@ -662,7 +662,7 @@ ]); }); - $router->get('json:api/v2/view_files', function() use ($theme_dir) { + $router->get('json:api/view_files', function() use ($theme_dir) { $absolute_theme_dir = Helper::getPath(Kernel::config('views') . "/$theme_dir"); $view_files = []; @@ -676,7 +676,7 @@ return json_encode(array_values($view_files)); }); - $router->post('json:api/v2/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + $router->post('json:api/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { switch ($_GET['mod']) { case 'pages': $mod = $page_mod; break; case 'posts': $mod = $post_mod; break; @@ -705,7 +705,7 @@ ]); }); - $router->delete('json:api/v2/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + $router->delete('json:api/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { $ids = isset($body['id']) ? array_map(fn($id) => (int) $id, is_array($body['id']) ? $body['id'] : explode(',', $body['id'])) : null; @@ -757,7 +757,7 @@ return json_encode([ 'success' => $success ]); }); - $router->get('json:api/v2/roles', function() use ($db) { + $router->get('json:api/roles', function() use ($db) { $roles = []; $permissions_data = $db->query('SELECT role_level, permission FROM roles_permissions ORDER BY role_level ASC, permission ASC')->fetchAll(); foreach ($db->query('SELECT * FROM roles ORDER BY level ASC')->fetchAll() as $role) { @@ -781,7 +781,7 @@ return json_encode($roles); }); - $router->get('json:api/v2/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + $router->get('json:api/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { switch ($_GET['mod'] ?? '') { case 'pages': $mod = $page_mod; break; case 'posts': $mod = $post_mod; break; diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index 3df8d9b..e4de2f0 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -5,8 +5,8 @@ import { getContentUrl, LoadingPage, useElement } from '../utils/utils'; export default function AdminPages() { const dark_theme_element = document.getElementById('css-dark'); - const [ user, fetch_user ] = useElement('/api/v2/me'); - const [ settings, fetch_settings ] = useElement('/api/v2/settings'); + const [ user, fetch_user ] = useElement('/api/me'); + const [ settings, fetch_settings ] = useElement('/api/settings'); const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); const navigate = useNavigate(); diff --git a/app/react/src/pages/Dashboard.js b/app/react/src/pages/Dashboard.js index f7ff096..dae9e67 100644 --- a/app/react/src/pages/Dashboard.js +++ b/app/react/src/pages/Dashboard.js @@ -7,15 +7,15 @@ export default function Dashboard() { const { settings } = useOutletContext(); const { data: links_req, is_loading: is_loading_links, fetch: fetch_links } = useRequest({ method: 'GET', - url: '/api/v2/links', + url: '/api/links', }); const { data: posts_req, is_loading: is_loading_posts, fetch: fetch_posts } = useRequest({ method: 'GET', - url: '/api/v2/posts?limit=6&status=1&order=published_at&sort=desc', + url: '/api/posts?limit=6&status=1&order=published_at&sort=desc', }); const { data: stats_req, is_loading: is_loading_stats, fetch: fetch_stats } = useRequest({ method: 'GET', - url: '/api/v2/stats', + url: '/api/stats', }); const links = links_req ? links_req.data?.data : null; const posts = posts_req ? posts_req.data?.data : null; diff --git a/app/react/src/pages/Link.js b/app/react/src/pages/Link.js index 4870f3b..0a3e488 100644 --- a/app/react/src/pages/Link.js +++ b/app/react/src/pages/Link.js @@ -15,7 +15,7 @@ export default function Link() { if (id) { makeRequest({ method: 'GET', - url: `/api/v2/links?id=${id}`, + url: `/api/links?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); } else { setData({}); @@ -26,7 +26,7 @@ export default function Link() { if (confirm('Are you sure you want to delete the link? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/links', + url: '/api/links', data: { id: id }, }).then(res => { if (res?.data?.success) { @@ -43,7 +43,7 @@ export default function Link() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/links' + (id ? `?id=${id}` : ''), + url: '/api/links' + (id ? `?id=${id}` : ''), data: data, }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index 6753563..3c68910 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -3,7 +3,7 @@ import { makeRequest, useElement } from '../utils/utils'; import { useNavigate } from 'react-router-dom'; export default function Login() { - const [ user ] = useElement('/api/v2/me'); + const [ user ] = useElement('/api/me'); const logo = document.querySelector('meta[name="logo"]')?.content; const [ loading, setLoading ] = useState(false); const [ email, setEmail ] = useState(''); @@ -16,7 +16,7 @@ export default function Login() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/auth', + url: '/api/auth', data: { email: email, password: password, @@ -36,7 +36,7 @@ export default function Login() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/password-reset/request', + url: '/api/password-reset/request', data: { email: email }, }).then(res => { alert(res?.data?.success diff --git a/app/react/src/pages/NewPassword.js b/app/react/src/pages/NewPassword.js index 0152282..76cd208 100644 --- a/app/react/src/pages/NewPassword.js +++ b/app/react/src/pages/NewPassword.js @@ -14,7 +14,7 @@ export default function NewPassword() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/password-reset/confirm', + url: '/api/password-reset/confirm', data: { hash: (new URLSearchParams(window.location.search)).get('hash'), password: password, diff --git a/app/react/src/pages/Page.js b/app/react/src/pages/Page.js index 6ebb59a..d45e8db 100644 --- a/app/react/src/pages/Page.js +++ b/app/react/src/pages/Page.js @@ -8,7 +8,7 @@ export default function Page() { const [ data, setData ] = useState(undefined); const { data: view_files_req, is_loading: is_loading_view_files, fetch: fetch_view_files } = useRequest({ method: 'GET', - url: '/api/v2/view_files', + url: '/api/view_files', }); const location = useLocation(); const navigate = useNavigate(); @@ -22,7 +22,7 @@ export default function Page() { if (id) { makeRequest({ method: 'GET', - url: `/api/v2/pages?id=${id}`, + url: `/api/pages?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); } else { setData({}); @@ -33,7 +33,7 @@ export default function Page() { if (confirm('Are you sure you want to delete the page? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/pages', + url: '/api/pages', data: { id: id }, }).then(res => { if (res?.data?.success) { @@ -50,7 +50,7 @@ export default function Page() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/pages' + (id ? `?id=${id}` : ''), + url: '/api/pages' + (id ? `?id=${id}` : ''), data: data, }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); diff --git a/app/react/src/pages/Post.js b/app/react/src/pages/Post.js index c83e0e8..e3fc1ca 100644 --- a/app/react/src/pages/Post.js +++ b/app/react/src/pages/Post.js @@ -9,7 +9,7 @@ export default function Post() { const [ open_image_dialog, setOpenImageDialog ] = useState(false); const { data: users_req, is_loading: is_loading_users, fetch: fetch_users } = useRequest({ method: 'GET', - url: '/api/v2/users', + url: '/api/users', data: { order: 'name', sort: 'asc', @@ -17,7 +17,7 @@ export default function Post() { }); const { data: tags_req, is_loading: is_loading_tags, fetch: fetch_tags } = useRequest({ method: 'GET', - url: '/api/v2/tags', + url: '/api/tags', data: { order: 'name', sort: 'asc', @@ -37,7 +37,7 @@ export default function Post() { if (id) { makeRequest({ method: 'GET', - url: `/api/v2/posts?id=${id}`, + url: `/api/posts?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); } else { setData({}); @@ -48,7 +48,7 @@ export default function Post() { if (confirm('Are you sure you want to delete the post? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/posts', + url: '/api/posts', data: { id: id }, }).then(res => { if (res?.data?.success) { @@ -65,7 +65,7 @@ export default function Post() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/posts' + (id ? `?id=${id}` : ''), + url: '/api/posts' + (id ? `?id=${id}` : ''), data: { ...data, tags: Object.keys(data.tags || {}).map(tag_slug => tags.find(tag => tag.slug == tag_slug)?.id), diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 6d830ec..f406261 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -99,7 +99,7 @@ const Data = ({ data, setData, user }) => { const downloadDatabase = () => { makeRequest({ method: 'GET', - url: '/api/v2/db', + url: '/api/db', options: { responseType: 'blob' }, }).then(res => downloadFile(res.data, 'data.json')); }; @@ -110,7 +110,7 @@ const Data = ({ data, setData, user }) => { form_data.append('file', database_file); makeRequest({ method: 'POST', - url: '/api/v2/db', + url: '/api/db', data: form_data, }).finally(() => { file_ref.current.value = ''; @@ -122,7 +122,7 @@ const Data = ({ data, setData, user }) => { if (confirm('Are you sure about resetting the views count of all items?')) { makeRequest({ method: 'GET', - url: '/api/v2/reset_views_count', + url: '/api/reset_views_count', }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } }; @@ -163,7 +163,7 @@ const Advanced = ({ data, setData, user }) => { const loadLogs = () => { makeRequest({ method: 'GET', - url: '/api/v2/logs', + url: '/api/logs', }).then(res => { setLogs(res?.data || ''); }); @@ -176,7 +176,7 @@ const Advanced = ({ data, setData, user }) => { const deleteLogs = () => { makeRequest({ method: 'DELETE', - url: '/api/v2/logs', + url: '/api/logs', }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); setLogs(undefined); @@ -230,7 +230,7 @@ const Info = () => { useEffect(() => { makeRequest({ method: 'GET', - url: '/api/v2/server', + url: '/api/server', }).then(res => setServer(res?.data)); }, []); @@ -339,7 +339,7 @@ export default function Settings() { delete new_data.meta; makeRequest({ method: 'POST', - url: '/api/v2/settings', + url: '/api/settings', data: new_data, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')) .finally(() => setLoading(false)); diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js index 859c882..2dc055d 100644 --- a/app/react/src/pages/Tag.js +++ b/app/react/src/pages/Tag.js @@ -15,7 +15,7 @@ export default function Tag() { if (id) { makeRequest({ method: 'GET', - url: `/api/v2/tags?id=${id}`, + url: `/api/tags?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); } else { setData({}); @@ -26,7 +26,7 @@ export default function Tag() { if (confirm('Are you sure you want to delete the tag? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/tags', + url: '/api/tags', data: { id: id }, }).then(res => { if (res?.data?.success) { @@ -43,7 +43,7 @@ export default function Tag() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/tags' + (id ? `?id=${id}` : ''), + url: '/api/tags' + (id ? `?id=${id}` : ''), data: data, }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index 8f73269..9d8a87e 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -9,7 +9,7 @@ export default function User() { const [ open_image_dialog, setOpenImageDialog ] = useState(false); const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ method: 'GET', - url: '/api/v2/roles', + url: '/api/roles', }); const location = useLocation(); const navigate = useNavigate(); @@ -24,7 +24,7 @@ export default function User() { if (id) { makeRequest({ method: 'GET', - url: `/api/v2/users?id=${id}`, + url: `/api/users?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); } else { setData({}); @@ -35,7 +35,7 @@ export default function User() { if (confirm('Are you sure you want to delete the user? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/users', + url: '/api/users', data: { id: id }, }).then(res => { if (res?.data?.success) { @@ -52,7 +52,7 @@ export default function User() { if (confirm('Are you sure you want to impersonate this user?')) { makeRequest({ method: 'GET', - url: '/api/v2/users/impersonate?id=' + id, + url: '/api/users/impersonate?id=' + id, }).then(res => { if (!res?.data?.success) { alert('Error'); @@ -68,7 +68,7 @@ export default function User() { e.preventDefault(); makeRequest({ method: 'POST', - url: '/api/v2/users' + (id ? `?id=${id}` : ''), + url: '/api/users' + (id ? `?id=${id}` : ''), data: data, }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); diff --git a/app/react/src/pages/tables/Links.js b/app/react/src/pages/tables/Links.js index d706c09..ce1a651 100644 --- a/app/react/src/pages/tables/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -10,7 +10,7 @@ export default function Links() { return
l.id) }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } @@ -98,7 +98,7 @@ export default function Links() { if (confirm('Are you sure you want to delete the link? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/links', + url: '/api/links', data: { id: link.id }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index 39ab700..0f556d1 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -26,7 +26,7 @@ const DialogEditFile = ({ file, onClose }) => { const save = () => { makeRequest({ method: 'POST', - url: '/api/v2/media/rename', + url: '/api/media/rename', data: { name: name, path: getContentUrl(file.path), @@ -65,7 +65,7 @@ const DialogDuplicate = ({ file, onClose }) => { const save = () => { makeRequest({ method: 'POST', - url: '/api/v2/media/duplicate', + url: '/api/media/duplicate', data: { name: name, path: getContentUrl(file.path), @@ -106,14 +106,14 @@ const DialogMove = ({ file, onClose }) => { useEffect(() => { makeRequest({ method: 'GET', - url: '/api/v2/media/folders', + url: '/api/media/folders', }).then(res => setFolders(res?.data)); }, []); const save = () => { makeRequest({ method: 'POST', - url: '/api/v2/media/move', + url: '/api/media/move', data: { name: getContentUrl(destination_folder), path: getContentUrl(file.path), @@ -154,7 +154,7 @@ const DialogCreateFolder = ({ path, onClose }) => { const save = () => { makeRequest({ method: 'POST', - url: '/api/v2/media/create_folder', + url: '/api/media/create_folder', data: { name: path + '/' + name }, }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); @@ -198,7 +198,7 @@ export default function Media() { if (confirm('Are you sure you want to delete the file? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/media', + url: '/api/media', data: [ getContentUrl(file.path) ], }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } @@ -236,7 +236,7 @@ export default function Media() { makeRequest({ method: 'POST', - url: '/api/v2/media/upload?path=' + encodeURIComponent(current_path), + url: '/api/media/upload?path=' + encodeURIComponent(current_path), data: form_data, }).then(res => { alert(res?.data?.success ? 'Files uploaded successfully' : 'Error uploading files'); @@ -249,7 +249,7 @@ export default function Media() { if (confirm('This will download all the media content in the current directory as a zip file.')) { makeRequest({ method: 'GET', - url: '/api/v2/media/download?path=' + current_path, + url: '/api/media/download?path=' + current_path, options: { responseType: 'blob' }, }).then(res => { downloadFile(res.data, current_path + ' ' + new Date().toISOString().slice(0, 19).replace('T', ' ') + '.zip') @@ -266,7 +266,7 @@ export default function Media() { return
getContentUrl(f.path)), }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js index a13d437..d688a94 100644 --- a/app/react/src/pages/tables/Pages.js +++ b/app/react/src/pages/tables/Pages.js @@ -10,7 +10,7 @@ export default function Pages() { return
l.id) }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } @@ -103,7 +103,7 @@ export default function Pages() { if (confirm('Are you sure you want to delete the page? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/pages', + url: '/api/pages', data: { id: page.id }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js index 12be939..42cce12 100644 --- a/app/react/src/pages/tables/Posts.js +++ b/app/react/src/pages/tables/Posts.js @@ -8,7 +8,7 @@ export default function Posts() { const { user, settings } = useOutletContext(); const { data: users_req, is_loading: is_loading_users, fetch: fetch_users } = useRequest({ method: 'GET', - url: '/api/v2/users', + url: '/api/users', data: { order: 'name', sort: 'asc', @@ -34,7 +34,7 @@ export default function Posts() { return
l.id) }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } @@ -143,7 +143,7 @@ export default function Posts() { if (confirm('Are you sure you want to delete the post? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/posts', + url: '/api/posts', data: { id: post.id }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } diff --git a/app/react/src/pages/tables/Tags.js b/app/react/src/pages/tables/Tags.js index 56c1bb1..58be4a1 100644 --- a/app/react/src/pages/tables/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -10,7 +10,7 @@ export default function Tags() { return
l.id) }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } @@ -84,7 +84,7 @@ export default function Tags() { if (confirm('Are you sure you want to delete the tag? This action cannot be undone.')) { makeRequest({ method: 'DELETE', - url: '/api/v2/tags', + url: '/api/tags', data: { id: tag.id }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index 4e9e706..08980e0 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -9,7 +9,7 @@ export default function Users() { const navigate = useNavigate(); const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ method: 'GET', - url: '/api/v2/roles', + url: '/api/roles', }); const roles_options = useMemo(() => { let roles = roles_req?.data ?? {}; @@ -30,7 +30,7 @@ export default function Users() { return
u.id) }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } @@ -140,7 +140,7 @@ export default function Users() { if (confirm('Are you sure you want to impersonate this user?')) { makeRequest({ method: 'GET', - url: '/api/v2/users/impersonate?id=' + item.id, + url: '/api/users/impersonate?id=' + item.id, }).then(res => { if (!res?.data?.success) { alert('Error'); @@ -160,7 +160,7 @@ export default function Users() { if (confirm(`Are you sure you want to delete ${item.name}? This action cannot be undone.`)) { makeRequest({ method: 'DELETE', - url: '/api/v2/users', + url: '/api/users', data: { id: item.id }, }).then(res => alert(res?.data?.success ? 'Done' : 'Error')); } diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index c74608b..f934569 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -238,12 +238,12 @@ export const getContentUrl = (path = '') => { }; export const ImageDialog = ({ onSave, onClose }) => { - const [ user ] = useElement('/api/v2/me'); - const [ settings ] = useElement('/api/v2/settings'); + const [ user ] = useElement('/api/me'); + const [ settings ] = useElement('/api/settings'); const [ path, setPath ] = useState(''); const { data: files_req, is_loading, fetch: fetch_files } = useRequest({ method: 'GET', - url: `/api/v2/media?images=1&path=${path}`, + url: `/api/media?images=1&path=${path}`, }); const folders = path.split('/'); const input_ref = useRef(null); @@ -257,7 +257,7 @@ export const ImageDialog = ({ onSave, onClose }) => { form_data.append('file', e.target.files[0]); makeRequest({ method: 'POST', - url: `/api/v2/media?path=${path}`, + url: `/api/media?path=${path}`, data: form_data, }).finally(() => { fetch_files(); @@ -370,7 +370,7 @@ export const Editor = ({ value, setValue, theme }) => { menubar: false, plugins: [ 'image', 'wordcount', 'autoresize', 'code', 'link', 'lists' ], toolbar: 'undo redo | bold italic | alignleft aligncenter alignright | bullist numlist outdent indent | link image code', - images_upload_url: '/api/v2/media/upload_image', + images_upload_url: '/api/media/upload_image', skin: theme === 'dark' ? 'oxide-dark' : 'oxide', content_css: theme === 'dark' ? 'dark' : 'default', setup: editor => { From ddfba0e7eba7d10c6bf7124c717931ca29d9bd78 Mon Sep 17 00:00:00 2001 From: Usbac Date: Wed, 24 Dec 2025 08:47:32 +0100 Subject: [PATCH 176/334] Fix posts endpoint url --- app/bootstrap/routes.php | 2 +- app/views/themes/default/blog.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 5e7c2cd..68c1266 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -22,7 +22,7 @@ * BLOG */ - $router->get('json:api/posts', function() use ($view, $post_mod, $theme_dir) { + $router->get('json:api/blog/posts', function() use ($view, $post_mod, $theme_dir) { $current_page = max(1, (int) ($_GET['page'] ?? 1)); $per_page = \Aurora\App\Setting::get('per_page'); $where = [ $post_mod->getCondition([ 'status' => 1 ]) ]; diff --git a/app/views/themes/default/blog.html b/app/views/themes/default/blog.html index 53a6f11..50d372e 100755 --- a/app/views/themes/default/blog.html +++ b/app/views/themes/default/blog.html @@ -59,7 +59,7 @@

t('no_results') ?>

btn.classList.add('loading'); - fetch('/api/posts?page=' + next_page + '&' + args) + fetch('/api/blog/posts?page=' + next_page + '&' + args) .then(res => res.json()) .then(res => { window.history.replaceState(null, null, window.location.pathname + '?page=' + next_page); From f53bd4a414938e1243d4ad556bed5ebd27f82a45 Mon Sep 17 00:00:00 2001 From: Usbac Date: Wed, 24 Dec 2025 08:49:14 +0100 Subject: [PATCH 177/334] Rename console to admin --- app/bootstrap/routes.php | 4 ++-- app/react/src/components/AdminPages.js | 22 +++++++++++----------- app/react/src/index.js | 6 +++--- app/react/src/pages/Dashboard.js | 8 ++++---- app/react/src/pages/Link.js | 4 ++-- app/react/src/pages/Login.js | 4 ++-- app/react/src/pages/NewPassword.js | 2 +- app/react/src/pages/Page.js | 4 ++-- app/react/src/pages/Post.js | 4 ++-- app/react/src/pages/Tag.js | 4 ++-- app/react/src/pages/User.js | 4 ++-- app/react/src/pages/tables/Links.js | 4 ++-- app/react/src/pages/tables/Pages.js | 4 ++-- app/react/src/pages/tables/Posts.js | 4 ++-- app/react/src/pages/tables/Tags.js | 4 ++-- app/react/src/pages/tables/Users.js | 4 ++-- app/views/emails/password_restore.html | 2 +- 17 files changed, 44 insertions(+), 44 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 68c1266..dee48b9 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -14,7 +14,7 @@ $rss = \Aurora\App\Setting::get('rss'); $router = $kernel->router; - $router->get([ 'console', 'console/*' ], function() use ($view) { + $router->get([ 'admin', 'admin/*' ], function() use ($view) { return $view->get('admin.html'); }); @@ -230,7 +230,7 @@ \Aurora\App\Permission::set($db->query('SELECT permission, role_level FROM roles_permissions ORDER BY permission')->fetchAll(\PDO::FETCH_KEY_PAIR), $GLOBALS['user']['role'] ?? 0); - if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'console') && !str_starts_with(Helper::getCurrentPath(), 'api') && !Helper::isValidId($GLOBALS['user']['id'] ?? false)) { + if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'admin') && !str_starts_with(Helper::getCurrentPath(), 'api') && !Helper::isValidId($GLOBALS['user']['id'] ?? false)) { echo $view->get("$theme_dir/information.html", [ 'description' => $lang->get('under_maintenance'), 'subdescription' => $lang->get('come_back_soon'), diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index e4de2f0..3e1871e 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -18,11 +18,11 @@ export default function AdminPages() { const logout = () => { localStorage.removeItem('auth_token'); - navigate('/console', { replace: true }); + navigate('/admin', { replace: true }); }; if (user === null) { - return ; + return ; } return
@@ -32,36 +32,36 @@ export default function AdminPages() {

Aurora

- + Dashboard View site - + Pages - + Posts - + Tags - + Media - + Users - + Links - + Settings
- +
diff --git a/app/react/src/index.js b/app/react/src/index.js index 878c330..b706a1d 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -25,9 +25,9 @@ const App = () => { return - }/> - }/> - }> + }/> + }/> + }> }/> }/> }/> diff --git a/app/react/src/pages/Dashboard.js b/app/react/src/pages/Dashboard.js index dae9e67..11fe55c 100644 --- a/app/react/src/pages/Dashboard.js +++ b/app/react/src/pages/Dashboard.js @@ -67,10 +67,10 @@ export default function Dashboard() {
diff --git a/app/react/src/pages/Link.js b/app/react/src/pages/Link.js index 0a3e488..850bbc5 100644 --- a/app/react/src/pages/Link.js +++ b/app/react/src/pages/Link.js @@ -31,7 +31,7 @@ export default function Link() { }).then(res => { if (res?.data?.success) { alert('Done'); - navigate('/console/links', { replace: true }); + navigate('/admin/links', { replace: true }); } else { alert('Error'); } @@ -48,7 +48,7 @@ export default function Link() { }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); if (res?.data?.id) { - navigate(`/console/links/edit?id=${res.data.id}`, { replace: true }); + navigate(`/admin/links/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); } }); diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index 3c68910..72d6bd0 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -26,7 +26,7 @@ export default function Login() { alert('Invalid email or password'); } else { localStorage.setItem('auth_token', res.data.token); - navigate('/console/dashboard'); + navigate('/admin/dashboard'); } }).finally(() => setLoading(false)); }; @@ -51,7 +51,7 @@ export default function Login() { } if (user) { - navigate('/console/dashboard'); + navigate('/admin/dashboard'); return null; } diff --git a/app/react/src/pages/NewPassword.js b/app/react/src/pages/NewPassword.js index 76cd208..9fa5010 100644 --- a/app/react/src/pages/NewPassword.js +++ b/app/react/src/pages/NewPassword.js @@ -25,7 +25,7 @@ export default function NewPassword() { alert('Invalid email or password'); } else { localStorage.setItem('auth_token', res.data.token); - navigate('/console/dashboard'); + navigate('/admin/dashboard'); } }).finally(() => setLoading(false)); }; diff --git a/app/react/src/pages/Page.js b/app/react/src/pages/Page.js index d45e8db..ec1a3cb 100644 --- a/app/react/src/pages/Page.js +++ b/app/react/src/pages/Page.js @@ -38,7 +38,7 @@ export default function Page() { }).then(res => { if (res?.data?.success) { alert('Done'); - navigate('/console/pages', { replace: true }); + navigate('/admin/pages', { replace: true }); } else { alert('Error'); } @@ -55,7 +55,7 @@ export default function Page() { }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); if (res?.data?.id) { - navigate(`/console/pages/edit?id=${res.data.id}`, { replace: true }); + navigate(`/admin/pages/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); } }); diff --git a/app/react/src/pages/Post.js b/app/react/src/pages/Post.js index e3fc1ca..f53820b 100644 --- a/app/react/src/pages/Post.js +++ b/app/react/src/pages/Post.js @@ -53,7 +53,7 @@ export default function Post() { }).then(res => { if (res?.data?.success) { alert('Done'); - navigate('/console/posts', { replace: true }); + navigate('/admin/posts', { replace: true }); } else { alert('Error'); } @@ -73,7 +73,7 @@ export default function Post() { }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); if (res?.data?.id) { - navigate(`/console/posts/edit?id=${res.data.id}`, { replace: true }); + navigate(`/admin/posts/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); } }); diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js index 2dc055d..0914a90 100644 --- a/app/react/src/pages/Tag.js +++ b/app/react/src/pages/Tag.js @@ -31,7 +31,7 @@ export default function Tag() { }).then(res => { if (res?.data?.success) { alert('Done'); - navigate('/console/tags', { replace: true }); + navigate('/admin/tags', { replace: true }); } else { alert('Error'); } @@ -48,7 +48,7 @@ export default function Tag() { }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); if (res?.data?.id) { - navigate(`/console/tags/edit?id=${res.data.id}`, { replace: true }); + navigate(`/admin/tags/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); } }); diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index 9d8a87e..dbb92f8 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -40,7 +40,7 @@ export default function User() { }).then(res => { if (res?.data?.success) { alert('Done'); - navigate('/console/users', { replace: true }); + navigate('/admin/users', { replace: true }); } else { alert('Error'); } @@ -73,7 +73,7 @@ export default function User() { }).then(res => { alert(res?.data?.success ? 'Done' : 'Error'); if (res?.data?.id) { - navigate(`/console/users/edit?id=${res.data.id}`, { replace: true }); + navigate(`/admin/users/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); } }); diff --git a/app/react/src/pages/tables/Links.js b/app/react/src/pages/tables/Links.js index ce1a651..f502fc7 100644 --- a/app/react/src/pages/tables/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -16,10 +16,10 @@ export default function Links() { { content: <>+ New, condition: Boolean(user?.actions?.edit_links), - onClick: () => navigate('/console/links/edit'), + onClick: () => navigate('/admin/links/edit'), }, ]} - rowOnClick={link => navigate(`/console/links/edit?id=${link.id}`)} + rowOnClick={link => navigate(`/admin/links/edit?id=${link.id}`)} filters={{ status: { title: 'Status', diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js index d688a94..600101a 100644 --- a/app/react/src/pages/tables/Pages.js +++ b/app/react/src/pages/tables/Pages.js @@ -16,10 +16,10 @@ export default function Pages() { { content: <>+ New, condition: Boolean(user?.actions?.edit_pages), - onClick: () => navigate('/console/pages/edit'), + onClick: () => navigate('/admin/pages/edit'), }, ]} - rowOnClick={page => navigate(`/console/pages/edit?id=${page.id}`)} + rowOnClick={page => navigate(`/admin/pages/edit?id=${page.id}`)} filters={{ status: { title: 'Status', diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js index 42cce12..d208961 100644 --- a/app/react/src/pages/tables/Posts.js +++ b/app/react/src/pages/tables/Posts.js @@ -40,10 +40,10 @@ export default function Posts() { { content: <>+ New, condition: Boolean(user?.actions?.edit_posts), - onClick: () => navigate('/console/posts/edit'), + onClick: () => navigate('/admin/posts/edit'), }, ]} - rowOnClick={post => navigate(`/console/posts/edit?id=${post.id}`)} + rowOnClick={post => navigate(`/admin/posts/edit?id=${post.id}`)} filters={{ user: { title: 'Author', diff --git a/app/react/src/pages/tables/Tags.js b/app/react/src/pages/tables/Tags.js index 58be4a1..fbd9be3 100644 --- a/app/react/src/pages/tables/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -16,10 +16,10 @@ export default function Tags() { { content: <>+ New, condition: Boolean(user?.actions?.edit_tags), - onClick: () => navigate('/console/tags/edit'), + onClick: () => navigate('/admin/tags/edit'), }, ]} - rowOnClick={tag => navigate(`/console/tags/edit?id=${tag.id}`)} + rowOnClick={tag => navigate(`/admin/tags/edit?id=${tag.id}`)} filters={{ order: { title: 'Sort by', diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index 08980e0..409f4fa 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -36,10 +36,10 @@ export default function Users() { { content: <>+ New, condition: Boolean(user?.actions?.edit_users), - onClick: () => navigate('/console/users/edit'), + onClick: () => navigate('/admin/users/edit'), }, ]} - rowOnClick={item => navigate(`/console/users/edit?id=${item.id}`)} + rowOnClick={item => navigate(`/admin/users/edit?id=${item.id}`)} filters={{ status: { title: 'Status', diff --git a/app/views/emails/password_restore.html b/app/views/emails/password_restore.html index 1032085..8884626 100644 --- a/app/views/emails/password_restore.html +++ b/app/views/emails/password_restore.html @@ -8,7 +8,7 @@ url()) ?> From feaf77438356e31bb04dc58d1dc536b7bdcc3cf1 Mon Sep 17 00:00:00 2001 From: Usbac Date: Wed, 24 Dec 2025 08:53:34 +0100 Subject: [PATCH 178/334] Minor style fixes --- app/react/src/pages/tables/Posts.js | 2 +- public/assets/css/admin/main.css | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js index d208961..46750ea 100644 --- a/app/react/src/pages/tables/Posts.js +++ b/app/react/src/pages/tables/Posts.js @@ -100,7 +100,7 @@ export default function Posts() { className="row-thumb" style={{ visibility: post.image ? 'visible' : 'hidden' }} /> -
+

{post.title} {!post.status && Draft} diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 194f1fb..2be41a0 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -72,10 +72,6 @@ h3 { font-size: 1.2em; } -h3 .title-label { - margin-left: 6px; -} - code { font-size: 1em; padding: 1px 5px; @@ -1159,6 +1155,13 @@ textarea.code { margin: 0; } +.listing-row h3 { + display: flex; + align-items: center; + gap: 8px; +} + + .listing-row:not(.header):hover { background: var(--fourth-color); } @@ -1286,10 +1289,6 @@ textarea.code { aspect-ratio: 16 / 9; } -.listing-row.post .main-data { - flex: 1; -} - .row-thumb { height: var(--image-size); width: auto; @@ -1325,11 +1324,6 @@ textarea.code { /* user */ -.listing-row.user h3 { - display: flex; - align-items: center; -} - .user-image { border-radius: 100%; overflow: hidden; From 3bb1773295542b88cf890e5957635b2409330590 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 28 Dec 2025 19:08:50 +0100 Subject: [PATCH 179/334] Return error codes instead of human readable message --- app/controllers/modules/Link.php | 4 ++-- app/controllers/modules/Page.php | 8 ++++---- app/controllers/modules/Post.php | 11 ++++++----- app/controllers/modules/Tag.php | 8 ++++---- app/controllers/modules/User.php | 30 +++++++++++++++--------------- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/controllers/modules/Link.php b/app/controllers/modules/Link.php index e939385..ab8f50a 100755 --- a/app/controllers/modules/Link.php +++ b/app/controllers/modules/Link.php @@ -56,12 +56,12 @@ public function checkFields(array $data, $id = null): array $errors = []; if (empty($data['title'])) { - $errors['title'] = $this->language->get('invalid_value'); + $errors[] = 'invalid_title'; } if (!\Aurora\App\Permission::can('edit_links')) { http_response_code(403); - $errors[0] = $this->language->get('no_permission'); + $errors[] = 'no_permission'; } return $errors; diff --git a/app/controllers/modules/Page.php b/app/controllers/modules/Page.php index f279e14..5bff53b 100755 --- a/app/controllers/modules/Page.php +++ b/app/controllers/modules/Page.php @@ -51,20 +51,20 @@ public function checkFields(array $data, $id = null): array $errors = []; if (empty($data['title'])) { - $errors['title'] = $this->language->get('invalid_value'); + $errors[] = 'invalid_title'; } if (isset($data['slug']) && !empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) { - $errors['slug'] = $this->language->get('repeated_slug'); + $errors[] = 'repeated_slug'; } if (!empty($data['slug']) && !\Aurora\Core\Helper::isSlugValid($data['slug'])) { - $errors['slug'] = $this->language->get('invalid_slug'); + $errors[] = 'invalid_slug'; } if (!\Aurora\App\Permission::can('edit_pages')) { http_response_code(403); - $errors[0] = $this->language->get('no_permission'); + $errors[] = 'no_permission'; } return $errors; diff --git a/app/controllers/modules/Post.php b/app/controllers/modules/Post.php index c8abab7..8c56394 100755 --- a/app/controllers/modules/Post.php +++ b/app/controllers/modules/Post.php @@ -82,24 +82,25 @@ public function checkFields(array $data, $id = null): array $errors = []; if (empty($data['title'])) { - $errors['title'] = $this->language->get('invalid_value'); + $errors[] = 'invalid_title'; } if (isset($data['slug']) && !empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) { - $errors['slug'] = $this->language->get('repeated_slug'); + $errors[] = 'repeated_slug'; } if (empty($data['slug']) || !\Aurora\Core\Helper::isSlugValid($data['slug'])) { - $errors['slug'] = $this->language->get('invalid_slug'); + $errors[] = 'invalid_slug'; } if (!\Aurora\App\Permission::can('edit_posts')) { http_response_code(403); - $errors[0] = $this->language->get('no_permission'); + $errors[] = 'no_permission'; } if (!empty($data['status']) && !\Aurora\App\Permission::can('publish_posts')) { - $errors[0] = $this->language->get('published_posts_permission_error'); + http_response_code(403); + $errors[] = 'no_publish_permission'; } return $errors; diff --git a/app/controllers/modules/Tag.php b/app/controllers/modules/Tag.php index c26b14b..4a25547 100755 --- a/app/controllers/modules/Tag.php +++ b/app/controllers/modules/Tag.php @@ -49,21 +49,21 @@ public function checkFields(array $data, $id = null): array $errors = []; if (empty($data['name'])) { - $errors['name'] = $this->language->get('invalid_value'); + $errors[] = 'invalid_name'; } if (!empty($data['slug']) && !empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) { - $errors['slug'] = $this->language->get('repeated_slug'); + $errors[] = 'repeated_slug'; } if (empty($data['slug']) || !\Aurora\Core\Helper::isSlugValid($data['slug'])) { - $errors['slug'] = $this->language->get('invalid_slug'); + $errors[] = 'invalid_slug'; } if (!\Aurora\App\Permission::can('edit_tags')) { http_response_code(403); - $errors[0] = $this->language->get('no_permission'); + $errors[] = 'no_permission'; } return $errors; diff --git a/app/controllers/modules/User.php b/app/controllers/modules/User.php index 6d99719..6699f3c 100755 --- a/app/controllers/modules/User.php +++ b/app/controllers/modules/User.php @@ -85,9 +85,9 @@ public function handleLogin(string $email, string $password): array $errors = []; if (!$user) { - $errors['email'] = $this->language->get('no_active_user'); + $errors[] = 'no_active_user'; } elseif (!password_verify($password, $user['password'])) { - $errors['password'] = $this->language->get('wrong_password'); + $errors[] = 'wrong_password'; } if (empty($errors)) { @@ -108,35 +108,35 @@ public function checkFields(array $data, $id = null): array $errors = []; if (empty($data['name'])) { - $errors['name'] = $this->language->get('invalid_value'); + $errors[] = 'invalid_name'; } if (!empty($data['slug']) && !empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) { - $errors['slug'] = $this->language->get('repeated_slug'); + $errors[] = 'repeated_slug'; } if (empty($data['slug']) || !\Aurora\Core\Helper::isSlugValid($data['slug'])) { - $errors['slug'] = $this->language->get('invalid_slug'); + $errors[] = 'invalid_slug'; } if (!empty($data['email']) && !empty($this->get([ 'email' => $data['email'], '!id' => $id ]))) { - $errors['email'] = $this->language->get('repeated_email'); + $errors[] = 'repeated_email'; } if (empty($data['email']) || filter_var($data['email'], FILTER_VALIDATE_EMAIL) === false) { - $errors['email'] = $this->language->get('invalid_value'); + $errors[] = 'invalid_value'; } if (empty($id) && empty($data['password'])) { - $errors['password'] = $this->language->get('bad_password'); + $errors[] = 'bad_password'; } if (!empty($data['password'])) { $password_error = $this->checkPassword($data['password'], $data['password_confirm'] ?? ''); if (!empty($password_error)) { - $errors['password'] = $password_error; + $errors[] = $password_error; } } @@ -146,7 +146,7 @@ public function checkFields(array $data, $id = null): array if (!$can_edit) { http_response_code(403); - $errors[0] = $this->language->get('no_permission'); + $errors[] = 'no_permission'; } return $errors; @@ -206,18 +206,18 @@ public function getPassword(string $password): string * Checks the given password and its confirmation * @param string $password the password * @param string $password_confirm the password confirmation - * @return string the error message, if empty it means both passwords are equal and valid + * @return string|false the error message, if false it means both passwords are equal and valid */ - public function checkPassword(string $password, string $password_confirm): string + public function checkPassword(string $password, string $password_confirm): string|false { if (mb_strlen($password) < 8) { - return $this->language->get('bad_password'); + return 'bad_password'; } if ($password !== $password_confirm) { - return $this->language->get('bad_password_confirm'); + return 'bad_password_confirm'; } - return ''; + return false; } } From a82adc7da12276a9632506a91e6191a1f9d98a00 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 28 Dec 2025 19:50:53 +0100 Subject: [PATCH 180/334] Add I18N provider --- app/react/src/providers/I18nProvider.js | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/react/src/providers/I18nProvider.js diff --git a/app/react/src/providers/I18nProvider.js b/app/react/src/providers/I18nProvider.js new file mode 100644 index 0000000..4c35513 --- /dev/null +++ b/app/react/src/providers/I18nProvider.js @@ -0,0 +1,56 @@ +import React, { createContext, useContext, useState, useEffect, useMemo } from 'react'; + +const I18nContext = createContext(); + +export const I18nProvider = ({ children, defaultLanguage = 'en' }) => { + const [ language, setLanguage ] = useState(localStorage.getItem('lang') || defaultLanguage); + const translations = useMemo(() => { + const translation_context = require.context('../lang', false, /\.js$/); + const res = {}; + + translation_context.keys().forEach((file_name) => { + res[file_name.replace('./', '').replace('.js', '')] = translation_context(file_name).default; + }); + + return res; + }, []); + + useEffect(() => { + localStorage.setItem('lang', language); + }, [ language ]); + + const t = (key, ...params) => { + const translation = translations[language]?.[key]; + + if (!translation) { + throw new Error(`Unknown translation key: "${key}" for language "${language}"`); + } + + let i = 0; + return translation.replace(/%s|%d|%f/g, e => params[i++] ?? e); + }; + + const changeLanguage = (lang) => { + if (translations[lang]) { + setLanguage(lang); + } + }; + + return + {children} + ; +}; + +export const useI18n = () => { + const context = useContext(I18nContext); + + if (!context) { + throw new Error('useI18n must be used within an I18nProvider'); + } + + return context; +}; From e2ca6e711f1f2c04040c5d5f5a0620d7862bfb2b Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 29 Dec 2025 23:41:52 +0100 Subject: [PATCH 181/334] Add base language files --- app/react/src/lang/en.js | 7 +++++++ app/react/src/lang/es.js | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 app/react/src/lang/en.js create mode 100644 app/react/src/lang/es.js diff --git a/app/react/src/lang/en.js b/app/react/src/lang/en.js new file mode 100644 index 0000000..751be37 --- /dev/null +++ b/app/react/src/lang/en.js @@ -0,0 +1,7 @@ +export default { + 'dashboard': 'Dashboard', + 'view_site': 'View site', + 'pages': 'Pages', + 'posts': 'Posts', + 'tags': 'Tags', +}; diff --git a/app/react/src/lang/es.js b/app/react/src/lang/es.js new file mode 100644 index 0000000..10a9e81 --- /dev/null +++ b/app/react/src/lang/es.js @@ -0,0 +1,7 @@ +export default { + 'dashboard': 'Panel de control', + 'view_site': 'Ver página', + 'pages': 'Páginas', + 'posts': 'Publicaciones', + 'tags': 'Etiquetas', +}; From fa63aa8add9f21a18c1aeed51a9a10ea3896028c Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 29 Dec 2025 23:42:02 +0100 Subject: [PATCH 182/334] Add i18n context --- app/react/src/index.js | 47 ++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index b706a1d..58a8c19 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -2,6 +2,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { I18nProvider } from './providers/I18nProvider'; import AdminPages from './components/AdminPages'; import NewPassword from './pages/NewPassword'; import Login from './pages/Login'; @@ -23,28 +24,30 @@ const App = () => { const query_client = new QueryClient(); return - - - }/> - }/> - }> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - - 404 Not Found

}/> - - + + + + }/> + }/> + }> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + + 404 Not Found
}/> + + + ; }; From 5a4e2e312fa56f163b914b02305a5d0f43ab03db Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 29 Dec 2025 23:46:16 +0100 Subject: [PATCH 183/334] Use i18n in AdminPages --- app/react/src/components/AdminPages.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index 3e1871e..495d899 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; import { Link, Navigate, Outlet, useNavigate } from 'react-router-dom'; import { getContentUrl, LoadingPage, useElement } from '../utils/utils'; +import { useI18n } from '../providers/I18nProvider'; export default function AdminPages() { const dark_theme_element = document.getElementById('css-dark'); @@ -9,6 +10,7 @@ export default function AdminPages() { const [ settings, fetch_settings ] = useElement('/api/settings'); const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); const navigate = useNavigate(); + const { t } = useI18n(); const toggleTheme = () => { const is_light_enabled = dark_theme_element.toggleAttribute('disabled'); @@ -33,41 +35,41 @@ export default function AdminPages() {
- Dashboard + {t('dashboard')} - View site + {t('view_site')} - Pages + {t('pages')} - Posts + {t('posts')} - Tags + {t('tags')} - Media + {t('media')} - Users + {t('users')} - Links + {t('links')} - Settings + {t('settings')}
-
+
{theme == 'light' ? : }
-
+
From abd7cd16a48c738edf3a61114eeda70f87381182 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 29 Dec 2025 23:58:26 +0100 Subject: [PATCH 184/334] Use i18n in remaining pages --- app/react/src/lang/en.js | 195 ++++++++++++++++++++++++++++ app/react/src/lang/es.js | 195 ++++++++++++++++++++++++++++ app/react/src/pages/Dashboard.js | 36 ++--- app/react/src/pages/Link.js | 24 ++-- app/react/src/pages/Login.js | 16 ++- app/react/src/pages/NewPassword.js | 10 +- app/react/src/pages/Page.js | 36 ++--- app/react/src/pages/Post.js | 44 ++++--- app/react/src/pages/Settings.js | 118 +++++++++-------- app/react/src/pages/Tag.js | 28 ++-- app/react/src/pages/User.js | 42 +++--- app/react/src/pages/tables/Links.js | 50 +++---- app/react/src/pages/tables/Media.js | 94 +++++++------- app/react/src/pages/tables/Pages.js | 52 ++++---- app/react/src/pages/tables/Posts.js | 60 ++++----- app/react/src/pages/tables/Tags.js | 38 +++--- app/react/src/pages/tables/Users.js | 68 +++++----- 17 files changed, 769 insertions(+), 337 deletions(-) diff --git a/app/react/src/lang/en.js b/app/react/src/lang/en.js index 751be37..9c89af0 100644 --- a/app/react/src/lang/en.js +++ b/app/react/src/lang/en.js @@ -4,4 +4,199 @@ export default { 'pages': 'Pages', 'posts': 'Posts', 'tags': 'Tags', + 'media': 'Media', + 'users': 'Users', + 'links': 'Links', + 'settings': 'Settings', + 'switch_theme': 'Switch theme', + 'logout': 'Logout', + 'rename': 'Rename', + 'name': 'Name', + 'cancel': 'Cancel', + 'save': 'Save', + 'item_renamed_successfully': 'Item has been renamed successfully', + 'error_renaming_item': 'Error renaming item. The name is invalid, the file does not comply with the server rules or the path is not writable.', + 'duplicate': 'Duplicate', + 'item_duplicated_successfully': 'Item has been duplicated successfully', + 'error_duplicating_item': 'Error duplicating item. The name is invalid, the file does not comply with the server rules or the path is not writable.', + 'move': 'Move', + 'folder': 'Folder', + 'item_moved_successfully': 'Item has been moved successfully', + 'error_moving_item': 'Error moving item', + 'create_folder': 'Create Folder', + 'folder_created_successfully': 'Folder has been created successfully', + 'error_creating_folder': 'Error creating folder', + 'confirm_delete_file': 'Are you sure you want to delete the file? This action cannot be undone.', + 'file_deleted_successfully': 'File has been deleted successfully', + 'error_deleting_file': 'Error deleting file', + 'path_copied_to_clipboard': 'Path copied to clipboard.', + 'files_uploaded_successfully': 'Files uploaded successfully', + 'error_uploading_files': 'Error uploading files', + 'confirm_download_media': 'This will download all the media content in the current directory as a zip file.', + 'sort_by': 'Sort by', + 'type': 'Type', + 'size': 'Size', + 'ascending': 'Ascending', + 'descending': 'Descending', + 'delete': 'Delete', + 'confirm_delete_selected_files': 'Are you sure you want to delete the selected files? This action cannot be undone.', + 'files_deleted_successfully': 'Selected files have been deleted successfully', + 'error_deleting_files': 'Error deleting selected files', + 'information': 'Information', + 'last_modification': 'Last Modification', + 'copy_path': 'Copy path', + 'new': 'New', + 'title': 'Title', + 'url': 'URL', + 'status': 'Status', + 'order': 'Order', + 'all': 'All', + 'active': 'Active', + 'inactive': 'Inactive', + 'view': 'View', + 'confirm_delete_selected_links': 'Are you sure you want to delete the selected links? This action cannot be undone.', + 'links_deleted_successfully': 'Selected links have been deleted successfully', + 'error_deleting_links': 'Error deleting selected links', + 'confirm_delete_link': 'Are you sure you want to delete the link? This action cannot be undone.', + 'link_deleted_successfully': 'Link has been deleted successfully', + 'error_deleting_link': 'Error deleting link', + 'published': 'Published', + 'draft': 'Draft', + 'slug': 'Slug', + 'edited': 'Edited', + 'no_views': 'No. views', + 'confirm_delete_selected_pages': 'Are you sure you want to delete the selected pages? This action cannot be undone.', + 'pages_deleted_successfully': 'Selected pages have been deleted successfully', + 'error_deleting_pages': 'Error deleting selected pages', + 'confirm_delete_page': 'Are you sure you want to delete the page? This action cannot be undone.', + 'page_deleted_successfully': 'Page has been deleted successfully', + 'error_deleting_page': 'Error deleting page', + 'author': 'Author', + 'publish_date': 'Publish Date', + 'scheduled': 'Scheduled', + 'confirm_delete_selected_posts': 'Are you sure you want to delete the selected posts? This action cannot be undone.', + 'posts_deleted_successfully': 'Selected posts have been deleted successfully', + 'error_deleting_posts': 'Error deleting selected posts', + 'confirm_delete_post': 'Are you sure you want to delete the post? This action cannot be undone.', + 'post_deleted_successfully': 'Post has been deleted successfully', + 'error_deleting_post': 'Error deleting post', + 'no_posts': 'No. posts', + 'confirm_delete_selected_tags': 'Are you sure you want to delete the selected tags? This action cannot be undone.', + 'tags_deleted_successfully': 'Selected tags have been deleted successfully', + 'error_deleting_tags': 'Error deleting selected tags', + 'confirm_delete_tag': 'Are you sure you want to delete the tag? This action cannot be undone.', + 'tag_deleted_successfully': 'Tag has been deleted successfully', + 'error_deleting_tag': 'Error deleting tag', + 'role': 'Role', + 'email': 'Email', + 'last_active': 'Last Active', + 'confirm_delete_selected_users': 'Are you sure you want to delete the selected users? This action cannot be undone.', + 'users_deleted_successfully': 'Selected users have been deleted successfully', + 'error_deleting_users': 'Error deleting selected users', + 'impersonate': 'Impersonate', + 'confirm_impersonate_user': 'Are you sure you want to impersonate this user?', + 'error_impersonating_user': 'Error impersonating user', + 'confirm_delete_user': 'Are you sure you want to delete %s? This action cannot be undone.', + 'user_deleted_successfully': 'User has been deleted successfully', + 'error_deleting_user': 'Error deleting user', + 'you': '(you)', + 'latest_published_posts': 'Latest published posts', + 'by': 'by', + 'no_results': 'No results', + 'start_creating': 'Start Creating', + 'create_page': 'Create Page', + 'write_post': 'Write Post', + 'add_user': 'Add User', + 'add_tag': 'Add Tag', + 'statistics': 'Statistics', + 'scheduled': 'Scheduled', + 'link_saved_successfully': 'Link has been saved successfully', + 'error_saving_link': 'Error saving link', + 'link': 'Link', + 'page': 'Page', + 'post': 'Post', + 'tag': 'Tag', + 'user': 'User', + 'error': 'Error', + 'description': 'Description', + 'password': 'Password', + 'password_confirm': 'Password confirm', + 'new_password': 'New Password', + 'reset_password': 'Reset Password', + 'sign_in': 'Sign In', + 'forgot_password': 'Forgot Password?', + 'go_back': 'Go Back', + 'invalid_email_or_password': 'Invalid email or password', + 'password_reset_email_sent': 'If the email is registered, you will receive an email with instructions to reset your password', + 'error_occurred': 'An error occurred, please try again later', + 'page_saved_successfully': 'Page has been saved successfully', + 'error_saving_page': 'Error saving page', + 'post_saved_successfully': 'Post has been saved successfully', + 'error_saving_post': 'Error saving post', + 'tag_saved_successfully': 'Tag has been saved successfully', + 'error_saving_tag': 'Error saving tag', + 'user_saved_successfully': 'User has been saved successfully', + 'error_saving_user': 'Error saving user', + 'published': 'Published', + 'static': 'Static', + 'static_file': 'Static file', + 'none': 'None', + 'meta_title': 'Meta title', + 'meta_description': 'Meta description', + 'canonical_url': 'Canonical URL', + 'image': 'Image', + 'image_alt': 'Image alt', + 'publish_date': 'Publish Date', + 'bio': 'Bio', + 'general': 'General', + 'meta': 'Meta', + 'data': 'Data', + 'advanced': 'Advanced', + 'server_info': 'Server Info', + 'code': 'Code', + 'version': 'Version', + 'logo': 'Logo', + 'blog_url': 'Blog URL', + 'rss_feed_url': 'RSS feed URL', + 'theme': 'Theme', + 'items_per_page': 'Items per page', + 'system_language': 'System language', + 'date_format': 'Date format', + 'timezone': 'Timezone', + 'maintenance_mode': 'Maintenance mode', + 'meta_keywords': 'Meta keywords', + 'download_database': 'Download database', + 'upload_database': 'Upload database', + 'select_file': 'Select file', + 'views_counter': 'Views counter', + 'reset_views_count': 'Reset views count', + 'confirm_update_database': 'Are you sure about updating the current database?', + 'confirm_reset_views': 'Are you sure about resetting the views count of all items?', + 'views_reset_successfully': 'Views count has been reset successfully', + 'error_resetting_views': 'Error resetting views count', + 'session_lifetime': 'Session lifetime', + 'session_samesite_cookie': 'Session SameSite cookie', + 'display_errors': 'Display errors', + 'log_errors': 'Log errors', + 'log_file': 'Log file', + 'logs': 'Logs', + 'loading': 'Loading...', + 'no_logs': 'No logs', + 'download': 'Download', + 'clear': 'Clear', + 'logs_deleted_successfully': 'Logs have been deleted successfully', + 'error_deleting_logs': 'Error deleting logs', + 'operating_system': 'Operating system', + 'php_version': 'PHP version', + 'database': 'Database', + 'host_name': 'Host name', + 'root_folder': 'Root folder', + 'time': 'Time', + 'memory_limit': 'Memory limit', + 'file_size_upload_limit': 'File size upload limit', + 'site_header': 'Site header', + 'site_footer': 'Site footer', + 'post_code': 'Post code', + 'settings_saved_successfully': 'Settings have been saved successfully', + 'error_saving_settings': 'Error saving settings', }; diff --git a/app/react/src/lang/es.js b/app/react/src/lang/es.js index 10a9e81..906200f 100644 --- a/app/react/src/lang/es.js +++ b/app/react/src/lang/es.js @@ -4,4 +4,199 @@ export default { 'pages': 'Páginas', 'posts': 'Publicaciones', 'tags': 'Etiquetas', + 'media': 'Medios', + 'users': 'Usuarios', + 'links': 'Enlaces', + 'settings': 'Configuración', + 'switch_theme': 'Cambiar tema', + 'logout': 'Cerrar sesión', + 'rename': 'Renombrar', + 'name': 'Nombre', + 'cancel': 'Cancelar', + 'save': 'Guardar', + 'item_renamed_successfully': 'El elemento ha sido renombrado exitosamente', + 'error_renaming_item': 'Error al renombrar el elemento. El nombre no es válido, el archivo no cumple con las reglas del servidor o la ruta no es escribible.', + 'duplicate': 'Duplicar', + 'item_duplicated_successfully': 'El elemento ha sido duplicado exitosamente', + 'error_duplicating_item': 'Error al duplicar el elemento. El nombre no es válido, el archivo no cumple con las reglas del servidor o la ruta no es escribible.', + 'move': 'Mover', + 'folder': 'Carpeta', + 'item_moved_successfully': 'El elemento ha sido movido exitosamente', + 'error_moving_item': 'Error al mover el elemento', + 'create_folder': 'Crear Carpeta', + 'folder_created_successfully': 'La carpeta ha sido creada exitosamente', + 'error_creating_folder': 'Error al crear la carpeta', + 'confirm_delete_file': '¿Estás seguro de que quieres eliminar el archivo? Esta acción no se puede deshacer.', + 'file_deleted_successfully': 'El archivo ha sido eliminado exitosamente', + 'error_deleting_file': 'Error al eliminar el archivo', + 'path_copied_to_clipboard': 'Ruta copiada al portapapeles.', + 'files_uploaded_successfully': 'Archivos subidos exitosamente', + 'error_uploading_files': 'Error al subir archivos', + 'confirm_download_media': 'Esto descargará todo el contenido multimedia del directorio actual como un archivo zip.', + 'sort_by': 'Ordenar por', + 'type': 'Tipo', + 'size': 'Tamaño', + 'ascending': 'Ascendente', + 'descending': 'Descendente', + 'delete': 'Eliminar', + 'confirm_delete_selected_files': '¿Estás seguro de que quieres eliminar los archivos seleccionados? Esta acción no se puede deshacer.', + 'files_deleted_successfully': 'Los archivos seleccionados han sido eliminados exitosamente', + 'error_deleting_files': 'Error al eliminar los archivos seleccionados', + 'information': 'Información', + 'last_modification': 'Última Modificación', + 'copy_path': 'Copiar ruta', + 'new': 'Nuevo', + 'title': 'Título', + 'url': 'URL', + 'status': 'Estado', + 'order': 'Orden', + 'all': 'Todos', + 'active': 'Activo', + 'inactive': 'Inactivo', + 'view': 'Ver', + 'confirm_delete_selected_links': '¿Estás seguro de que quieres eliminar los enlaces seleccionados? Esta acción no se puede deshacer.', + 'links_deleted_successfully': 'Los enlaces seleccionados han sido eliminados exitosamente', + 'error_deleting_links': 'Error al eliminar los enlaces seleccionados', + 'confirm_delete_link': '¿Estás seguro de que quieres eliminar el enlace? Esta acción no se puede deshacer.', + 'link_deleted_successfully': 'El enlace ha sido eliminado exitosamente', + 'error_deleting_link': 'Error al eliminar el enlace', + 'published': 'Publicado', + 'draft': 'Borrador', + 'slug': 'Slug', + 'edited': 'Editado', + 'no_views': 'No. vistas', + 'confirm_delete_selected_pages': '¿Estás seguro de que quieres eliminar las páginas seleccionadas? Esta acción no se puede deshacer.', + 'pages_deleted_successfully': 'Las páginas seleccionadas han sido eliminadas exitosamente', + 'error_deleting_pages': 'Error al eliminar las páginas seleccionadas', + 'confirm_delete_page': '¿Estás seguro de que quieres eliminar la página? Esta acción no se puede deshacer.', + 'page_deleted_successfully': 'La página ha sido eliminada exitosamente', + 'error_deleting_page': 'Error al eliminar la página', + 'author': 'Autor', + 'publish_date': 'Fecha de Publicación', + 'scheduled': 'Programado', + 'confirm_delete_selected_posts': '¿Estás seguro de que quieres eliminar las publicaciones seleccionadas? Esta acción no se puede deshacer.', + 'posts_deleted_successfully': 'Las publicaciones seleccionadas han sido eliminadas exitosamente', + 'error_deleting_posts': 'Error al eliminar las publicaciones seleccionadas', + 'confirm_delete_post': '¿Estás seguro de que quieres eliminar la publicación? Esta acción no se puede deshacer.', + 'post_deleted_successfully': 'La publicación ha sido eliminada exitosamente', + 'error_deleting_post': 'Error al eliminar la publicación', + 'no_posts': 'No. publicaciones', + 'confirm_delete_selected_tags': '¿Estás seguro de que quieres eliminar las etiquetas seleccionadas? Esta acción no se puede deshacer.', + 'tags_deleted_successfully': 'Las etiquetas seleccionadas han sido eliminadas exitosamente', + 'error_deleting_tags': 'Error al eliminar las etiquetas seleccionadas', + 'confirm_delete_tag': '¿Estás seguro de que quieres eliminar la etiqueta? Esta acción no se puede deshacer.', + 'tag_deleted_successfully': 'La etiqueta ha sido eliminada exitosamente', + 'error_deleting_tag': 'Error al eliminar la etiqueta', + 'role': 'Rol', + 'email': 'Correo electrónico', + 'last_active': 'Última Actividad', + 'confirm_delete_selected_users': '¿Estás seguro de que quieres eliminar los usuarios seleccionados? Esta acción no se puede deshacer.', + 'users_deleted_successfully': 'Los usuarios seleccionados han sido eliminados exitosamente', + 'error_deleting_users': 'Error al eliminar los usuarios seleccionados', + 'impersonate': 'Suplantar', + 'confirm_impersonate_user': '¿Estás seguro de que quieres suplantar a este usuario?', + 'error_impersonating_user': 'Error al suplantar usuario', + 'confirm_delete_user': '¿Estás seguro de que quieres eliminar a %s? Esta acción no se puede deshacer.', + 'user_deleted_successfully': 'El usuario ha sido eliminado exitosamente', + 'error_deleting_user': 'Error al eliminar el usuario', + 'you': '(tú)', + 'latest_published_posts': 'Últimas publicaciones', + 'by': 'por', + 'no_results': 'Sin resultados', + 'start_creating': 'Empezar a Crear', + 'create_page': 'Crear Página', + 'write_post': 'Escribir Publicación', + 'add_user': 'Agregar Usuario', + 'add_tag': 'Agregar Etiqueta', + 'statistics': 'Estadísticas', + 'scheduled': 'Programado', + 'link_saved_successfully': 'El enlace ha sido guardado exitosamente', + 'error_saving_link': 'Error al guardar el enlace', + 'link': 'Enlace', + 'page': 'Página', + 'post': 'Publicación', + 'tag': 'Etiqueta', + 'user': 'Usuario', + 'error': 'Error', + 'description': 'Descripción', + 'password': 'Contraseña', + 'password_confirm': 'Confirmar contraseña', + 'new_password': 'Nueva Contraseña', + 'reset_password': 'Restablecer Contraseña', + 'sign_in': 'Iniciar Sesión', + 'forgot_password': '¿Olvidaste tu contraseña?', + 'go_back': 'Volver', + 'invalid_email_or_password': 'Correo electrónico o contraseña inválidos', + 'password_reset_email_sent': 'Si el correo está registrado, recibirás un email con instrucciones para restablecer tu contraseña', + 'error_occurred': 'Ocurrió un error, por favor intenta de nuevo más tarde', + 'page_saved_successfully': 'La página ha sido guardada exitosamente', + 'error_saving_page': 'Error al guardar la página', + 'post_saved_successfully': 'La publicación ha sido guardada exitosamente', + 'error_saving_post': 'Error al guardar la publicación', + 'tag_saved_successfully': 'La etiqueta ha sido guardada exitosamente', + 'error_saving_tag': 'Error al guardar la etiqueta', + 'user_saved_successfully': 'El usuario ha sido guardado exitosamente', + 'error_saving_user': 'Error al guardar el usuario', + 'published': 'Publicado', + 'static': 'Estático', + 'static_file': 'Archivo estático', + 'none': 'Ninguno', + 'meta_title': 'Título meta', + 'meta_description': 'Descripción meta', + 'canonical_url': 'URL canónica', + 'image': 'Imagen', + 'image_alt': 'Texto alternativo de imagen', + 'publish_date': 'Fecha de Publicación', + 'bio': 'Biografía', + 'general': 'General', + 'meta': 'Meta', + 'data': 'Datos', + 'advanced': 'Avanzado', + 'server_info': 'Información del Servidor', + 'code': 'Código', + 'version': 'Versión', + 'logo': 'Logo', + 'blog_url': 'URL del blog', + 'rss_feed_url': 'URL del feed RSS', + 'theme': 'Tema', + 'items_per_page': 'Elementos por página', + 'system_language': 'Idioma del sistema', + 'date_format': 'Formato de fecha', + 'timezone': 'Zona horaria', + 'maintenance_mode': 'Modo mantenimiento', + 'meta_keywords': 'Palabras clave meta', + 'download_database': 'Descargar base de datos', + 'upload_database': 'Subir base de datos', + 'select_file': 'Seleccionar archivo', + 'views_counter': 'Contador de vistas', + 'reset_views_count': 'Restablecer contador de vistas', + 'confirm_update_database': '¿Estás seguro de actualizar la base de datos actual?', + 'confirm_reset_views': '¿Estás seguro de restablecer el contador de vistas de todos los elementos?', + 'views_reset_successfully': 'El contador de vistas ha sido restablecido exitosamente', + 'error_resetting_views': 'Error al restablecer el contador de vistas', + 'session_lifetime': 'Duración de sesión', + 'session_samesite_cookie': 'Cookie SameSite de sesión', + 'display_errors': 'Mostrar errores', + 'log_errors': 'Registrar errores', + 'log_file': 'Archivo de registro', + 'logs': 'Registros', + 'loading': 'Cargando...', + 'no_logs': 'Sin registros', + 'download': 'Descargar', + 'clear': 'Limpiar', + 'logs_deleted_successfully': 'Los registros han sido eliminados exitosamente', + 'error_deleting_logs': 'Error al eliminar los registros', + 'operating_system': 'Sistema operativo', + 'php_version': 'Versión de PHP', + 'database': 'Base de datos', + 'host_name': 'Nombre del host', + 'root_folder': 'Carpeta raíz', + 'time': 'Hora', + 'memory_limit': 'Límite de memoria', + 'file_size_upload_limit': 'Límite de tamaño de archivo para subir', + 'site_header': 'Encabezado del sitio', + 'site_footer': 'Pie de página del sitio', + 'post_code': 'Código de publicación', + 'settings_saved_successfully': 'La configuración ha sido guardada exitosamente', + 'error_saving_settings': 'Error al guardar la configuración', }; diff --git a/app/react/src/pages/Dashboard.js b/app/react/src/pages/Dashboard.js index 11fe55c..f1a8990 100644 --- a/app/react/src/pages/Dashboard.js +++ b/app/react/src/pages/Dashboard.js @@ -2,9 +2,11 @@ import React, { useEffect } from 'react'; import { getContentUrl, LoadingPage, MenuButton, useRequest } from '../utils/utils'; import { IconBook, IconPencil, IconTag, IconUser } from '../utils/icons'; import { useOutletContext } from 'react-router-dom'; +import { useI18n } from '../providers/I18nProvider'; export default function Dashboard() { const { settings } = useOutletContext(); + const { t } = useI18n(); const { data: links_req, is_loading: is_loading_links, fetch: fetch_links } = useRequest({ method: 'GET', url: '/api/links', @@ -35,58 +37,58 @@ export default function Dashboard() {
-

Dashboard

+

{t('dashboard')}

{links &&
-

Links

+

{t('links')}

}
-

Latest published posts

+

{t('latest_published_posts')}

{posts && posts.length > 0 && <> {posts.map(post => {post.title}
{post.title} - {post.user_id ? `by ${post.user_name}` : <> } + {post.user_id ? `${t('by')} ${post.user_name}` : <> }
)} } - {posts && posts.length == 0 && No results} + {posts && posts.length == 0 && {t('no_results')}}
-

Statistics

+

{t('statistics')}

- Posts - {stats.total_posts} Published, {stats.total_scheduled_posts} Scheduled, {stats.total_draft_posts} Draft + {t('posts')} + {stats.total_posts} {t('published')}, {stats.total_scheduled_posts} {t('scheduled')}, {stats.total_draft_posts} {t('draft')}
- Pages - {stats.total_pages} Published, {stats.total_draft_pages} Draft + {t('pages')} + {stats.total_pages} {t('published')}, {stats.total_draft_pages} {t('draft')}
- Users - {stats.total_users} Active, {stats.total_inactive_users} Inactive + {t('users')} + {stats.total_users} {t('active')}, {stats.total_inactive_users} {t('inactive')}
diff --git a/app/react/src/pages/Link.js b/app/react/src/pages/Link.js index 850bbc5..37822dd 100644 --- a/app/react/src/pages/Link.js +++ b/app/react/src/pages/Link.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Input, LoadingPage, makeRequest, MenuButton, Switch } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; +import { useI18n } from '../providers/I18nProvider'; export default function Link() { const { user } = useOutletContext(); @@ -10,6 +11,7 @@ export default function Link() { const navigate = useNavigate(); const params = new URLSearchParams(location.search); const [ id, setId ] = useState(params.get('id')); + const { t } = useI18n(); useEffect(() => { if (id) { @@ -23,17 +25,17 @@ export default function Link() { }, []); const remove = () => { - if (confirm('Are you sure you want to delete the link? This action cannot be undone.')) { + if (confirm(t('confirm_delete_link'))) { makeRequest({ method: 'DELETE', url: '/api/links', data: { id: id }, }).then(res => { if (res?.data?.success) { - alert('Done'); + alert(t('link_deleted_successfully')); navigate('/admin/links', { replace: true }); } else { - alert('Error'); + alert(t('error_deleting_link')); } }); } @@ -46,7 +48,7 @@ export default function Link() { url: '/api/links' + (id ? `?id=${id}` : ''), data: data, }).then(res => { - alert(res?.data?.success ? 'Done' : 'Error'); + alert(res?.data?.success ? t('link_saved_successfully') : t('error_saving_link')); if (res?.data?.id) { navigate(`/admin/links/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); @@ -59,14 +61,14 @@ export default function Link() { } if (!data) { - return <>Error; + return <>{t('error')}; } return (
-

Link

+

{t('link')}

{id && <> @@ -75,13 +77,13 @@ export default function Link() { } - +
- +
- +
- +
- + setData({...data, status: e.target.checked })}/>
{id &&
diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index 72d6bd0..00f47cd 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { makeRequest, useElement } from '../utils/utils'; import { useNavigate } from 'react-router-dom'; +import { useI18n } from '../providers/I18nProvider'; export default function Login() { const [ user ] = useElement('/api/me'); @@ -10,6 +11,7 @@ export default function Login() { const [ password, setPassword ] = useState(''); const [ reset_password, setResetPassword ] = useState(false); const navigate = useNavigate(); + const { t } = useI18n(); const submitLogin = async e => { setLoading(true); @@ -23,7 +25,7 @@ export default function Login() { }, }).then(res => { if (!res?.data?.success) { - alert('Invalid email or password'); + alert(t('invalid_email_or_password')); } else { localStorage.setItem('auth_token', res.data.token); navigate('/admin/dashboard'); @@ -40,8 +42,8 @@ export default function Login() { data: { email: email }, }).then(res => { alert(res?.data?.success - ? 'If the email is registered, you will receive an email with instructions to reset your password' - : 'An error occurred, please try again later'); + ? t('password_reset_email_sent') + : t('error_occurred')); setEmail(''); }).finally(() => setLoading(false)); }; @@ -59,15 +61,15 @@ export default function Login() { {logo && }
- + setEmail(e.target.value)}/>
{!reset_password &&
- + setPassword(e.target.value)}/>
} - - + +
; } \ No newline at end of file diff --git a/app/react/src/pages/NewPassword.js b/app/react/src/pages/NewPassword.js index 9fa5010..dc656da 100644 --- a/app/react/src/pages/NewPassword.js +++ b/app/react/src/pages/NewPassword.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { makeRequest } from '../utils/utils'; import { useNavigate } from 'react-router-dom'; +import { useI18n } from '../providers/I18nProvider'; export default function NewPassword() { const logo = document.querySelector('meta[name="logo"]')?.content; @@ -8,6 +9,7 @@ export default function NewPassword() { const [ password, setPassword ] = useState(''); const [ password_confirm, setPasswordConfirm ] = useState(''); const navigate = useNavigate(); + const { t } = useI18n(); const submit = async e => { setLoading(true); @@ -22,7 +24,7 @@ export default function NewPassword() { }, }).then(res => { if (!res?.data?.success) { - alert('Invalid email or password'); + alert(t('invalid_email_or_password')); } else { localStorage.setItem('auth_token', res.data.token); navigate('/admin/dashboard'); @@ -34,14 +36,14 @@ export default function NewPassword() {
{logo && }
- + setPassword(e.target.value)}/>
- + setPasswordConfirm(e.target.value)}/>
- +
; } \ No newline at end of file diff --git a/app/react/src/pages/Page.js b/app/react/src/pages/Page.js index ec1a3cb..73a2790 100644 --- a/app/react/src/pages/Page.js +++ b/app/react/src/pages/Page.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Editor, getSlug, getUrl, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; +import { useI18n } from '../providers/I18nProvider'; export default function Page() { const { user, theme } = useOutletContext(); @@ -15,6 +16,7 @@ export default function Page() { const params = new URLSearchParams(location.search); const [ id, setId ] = useState(params.get('id')); const view_files = view_files_req?.data ?? []; + const { t } = useI18n(); useEffect(() => { fetch_view_files(); @@ -30,17 +32,17 @@ export default function Page() { }, []); const remove = () => { - if (confirm('Are you sure you want to delete the page? This action cannot be undone.')) { + if (confirm(t('confirm_delete_page'))) { makeRequest({ method: 'DELETE', url: '/api/pages', data: { id: id }, }).then(res => { if (res?.data?.success) { - alert('Done'); + alert(t('page_deleted_successfully')); navigate('/admin/pages', { replace: true }); } else { - alert('Error'); + alert(t('error_deleting_page')); } }); } @@ -53,7 +55,7 @@ export default function Page() { url: '/api/pages' + (id ? `?id=${id}` : ''), data: data, }).then(res => { - alert(res?.data?.success ? 'Done' : 'Error'); + alert(res?.data?.success ? t('page_saved_successfully') : t('error_saving_page')); if (res?.data?.id) { navigate(`/admin/pages/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); @@ -66,14 +68,14 @@ export default function Page() { } if (!data) { - return <>Error; + return <>{t('error')}; } return (
-

Page

+

{t('page')}

{id && <> @@ -82,14 +84,14 @@ export default function Page() { } - +
- + setData({ ...data, title: e.target.value })} charCount={true}/>
@@ -100,43 +102,43 @@ export default function Page() {
- + setData({ ...data, slug: getSlug(e.target.value) })} maxLength="255" charCount={true}/> {getUrl(data.slug)}
{id &&
ID: {id} - No. views: {data.views} + {t('no_views')}: {data.views}
}
- + setData({ ...data, status: e.target.checked ? 1 : 0 })}/>
- + setData({ ...data, static: e.target.checked ? 1 : 0 })}/>
- +
- + setData({...data, meta_title: e.target.value})} charCount={true}/>
- + + +
- - + +
} @@ -226,6 +232,7 @@ const Advanced = ({ data, setData, user }) => { const Info = () => { const [ server, setServer ] = useState(undefined); + const { t } = useI18n(); useEffect(() => { makeRequest({ @@ -241,35 +248,35 @@ const Info = () => { return
- + {server.os}
- + {server.php_version}
- + {server.db_dsn}
- + {server.host_name}
- + {server.root_folder}
- + {server.date}
- + {formatSize(server.memory_limit)}
- + The value is the lowest possible value between the post_max_size and the upload_max_filesize options of your PHP configuration. {formatSize(server.file_size_limit)}
@@ -278,20 +285,22 @@ const Info = () => { }; const Code = ({ data, setData }) => { + const { t } = useI18n(); + return
- + Code here will be injected into the header of all pages.
- + Code here will be injected into the footer of all pages.
- + Code here will be injected at the bottom of all post pages. Useful for things like adding a comment system.
@@ -306,13 +315,14 @@ export default function Settings() { const { user, settings } = useOutletContext(); const [ data, setData ] = useState(undefined); const [ loading, setLoading ] = useState(false); + const { t } = useI18n(); const SECTIONS = [ - { id: 'general', name: 'General', icon: IconSettings, section: General }, - { id: 'meta', name: 'Meta', icon: IconNote, section: Meta }, - { id: 'data', name: 'Data', icon: IconDatabase, section: Data }, - { id: 'advanced', name: 'Advanced', icon: IconTerminal, section: Advanced }, - { id: 'info', name: 'Server Info', icon: IconServer, section: Info }, - { id: 'code', name: 'Code', icon: IconCode, section: Code }, + { id: 'general', name: t('general'), icon: IconSettings, section: General }, + { id: 'meta', name: t('meta'), icon: IconNote, section: Meta }, + { id: 'data', name: t('data'), icon: IconDatabase, section: Data }, + { id: 'advanced', name: t('advanced'), icon: IconTerminal, section: Advanced }, + { id: 'info', name: t('server_info'), icon: IconServer, section: Info }, + { id: 'code', name: t('code'), icon: IconCode, section: Code }, //{ id: 'update', name: 'Update', icon: IconSync, section: <> }, ]; @@ -341,7 +351,7 @@ export default function Settings() { method: 'POST', url: '/api/settings', data: new_data, - }).then(res => alert(res?.data?.success ? 'Done' : 'Error')) + }).then(res => alert(res?.data?.success ? t('settings_saved_successfully') : t('error_saving_settings'))) .finally(() => setLoading(false)); }; @@ -353,10 +363,10 @@ export default function Settings() {
-

Settings

+

{t('settings')}

- +
@@ -365,7 +375,7 @@ export default function Settings() {
{SECTIONS.map(section => {section.name})}
-

Version: {version}

+

{t('version')}: {version}

{settings && SECTIONS.map(section => (hash == ('#' + section.id) && ))} diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js index 0914a90..c017079 100644 --- a/app/react/src/pages/Tag.js +++ b/app/react/src/pages/Tag.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { getSlug, Input, LoadingPage, makeRequest, MenuButton, Textarea } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; +import { useI18n } from '../providers/I18nProvider'; export default function Tag() { const { user, settings } = useOutletContext(); @@ -10,6 +11,7 @@ export default function Tag() { const navigate = useNavigate(); const params = new URLSearchParams(location.search); const [ id, setId ] = useState(params.get('id')); + const { t } = useI18n(); useEffect(() => { if (id) { @@ -23,17 +25,17 @@ export default function Tag() { }, []); const remove = () => { - if (confirm('Are you sure you want to delete the tag? This action cannot be undone.')) { + if (confirm(t('confirm_delete_tag'))) { makeRequest({ method: 'DELETE', url: '/api/tags', data: { id: id }, }).then(res => { if (res?.data?.success) { - alert('Done'); + alert(t('tag_deleted_successfully')); navigate('/admin/tags', { replace: true }); } else { - alert('Error'); + alert(t('error_deleting_tag')); } }); } @@ -46,7 +48,7 @@ export default function Tag() { url: '/api/tags' + (id ? `?id=${id}` : ''), data: data, }).then(res => { - alert(res?.data?.success ? 'Done' : 'Error'); + alert(res?.data?.success ? t('tag_saved_successfully') : t('error_saving_tag')); if (res?.data?.id) { navigate(`/admin/tags/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); @@ -59,14 +61,14 @@ export default function Tag() { } if (!data) { - return <>Error; + return <>{t('error')}; } return (
-

Tag

+

{t('tag')}

{id && <> @@ -75,13 +77,13 @@ export default function Tag() { } - +
- +
- +
- + +
@@ -351,7 +351,7 @@ export default function Settings() { method: 'POST', url: '/api/settings', data: new_data, - }).then(res => alert(res?.data?.success ? t('settings_saved_successfully') : t('error_saving_settings'))) + }).then(res => alert(t(res?.data?.success ? 'settings_saved_successfully' : 'error_saving_settings'))) .finally(() => setLoading(false)); }; diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js index c017079..1b7fe92 100644 --- a/app/react/src/pages/Tag.js +++ b/app/react/src/pages/Tag.js @@ -48,7 +48,7 @@ export default function Tag() { url: '/api/tags' + (id ? `?id=${id}` : ''), data: data, }).then(res => { - alert(res?.data?.success ? t('tag_saved_successfully') : t('error_saving_tag')); + alert(t(res?.data?.success ? 'tag_saved_successfully' : 'error_saving_tag')); if (res?.data?.id) { navigate(`/admin/tags/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index 465e038..b38d02b 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -73,7 +73,7 @@ export default function User() { url: '/api/users' + (id ? `?id=${id}` : ''), data: data, }).then(res => { - alert(res?.data?.success ? t('user_saved_successfully') : t('error_saving_user')); + alert(t(res?.data?.success ? 'user_saved_successfully' : 'error_saving_user')); if (res?.data?.id) { navigate(`/admin/users/edit?id=${res.data.id}`, { replace: true }); setId(res.data.id); diff --git a/app/react/src/pages/tables/Links.js b/app/react/src/pages/tables/Links.js index bd0ae01..a5272b9 100644 --- a/app/react/src/pages/tables/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -58,7 +58,7 @@ export default function Links() { method: 'DELETE', url: '/api/links', data: { id: links.map(l => l.id) }, - }).then(res => alert(res?.data?.success ? t('links_deleted_successfully') : t('error_deleting_links'))); + }).then(res => alert(t(res?.data?.success ? 'links_deleted_successfully' : 'error_deleting_links'))); } }, }, @@ -76,7 +76,7 @@ export default function Links() { { title: t('status'), class: 'w20', - content: link => {link.status == 1 ? t('active') : t('inactive')}, + content: link => {t(link.status == 1 ? 'active' : 'inactive')}, }, { title: t('order'), @@ -102,7 +102,7 @@ export default function Links() { method: 'DELETE', url: '/api/links', data: { id: link.id }, - }).then(res => alert(res?.data?.success ? t('link_deleted_successfully') : t('error_deleting_link'))); + }).then(res => alert(t(res?.data?.success ? 'link_deleted_successfully' : 'error_deleting_link'))); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index 0393896..d94c15a 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -34,7 +34,7 @@ const DialogEditFile = ({ file, onClose }) => { path: getContentUrl(file.path), }, }).then(res => { - alert(res?.data?.success ? t('item_renamed_successfully') : t('error_renaming_item')); + alert(t(res?.data?.success ? 'item_renamed_successfully' : 'error_renaming_item')); onClose(); }); }; @@ -74,7 +74,7 @@ const DialogDuplicate = ({ file, onClose }) => { path: getContentUrl(file.path), }, }).then(res => { - alert(res?.data?.success ? t('item_duplicated_successfully') : t('error_duplicating_item')); + alert(t(res?.data?.success ? 'item_duplicated_successfully' : 'error_duplicating_item')); onClose(); }); }; @@ -123,7 +123,7 @@ const DialogMove = ({ file, onClose }) => { path: getContentUrl(file.path), }, }).then(res => { - alert(res?.data?.success ? t('item_moved_successfully') : t('error_moving_item')); + alert(t(res?.data?.success ? 'item_moved_successfully' : 'error_moving_item')); onClose(); }); }; @@ -162,7 +162,7 @@ const DialogCreateFolder = ({ path, onClose }) => { url: '/api/media/create_folder', data: { name: path + '/' + name }, }).then(res => { - alert(res?.data?.success ? t('folder_created_successfully') : t('error_creating_folder')); + alert(t(res?.data?.success ? 'folder_created_successfully' : 'error_creating_folder')); onClose(); }); }; @@ -206,7 +206,7 @@ export default function Media() { method: 'DELETE', url: '/api/media', data: [ getContentUrl(file.path) ], - }).then(res => alert(res?.data?.success ? t('file_deleted_successfully') : t('error_deleting_file'))); + }).then(res => alert(t(res?.data?.success ? 'file_deleted_successfully' : 'error_deleting_file'))); } }; @@ -245,7 +245,7 @@ export default function Media() { url: '/api/media/upload?path=' + encodeURIComponent(current_path), data: form_data, }).then(res => { - alert(res?.data?.success ? t('files_uploaded_successfully') : t('error_uploading_files')); + alert(t(res?.data?.success ? 'files_uploaded_successfully' : 'error_uploading_files')); }); e.target.value = null; @@ -319,7 +319,7 @@ export default function Media() { method: 'DELETE', url: '/api/media', data: files.map(f => getContentUrl(f.path)), - }).then(res => alert(res?.data?.success ? t('files_deleted_successfully') : t('error_deleting_files'))); + }).then(res => alert(t(res?.data?.success ? 'files_deleted_successfully' : 'error_deleting_files'))); } }, }, diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js index cabdf7b..824a617 100644 --- a/app/react/src/pages/tables/Pages.js +++ b/app/react/src/pages/tables/Pages.js @@ -59,7 +59,7 @@ export default function Pages() { method: 'DELETE', url: '/api/pages', data: { id: pages.map(l => l.id) }, - }).then(res => alert(res?.data?.success ? t('pages_deleted_successfully') : t('error_deleting_pages'))); + }).then(res => alert(t(res?.data?.success ? 'pages_deleted_successfully' : 'error_deleting_pages'))); } }, }, @@ -107,7 +107,7 @@ export default function Pages() { method: 'DELETE', url: '/api/pages', data: { id: page.id }, - }).then(res => alert(res?.data?.success ? t('page_deleted_successfully') : t('error_deleting_page'))); + }).then(res => alert(t(res?.data?.success ? 'page_deleted_successfully' : 'error_deleting_page'))); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js index e19c231..48078ce 100644 --- a/app/react/src/pages/tables/Posts.js +++ b/app/react/src/pages/tables/Posts.js @@ -87,7 +87,7 @@ export default function Posts() { method: 'DELETE', url: '/api/posts', data: { id: posts.map(l => l.id) }, - }).then(res => alert(res?.data?.success ? t('posts_deleted_successfully') : t('error_deleting_posts'))); + }).then(res => alert(t(res?.data?.success ? 'posts_deleted_successfully' : 'error_deleting_posts'))); } }, }, @@ -147,7 +147,7 @@ export default function Posts() { method: 'DELETE', url: '/api/posts', data: { id: post.id }, - }).then(res => alert(res?.data?.success ? t('post_deleted_successfully') : t('error_deleting_post'))); + }).then(res => alert(t(res?.data?.success ? 'post_deleted_successfully' : 'error_deleting_post'))); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Tags.js b/app/react/src/pages/tables/Tags.js index e23137f..ab5d9b1 100644 --- a/app/react/src/pages/tables/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -49,7 +49,7 @@ export default function Tags() { method: 'DELETE', url: '/api/tags', data: { id: tags.map(l => l.id) }, - }).then(res => alert(res?.data?.success ? t('tags_deleted_successfully') : t('error_deleting_tags'))); + }).then(res => alert(t(res?.data?.success ? 'tags_deleted_successfully' : 'error_deleting_tags'))); } }, }, @@ -88,7 +88,7 @@ export default function Tags() { method: 'DELETE', url: '/api/tags', data: { id: tag.id }, - }).then(res => alert(res?.data?.success ? t('tag_deleted_successfully') : t('error_deleting_tag'))); + }).then(res => alert(t(res?.data?.success ? 'tag_deleted_successfully' : 'error_deleting_tag'))); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index e1d47ce..917e2bf 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -84,7 +84,7 @@ export default function Users() { method: 'DELETE', url: '/api/users', data: { id: users.map(u => u.id) }, - }).then(res => alert(res?.data?.success ? t('users_deleted_successfully') : t('error_deleting_users'))); + }).then(res => alert(t(res?.data?.success ? 'users_deleted_successfully' : 'error_deleting_users'))); } }, }, @@ -164,7 +164,7 @@ export default function Users() { method: 'DELETE', url: '/api/users', data: { id: item.id }, - }).then(res => alert(res?.data?.success ? t('user_deleted_successfully') : t('error_deleting_user'))); + }).then(res => alert(t(res?.data?.success ? 'user_deleted_successfully' : 'error_deleting_user'))); } }, content: <> {t('delete')} From d8f285b8167c001e49e8f13e178328dacba1b497 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 00:24:41 +0100 Subject: [PATCH 188/334] Fix 404 page --- app/react/src/index.js | 3 ++- app/react/src/pages/Information.js | 8 ++++++++ public/assets/css/admin/main.css | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 app/react/src/pages/Information.js diff --git a/app/react/src/index.js b/app/react/src/index.js index 58a8c19..3543cb5 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -19,6 +19,7 @@ import Tag from './pages/Tag'; import Page from './pages/Page'; import Post from './pages/Post'; import User from './pages/User'; +import Information from './pages/Information'; const App = () => { const query_client = new QueryClient(); @@ -43,8 +44,8 @@ const App = () => { }/> }/> }/> + }/> - 404 Not Found
}/> diff --git a/app/react/src/pages/Information.js b/app/react/src/pages/Information.js new file mode 100644 index 0000000..01f6bdb --- /dev/null +++ b/app/react/src/pages/Information.js @@ -0,0 +1,8 @@ +import React from 'react'; + +export default function Information({ title, subtitle }) { + return (
+

{title}

+ {subtitle &&

{subtitle}

} +
); +} \ No newline at end of file diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 2be41a0..d0c3739 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -874,6 +874,23 @@ textarea.code { width: 100%; } +.admin > .content.information-page { + justify-content: center; + flex-direction: column; + align-items: center; +} + +.admin > .content.information-page h1 { + display: flex; + justify-content: center; + font-size: 100px; + font-weight: 500; +} + +.admin > .content.information-page h2 { + font-weight: 500; +} + .page-title { display: flex; align-items: center; From f0aa375e0e8322dbaef2245ff529d84e1ff0b3b4 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 00:28:40 +0100 Subject: [PATCH 189/334] Update the delete method in DB so it returns true only if the row(s) has been deleted --- core/DB.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/DB.php b/core/DB.php index a54654c..69f203e 100755 --- a/core/DB.php +++ b/core/DB.php @@ -85,13 +85,17 @@ public function update(string $table, array $data, int $id): bool * @param string $table the table * @param [mixed] $id the row id * @param [string] $column the column to be compared with the given id - * @return bool true on success, false otherwise + * @return bool true if the row(s) has been deleted, false otherwise */ public function delete(string $table, $id = null, string $column = 'id'): bool { - return !isset($id) - ? $this->connection->exec("DELETE FROM $table") !== false - : $this->connection->prepare("DELETE FROM $table WHERE `$column` = ?")->execute([ $id ]); + if (!isset($id)) { + $stmt = $this->connection->prepare("DELETE FROM $table"); + return $stmt->execute() && $stmt->rowCount() > 0; + } + + $stmt = $this->connection->prepare("DELETE FROM $table WHERE `$column` = ?"); + return $stmt->execute([ $id ]) && $stmt->rowCount() > 0; } /** From 35f0830537837a7de48f9e032de2f1e03bf1adef Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 00:33:15 +0100 Subject: [PATCH 190/334] Remove unused csrf code --- app/controllers/ViewHelper.php | 20 -------------------- core/Helper.php | 10 ---------- 2 files changed, 30 deletions(-) diff --git a/app/controllers/ViewHelper.php b/app/controllers/ViewHelper.php index 8dfbf42..c2187bc 100644 --- a/app/controllers/ViewHelper.php +++ b/app/controllers/ViewHelper.php @@ -96,24 +96,4 @@ public function url(string $path = ''): string { return \Aurora\Core\Helper::getUrl($path); } - - /** - * Returns the current CSRF token, it creates it if it's not set - * @return string the CSRF token - */ - public function csrfToken(): string - { - if (!isset($_COOKIE['csrf_token'])) { - $token = bin2hex(random_bytes(8)); - - $_COOKIE['csrf_token'] = $token; - setcookie('csrf_token', $token, [ - 'path' => '/', - 'httponly' => true, - 'samesite' => 'Lax', - ]); - } - - return $_COOKIE['csrf_token']; - } } diff --git a/core/Helper.php b/core/Helper.php index d0d1f2f..8811db4 100755 --- a/core/Helper.php +++ b/core/Helper.php @@ -149,16 +149,6 @@ public static function getPhpSize(string $size_str): int return (int) $size; } - /** - * Returns true if the given CSRF token is valid, false otherwise - * @param string $value the CSRF token - * @return bool true if the given CSRF token is valid, false otherwise - */ - public static function isCsrfTokenValid(string $value): bool - { - return isset($_COOKIE['csrf_token']) && $_COOKIE['csrf_token'] === $value; - } - /** * Returns true if the given slug is valid * @param string $value the slug From 7f63949cbad0ebcde9ecb1b014cb8939c1fcbdde Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 00:34:52 +0100 Subject: [PATCH 191/334] Remove old HTML editor references from settings docs --- docs/en/Settings.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/docs/en/Settings.md b/docs/en/Settings.md index ff4cd6d..b181d46 100644 --- a/docs/en/Settings.md +++ b/docs/en/Settings.md @@ -130,29 +130,6 @@ Code that will be added to the footer of the website. This is useful to add cust Code that will be added at the bottom of all posts pages. This is useful to add something like a comments section via js. The `$post` PHP variable will be available in this code and will contain all the post data. -### HTML editor - -The code that defines the WYSIWYG editor used for the pages and posts in the admin panel. - -By default the [TinyMCE](https://www.tiny.cloud) editor is used, but you can use any other editor that can be loaded via js. - -The query selector for the textarea field used in both pages and posts is `textarea#html` and the endpoint which can be used to upload images is `/admin/posts/upload_image`. - -This is an example using the default editor. - -```js - - -``` - ## Update This section can be used to update Aurora to the latest compatible version. \ No newline at end of file From b75a7d209e25b6f4530198b1d071b67aa0c8e558 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 00:37:46 +0100 Subject: [PATCH 192/334] Remove old session code --- app/bootstrap/index.php | 6 ------ app/database/fixtures.json | 8 -------- app/react/src/lang/en.js | 2 -- app/react/src/lang/es.js | 2 -- app/react/src/pages/Settings.js | 12 ------------ bin/settings/Listing.php | 2 -- bin/settings/Set.php | 2 -- docs/en/Settings.md | 8 -------- 8 files changed, 42 deletions(-) diff --git a/app/bootstrap/index.php b/app/bootstrap/index.php index bf3e887..d7984f3 100644 --- a/app/bootstrap/index.php +++ b/app/bootstrap/index.php @@ -26,17 +26,11 @@ function setting(?string $key = null): mixed $settings = $db->query('SELECT `key`, value FROM settings')->fetchAll(\PDO::FETCH_KEY_PAIR); header('X-Content-Type-Options: nosniff'); - ini_set('session.cookie_httponly', 1); ini_set('error_log', \Aurora\Core\Helper::getPath($settings['log_file'])); ini_set('display_errors', $settings['display_errors'] ? 1 : 0); ini_set('display_startup_errors', $settings['display_errors'] ? 1 : 0); error_reporting($settings['log_errors'] ? E_ALL : 0); date_default_timezone_set($settings['timezone']); - session_set_cookie_params([ - 'lifetime' => (int) $settings['session_lifetime'], - 'samesite' => $settings['samesite_cookie'], - ]); - session_start(); $languages = []; foreach (glob(\Aurora\Core\Helper::getPath('app/languages/*.php')) as $file) { diff --git a/app/database/fixtures.json b/app/database/fixtures.json index 4fdcaaa..8a2c157 100644 --- a/app/database/fixtures.json +++ b/app/database/fixtures.json @@ -208,14 +208,6 @@ "key": "post_code", "value": "" }, - { - "key": "session_lifetime", - "value": "2592000" - }, - { - "key": "samesite_cookie", - "value": "Lax" - }, { "key": "display_errors", "value": "1" diff --git a/app/react/src/lang/en.js b/app/react/src/lang/en.js index 9c89af0..36deed9 100644 --- a/app/react/src/lang/en.js +++ b/app/react/src/lang/en.js @@ -174,8 +174,6 @@ export default { 'confirm_reset_views': 'Are you sure about resetting the views count of all items?', 'views_reset_successfully': 'Views count has been reset successfully', 'error_resetting_views': 'Error resetting views count', - 'session_lifetime': 'Session lifetime', - 'session_samesite_cookie': 'Session SameSite cookie', 'display_errors': 'Display errors', 'log_errors': 'Log errors', 'log_file': 'Log file', diff --git a/app/react/src/lang/es.js b/app/react/src/lang/es.js index 906200f..216a219 100644 --- a/app/react/src/lang/es.js +++ b/app/react/src/lang/es.js @@ -174,8 +174,6 @@ export default { 'confirm_reset_views': '¿Estás seguro de restablecer el contador de vistas de todos los elementos?', 'views_reset_successfully': 'El contador de vistas ha sido restablecido exitosamente', 'error_resetting_views': 'Error al restablecer el contador de vistas', - 'session_lifetime': 'Duración de sesión', - 'session_samesite_cookie': 'Cookie SameSite de sesión', 'display_errors': 'Mostrar errores', 'log_errors': 'Registrar errores', 'log_file': 'Archivo de registro', diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 5f005a8..d0face5 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -191,18 +191,6 @@ const Advanced = ({ data, setData, user }) => { return
-
- - PHP Session lifetime in seconds (e.g. 3600 = 1 hour) - setData({ ...data, session_lifetime: e.target.value })}/> -
-
- - PHP session SameSite cookie - -
setData({ ...data, display_errors: e.target.checked ? 1 : 0 })}/> diff --git a/bin/settings/Listing.php b/bin/settings/Listing.php index a871b65..edaf9cf 100644 --- a/bin/settings/Listing.php +++ b/bin/settings/Listing.php @@ -37,8 +37,6 @@ protected function execute(InputInterface $input, OutputInterface $output) [ 'Meta title', $settings['meta_title'] ], [ 'Post code', $settings['post_code'] ], [ 'RSS feed URL', $settings['rss'] ], - [ 'SameSite cookie', $settings['samesite_cookie'] ], - [ 'Session lifetime', $settings['session_lifetime'] ], [ 'Theme', $settings['theme'] ], [ 'Timezone', $settings['timezone'] ], [ 'Title', $settings['title'] ], diff --git a/bin/settings/Set.php b/bin/settings/Set.php index 1dc6466..09ace6e 100644 --- a/bin/settings/Set.php +++ b/bin/settings/Set.php @@ -38,8 +38,6 @@ protected function execute(InputInterface $input, OutputInterface $output) 'Meta title' => 'meta_title', 'Post code' => 'post_code', 'RSS feed URL' => 'rss', - 'SameSite cookie' => 'samesite_cookie', - 'Session lifetime' => 'session_lifetime', 'Theme' => 'theme', 'Timezone' => 'timezone', 'Title' => 'title', diff --git a/docs/en/Settings.md b/docs/en/Settings.md index b181d46..20a0fe0 100644 --- a/docs/en/Settings.md +++ b/docs/en/Settings.md @@ -92,14 +92,6 @@ The `Reset views count` button will reset the views counter of all posts and pag ## Advanced -### Session lifetime - -PHP Session lifetime in seconds (e.g. 3600 = 1 hour). - -### Session SameSite cookie - -Value for the PHP session SameSite cookie. This controls whether or not a cookie is sent with cross-site requests, providing some protection against cross-site request forgery attacks (CSRF). - ### Display errors Display PHP errors on the website. It's recommended to enable this option ONLY in non-production environments. For it to work properly, the Log errors option must also be enabled. From 81fbe8921dec90fbfa130ca37d8c6cc58b252d0d Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:14:03 +0100 Subject: [PATCH 193/334] Refactor https related code --- core/Helper.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/Helper.php b/core/Helper.php index 8811db4..541762d 100755 --- a/core/Helper.php +++ b/core/Helper.php @@ -32,9 +32,16 @@ public static function getCurrentPath(): string public static function getUrl(string $path = ''): string { $path = ltrim($path, '/'); - $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? 80) == 443; + return 'http' . (self::isHttps() ? 's' : '') . '://' . ($_SERVER['SERVER_NAME'] ?? 'localhost') . (empty($path) ? '' : "/$path"); + } - return 'http' . ($https ? 's' : '') . '://' . ($_SERVER['SERVER_NAME'] ?? 'localhost') . (empty($path) ? '' : "/$path"); + /** + * Returns true if the current request is made via HTTPS, false otherwise + * @return bool true if the current request is made via HTTPS, false otherwise + */ + public static function isHttps(): bool + { + return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? 80) == 443; } /** From 0b4c74bdb289a33af0a91dd2b00e253ec9b60e1c Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:33:21 +0100 Subject: [PATCH 194/334] Improve auth code --- app/bootstrap/routes.php | 49 ++++++++++++++++++++------ app/controllers/modules/User.php | 38 -------------------- app/react/src/components/AdminPages.js | 9 +++-- app/react/src/pages/Login.js | 1 - app/react/src/pages/NewPassword.js | 1 - app/react/src/pages/User.js | 1 - app/react/src/pages/tables/Users.js | 1 - app/react/src/utils/utils.js | 2 +- 8 files changed, 46 insertions(+), 56 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index b394671..cb10600 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -14,6 +14,27 @@ $rss = \Aurora\App\Setting::get('rss'); $router = $kernel->router; + $getAuthToken = function() { + $headers = getallheaders(); + + return preg_match('/Bearer\s(\S+)/', $headers['Authorization'] ?? '', $matches) + ? $matches[1] + : ($_COOKIE['auth_token'] ?? false); + }; + + $setAuthToken = function($token, $time) { + return setcookie('auth_token', + $token, + [ + 'expires' => $time, + 'path' => '/', + 'domain' => '', + 'secure' => \Aurora\Core\Helper::isHttps(), + 'httponly' => true, + 'samesite' => 'Lax', + ]); + }; + $router->get([ 'admin', 'admin/*' ], function() use ($view) { return $view->get('admin.html'); }); @@ -183,7 +204,7 @@ ]); }); - $login = function($user_id) use ($db) { + $login = function($user_id) use ($db, $setAuthToken) { $data = [ 'token' => bin2hex(random_bytes(64)) ]; try { @@ -193,13 +214,15 @@ 'created_at' => time(), ]); } catch (\Exception) { - $data = [ + return [ 'success' => false, 'error' => 'server_error', ]; } - if (!$data['success']) { + if ($data['success']) { + $setAuthToken($data['token'], time() + (60 * 60 * 24 * 30)); // 30 days + } else { unset($data['token']); } @@ -218,13 +241,9 @@ ]); }); - $router->middleware('*', function() use ($db, $view, $lang, $theme_dir, $user_mod) { - $token = preg_match('/Bearer\s(\S+)/', getallheaders()['Authorization'] ?? '', $matches) - ? $matches[1] - : false; - + $router->middleware('*', function() use ($db, $view, $lang, $theme_dir, $user_mod, $getAuthToken) { $GLOBALS['user'] = $user_mod->get([ - 'id' => $db->query('SELECT user_id FROM tokens WHERE token = ?', $token)->fetchColumn(), + 'id' => $db->query('SELECT user_id FROM tokens WHERE token = ?', $getAuthToken())->fetchColumn(), 'status' => 1, ]); @@ -293,7 +312,7 @@ }); $router->middleware('api/*', function() { - if (empty($GLOBALS['user']) && !in_array(Helper::getCurrentPath(), [ 'api/auth', 'api/password-reset/request', 'api/password-reset/confirm' ])) { + if (empty($GLOBALS['user']) && !in_array(Helper::getCurrentPath(), [ 'api/auth', 'api/password-reset/request', 'api/password-reset/confirm', 'api/logout' ])) { http_response_code(401); exit; } @@ -317,6 +336,16 @@ return json_encode($login($user['id'])); }); + $router->post('json:api/logout', function() use ($db, $getAuthToken, $setAuthToken) { + $token = $getAuthToken(); + + if ($token) { + $db->delete('tokens', $token, 'token'); + } + + return json_encode([ 'success' => $setAuthToken('', time() - 3600) ]); + }); + $router->get('json:api/me', function() { $user = $GLOBALS['user']; foreach (\Aurora\App\Permission::getPermissions() as $action) { diff --git a/app/controllers/modules/User.php b/app/controllers/modules/User.php index 2fe40cd..0cca3c6 100755 --- a/app/controllers/modules/User.php +++ b/app/controllers/modules/User.php @@ -70,33 +70,6 @@ public function add(array $data): string|bool ]); } - /** - * Handles the login of an user - * @param string $email the user's email - * @param string $password the user's password - * @return array the array with the login errors, if empty it means the user has successfully logged in. - */ - public function handleLogin(string $email, string $password): array - { - $user = $this->get([ - 'email' => $email, - 'status' => 1, - ]); - $errors = []; - - if (!$user) { - $errors[] = 'no_active_user'; - } elseif (!password_verify($password, $user['password'])) { - $errors[] = 'wrong_password'; - } - - if (empty($errors)) { - $GLOBALS['user'] = $user; - } - - return $errors; - } - /** * Returns an array with all the user fields that contain an error * @param array $data the user fields @@ -181,17 +154,6 @@ public function getCondition(array $filters): string return implode(' AND ', $where); } - /** - * Returns the user with additional data mapped into it - * @param mixed $data the user data - * @return mixed the user with additional data - */ - protected function getRowData($data): mixed - { - unset($data['password']); - return $data; - } - /** * Returns the given password hashed * @param string $password the password diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js index 495d899..63864ba 100644 --- a/app/react/src/components/AdminPages.js +++ b/app/react/src/components/AdminPages.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; import { Link, Navigate, Outlet, useNavigate } from 'react-router-dom'; -import { getContentUrl, LoadingPage, useElement } from '../utils/utils'; +import { getContentUrl, LoadingPage, makeRequest, useElement } from '../utils/utils'; import { useI18n } from '../providers/I18nProvider'; export default function AdminPages() { @@ -19,8 +19,11 @@ export default function AdminPages() { }; const logout = () => { - localStorage.removeItem('auth_token'); - navigate('/admin', { replace: true }); + makeRequest({ + method: 'POST', + url: '/api/logout', + }).catch(err => alert('Error during logout: ' + err)) + .finally(() => navigate('/admin', { replace: true })); }; if (user === null) { diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index 0235f0b..0e0e7f2 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -27,7 +27,6 @@ export default function Login() { if (!res?.data?.success) { alert(t('invalid_email_or_password')); } else { - localStorage.setItem('auth_token', res.data.token); navigate('/admin/dashboard'); } }).finally(() => setLoading(false)); diff --git a/app/react/src/pages/NewPassword.js b/app/react/src/pages/NewPassword.js index dc656da..c3c99b8 100644 --- a/app/react/src/pages/NewPassword.js +++ b/app/react/src/pages/NewPassword.js @@ -26,7 +26,6 @@ export default function NewPassword() { if (!res?.data?.success) { alert(t('invalid_email_or_password')); } else { - localStorage.setItem('auth_token', res.data.token); navigate('/admin/dashboard'); } }).finally(() => setLoading(false)); diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index b38d02b..4e59ef6 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -59,7 +59,6 @@ export default function User() { if (!res?.data?.success) { alert(t('error_impersonating_user')); } else { - localStorage.setItem('auth_token', res.data.token); fetch_user(); } }); diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index 917e2bf..5772f10 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -147,7 +147,6 @@ export default function Users() { if (!res?.data?.success) { alert(t('error_impersonating_user')); } else { - localStorage.setItem('auth_token', res.data.token); fetch_user(); } }); diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index f934569..8a7145b 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -10,10 +10,10 @@ export const makeRequest = async ({ method = 'GET', url, data = {}, options = {} method, url, headers: { - Authorization: `Bearer ${localStorage.getItem('auth_token')}`, 'Content-Type': data instanceof FormData ? 'multipart/form-data' : 'application/json', }, data: data instanceof FormData ? data : JSON.stringify(data), + withCredentials: true, ...options, }); From df05077cc9a858c058a58f71492341256c73cede Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:39:30 +0100 Subject: [PATCH 195/334] Remove old js file from gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index c8662d7..e30e3da 100755 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ composer.lock app/database/db.sqlite public/content/* public/assets/js/tinymce -public/assets/js/admin2.* app/react/node_modules app/react/package-lock.json vendor \ No newline at end of file From 86204bd5f11694781ce184c3537eba827e340e88 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:46:08 +0100 Subject: [PATCH 196/334] Refactor public html code --- app/controllers/ViewHelper.php | 8 -------- app/views/emails/password_restore.html | 2 +- app/views/themes/default/blog.html | 2 +- app/views/themes/default/information.html | 2 +- app/views/themes/default/page.html | 2 +- app/views/themes/default/partials/head.html | 2 +- app/views/themes/default/post.html | 2 +- app/views/themes/default/rss.html | 8 ++++---- 8 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/controllers/ViewHelper.php b/app/controllers/ViewHelper.php index c2187bc..2dd0651 100644 --- a/app/controllers/ViewHelper.php +++ b/app/controllers/ViewHelper.php @@ -88,12 +88,4 @@ public function getLanguageCode(): string { return $this->language->getCode(); } - - /** - * @see \Aurora\Core\Helper::getUrl - */ - public function url(string $path = ''): string - { - return \Aurora\Core\Helper::getUrl($path); - } } diff --git a/app/views/emails/password_restore.html b/app/views/emails/password_restore.html index 8884626..6444ad6 100644 --- a/app/views/emails/password_restore.html +++ b/app/views/emails/password_restore.html @@ -10,6 +10,6 @@

t('someone_requested_password_restore') ?>

" target="_blank">t('click_to_restore_password') ?>
- url()) ?> + diff --git a/app/views/themes/default/blog.html b/app/views/themes/default/blog.html index 50d372e..d59d71f 100755 --- a/app/views/themes/default/blog.html +++ b/app/views/themes/default/blog.html @@ -10,7 +10,7 @@ - + include('themes/default/partials/header.html') ?> diff --git a/app/views/themes/default/information.html b/app/views/themes/default/information.html index ac72e60..1be93e3 100755 --- a/app/views/themes/default/information.html +++ b/app/views/themes/default/information.html @@ -4,7 +4,7 @@ <?= e(setting('title')) ?> include('themes/default/partials/head.html') ?> - + diff --git a/app/views/themes/default/page.html b/app/views/themes/default/page.html index 9d7e01c..365c3e9 100644 --- a/app/views/themes/default/page.html +++ b/app/views/themes/default/page.html @@ -6,7 +6,7 @@ - + include('themes/default/partials/header.html') ?> diff --git a/app/views/themes/default/partials/head.html b/app/views/themes/default/partials/head.html index 7ad3e5c..2dfc203 100755 --- a/app/views/themes/default/partials/head.html +++ b/app/views/themes/default/partials/head.html @@ -5,7 +5,7 @@ - + diff --git a/app/views/themes/default/post.html b/app/views/themes/default/post.html index 83b6bfd..d5a6ee1 100755 --- a/app/views/themes/default/post.html +++ b/app/views/themes/default/post.html @@ -10,7 +10,7 @@ - + include('themes/default/partials/header.html') ?> diff --git a/app/views/themes/default/rss.html b/app/views/themes/default/rss.html index 3ea0c58..077f902 100644 --- a/app/views/themes/default/rss.html +++ b/app/views/themes/default/rss.html @@ -1,20 +1,20 @@ <?= e(setting('title')) ?> - url()) ?> + getLanguageCode()) ?> getContentUrl(setting('logo'))) ?> <?= e(setting('title')) ?> - url()) ?> + <?= e($post['title']) ?> - url('/' . setting('blog_url') . '/' . $post['slug'])) ?> + @@ -23,7 +23,7 @@ - url('/' . setting('blog_url') . '/' . $post['slug'])) ?> + From 38048a0c27b7223b3b52e6e48b8d563f6a55d477 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:53:33 +0100 Subject: [PATCH 197/334] Add admin js files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e30e3da..eb54209 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ composer.lock app/database/db.sqlite public/content/* public/assets/js/tinymce +public/assets/js/admin.* app/react/node_modules app/react/package-lock.json vendor \ No newline at end of file From 58141156e99ee4abe2c8e6af97ba1b4b5ad8e13b Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:53:43 +0100 Subject: [PATCH 198/334] Fix admin js filename --- app/react/webpack.config.js | 2 +- app/views/admin.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/webpack.config.js b/app/react/webpack.config.js index c99c66d..2bb4123 100644 --- a/app/react/webpack.config.js +++ b/app/react/webpack.config.js @@ -7,7 +7,7 @@ module.exports = (env, argv) => { entry: './src/index.js', output: { path: path.resolve(__dirname, '../../public/assets/js'), - filename: 'admin2.js', + filename: 'admin.js', clean: false, }, devtool: is_prod ? false : 'source-map', diff --git a/app/views/admin.html b/app/views/admin.html index d681302..236031b 100644 --- a/app/views/admin.html +++ b/app/views/admin.html @@ -15,4 +15,4 @@ - + From f3a81c8ec4c59902ba5ad6e3f618050e8f53337c Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:57:39 +0100 Subject: [PATCH 199/334] Move AdminPages code --- app/react/src/components/AdminPages.js | 82 ------------------------- app/react/src/index.js | 84 +++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 84 deletions(-) delete mode 100644 app/react/src/components/AdminPages.js diff --git a/app/react/src/components/AdminPages.js b/app/react/src/components/AdminPages.js deleted file mode 100644 index 63864ba..0000000 --- a/app/react/src/components/AdminPages.js +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useState } from 'react'; -import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from '../utils/icons'; -import { Link, Navigate, Outlet, useNavigate } from 'react-router-dom'; -import { getContentUrl, LoadingPage, makeRequest, useElement } from '../utils/utils'; -import { useI18n } from '../providers/I18nProvider'; - -export default function AdminPages() { - const dark_theme_element = document.getElementById('css-dark'); - const [ user, fetch_user ] = useElement('/api/me'); - const [ settings, fetch_settings ] = useElement('/api/settings'); - const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); - const navigate = useNavigate(); - const { t } = useI18n(); - - const toggleTheme = () => { - const is_light_enabled = dark_theme_element.toggleAttribute('disabled'); - setTheme(is_light_enabled ? 'light' : 'dark'); - document.cookie = 'theme=' + (is_light_enabled ? 'light' : 'dark') + ';path=/'; - }; - - const logout = () => { - makeRequest({ - method: 'POST', - url: '/api/logout', - }).catch(err => alert('Error during logout: ' + err)) - .finally(() => navigate('/admin', { replace: true })); - }; - - if (user === null) { - return ; - } - - return
- - {user && settings ? : } -
; -}; \ No newline at end of file diff --git a/app/react/src/index.js b/app/react/src/index.js index 3543cb5..2892896 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -1,9 +1,12 @@ -import React from 'react'; +import React, { useState } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { I18nProvider } from './providers/I18nProvider'; -import AdminPages from './components/AdminPages'; +import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from './utils/icons'; +import { Link as RouterLink, Navigate, Outlet, useNavigate } from 'react-router-dom'; +import { getContentUrl, LoadingPage, makeRequest, useElement } from './utils/utils'; +import { useI18n } from './providers/I18nProvider'; import NewPassword from './pages/NewPassword'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; @@ -21,6 +24,83 @@ import Post from './pages/Post'; import User from './pages/User'; import Information from './pages/Information'; +const AdminPages = () => { + const dark_theme_element = document.getElementById('css-dark'); + const [ user, fetch_user ] = useElement('/api/me'); + const [ settings, fetch_settings ] = useElement('/api/settings'); + const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); + const navigate = useNavigate(); + const { t } = useI18n(); + + const toggleTheme = () => { + const is_light_enabled = dark_theme_element.toggleAttribute('disabled'); + setTheme(is_light_enabled ? 'light' : 'dark'); + document.cookie = 'theme=' + (is_light_enabled ? 'light' : 'dark') + ';path=/'; + }; + + const logout = () => { + makeRequest({ + method: 'POST', + url: '/api/logout', + }).catch(err => alert('Error during logout: ' + err)) + .finally(() => navigate('/admin', { replace: true })); + }; + + if (user === null) { + return ; + } + + return
+ + {user && settings ? : } +
; +}; + const App = () => { const query_client = new QueryClient(); From 1590a7cd22c3ef10ed9f2322fb844589efd72494 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 01:58:39 +0100 Subject: [PATCH 200/334] Move Table component --- app/react/src/pages/tables/Links.js | 2 +- app/react/src/pages/tables/Media.js | 2 +- app/react/src/pages/tables/Pages.js | 2 +- app/react/src/pages/tables/Posts.js | 2 +- app/react/src/pages/tables/Tags.js | 2 +- app/react/src/pages/tables/Users.js | 2 +- app/react/src/{components => utils}/Table.js | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) rename app/react/src/{components => utils}/Table.js (98%) diff --git a/app/react/src/pages/tables/Links.js b/app/react/src/pages/tables/Links.js index a5272b9..6dac3d7 100644 --- a/app/react/src/pages/tables/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Table } from '../../components/Table'; +import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, makeRequest } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index d94c15a..f64af47 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Table } from '../../components/Table'; +import { Table } from '../../utils/Table'; import { useOutletContext, useSearchParams } from 'react-router-dom'; import { downloadFile, DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; import { IconClipboard, IconDuplicate, IconFile, IconFolder, IconFolderFill, IconHome, IconMoveFile, IconPencil, IconThreeDots, IconTrash, IconX, IconZip } from '../../utils/icons'; diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js index 824a617..0be3e19 100644 --- a/app/react/src/pages/tables/Pages.js +++ b/app/react/src/pages/tables/Pages.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Table } from '../../components/Table'; +import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, formatDate, makeRequest } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js index 48078ce..de77396 100644 --- a/app/react/src/pages/tables/Posts.js +++ b/app/react/src/pages/tables/Posts.js @@ -1,5 +1,5 @@ import React, { useEffect, useMemo } from 'react'; -import { Table } from '../../components/Table'; +import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, formatDate, getContentUrl, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; diff --git a/app/react/src/pages/tables/Tags.js b/app/react/src/pages/tables/Tags.js index ab5d9b1..b87ac38 100644 --- a/app/react/src/pages/tables/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Table } from '../../components/Table'; +import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, makeRequest } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index 5772f10..3b5e3cd 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -1,5 +1,5 @@ import React, { useEffect, useMemo } from 'react'; -import { Table } from '../../components/Table'; +import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, formatDate, getContentUrl, getRoleTitle, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash, IconUsers } from '../../utils/icons'; diff --git a/app/react/src/components/Table.js b/app/react/src/utils/Table.js similarity index 98% rename from app/react/src/components/Table.js rename to app/react/src/utils/Table.js index 6204e1c..1689127 100644 --- a/app/react/src/components/Table.js +++ b/app/react/src/utils/Table.js @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { MenuButton, useRequest } from '../utils/utils'; -import { IconGlass } from '../utils/icons'; +import { MenuButton, useRequest } from './utils'; +import { IconGlass } from './icons'; const Header = ({ title, totalItems, selectedItems = 0, options = [] }) => { return
From 1e54aa5e64b304d428bc830b8405d03379219e5e Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 02:01:33 +0100 Subject: [PATCH 201/334] Remove unused dependencies --- app/react/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/react/package.json b/app/react/package.json index 1aee91a..284dea5 100644 --- a/app/react/package.json +++ b/app/react/package.json @@ -23,11 +23,9 @@ "@babel/preset-env": "^7.25.0", "@babel/preset-react": "^7.24.7", "babel-loader": "^9.1.3", - "html-webpack-plugin": "^5.6.0", "shx": "^0.4.0", "webpack": "^5.95.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.15.1" + "webpack-cli": "^5.1.4" }, "author": "usbac", "license": "MIT" From 0e8274f581a64ee947e04a8977b34c871cf01657 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 03:26:08 +0100 Subject: [PATCH 202/334] Add missing loading animation and no items found to table component --- app/react/src/lang/en.js | 1 + app/react/src/lang/es.js | 1 + app/react/src/utils/Table.js | 17 ++++++++++++++--- app/react/src/utils/icons.js | 3 ++- public/assets/css/admin/main.css | 7 +++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/react/src/lang/en.js b/app/react/src/lang/en.js index 36deed9..ba94db3 100644 --- a/app/react/src/lang/en.js +++ b/app/react/src/lang/en.js @@ -197,4 +197,5 @@ export default { 'post_code': 'Post code', 'settings_saved_successfully': 'Settings have been saved successfully', 'error_saving_settings': 'Error saving settings', + 'no_items_found': 'No items found', }; diff --git a/app/react/src/lang/es.js b/app/react/src/lang/es.js index 216a219..d0e9a6a 100644 --- a/app/react/src/lang/es.js +++ b/app/react/src/lang/es.js @@ -197,4 +197,5 @@ export default { 'post_code': 'Código de publicación', 'settings_saved_successfully': 'La configuración ha sido guardada exitosamente', 'error_saving_settings': 'Error al guardar la configuración', + 'no_items_found': 'No se encontraron elementos', }; diff --git a/app/react/src/utils/Table.js b/app/react/src/utils/Table.js index 1689127..cf0b292 100644 --- a/app/react/src/utils/Table.js +++ b/app/react/src/utils/Table.js @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { MenuButton, useRequest } from './utils'; -import { IconGlass } from './icons'; +import { IconGlass, IconSpinner } from './icons'; +import { useI18n } from '../providers/I18nProvider'; const Header = ({ title, totalItems, selectedItems = 0, options = [] }) => { return
@@ -62,6 +63,7 @@ export const Table = ({ method: 'GET', url: url + (query_string ? `${url.includes('?') ? '&' : '?'}${query_string}` : ''), }); + const { t } = useI18n(); useEffect(() => { let aux = { ...initialFilters }; @@ -146,8 +148,17 @@ export const Table = ({ }; const Rows = () => { - if (is_loading) return

Cargando...

; - if (is_error) return

Error al cargar los datos.

; + if (is_loading) { + return ; + } + + if (is_error) { + return

{t('error_occurred')}

; + } + + if (rows.length == 0) { + return

{t('no_items_found')}

; + } return rows.map((row, i) =>
; export const IconX = (props) => ; export const IconZip = (props) => ; -export const IconThreeDots = (props) => ; \ No newline at end of file +export const IconThreeDots = (props) => ; +export const IconSpinner = (props) => ; diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index d0c3739..185c266 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -1092,6 +1092,13 @@ textarea.code { margin-top: 40px; } +.listing-title { + text-align: center; + font-weight: 400; + font-size: 18px; + margin: 40px 0; +} + .batch-options-container { display: flex; justify-content: flex-end; From f77b72b45bd9d94f0abe5d6156ede6a9156fde8d Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 03:34:16 +0100 Subject: [PATCH 203/334] Fix sidebar in mobile view --- app/react/src/index.js | 1 + public/assets/css/admin/main.css | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index 2892896..fd05eb2 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -98,6 +98,7 @@ const AdminPages = () => {
{user && settings ? : } +
; }; diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 185c266..57a7023 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -767,7 +767,7 @@ textarea.code { /* header and containers */ -.admin[data-nav-open] { +body[data-nav-open] { overflow: hidden; } @@ -1490,16 +1490,18 @@ textarea.code { margin-left: initial; } - .admin[data-nav-open] .nav-background { + body[data-nav-open] .nav-background { width: 100vw; height: 100vh; z-index: calc(var(--nav-z-index) - 1); position: fixed; background-color: black; + top: 0; + left: 0; opacity: 0.2; } - .admin:not([data-nav-open]) nav { + body:not([data-nav-open]) nav { left: calc(-1 * var(--nav-width)); } From 7fea396400f2062df9d814fe3198a47a6f3072c4 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:00:07 +0100 Subject: [PATCH 204/334] Improve i18n provider --- app/react/src/providers/I18nProvider.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/react/src/providers/I18nProvider.js b/app/react/src/providers/I18nProvider.js index 4c35513..9cffe57 100644 --- a/app/react/src/providers/I18nProvider.js +++ b/app/react/src/providers/I18nProvider.js @@ -36,10 +36,15 @@ export const I18nProvider = ({ children, defaultLanguage = 'en' }) => { } }; + const getLanguages = () => { + return Object.keys(translations); + }; + return {children} ; From 1c073ce8880e48faeadc1304ad110f58acb38f7c Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:00:18 +0100 Subject: [PATCH 205/334] Add language switch --- app/react/src/index.js | 5 ++++- public/assets/css/admin/main.css | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index fd05eb2..91c0b31 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -30,7 +30,7 @@ const AdminPages = () => { const [ settings, fetch_settings ] = useElement('/api/settings'); const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); const navigate = useNavigate(); - const { t } = useI18n(); + const { t, language, getLanguages, changeLanguage } = useI18n(); const toggleTheme = () => { const is_light_enabled = dark_theme_element.toggleAttribute('disabled'); @@ -92,6 +92,9 @@ const AdminPages = () => {
{theme == 'light' ? : }
+
diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 57a7023..b428273 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -862,7 +862,6 @@ body[data-nav-open] { #toggle-theme { display: flex; user-select: none; - margin: 0 auto 0 10px; } .admin > .content > *:first-child { @@ -929,6 +928,7 @@ body[data-nav-open] { flex-direction: row; justify-content: space-between; align-items: center; + gap: 10px; text-decoration: none; color: white; margin-top: auto; @@ -953,6 +953,21 @@ body[data-nav-open] { fill: white; } +.current-user select { + padding: 0; + background: transparent; + border: none; + color: white; + font-weight: 500; + font-size: 16px; + cursor: pointer; + outline: none; +} + +.current-user > *:last-child { + margin-left: auto; +} + /* dashboard */ .start-creating > * { From 85b8efc894e145b82c2ef9034723d0259a6be1bf Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:03:19 +0100 Subject: [PATCH 206/334] Minor fixes --- app/react/src/lang/en.js | 3 ++- app/react/src/lang/es.js | 3 ++- app/react/src/pages/Settings.js | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/react/src/lang/en.js b/app/react/src/lang/en.js index ba94db3..48cc6f2 100644 --- a/app/react/src/lang/en.js +++ b/app/react/src/lang/en.js @@ -160,7 +160,8 @@ export default { 'rss_feed_url': 'RSS feed URL', 'theme': 'Theme', 'items_per_page': 'Items per page', - 'system_language': 'System language', + 'website_language': 'Website language', + 'website_language_description': 'The language used in the website for visitors.', 'date_format': 'Date format', 'timezone': 'Timezone', 'maintenance_mode': 'Maintenance mode', diff --git a/app/react/src/lang/es.js b/app/react/src/lang/es.js index d0e9a6a..90eb6a2 100644 --- a/app/react/src/lang/es.js +++ b/app/react/src/lang/es.js @@ -160,7 +160,8 @@ export default { 'rss_feed_url': 'URL del feed RSS', 'theme': 'Tema', 'items_per_page': 'Elementos por página', - 'system_language': 'Idioma del sistema', + 'website_language': 'Idioma del sitio web', + 'website_language_description': 'El idioma utilizado en el sitio web para los visitantes.', 'date_format': 'Formato de fecha', 'timezone': 'Zona horaria', 'maintenance_mode': 'Modo mantenimiento', diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index d0face5..5755f31 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -43,8 +43,8 @@ const General = ({ data, setData }) => {
- - {t('system_language')} + + {t('website_language_description')} From 37c97525784172476ea280bee679640956c15ea4 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:06:43 +0100 Subject: [PATCH 207/334] Use document language --- app/react/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index 91c0b31..3a35282 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -109,7 +109,7 @@ const App = () => { const query_client = new QueryClient(); return - + }/> From 268317f3e2f6759a8ee0f6d65500e4f56de695a6 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:24:01 +0100 Subject: [PATCH 208/334] Fix loading page --- app/react/src/utils/utils.js | 4 ++-- public/assets/css/admin/main.css | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 8a7145b..bc2be24 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { IconFolderFill, IconHome, IconUploadFile, IconX } from './icons'; +import { IconFolderFill, IconHome, IconSpinner, IconUploadFile, IconX } from './icons'; import { createPortal } from 'react-dom'; import { Editor as TinyMCE } from '@tinymce/tinymce-react'; import axios from 'axios'; @@ -73,7 +73,7 @@ export const MenuButton = () =>
-
+
; diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index b428273..3013c19 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -151,12 +151,23 @@ button.loading, cursor: wait; } +.loading-page { + align-items: center !important; + justify-content: center !important; + min-height: calc(100vh - (var(--content-spacing) * 2)); +} + +.loading-page svg { + width: 50px; + height: 50px; +} + .loading-icon { --size: 19.5px; animation: rotation 1s infinite linear; width: var(--size); height: var(--size); - stroke: var(--main-color); + fill: var(--main-color); } .light .loading-icon { From 6b02916a5f725e32eac5bb6485af9b0d5ded3921 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:25:38 +0100 Subject: [PATCH 209/334] Fix loading icon --- app/react/src/utils/Table.js | 4 ++-- public/assets/css/admin/main.css | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/react/src/utils/Table.js b/app/react/src/utils/Table.js index cf0b292..ff29515 100644 --- a/app/react/src/utils/Table.js +++ b/app/react/src/utils/Table.js @@ -148,8 +148,8 @@ export const Table = ({ }; const Rows = () => { - if (is_loading) { - return ; + if (is_loading || true) { + return ; } if (is_error) { diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 3013c19..e57d02f 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -1110,12 +1110,11 @@ body[data-nav-open] { } .listing > svg { - width: var(--image-size); - height: var(--image-size); display: flex; - animation: rotation 1s infinite linear; align-self: center; margin-top: 40px; + width: var(--image-size); + height: var(--image-size); } .listing-title { From 654bb313b06ab7cc9c4e6aac4793bd8cf3047fd2 Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:26:25 +0100 Subject: [PATCH 210/334] Remove unused style --- public/assets/css/admin/main.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index e57d02f..89ef43b 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -170,10 +170,6 @@ button.loading, fill: var(--main-color); } -.light .loading-icon { - stroke: var(--main-color); -} - input, select { padding: var(--input-padding); From f1b5eecc2cda0542287236ded013bfcc4ba09cfb Mon Sep 17 00:00:00 2001 From: Usbac Date: Tue, 30 Dec 2025 04:28:52 +0100 Subject: [PATCH 211/334] Fix loading icon --- app/react/src/utils/Table.js | 2 +- app/react/src/utils/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/utils/Table.js b/app/react/src/utils/Table.js index ff29515..80e940d 100644 --- a/app/react/src/utils/Table.js +++ b/app/react/src/utils/Table.js @@ -148,7 +148,7 @@ export const Table = ({ }; const Rows = () => { - if (is_loading || true) { + if (is_loading) { return ; } diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index bc2be24..11d49a1 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -269,7 +269,7 @@ export const ImageDialog = ({ onSave, onClose }) => { const files = files_req ? files_req.data?.data : []; if (is_loading || !user || !settings) { - return ; + return ; } return <> From 84cda0b08e9db2b18a29467bad780c136ea79b6a Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 1 Jan 2026 15:48:51 +0100 Subject: [PATCH 212/334] Add reference to fetch function --- app/react/src/utils/Table.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/react/src/utils/Table.js b/app/react/src/utils/Table.js index 80e940d..88f0f92 100644 --- a/app/react/src/utils/Table.js +++ b/app/react/src/utils/Table.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from 'react'; import { MenuButton, useRequest } from './utils'; import { IconGlass, IconSpinner } from './icons'; import { useI18n } from '../providers/I18nProvider'; @@ -40,7 +40,7 @@ const getQueryString = (filters, search, page) => { return (new URLSearchParams(values)).toString(); }; -export const Table = ({ +export const Table = forwardRef(({ url, title = '', topOptions = [], @@ -48,7 +48,7 @@ export const Table = ({ columns = [], rowOnClick = null, options: initialOptions = [], -}) => { +}, ref) => { const params = useMemo(() => new URLSearchParams(window.location.search), []); const [ page, setPage ] = useState(params.get('page') ? parseInt(params.get('page')) : 1); const [ select_mode, setSelectMode ] = useState(false); @@ -63,8 +63,13 @@ export const Table = ({ method: 'GET', url: url + (query_string ? `${url.includes('?') ? '&' : '?'}${query_string}` : ''), }); + const fetch_ref = useRef(fetch); const { t } = useI18n(); + useEffect(() => { + fetch_ref.current = fetch; + }, [ fetch ]); + useEffect(() => { let aux = { ...initialFilters }; @@ -98,6 +103,13 @@ export const Table = ({ setSelectedRows([]); }, [ select_mode ]); + useImperativeHandle(ref, () => ({ + refetch: () => { + setPage(1); + fetch_ref.current(); + }, + }), []); + const submit = e => { e.preventDefault(); setPage(1); @@ -205,4 +217,4 @@ export const Table = ({
{page_req?.data?.meta?.next_page && } ; -}; +}); From b33d14877ad219bb5aa4f30c879d84a6a1e6fd1e Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 2 Jan 2026 16:16:51 +0100 Subject: [PATCH 213/334] Refresh tables when doing changes --- app/react/src/pages/tables/Links.js | 18 ++++++++-- app/react/src/pages/tables/Media.js | 51 +++++++++++++++++++++++------ app/react/src/pages/tables/Pages.js | 18 ++++++++-- app/react/src/pages/tables/Posts.js | 18 ++++++++-- app/react/src/pages/tables/Tags.js | 18 ++++++++-- app/react/src/pages/tables/Users.js | 18 ++++++++-- 6 files changed, 116 insertions(+), 25 deletions(-) diff --git a/app/react/src/pages/tables/Links.js b/app/react/src/pages/tables/Links.js index 6dac3d7..a9af14e 100644 --- a/app/react/src/pages/tables/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, makeRequest } from '../../utils/utils'; @@ -9,9 +9,11 @@ export default function Links() { const { user } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const table_ref = useRef(null); return
l.id) }, - }).then(res => alert(t(res?.data?.success ? 'links_deleted_successfully' : 'error_deleting_links'))); + }).then(res => { + alert(t(res?.data?.success ? 'links_deleted_successfully' : 'error_deleting_links')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, }, @@ -102,7 +109,12 @@ export default function Links() { method: 'DELETE', url: '/api/links', data: { id: link.id }, - }).then(res => alert(t(res?.data?.success ? 'link_deleted_successfully' : 'error_deleting_link'))); + }).then(res => { + alert(t(res?.data?.success ? 'link_deleted_successfully' : 'error_deleting_link')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index f64af47..33e599c 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -21,7 +21,7 @@ const MediaPath = ({ path, setPath }) => { ; }; -const DialogEditFile = ({ file, onClose }) => { +const DialogEditFile = ({ file, onClose, onSuccess }) => { const [ name, setName ] = useState(file.name); const { t } = useI18n(); @@ -35,6 +35,9 @@ const DialogEditFile = ({ file, onClose }) => { }, }).then(res => { alert(t(res?.data?.success ? 'item_renamed_successfully' : 'error_renaming_item')); + if (res?.data?.success && onSuccess) { + onSuccess(); + } onClose(); }); }; @@ -61,7 +64,7 @@ const DialogEditFile = ({ file, onClose }) => { , document.body); }; -const DialogDuplicate = ({ file, onClose }) => { +const DialogDuplicate = ({ file, onClose, onSuccess }) => { const [ name, setName ] = useState(file.name); const { t } = useI18n(); @@ -75,6 +78,9 @@ const DialogDuplicate = ({ file, onClose }) => { }, }).then(res => { alert(t(res?.data?.success ? 'item_duplicated_successfully' : 'error_duplicating_item')); + if (res?.data?.success && onSuccess) { + onSuccess(); + } onClose(); }); }; @@ -101,7 +107,7 @@ const DialogDuplicate = ({ file, onClose }) => { , document.body); }; -const DialogMove = ({ file, onClose }) => { +const DialogMove = ({ file, onClose, onSuccess }) => { const [ folders, setFolders ] = useState(undefined); const initial_destination_folder = file.path.slice(0, file.path.lastIndexOf('/')).replace(/^\/+|\/+$/g, ''); const [ destination_folder, setDestinationFolder ] = useState(initial_destination_folder.length == 0 ? '/' : initial_destination_folder); @@ -124,6 +130,9 @@ const DialogMove = ({ file, onClose }) => { }, }).then(res => { alert(t(res?.data?.success ? 'item_moved_successfully' : 'error_moving_item')); + if (res?.data?.success && onSuccess) { + onSuccess(); + } onClose(); }); }; @@ -152,7 +161,7 @@ const DialogMove = ({ file, onClose }) => { , document.body); }; -const DialogCreateFolder = ({ path, onClose }) => { +const DialogCreateFolder = ({ path, onClose, onSuccess }) => { const [ name, setName ] = useState(''); const { t } = useI18n(); @@ -163,6 +172,9 @@ const DialogCreateFolder = ({ path, onClose }) => { data: { name: path + '/' + name }, }).then(res => { alert(t(res?.data?.success ? 'folder_created_successfully' : 'error_creating_folder')); + if (res?.data?.success && onSuccess) { + onSuccess(); + } onClose(); }); }; @@ -195,6 +207,7 @@ export default function Media() { const [ current_dialog, setCurrentDialog ] = useState(null); const [ current_file, setCurrentFile ] = useState(null); const file_input_ref = useRef(null); + const table_ref = useRef(null); const current_path = search_params.get('path') || ''; const { t } = useI18n(); @@ -206,7 +219,12 @@ export default function Media() { method: 'DELETE', url: '/api/media', data: [ getContentUrl(file.path) ], - }).then(res => alert(t(res?.data?.success ? 'file_deleted_successfully' : 'error_deleting_file'))); + }).then(res => { + alert(t(res?.data?.success ? 'file_deleted_successfully' : 'error_deleting_file')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }; @@ -246,6 +264,9 @@ export default function Media() { data: form_data, }).then(res => { alert(t(res?.data?.success ? 'files_uploaded_successfully' : 'error_uploading_files')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } }); e.target.value = null; @@ -270,8 +291,13 @@ export default function Media() { const closeDialog = () => setCurrentDialog(null); + const refreshTable = () => { + table_ref?.current?.refetch(); + }; + return
getContentUrl(f.path)), - }).then(res => alert(t(res?.data?.success ? 'files_deleted_successfully' : 'error_deleting_files'))); + }).then(res => { + alert(t(res?.data?.success ? 'files_deleted_successfully' : 'error_deleting_files')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, }, @@ -386,10 +417,10 @@ export default function Media() { }, ]} /> - {current_dialog == 'duplicate_file' && } - {current_dialog == 'move_file' && } - {current_dialog == 'edit_file' && } - {current_dialog == 'create_folder' && } + {current_dialog == 'duplicate_file' && } + {current_dialog == 'move_file' && } + {current_dialog == 'edit_file' && } + {current_dialog == 'create_folder' && } } \ No newline at end of file diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js index 0be3e19..2fc6e0e 100644 --- a/app/react/src/pages/tables/Pages.js +++ b/app/react/src/pages/tables/Pages.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, formatDate, makeRequest } from '../../utils/utils'; @@ -9,9 +9,11 @@ export default function Pages() { const { user, settings } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const table_ref = useRef(null); return
l.id) }, - }).then(res => alert(t(res?.data?.success ? 'pages_deleted_successfully' : 'error_deleting_pages'))); + }).then(res => { + alert(t(res?.data?.success ? 'pages_deleted_successfully' : 'error_deleting_pages')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, }, @@ -107,7 +114,12 @@ export default function Pages() { method: 'DELETE', url: '/api/pages', data: { id: page.id }, - }).then(res => alert(t(res?.data?.success ? 'page_deleted_successfully' : 'error_deleting_page'))); + }).then(res => { + alert(t(res?.data?.success ? 'page_deleted_successfully' : 'error_deleting_page')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js index de77396..2e0217c 100644 --- a/app/react/src/pages/tables/Posts.js +++ b/app/react/src/pages/tables/Posts.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, formatDate, getContentUrl, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; @@ -17,6 +17,7 @@ export default function Posts() { }); const navigate = useNavigate(); const { t } = useI18n(); + const table_ref = useRef(null); const users_options = useMemo(() => { let users = users_req?.data?.data ?? {}; @@ -36,6 +37,7 @@ export default function Posts() { return
l.id) }, - }).then(res => alert(t(res?.data?.success ? 'posts_deleted_successfully' : 'error_deleting_posts'))); + }).then(res => { + alert(t(res?.data?.success ? 'posts_deleted_successfully' : 'error_deleting_posts')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, }, @@ -147,7 +154,12 @@ export default function Posts() { method: 'DELETE', url: '/api/posts', data: { id: post.id }, - }).then(res => alert(t(res?.data?.success ? 'post_deleted_successfully' : 'error_deleting_post'))); + }).then(res => { + alert(t(res?.data?.success ? 'post_deleted_successfully' : 'error_deleting_post')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Tags.js b/app/react/src/pages/tables/Tags.js index b87ac38..dcb7737 100644 --- a/app/react/src/pages/tables/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, makeRequest } from '../../utils/utils'; @@ -9,9 +9,11 @@ export default function Tags() { const { user, settings } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const table_ref = useRef(null); return
l.id) }, - }).then(res => alert(t(res?.data?.success ? 'tags_deleted_successfully' : 'error_deleting_tags'))); + }).then(res => { + alert(t(res?.data?.success ? 'tags_deleted_successfully' : 'error_deleting_tags')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, }, @@ -88,7 +95,12 @@ export default function Tags() { method: 'DELETE', url: '/api/tags', data: { id: tag.id }, - }).then(res => alert(t(res?.data?.success ? 'tag_deleted_successfully' : 'error_deleting_tag'))); + }).then(res => { + alert(t(res?.data?.success ? 'tag_deleted_successfully' : 'error_deleting_tag')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, content: <> {t('delete')} diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index 3b5e3cd..fda7520 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { DropdownMenu, formatDate, getContentUrl, getRoleTitle, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; @@ -9,6 +9,7 @@ export default function Users() { const { user, settings, fetch_user } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const table_ref = useRef(null); const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ method: 'GET', url: '/api/roles', @@ -32,6 +33,7 @@ export default function Users() { return
u.id) }, - }).then(res => alert(t(res?.data?.success ? 'users_deleted_successfully' : 'error_deleting_users'))); + }).then(res => { + alert(t(res?.data?.success ? 'users_deleted_successfully' : 'error_deleting_users')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, }, @@ -163,7 +170,12 @@ export default function Users() { method: 'DELETE', url: '/api/users', data: { id: item.id }, - }).then(res => alert(t(res?.data?.success ? 'user_deleted_successfully' : 'error_deleting_user'))); + }).then(res => { + alert(t(res?.data?.success ? 'user_deleted_successfully' : 'error_deleting_user')); + if (res?.data?.success) { + table_ref?.current?.refetch(); + } + }); } }, content: <> {t('delete')} From 4d36fd791e4837721dc96fb26d8942f23802743c Mon Sep 17 00:00:00 2001 From: Usbac Date: Fri, 2 Jan 2026 16:55:16 +0100 Subject: [PATCH 214/334] Minor style fix --- public/assets/css/admin/main.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/assets/css/admin/main.css b/public/assets/css/admin/main.css index 89ef43b..f4a86d7 100755 --- a/public/assets/css/admin/main.css +++ b/public/assets/css/admin/main.css @@ -971,6 +971,10 @@ body[data-nav-open] { outline: none; } +.current-user > * { + display: flex; +} + .current-user > *:last-child { margin-left: auto; } From 905dd660cea8e21314d55b70038fb56597d8eddd Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Apr 2026 01:02:25 +0200 Subject: [PATCH 215/334] Fix url in password restore template --- app/views/emails/password_restore.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/emails/password_restore.html b/app/views/emails/password_restore.html index 6444ad6..e200985 100644 --- a/app/views/emails/password_restore.html +++ b/app/views/emails/password_restore.html @@ -8,7 +8,7 @@ From a6a791d4e4e1a08b48a200ac65527d833ccaa58f Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Apr 2026 13:09:15 +0200 Subject: [PATCH 216/334] Add forbidden_action translation --- app/react/src/lang/en.js | 1 + app/react/src/lang/es.js | 1 + 2 files changed, 2 insertions(+) diff --git a/app/react/src/lang/en.js b/app/react/src/lang/en.js index 48cc6f2..3e27b9e 100644 --- a/app/react/src/lang/en.js +++ b/app/react/src/lang/en.js @@ -14,6 +14,7 @@ export default { 'name': 'Name', 'cancel': 'Cancel', 'save': 'Save', + 'forbidden_action': 'You do not have permission to perform this action.', 'item_renamed_successfully': 'Item has been renamed successfully', 'error_renaming_item': 'Error renaming item. The name is invalid, the file does not comply with the server rules or the path is not writable.', 'duplicate': 'Duplicate', diff --git a/app/react/src/lang/es.js b/app/react/src/lang/es.js index 90eb6a2..7f96cc8 100644 --- a/app/react/src/lang/es.js +++ b/app/react/src/lang/es.js @@ -14,6 +14,7 @@ export default { 'name': 'Nombre', 'cancel': 'Cancelar', 'save': 'Guardar', + 'forbidden_action': 'No tienes permisos para ejecutar esa acción.', 'item_renamed_successfully': 'El elemento ha sido renombrado exitosamente', 'error_renaming_item': 'Error al renombrar el elemento. El nombre no es válido, el archivo no cumple con las reglas del servidor o la ruta no es escribible.', 'duplicate': 'Duplicar', From 172ab99772c1ddeda1d5c001b427175c9513afdc Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Apr 2026 13:18:04 +0200 Subject: [PATCH 217/334] Add error_generic translation --- app/react/src/lang/en.js | 1 + app/react/src/lang/es.js | 1 + 2 files changed, 2 insertions(+) diff --git a/app/react/src/lang/en.js b/app/react/src/lang/en.js index 3e27b9e..7eacba3 100644 --- a/app/react/src/lang/en.js +++ b/app/react/src/lang/en.js @@ -15,6 +15,7 @@ export default { 'cancel': 'Cancel', 'save': 'Save', 'forbidden_action': 'You do not have permission to perform this action.', + 'error_generic': 'Something went wrong. Please try again.', 'item_renamed_successfully': 'Item has been renamed successfully', 'error_renaming_item': 'Error renaming item. The name is invalid, the file does not comply with the server rules or the path is not writable.', 'duplicate': 'Duplicate', diff --git a/app/react/src/lang/es.js b/app/react/src/lang/es.js index 7f96cc8..9c91c6f 100644 --- a/app/react/src/lang/es.js +++ b/app/react/src/lang/es.js @@ -15,6 +15,7 @@ export default { 'cancel': 'Cancelar', 'save': 'Guardar', 'forbidden_action': 'No tienes permisos para ejecutar esa acción.', + 'error_generic': 'Algo salió mal. Inténtalo de nuevo.', 'item_renamed_successfully': 'El elemento ha sido renombrado exitosamente', 'error_renaming_item': 'Error al renombrar el elemento. El nombre no es válido, el archivo no cumple con las reglas del servidor o la ruta no es escribible.', 'duplicate': 'Duplicar', From 7dcbcd7fbe14f6bef85261d9bfd6f883ed6dab99 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Apr 2026 13:24:16 +0200 Subject: [PATCH 218/334] Move main request function to a hook --- app/react/src/index.js | 8 ++-- app/react/src/pages/Link.js | 9 +++-- app/react/src/pages/Login.js | 7 ++-- app/react/src/pages/NewPassword.js | 5 ++- app/react/src/pages/Page.js | 9 +++-- app/react/src/pages/Post.js | 9 +++-- app/react/src/pages/Settings.js | 29 ++++++++------- app/react/src/pages/Tag.js | 9 +++-- app/react/src/pages/User.js | 11 +++--- app/react/src/pages/tables/Links.js | 7 ++-- app/react/src/pages/tables/Media.js | 25 ++++++++----- app/react/src/pages/tables/Pages.js | 7 ++-- app/react/src/pages/tables/Posts.js | 7 ++-- app/react/src/pages/tables/Tags.js | 7 ++-- app/react/src/pages/tables/Users.js | 9 +++-- app/react/src/utils/utils.js | 57 +++++++++++++++++++---------- 16 files changed, 126 insertions(+), 89 deletions(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index 3a35282..6bcfc27 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -5,7 +5,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { I18nProvider } from './providers/I18nProvider'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from './utils/icons'; import { Link as RouterLink, Navigate, Outlet, useNavigate } from 'react-router-dom'; -import { getContentUrl, LoadingPage, makeRequest, useElement } from './utils/utils'; +import { getContentUrl, LoadingPage, useApi, useElement } from './utils/utils'; import { useI18n } from './providers/I18nProvider'; import NewPassword from './pages/NewPassword'; import Login from './pages/Login'; @@ -31,6 +31,7 @@ const AdminPages = () => { const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); const navigate = useNavigate(); const { t, language, getLanguages, changeLanguage } = useI18n(); + const { request } = useApi(); const toggleTheme = () => { const is_light_enabled = dark_theme_element.toggleAttribute('disabled'); @@ -39,11 +40,10 @@ const AdminPages = () => { }; const logout = () => { - makeRequest({ + request({ method: 'POST', url: '/api/logout', - }).catch(err => alert('Error during logout: ' + err)) - .finally(() => navigate('/admin', { replace: true })); + }).finally(() => navigate('/admin', { replace: true })); }; if (user === null) { diff --git a/app/react/src/pages/Link.js b/app/react/src/pages/Link.js index b67c20c..f8360eb 100644 --- a/app/react/src/pages/Link.js +++ b/app/react/src/pages/Link.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Input, LoadingPage, makeRequest, MenuButton, Switch } from '../utils/utils'; +import { Input, LoadingPage, MenuButton, Switch, useApi } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -12,10 +12,11 @@ export default function Link() { const params = new URLSearchParams(location.search); const [ id, setId ] = useState(params.get('id')); const { t } = useI18n(); + const { request } = useApi(); useEffect(() => { if (id) { - makeRequest({ + request({ method: 'GET', url: `/api/links?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); @@ -26,7 +27,7 @@ export default function Link() { const remove = () => { if (confirm(t('confirm_delete_link'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/links', data: { id: id }, @@ -43,7 +44,7 @@ export default function Link() { const submit = e => { e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/links' + (id ? `?id=${id}` : ''), data: data, diff --git a/app/react/src/pages/Login.js b/app/react/src/pages/Login.js index 0e0e7f2..2aaf9c0 100644 --- a/app/react/src/pages/Login.js +++ b/app/react/src/pages/Login.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { makeRequest, useElement } from '../utils/utils'; +import { useApi, useElement } from '../utils/utils'; import { useNavigate } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -12,11 +12,12 @@ export default function Login() { const [ reset_password, setResetPassword ] = useState(false); const navigate = useNavigate(); const { t } = useI18n(); + const { request } = useApi(); const submitLogin = async e => { setLoading(true); e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/auth', data: { @@ -35,7 +36,7 @@ export default function Login() { const resetPassword = async e => { setLoading(true); e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/password-reset/request', data: { email: email }, diff --git a/app/react/src/pages/NewPassword.js b/app/react/src/pages/NewPassword.js index c3c99b8..86d7c90 100644 --- a/app/react/src/pages/NewPassword.js +++ b/app/react/src/pages/NewPassword.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { makeRequest } from '../utils/utils'; +import { useApi } from '../utils/utils'; import { useNavigate } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -10,11 +10,12 @@ export default function NewPassword() { const [ password_confirm, setPasswordConfirm ] = useState(''); const navigate = useNavigate(); const { t } = useI18n(); + const { request } = useApi(); const submit = async e => { setLoading(true); e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/password-reset/confirm', data: { diff --git a/app/react/src/pages/Page.js b/app/react/src/pages/Page.js index 5eb90a4..0355f9a 100644 --- a/app/react/src/pages/Page.js +++ b/app/react/src/pages/Page.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Editor, getSlug, getUrl, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest } from '../utils/utils'; +import { Editor, getSlug, getUrl, Input, LoadingPage, MenuButton, Switch, Textarea, useApi, useRequest } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -17,12 +17,13 @@ export default function Page() { const [ id, setId ] = useState(params.get('id')); const view_files = view_files_req?.data ?? []; const { t } = useI18n(); + const { request } = useApi(); useEffect(() => { fetch_view_files(); if (id) { - makeRequest({ + request({ method: 'GET', url: `/api/pages?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); @@ -33,7 +34,7 @@ export default function Page() { const remove = () => { if (confirm(t('confirm_delete_page'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/pages', data: { id: id }, @@ -50,7 +51,7 @@ export default function Page() { const submit = e => { e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/pages' + (id ? `?id=${id}` : ''), data: data, diff --git a/app/react/src/pages/Post.js b/app/react/src/pages/Post.js index 3c266eb..e7ff617 100644 --- a/app/react/src/pages/Post.js +++ b/app/react/src/pages/Post.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { DateTimeInput, Editor, getContentUrl, getSlug, getUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest } from '../utils/utils'; +import { DateTimeInput, Editor, getContentUrl, getSlug, getUrl, ImageDialog, Input, LoadingPage, MenuButton, Switch, Textarea, useApi, useRequest } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -31,13 +31,14 @@ export default function Post() { const users = users_req?.data?.data ?? {}; const tags = tags_req?.data?.data ?? []; const { t } = useI18n(); + const { request } = useApi(); useEffect(() => { fetch_users(); fetch_tags(); if (id) { - makeRequest({ + request({ method: 'GET', url: `/api/posts?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); @@ -48,7 +49,7 @@ export default function Post() { const remove = () => { if (confirm(t('confirm_delete_post'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/posts', data: { id: id }, @@ -65,7 +66,7 @@ export default function Post() { const submit = e => { e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/posts' + (id ? `?id=${id}` : ''), data: { diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 5755f31..434c267 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { downloadFile, formatSize, getContentUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea } from '../utils/utils'; +import { downloadFile, formatSize, getContentUrl, ImageDialog, Input, LoadingPage, useApi, MenuButton, Switch, Textarea } from '../utils/utils'; import { IconCode, IconDatabase, IconNote, IconServer, IconSettings, IconSync, IconTerminal } from '../utils/icons'; import { useLocation, useOutletContext } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -100,9 +100,10 @@ const Data = ({ data, setData, user }) => { const file_ref = useRef(null); const [ database_file, setDatabaseFile ] = useState(null); const { t } = useI18n(); + const { request } = useApi(); const downloadDatabase = () => { - makeRequest({ + request({ method: 'GET', url: '/api/db', options: { responseType: 'blob' }, @@ -113,7 +114,7 @@ const Data = ({ data, setData, user }) => { if (confirm(t('confirm_update_database'))) { let form_data = new FormData(); form_data.append('file', database_file); - makeRequest({ + request({ method: 'POST', url: '/api/db', data: form_data, @@ -125,7 +126,7 @@ const Data = ({ data, setData, user }) => { const resetViewsCount = () => { if (confirm(t('confirm_reset_views'))) { - makeRequest({ + request({ method: 'GET', url: '/api/reset_views_count', }).then(res => alert(t(res?.data?.success ? 'views_reset_successfully' : 'error_resetting_views'))); @@ -161,13 +162,9 @@ const Data = ({ data, setData, user }) => { const Advanced = ({ data, setData, user }) => { const [ logs, setLogs ] = useState(undefined); const { t } = useI18n(); - - useEffect(() => { - loadLogs(); - }, []); - + const { request } = useApi(); const loadLogs = () => { - makeRequest({ + request({ method: 'GET', url: '/api/logs', }).then(res => { @@ -175,12 +172,16 @@ const Advanced = ({ data, setData, user }) => { }); }; + useEffect(() => { + loadLogs(); + }, []); + const downloadLogs = () => { downloadFile(logs, `Aurora ${new Date().toISOString().slice(0, 19).replace('T', ' ')}.log`); }; const deleteLogs = () => { - makeRequest({ + request({ method: 'DELETE', url: '/api/logs', }).then(res => { @@ -221,9 +222,10 @@ const Advanced = ({ data, setData, user }) => { const Info = () => { const [ server, setServer ] = useState(undefined); const { t } = useI18n(); + const { request } = useApi(); useEffect(() => { - makeRequest({ + request({ method: 'GET', url: '/api/server', }).then(res => setServer(res?.data)); @@ -304,6 +306,7 @@ export default function Settings() { const [ data, setData ] = useState(undefined); const [ loading, setLoading ] = useState(false); const { t } = useI18n(); + const { request } = useApi(); const SECTIONS = [ { id: 'general', name: t('general'), icon: IconSettings, section: General }, { id: 'meta', name: t('meta'), icon: IconNote, section: Meta }, @@ -335,7 +338,7 @@ export default function Settings() { setLoading(true); let new_data = { ...data }; delete new_data.meta; - makeRequest({ + request({ method: 'POST', url: '/api/settings', data: new_data, diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js index 1b7fe92..ca2168b 100644 --- a/app/react/src/pages/Tag.js +++ b/app/react/src/pages/Tag.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { getSlug, Input, LoadingPage, makeRequest, MenuButton, Textarea } from '../utils/utils'; +import { getSlug, Input, LoadingPage, MenuButton, Textarea, useApi } from '../utils/utils'; import { IconEye, IconTrash } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -12,10 +12,11 @@ export default function Tag() { const params = new URLSearchParams(location.search); const [ id, setId ] = useState(params.get('id')); const { t } = useI18n(); + const { request } = useApi(); useEffect(() => { if (id) { - makeRequest({ + request({ method: 'GET', url: `/api/tags?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); @@ -26,7 +27,7 @@ export default function Tag() { const remove = () => { if (confirm(t('confirm_delete_tag'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/tags', data: { id: id }, @@ -43,7 +44,7 @@ export default function Tag() { const submit = e => { e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/tags' + (id ? `?id=${id}` : ''), data: data, diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index 4e59ef6..1683889 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { getContentUrl, getUrl, ImageDialog, Input, LoadingPage, makeRequest, MenuButton, Switch, Textarea, useRequest, formatDate, getRoleTitle, getSlug } from '../utils/utils'; +import { getContentUrl, getUrl, ImageDialog, Input, LoadingPage, MenuButton, Switch, Textarea, useApi, useRequest, formatDate, getRoleTitle, getSlug } from '../utils/utils'; import { IconEye, IconTrash, IconUsers } from '../utils/icons'; import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; @@ -19,12 +19,13 @@ export default function User() { const roles = roles_req?.data ?? {}; const is_current_user = id && user?.id == id; const { t } = useI18n(); + const { request } = useApi(); useEffect(() => { fetch_roles(); if (id) { - makeRequest({ + request({ method: 'GET', url: `/api/users?id=${id}`, }).then(res => setData(res?.data?.data[0] ?? null)); @@ -35,7 +36,7 @@ export default function User() { const remove = () => { if (confirm(t('confirm_delete_user', data.name))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/users', data: { id: id }, @@ -52,7 +53,7 @@ export default function User() { const impersonate = () => { if (confirm(t('confirm_impersonate_user'))) { - makeRequest({ + request({ method: 'GET', url: '/api/users/impersonate?id=' + id, }).then(res => { @@ -67,7 +68,7 @@ export default function User() { const submit = e => { e.preventDefault(); - makeRequest({ + request({ method: 'POST', url: '/api/users' + (id ? `?id=${id}` : ''), data: data, diff --git a/app/react/src/pages/tables/Links.js b/app/react/src/pages/tables/Links.js index a9af14e..0db0443 100644 --- a/app/react/src/pages/tables/Links.js +++ b/app/react/src/pages/tables/Links.js @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, makeRequest } from '../../utils/utils'; +import { DropdownMenu, useApi } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; import { useI18n } from '../../providers/I18nProvider'; @@ -9,6 +9,7 @@ export default function Links() { const { user } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const { request } = useApi(); const table_ref = useRef(null); return
@@ -56,7 +57,7 @@ export default function Links() { condition: Boolean(user?.actions?.edit_links), onClick: (links) => { if (confirm(t('confirm_delete_selected_links'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/links', data: { id: links.map(l => l.id) }, @@ -105,7 +106,7 @@ export default function Links() { condition: Boolean(user?.actions?.edit_links), onClick: () => { if (confirm(t('confirm_delete_link'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/links', data: { id: link.id }, diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index 33e599c..d44b650 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Table } from '../../utils/Table'; import { useOutletContext, useSearchParams } from 'react-router-dom'; -import { downloadFile, DropdownMenu, formatDate, formatSize, getContentUrl, makeRequest } from '../../utils/utils'; +import { downloadFile, DropdownMenu, formatDate, formatSize, getContentUrl, useApi } from '../../utils/utils'; import { IconClipboard, IconDuplicate, IconFile, IconFolder, IconFolderFill, IconHome, IconMoveFile, IconPencil, IconThreeDots, IconTrash, IconX, IconZip } from '../../utils/icons'; import { createPortal } from 'react-dom'; import { useI18n } from '../../providers/I18nProvider'; @@ -24,9 +24,10 @@ const MediaPath = ({ path, setPath }) => { const DialogEditFile = ({ file, onClose, onSuccess }) => { const [ name, setName ] = useState(file.name); const { t } = useI18n(); + const { request } = useApi(); const save = () => { - makeRequest({ + request({ method: 'POST', url: '/api/media/rename', data: { @@ -67,9 +68,10 @@ const DialogEditFile = ({ file, onClose, onSuccess }) => { const DialogDuplicate = ({ file, onClose, onSuccess }) => { const [ name, setName ] = useState(file.name); const { t } = useI18n(); + const { request } = useApi(); const save = () => { - makeRequest({ + request({ method: 'POST', url: '/api/media/duplicate', data: { @@ -112,16 +114,17 @@ const DialogMove = ({ file, onClose, onSuccess }) => { const initial_destination_folder = file.path.slice(0, file.path.lastIndexOf('/')).replace(/^\/+|\/+$/g, ''); const [ destination_folder, setDestinationFolder ] = useState(initial_destination_folder.length == 0 ? '/' : initial_destination_folder); const { t } = useI18n(); + const { request } = useApi(); useEffect(() => { - makeRequest({ + request({ method: 'GET', url: '/api/media/folders', }).then(res => setFolders(res?.data)); }, []); const save = () => { - makeRequest({ + request({ method: 'POST', url: '/api/media/move', data: { @@ -164,9 +167,10 @@ const DialogMove = ({ file, onClose, onSuccess }) => { const DialogCreateFolder = ({ path, onClose, onSuccess }) => { const [ name, setName ] = useState(''); const { t } = useI18n(); + const { request } = useApi(); const save = () => { - makeRequest({ + request({ method: 'POST', url: '/api/media/create_folder', data: { name: path + '/' + name }, @@ -210,12 +214,13 @@ export default function Media() { const table_ref = useRef(null); const current_path = search_params.get('path') || ''; const { t } = useI18n(); + const { request } = useApi(); const setPath = (new_path) => setSearchParams({ ...search_params, path: new_path }); const deleteFile = (file) => { if (confirm(t('confirm_delete_file'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/media', data: [ getContentUrl(file.path) ], @@ -258,7 +263,7 @@ export default function Media() { form_data.append('file[]', files[i]); } - makeRequest({ + request({ method: 'POST', url: '/api/media/upload?path=' + encodeURIComponent(current_path), data: form_data, @@ -274,7 +279,7 @@ export default function Media() { const downloadFiles = () => { if (confirm(t('confirm_download_media'))) { - makeRequest({ + request({ method: 'GET', url: '/api/media/download?path=' + current_path, options: { responseType: 'blob' }, @@ -341,7 +346,7 @@ export default function Media() { condition: Boolean(user?.actions?.edit_media), onClick: (files) => { if (confirm(t('confirm_delete_selected_files'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/media', data: files.map(f => getContentUrl(f.path)), diff --git a/app/react/src/pages/tables/Pages.js b/app/react/src/pages/tables/Pages.js index 2fc6e0e..762c380 100644 --- a/app/react/src/pages/tables/Pages.js +++ b/app/react/src/pages/tables/Pages.js @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, formatDate, makeRequest } from '../../utils/utils'; +import { DropdownMenu, formatDate, useApi } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; import { useI18n } from '../../providers/I18nProvider'; @@ -9,6 +9,7 @@ export default function Pages() { const { user, settings } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const { request } = useApi(); const table_ref = useRef(null); return
@@ -57,7 +58,7 @@ export default function Pages() { condition: Boolean(user?.actions?.edit_pages), onClick: (pages) => { if (confirm(t('confirm_delete_selected_pages'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/pages', data: { id: pages.map(l => l.id) }, @@ -110,7 +111,7 @@ export default function Pages() { condition: Boolean(user?.actions?.edit_pages), onClick: () => { if (confirm(t('confirm_delete_page'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/pages', data: { id: page.id }, diff --git a/app/react/src/pages/tables/Posts.js b/app/react/src/pages/tables/Posts.js index 2e0217c..f5bfc98 100644 --- a/app/react/src/pages/tables/Posts.js +++ b/app/react/src/pages/tables/Posts.js @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, formatDate, getContentUrl, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; +import { DropdownMenu, formatDate, getContentUrl, LoadingPage, useApi, useRequest } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; import { useI18n } from '../../providers/I18nProvider'; @@ -17,6 +17,7 @@ export default function Posts() { }); const navigate = useNavigate(); const { t } = useI18n(); + const { request } = useApi(); const table_ref = useRef(null); const users_options = useMemo(() => { let users = users_req?.data?.data ?? {}; @@ -85,7 +86,7 @@ export default function Posts() { condition: Boolean(user?.actions?.edit_posts), onClick: (posts) => { if (confirm(t('confirm_delete_selected_posts'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/posts', data: { id: posts.map(l => l.id) }, @@ -150,7 +151,7 @@ export default function Posts() { condition: Boolean(user?.actions?.edit_posts), onClick: () => { if (confirm(t('confirm_delete_post'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/posts', data: { id: post.id }, diff --git a/app/react/src/pages/tables/Tags.js b/app/react/src/pages/tables/Tags.js index dcb7737..f8e0c67 100644 --- a/app/react/src/pages/tables/Tags.js +++ b/app/react/src/pages/tables/Tags.js @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, makeRequest } from '../../utils/utils'; +import { DropdownMenu, useApi } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash } from '../../utils/icons'; import { useI18n } from '../../providers/I18nProvider'; @@ -9,6 +9,7 @@ export default function Tags() { const { user, settings } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const { request } = useApi(); const table_ref = useRef(null); return
@@ -47,7 +48,7 @@ export default function Tags() { condition: Boolean(user?.actions?.edit_tags), onClick: (tags) => { if (confirm(t('confirm_delete_selected_tags'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/tags', data: { id: tags.map(l => l.id) }, @@ -91,7 +92,7 @@ export default function Tags() { condition: Boolean(user?.actions?.edit_tags), onClick: () => { if (confirm(t('confirm_delete_tag'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/tags', data: { id: tag.id }, diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index fda7520..0ddb204 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef } from 'react'; import { Table } from '../../utils/Table'; import { useNavigate, useOutletContext } from 'react-router-dom'; -import { DropdownMenu, formatDate, getContentUrl, getRoleTitle, LoadingPage, makeRequest, useRequest } from '../../utils/utils'; +import { DropdownMenu, formatDate, getContentUrl, getRoleTitle, LoadingPage, useApi, useRequest } from '../../utils/utils'; import { IconEye, IconThreeDots, IconTrash, IconUsers } from '../../utils/icons'; import { useI18n } from '../../providers/I18nProvider'; @@ -9,6 +9,7 @@ export default function Users() { const { user, settings, fetch_user } = useOutletContext(); const navigate = useNavigate(); const { t } = useI18n(); + const { request } = useApi(); const table_ref = useRef(null); const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ method: 'GET', @@ -82,7 +83,7 @@ export default function Users() { condition: Boolean(user?.actions?.edit_users), onClick: (users) => { if (confirm(t('confirm_delete_selected_users'))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/users', data: { id: users.map(u => u.id) }, @@ -147,7 +148,7 @@ export default function Users() { condition: item.id != user?.id && user.role > item.role, onClick: () => { if (confirm(t('confirm_impersonate_user'))) { - makeRequest({ + request({ method: 'GET', url: '/api/users/impersonate?id=' + item.id, }).then(res => { @@ -166,7 +167,7 @@ export default function Users() { condition: item.id != user?.id && Boolean(user?.actions?.edit_users), onClick: () => { if (confirm(t('confirm_delete_user', item.name))) { - makeRequest({ + request({ method: 'DELETE', url: '/api/users', data: { id: item.id }, diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 11d49a1..5e200cd 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -4,25 +4,31 @@ import { createPortal } from 'react-dom'; import { Editor as TinyMCE } from '@tinymce/tinymce-react'; import axios from 'axios'; -export const makeRequest = async ({ method = 'GET', url, data = {}, options = {} }) => { - try { - const res = await axios({ - method, - url, - headers: { - 'Content-Type': data instanceof FormData ? 'multipart/form-data' : 'application/json', - }, - data: data instanceof FormData ? data : JSON.stringify(data), - withCredentials: true, - ...options, - }); +export const useApi = () => { + const { t } = useI18n(); + + const request = useCallback(async ({ method = 'GET', url, data = {}, options = {} }) => { + try { + return await axios({ + method, + url, + headers: { + 'Content-Type': data instanceof FormData ? 'multipart/form-data' : 'application/json', + }, + data: data instanceof FormData ? data : JSON.stringify(data), + withCredentials: true, + ...options, + }); + } catch (err) { + console.error(err); + alert(t(err.response?.status === 403 ? 'forbidden_action' : 'error_generic')); + throw err; + } + }, [ t ]); + + return { request }; +} - return res; - } catch (err) { - console.error(err); - throw err; - } -}; export const useRequest = (params) => { const [ data, setData ] = useState(null); @@ -34,7 +40,17 @@ export const useRequest = (params) => { setIsError(false); try { - const res = await makeRequest(params); + const { method = 'GET', url, data = {}, options = {} } = params; + const res = await axios({ + method, + url, + headers: { + 'Content-Type': data instanceof FormData ? 'multipart/form-data' : 'application/json', + }, + data: data instanceof FormData ? data : JSON.stringify(data), + withCredentials: true, + ...options, + }); setData(res); } catch (err) { setIsError(true); @@ -247,6 +263,7 @@ export const ImageDialog = ({ onSave, onClose }) => { }); const folders = path.split('/'); const input_ref = useRef(null); + const { request } = useApi(); useEffect(() => { fetch_files(); @@ -255,7 +272,7 @@ export const ImageDialog = ({ onSave, onClose }) => { const uploadFile = async (e) => { const form_data = new FormData(); form_data.append('file', e.target.files[0]); - makeRequest({ + request({ method: 'POST', url: `/api/media?path=${path}`, data: form_data, From f0605875987c9640122e33df1f5336668178ac50 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Apr 2026 13:52:43 +0200 Subject: [PATCH 219/334] Use native fetch function instead of axios --- app/react/package.json | 1 - app/react/src/pages/Settings.js | 2 +- app/react/src/pages/tables/Media.js | 2 +- app/react/src/utils/utils.js | 90 +++++++++++++++++++++-------- 4 files changed, 68 insertions(+), 27 deletions(-) diff --git a/app/react/package.json b/app/react/package.json index 284dea5..928484d 100644 --- a/app/react/package.json +++ b/app/react/package.json @@ -12,7 +12,6 @@ "dependencies": { "@tanstack/react-query": "^5.90.2", "@tinymce/tinymce-react": "^6.3.0", - "axios": "^1.12.2", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^6.22.3", diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 434c267..9f8985d 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -106,7 +106,7 @@ const Data = ({ data, setData, user }) => { request({ method: 'GET', url: '/api/db', - options: { responseType: 'blob' }, + options: { response_type: 'blob' }, }).then(res => downloadFile(res.data, 'data.json')); }; diff --git a/app/react/src/pages/tables/Media.js b/app/react/src/pages/tables/Media.js index d44b650..8d937bc 100644 --- a/app/react/src/pages/tables/Media.js +++ b/app/react/src/pages/tables/Media.js @@ -282,7 +282,7 @@ export default function Media() { request({ method: 'GET', url: '/api/media/download?path=' + current_path, - options: { responseType: 'blob' }, + options: { response_type: 'blob' }, }).then(res => { downloadFile(res.data, current_path + ' ' + new Date().toISOString().slice(0, 19).replace('T', ' ') + '.zip') }); diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 5e200cd..1fe1e16 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -2,23 +2,76 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { IconFolderFill, IconHome, IconSpinner, IconUploadFile, IconX } from './icons'; import { createPortal } from 'react-dom'; import { Editor as TinyMCE } from '@tinymce/tinymce-react'; -import axios from 'axios'; +import { useI18n } from '../providers/I18nProvider'; + +const apiFetch = async ({ method = 'GET', url, data = {}, options = {} }) => { + const { + response_type, + headers = {}, + ...fetch_options + } = options; + const request_headers = new Headers(headers); + const http_method = (method || 'GET').toString().toUpperCase(); + let final_url = url; + let body; + + if (data instanceof FormData) { + body = data; + } else if (http_method === 'GET' || http_method === 'HEAD') { + const query_params = new URLSearchParams(); + if (data && typeof data === 'object') { + for (const [ key, value ] of Object.entries(data)) { + if (value !== undefined && value !== null) { + query_params.append(key, String(value)); + } + } + } + const query_string = query_params.toString(); + if (query_string) { + final_url += (final_url.includes('?') ? '&' : '?') + query_string; + } + } else { + if (!request_headers.has('Content-Type')) { + request_headers.set('Content-Type', 'application/json'); + } + body = JSON.stringify(data ?? {}); + } + + const http_response = await fetch(final_url, { + method: http_method, + credentials: 'include', + headers: request_headers, + body, + ...fetch_options, + }); + + if (!http_response.ok) { + const error = new Error(`HTTP ${http_response.status}`); + error.response = { status: http_response.status }; + throw error; + } + + let parsed_body; + if (response_type === 'blob') { + parsed_body = await http_response.blob(); + } else { + const response_text = await http_response.text(); + parsed_body = response_text ? JSON.parse(response_text) : null; + } + + return { + data: parsed_body, + status: http_response.status, + statusText: http_response.statusText, + }; +} export const useApi = () => { const { t } = useI18n(); - const request = useCallback(async ({ method = 'GET', url, data = {}, options = {} }) => { + const request = useCallback(async (params) => { try { - return await axios({ - method, - url, - headers: { - 'Content-Type': data instanceof FormData ? 'multipart/form-data' : 'application/json', - }, - data: data instanceof FormData ? data : JSON.stringify(data), - withCredentials: true, - ...options, - }); + return await apiFetch(params); } catch (err) { console.error(err); alert(t(err.response?.status === 403 ? 'forbidden_action' : 'error_generic')); @@ -29,7 +82,6 @@ export const useApi = () => { return { request }; } - export const useRequest = (params) => { const [ data, setData ] = useState(null); const [ is_loading, setIsLoading ] = useState(true); @@ -40,17 +92,7 @@ export const useRequest = (params) => { setIsError(false); try { - const { method = 'GET', url, data = {}, options = {} } = params; - const res = await axios({ - method, - url, - headers: { - 'Content-Type': data instanceof FormData ? 'multipart/form-data' : 'application/json', - }, - data: data instanceof FormData ? data : JSON.stringify(data), - withCredentials: true, - ...options, - }); + const res = await apiFetch(params); setData(res); } catch (err) { setIsError(true); From 1e84d724a71d4509079af2c05531934c0a989662 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Apr 2026 13:56:45 +0200 Subject: [PATCH 220/334] Add support for response_type text in request function --- app/react/src/pages/Settings.js | 1 + app/react/src/utils/utils.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 9f8985d..5f8f441 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -167,6 +167,7 @@ const Advanced = ({ data, setData, user }) => { request({ method: 'GET', url: '/api/logs', + options: { response_type: 'text' }, }).then(res => { setLogs(res?.data || ''); }); diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index 1fe1e16..e662aff 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -52,11 +52,17 @@ const apiFetch = async ({ method = 'GET', url, data = {}, options = {} }) => { } let parsed_body; - if (response_type === 'blob') { - parsed_body = await http_response.blob(); - } else { - const response_text = await http_response.text(); - parsed_body = response_text ? JSON.parse(response_text) : null; + switch (response_type) { + case 'blob': + parsed_body = await http_response.blob(); + break; + case 'text': + parsed_body = await http_response.text(); + break; + default: + const response_text = await http_response.text(); + parsed_body = response_text ? JSON.parse(response_text) : null; + break; } return { From c8479884e06bab6845ec1cfaa162c6029ebf3f74 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sat, 18 Apr 2026 14:16:31 +0200 Subject: [PATCH 221/334] Add doc comments for functions in utils --- app/react/src/utils/utils.js | 153 +++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index e662aff..55ac95b 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -4,6 +4,35 @@ import { createPortal } from 'react-dom'; import { Editor as TinyMCE } from '@tinymce/tinymce-react'; import { useI18n } from '../providers/I18nProvider'; +/** + * Performs an HTTP request using fetch with built-in handling for: + * - query parameters for GET/HEAD requests + * - JSON serialization for request bodies + * - FormData support + * - response parsing (json, text, blob) + * @param {Object} params + * @param {string} [params.method='GET'] - HTTP method (GET, POST, PUT, DELETE, etc.) + * @param {string} params.url - Request URL + * @param {Object|FormData} [params.data={}] - Data to send (query params or request body) + * @param {Object} [params.options={}] - Additional fetch options + * @param {'json'|'text'|'blob'} [params.options.response_type='json'] - Expected response type + * @param {Object} [params.options.headers={}] - Custom request headers + * @param {Object} [params.options.*] - Other fetch-compatible options (mode, signal, etc.) + * @returns {Promise<{ data: any, status: number, statusText: string }>} + * @throws {Error} Throws if the HTTP response is not OK (status >= 400). + * The error includes `error.response.status`. + * @example + * const res = await apiFetch({ + * method: 'POST', + * url: '/api/users', + * data: { name: 'John' } + * }); + * @example + * const res = await apiFetch({ + * url: '/api/users', + * data: { page: 1 } + * }); +*/ const apiFetch = async ({ method = 'GET', url, data = {}, options = {} }) => { const { response_type, @@ -72,6 +101,13 @@ const apiFetch = async ({ method = 'GET', url, data = {}, options = {} }) => { }; } +/** + * React hook that wraps {@link apiFetch} as `request` and shows translated alerts on failure (403 vs generic). + * + * Must run under `I18nProvider` so `useI18n()` resolves. + * @returns {{ request: (params: Object) => Promise<{ data: *, status: number, statusText: string }> }} + * The `request` function delegates to {@link apiFetch}; on rejection it `alert`s and rethrows. + */ export const useApi = () => { const { t } = useI18n(); @@ -88,6 +124,17 @@ export const useApi = () => { return { request }; } +/** + * Fetches data with {@link apiFetch} on demand; exposes loading and error state (no global alerts). + * @param {Object} params - The same shape as {@link apiFetch} (`method`, `url`, `data`, `options`). + * @returns {{ + * data: { data: *, status: number, statusText: string } | null, + * is_loading: boolean, + * is_error: boolean, + * fetch: () => Promise + * }} + * Call `fetch()` to run or retry the request. `data` is the last successful envelope, or `null`. + */ export const useRequest = (params) => { const [ data, setData ] = useState(null); const [ is_loading, setIsLoading ] = useState(true); @@ -115,6 +162,11 @@ export const useRequest = (params) => { }; }; +/** + * GETs a JSON URL once and returns `[ value, refetch ]` for simple read-only resources (e.g. `/api/me`). + * @param {string} url - The request URL (GET, no body). + * @returns {[*, () => Promise]} Tuple: parsed `data` from the response body, or `undefined` while loading / on error; then a function to repeat the request. + */ export const useElement = (url) => { const { data, is_loading, is_error, fetch } = useRequest({ method: 'GET', @@ -131,16 +183,29 @@ export const useElement = (url) => { ]; }; +/** + * Renders the admin nav hamburger control; toggles `document.body` attribute `data-nav-open` on click. + * @returns {React.ReactElement} + */ export const MenuButton = () => document.body.toggleAttribute('data-nav-open')}> ; +/** + * Full-page centered spinner while async data is loading. + * @returns {React.ReactElement} + */ export const LoadingPage = () =>
; +/** + * Text input with optional character counter when `charCount` is set on props. + * @param {React.InputHTMLAttributes & { charCount?: boolean }} props - Forwarded to ``. + * @returns {React.ReactElement} + */ export const Input = (props) => { const char_count = props.value?.length || 0; @@ -150,6 +215,11 @@ export const Input = (props) => { ; }; +/** + * Multiline input with optional character counter when `charCount` is set on props. + * @param {React.TextareaHTMLAttributes & { charCount?: boolean }} props - Forwarded to ` -
- - +
+ +
} @@ -232,39 +232,39 @@ const Info = () => { return null; } - return
-
-
+ return
+
+
{server.os}
-
+
{server.php_version}
-
+
{server.db_dsn}
-
+
{server.host_name}
-
+
{server.root_folder}
-
+
{server.date}
-
+
{formatSize(server.memory_limit)}
-
+
- The value is the lowest possible value between the post_max_size and the upload_max_filesize options of your PHP configuration. + The value is the lowest possible value between the post_max_size and the upload_max_filesize options of your PHP configuration. {formatSize(server.file_size_limit)}
@@ -327,15 +327,15 @@ const Update = ({ user }) => { error: t('update_check_error'), }[status]; - return
-
-
+ return
+
+
- {t('update_description')} - {t('update_description_cli')} php aurora update {t('update_description_terminal')} + {t('update_description')} + {t('update_description_cli')} php aurora update {t('update_description_terminal')} {status === 'error' - ? - : } + ? + : }
; @@ -344,22 +344,22 @@ const Update = ({ user }) => { const Code = ({ data, setData }) => { const { t } = useI18n(); - return
-
-
+ return
+
+
- Code here will be injected into the header of all pages. - + Code here will be injected into the header of all pages. +
-
+
- Code here will be injected into the footer of all pages. - + Code here will be injected into the footer of all pages. +
-
+
- Code here will be injected at the bottom of all post pages. Useful for things like adding a comment system. - + Code here will be injected at the bottom of all post pages. Useful for things like adding a comment system. +
; @@ -421,23 +421,23 @@ export default function Settings() { return ; } - return ( + return (
-
+

{t('settings')}

-
+
-
-
+
+
-
+
{SECTIONS.map(section => {section.name})}
-

{t('version')}: {version}

+

{t('version')}: {version}

{settings && SECTIONS.map(section => (hash == ('#' + section.id) && ))} diff --git a/app/react/src/pages/Tag.js b/app/react/src/pages/Tag.js index ca2168b..22c2a80 100644 --- a/app/react/src/pages/Tag.js +++ b/app/react/src/pages/Tag.js @@ -67,13 +67,13 @@ export default function Tag() { return (
-
+

{t('tag')}

-
+
{id && <> - @@ -81,9 +81,9 @@ export default function Tag() {
-
-
-
+
+
+
-
+
-
+
- {props.charCount && {char_count} character{char_count !== 1 ? 's' : ''}} + {props.charCount && {char_count} character{char_count !== 1 ? 's' : ''}} }; @@ -274,9 +274,9 @@ export const DateTimeInput = ({ value, onChange, ...props }) => { export const Switch = (props) => { const ref = useRef(null); - return
+ return
- +
; }; @@ -346,7 +346,7 @@ export const Dropdown = ({ trigger, children, className, panelClassName, align = return
{ e.stopPropagation(); if (!panel_ref?.current?.contains(e.target)) { @@ -357,7 +357,7 @@ export const Dropdown = ({ trigger, children, className, panelClassName, align = {trigger}
( {options.filter(opt => opt.condition === undefined || opt.condition).map((opt, i) => ( -
{opt.content}
+
{opt.content}
))}
); @@ -498,15 +498,15 @@ export const ImageDialog = ({ onSave, onClose }) => { } return <> -
-
-
Information
-
Last modification
+
+
+
Information
+
Last modification
{files.map(file => { const file_path = getContentUrl(file.path); return
{ if (file.is_file) { onSave(file.path); @@ -516,48 +516,48 @@ export const ImageDialog = ({ onSave, onClose }) => { } }} > -
+
{file.is_file - ? e.stopPropagation()}> + ? e.stopPropagation()}> :
} - {file.name} + {file.name}
-
+
{file.is_file &&

{formatSize(file.size)}

}

{file.mime}

-
{formatDate(file.time, settings.timezone, settings.language)}
+
{formatDate(file.time, settings.timezone, settings.language)}
; })} - {files.length == 0 && No items} + {files.length == 0 && No items} ; }; - return createPortal(
+ return createPortal(
-
-
+
+

Image picker

onClose()}>
-
+
- + - +
-
+
{folders.map((folder, i) => <> -
setPath(folders.slice(0, i + 1).join('/'))}>{i == 0 ? : folder}
+
setPath(folders.slice(0, i + 1).join('/'))}>{i == 0 ? : folder}
/ )}
From 7d34cbc7805ad2892488a6bdf2c00dbcf2ced219 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 04:20:03 +0200 Subject: [PATCH 306/334] Added useLocation hook to AdminPages component --- app/react/src/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index 4123e75..c975465 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { I18nProvider } from './providers/I18nProvider'; import { IconBook, IconHome, IconImage, IconLink, IconLogout, IconMoon, IconPencil, IconSettings, IconSun, IconTag, IconUser, IconWindow } from './utils/icons'; -import { Link as RouterLink, Navigate, Outlet, useNavigate } from 'react-router-dom'; +import { Link as RouterLink, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { getContentUrl, LoadingPage, useApi, useElement } from './utils/utils'; import { useI18n } from './providers/I18nProvider'; import NewPassword from './pages/NewPassword'; @@ -29,6 +29,7 @@ const AdminPages = () => { const [ settings, fetch_settings ] = useElement('/api/settings'); const [ theme, setTheme ] = useState(dark_theme_element?.hasAttribute('disabled') ? 'light' : 'dark'); const navigate = useNavigate(); + const location = useLocation(); const { t, language, getLanguages, changeLanguage } = useI18n(); const { request } = useApi(); From cc7423a6a92f7c49e4e83bf3762ad60372adc12b Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 04:40:05 +0200 Subject: [PATCH 307/334] Make $user parameter a required parameter --- app/bootstrap/routes.php | 4 +- app/controllers/ModuleInterface.php | 2 +- app/controllers/modules/Link.php | 5 ++- app/controllers/modules/Page.php | 5 ++- app/controllers/modules/Post.php | 5 ++- app/controllers/modules/Tag.php | 5 ++- tests/unit/controllers/modules/LinkTest.php | 6 +-- tests/unit/controllers/modules/PageTest.php | 10 ++--- tests/unit/controllers/modules/PostTest.php | 14 +++---- tests/unit/controllers/modules/TagTest.php | 8 ++-- tests/unit/controllers/modules/UserTest.php | 42 ++++++++++++++++----- 11 files changed, 67 insertions(+), 39 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 99032a8..f87b832 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -789,7 +789,7 @@ return json_encode(array_values($view_files)); }); - $router->post('json:api/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) { + $router->post('json:api/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod, $user) { switch ($_GET['mod']) { case 'pages': $mod = $page_mod; break; case 'posts': $mod = $post_mod; break; @@ -802,7 +802,7 @@ } $id = $_GET['id'] ?? ''; - $errors = $mod->checkFields($body, $id); + $errors = $mod->checkFields($body, $id, $user); if (in_array('no_permission', $errors, true)) { http_response_code(403); diff --git a/app/controllers/ModuleInterface.php b/app/controllers/ModuleInterface.php index b29fc69..cde2f35 100644 --- a/app/controllers/ModuleInterface.php +++ b/app/controllers/ModuleInterface.php @@ -8,7 +8,7 @@ public function add(array $data): string|bool; public function save(int $id, array $data): bool; - public function checkFields(array $data, $id = null): array; + public function checkFields(array $data, $id, $user): array; public function getCondition(array $search): string; } diff --git a/app/controllers/modules/Link.php b/app/controllers/modules/Link.php index ab8f50a..b9b2e5c 100755 --- a/app/controllers/modules/Link.php +++ b/app/controllers/modules/Link.php @@ -48,10 +48,11 @@ public function save(int $id, array $data): bool /** * Returns an array with all the link fields that contain an error * @param array $data the link fields - * @param [mixed] $id the link id + * @param mixed $id the link id + * @param mixed $user the user data * @return array the array with the link fields that contain an error */ - public function checkFields(array $data, $id = null): array + public function checkFields(array $data, $id, $user): array { $errors = []; diff --git a/app/controllers/modules/Page.php b/app/controllers/modules/Page.php index 5bff53b..2eaffd9 100755 --- a/app/controllers/modules/Page.php +++ b/app/controllers/modules/Page.php @@ -43,10 +43,11 @@ public function save(int $id, array $data): bool /** * Returns an array with all the page fields that contain an error * @param array $data the page fields - * @param [mixed] $id the page id + * @param mixed $id the page id + * @param mixed $user the user data * @return array the array with the page fields that contain an error */ - public function checkFields(array $data, $id = null): array + public function checkFields(array $data, $id, $user): array { $errors = []; diff --git a/app/controllers/modules/Post.php b/app/controllers/modules/Post.php index 8c56394..9c5bc88 100755 --- a/app/controllers/modules/Post.php +++ b/app/controllers/modules/Post.php @@ -74,10 +74,11 @@ public function save(int $id, array $data): bool /** * Returns an array with all the post fields that contain an error * @param array $data the post fields - * @param [mixed] $id the post id + * @param mixed $id the post id + * @param mixed $user the user data * @return array the array with the post fields that contain an error */ - public function checkFields(array $data, $id = null): array + public function checkFields(array $data, $id, $user): array { $errors = []; diff --git a/app/controllers/modules/Tag.php b/app/controllers/modules/Tag.php index 4a25547..8491ace 100755 --- a/app/controllers/modules/Tag.php +++ b/app/controllers/modules/Tag.php @@ -41,10 +41,11 @@ public function save(int $id, array $data): bool /** * Returns an array with all the tag fields that contain an error * @param array $data the tag fields - * @param [mixed] $id the tag id + * @param mixed $id the tag id + * @param mixed $user the user data * @return array the array with the tag fields that contain an error */ - public function checkFields(array $data, $id = null): array + public function checkFields(array $data, $id, $user): array { $errors = []; diff --git a/tests/unit/controllers/modules/LinkTest.php b/tests/unit/controllers/modules/LinkTest.php index 8a25f1c..715f9b6 100644 --- a/tests/unit/controllers/modules/LinkTest.php +++ b/tests/unit/controllers/modules/LinkTest.php @@ -101,15 +101,15 @@ public function testCheckFields(): void \Aurora\App\Permission::set([ 'edit_links' => 1 ], 1); $this->assertEquals([ 'title' => 'Invalid value', - ], $this->mod->checkFields([ 'title' => '' ])); + ], $this->mod->checkFields([ 'title' => '' ], '', [])); - $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Home' ])); + $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Home' ], '', [])); \Aurora\App\Permission::set([ 'edit_links' => 2 ], 1); $this->assertEquals([ 'You do not have permissions to perform this action', 'title' => 'Invalid value', - ], $this->mod->checkFields([ 'title' => '' ])); + ], $this->mod->checkFields([ 'title' => '' ], '', [])); } public function testGetCondition(): void diff --git a/tests/unit/controllers/modules/PageTest.php b/tests/unit/controllers/modules/PageTest.php index e4514b9..716ef7e 100644 --- a/tests/unit/controllers/modules/PageTest.php +++ b/tests/unit/controllers/modules/PageTest.php @@ -125,20 +125,20 @@ public function testCheckFields(): void \Aurora\App\Permission::set([ 'edit_pages' => 1 ], 1); $this->assertEquals([ 'title' => 'Invalid value', - ], $this->mod->checkFields([ 'title' => '', 'slug' => '' ], 0)); + ], $this->mod->checkFields([ 'title' => '', 'slug' => '' ], 0, [])); - $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => '' ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => '' ], 1, [])); - $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech' ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech' ], 1, [])); $this->assertEquals([ 'slug' => 'Slug already in use, try a different one', - ], $this->mod->checkFields([ 'title' => 'Health & Wellness', 'slug' => 'home' ], 2)); + ], $this->mod->checkFields([ 'title' => 'Health & Wellness', 'slug' => 'home' ], 2, [])); \Aurora\App\Permission::set([ 'edit_pages' => 2 ], 1); $this->assertEquals([ 'You do not have permissions to perform this action', - ], $this->mod->checkFields([ 'title' => 'Travel', 'slug' => 'travel' ], 0)); + ], $this->mod->checkFields([ 'title' => 'Travel', 'slug' => 'travel' ], 0, [])); } public function testGetCondition(): void diff --git a/tests/unit/controllers/modules/PostTest.php b/tests/unit/controllers/modules/PostTest.php index c842fc8..aac8ea5 100644 --- a/tests/unit/controllers/modules/PostTest.php +++ b/tests/unit/controllers/modules/PostTest.php @@ -138,28 +138,28 @@ public function testCheckFields(): void $this->assertEquals([ 'title' => 'Invalid value', 'slug' => 'Invalid value. Slug may only contain alpha-numeric characters, underscores, and dashes', - ], $this->mod->checkFields([ 'title' => '', 'slug' => '' ], 0)); + ], $this->mod->checkFields([ 'title' => '', 'slug' => '' ], 0, [])); - $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Top countries', 'slug' => 'top-countries' ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Top countries', 'slug' => 'top-countries' ], 1, [])); - $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech' ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech' ], 1, [])); $this->assertEquals([ 'slug' => 'Slug already in use, try a different one', - ], $this->mod->checkFields([ 'title' => 'Top beaches', 'slug' => 'top-beaches' ], 1)); + ], $this->mod->checkFields([ 'title' => 'Top beaches', 'slug' => 'top-beaches' ], 1, [])); \Aurora\App\Permission::set([ 'edit_posts' => 2 ], 1); $this->assertEquals([ 'You do not have permissions to perform this action', - ], $this->mod->checkFields([ 'title' => 'Travel', 'slug' => 'travel' ], 0)); + ], $this->mod->checkFields([ 'title' => 'Travel', 'slug' => 'travel' ], 0, [])); \Aurora\App\Permission::set([ 'edit_posts' => 1, 'publish_posts' => 2 ], 1); - $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech', 'status' => 0 ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech', 'status' => 0 ], 1, [])); $this->assertEquals([ 'You are not allowed to handle published posts', - ], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech', 'status' => 1 ], 1)); + ], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech', 'status' => 1 ], 1, [])); } public function testGetCondition(): void diff --git a/tests/unit/controllers/modules/TagTest.php b/tests/unit/controllers/modules/TagTest.php index 58fe4e3..b8c2641 100644 --- a/tests/unit/controllers/modules/TagTest.php +++ b/tests/unit/controllers/modules/TagTest.php @@ -71,18 +71,18 @@ public function testCheckFields(): void $this->assertEquals([ 'name' => 'Invalid value', 'slug' => 'Invalid value. Slug may only contain alpha-numeric characters, underscores, and dashes', - ], $this->mod->checkFields([ 'name' => '', 'slug' => '' ], 0)); + ], $this->mod->checkFields([ 'name' => '', 'slug' => '' ], 0, [])); - $this->assertEquals([], $this->mod->checkFields([ 'name' => 'Tech', 'slug' => 'tech' ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'name' => 'Tech', 'slug' => 'tech' ], 1, [])); $this->assertEquals([ 'slug' => 'Slug already in use, try a different one', - ], $this->mod->checkFields([ 'name' => 'Health & Wellness', 'slug' => 'health-wellness' ], 10000)); + ], $this->mod->checkFields([ 'name' => 'Health & Wellness', 'slug' => 'health-wellness' ], 10000, [])); \Aurora\App\Permission::set([ 'edit_tags' => 2 ], 1); $this->assertEquals([ 'You do not have permissions to perform this action', - ], $this->mod->checkFields([ 'name' => 'Travel', 'slug' => 'travel' ], 0)); + ], $this->mod->checkFields([ 'name' => 'Travel', 'slug' => 'travel' ], 0, [])); } public function testGetCondition(): void diff --git a/tests/unit/controllers/modules/UserTest.php b/tests/unit/controllers/modules/UserTest.php index de87f76..8facd4c 100644 --- a/tests/unit/controllers/modules/UserTest.php +++ b/tests/unit/controllers/modules/UserTest.php @@ -116,36 +116,60 @@ public function testCheckFields(): void $user = [ 'role' => 1 ]; \Aurora\App\Permission::set([ 'edit_users' => 1 ], 1); \Aurora\App\Permission::addMethod('editUser', function ($subject) use (&$user) { - return ($subject['role'] ?? 0) <= ($user['role'] ?? 0) && \Aurora\App\Permission::can('edit_users'); + if (!\Aurora\App\Permission::can('edit_users')) { + return false; + } + + if (($subject['id'] ?? null) == ($user['id'] ?? null)) { + return true; + } + + $user_role = (int) ($user['role'] ?? 0); + $subject_role = (int) ($subject['role'] ?? 0); + + if (($user['role_slug'] ?? '') === 'owner') { + return $subject_role <= $user_role; + } + + return $subject_role < $user_role; }); $this->assertEquals([ 'slug' => 'Invalid value. Slug may only contain alpha-numeric characters, underscores, and dashes', 'password' => 'Password must be at least 8 characters long', 'email' => 'Invalid value', - ], $this->mod->checkFields([ 'name' => 'John', 'slug' => '' ])); + ], $this->mod->checkFields([ 'name' => 'John', 'slug' => '' ], '', $user)); $this->assertEquals([ 'You do not have permissions to perform this action', - ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com' ], 1)); + ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com' ], 1, $user)); - $user = [ 'role' => 2 ]; + $user = [ 'id' => 3, 'role' => 3, 'role_slug' => 'admin' ]; - $this->assertEquals([], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com' ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com' ], 1, $user)); + + $user = [ 'id' => 3, 'role' => 3, 'role_slug' => 'admin' ]; + + $this->assertContains('no_permission', $this->mod->checkFields([ + 'name' => 'John', + 'slug' => 'john', + 'email' => 'john@mail.com', + 'role' => 3, + ], 2, $user)); $this->assertEquals([ 'slug' => 'Slug already in use, try a different one', - ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'leon-kennedy', 'email' => 'john@mail.com' ], 1)); + ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'leon-kennedy', 'email' => 'john@mail.com' ], 1, $user)); $this->assertEquals([ 'password' => 'Password must be at least 8 characters long', - ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123', 'password_confirm' => '123' ], 1)); + ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123', 'password_confirm' => '123' ], 1, $user)); $this->assertEquals([ 'password' => 'Password and its confirmation must match', - ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123456789', 'password_confirm' => '123' ], 1)); + ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123456789', 'password_confirm' => '123' ], 1, $user)); - $this->assertEquals([], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123456789', 'password_confirm' => '123456789' ], 1)); + $this->assertEquals([], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123456789', 'password_confirm' => '123456789' ], 1, $user)); } public function testGetCondition(): void From 40575411d02804099d84428c772eb249b9e364d0 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 05:03:30 +0200 Subject: [PATCH 308/334] Unify can edit user checks --- app/bootstrap/index.php | 2 +- app/controllers/modules/User.php | 57 ++++++++++++++++++--- tests/unit/controllers/modules/UserTest.php | 18 ------- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/app/bootstrap/index.php b/app/bootstrap/index.php index 174feb3..ee7f37c 100644 --- a/app/bootstrap/index.php +++ b/app/bootstrap/index.php @@ -47,7 +47,7 @@ function setting(?string $key = null): mixed return ($subject['status'] ?? false) && $subject['role'] <= ($user['role'] ?? 0) && \Aurora\App\Permission::can('impersonate'); }); \Aurora\App\Permission::addMethod('editUser', function ($subject) use (&$user) { - return ($subject['role'] ?? 0) <= ($user['role'] ?? 0) && \Aurora\App\Permission::can('edit_users'); + return \Aurora\App\Modules\User::canEdit($user, $subject); }); \Aurora\App\Setting::set($settings); \Aurora\App\Media::setDirectory($kernel->config('content')); diff --git a/app/controllers/modules/User.php b/app/controllers/modules/User.php index 1499d2e..e62edd5 100755 --- a/app/controllers/modules/User.php +++ b/app/controllers/modules/User.php @@ -76,10 +76,11 @@ public function add(array $data): string|bool /** * Returns an array with all the user fields that contain an error * @param array $data the user fields - * @param [mixed] $id the user id + * @param mixed $id the user id + * @param mixed $user the user data * @return array the array with the user fields that contain an error */ - public function checkFields(array $data, $id = null): array + public function checkFields(array $data, $id, $user): array { $errors = []; @@ -116,17 +117,61 @@ public function checkFields(array $data, $id = null): array } } - $can_edit = empty($id) - ? \Aurora\App\Permission::can('edit_users') - : \Aurora\App\Permission::editUser($this->get([ 'id' => $id ])); + $subject = \Aurora\Core\Helper::isValidId($id) ? $this->get([ 'id' => $id ]) : null; - if (!$can_edit) { + if (!self::canEdit($user, $subject, (int) ($data['role'] ?? 0))) { $errors[] = 'no_permission'; } return $errors; } + /** + * Returns true if the given actor user can edit the subject user and assign the given role + * @param array $actor the user performing the action + * @param array|null $subject the user being edited, null when creating a new user + * @param int|null $new_role the role to assign, null to skip role assignment checks + * @return bool true if the actor can edit the subject user, false otherwise + */ + public static function canEdit(array $actor, ?array $subject, ?int $new_role = null): bool + { + if (!\Aurora\App\Permission::can('edit_users')) { + return false; + } + + $actor_role = (int) ($actor['role'] ?? 0); + $is_owner = ($actor['role_slug'] ?? '') === 'owner'; + + if ($subject !== null) { + if (((int) ($subject['id'] ?? 0)) === ((int) ($actor['id'] ?? 0))) { + return $new_role === null || $new_role <= $actor_role; + } + + $subject_role = (int) ($subject['role'] ?? 0); + $can_edit_subject = $is_owner + ? $subject_role <= $actor_role + : $subject_role < $actor_role; + + if (!$can_edit_subject) { + return false; + } + } + + if ($new_role !== null) { + if ($new_role > $actor_role) { + return false; + } + + $editing_self = $subject !== null && ((int) ($subject['id'] ?? 0)) === ((int) ($actor['id'] ?? 0)); + + if (!$is_owner && !$editing_self && $new_role >= $actor_role) { + return false; + } + } + + return true; + } + /** * Returns the query conditions to obtain users based on the given filters * @param array $filters the filters diff --git a/tests/unit/controllers/modules/UserTest.php b/tests/unit/controllers/modules/UserTest.php index 8facd4c..48af65f 100644 --- a/tests/unit/controllers/modules/UserTest.php +++ b/tests/unit/controllers/modules/UserTest.php @@ -115,24 +115,6 @@ public function testCheckFields(): void $user = &$GLOBALS['user']; $user = [ 'role' => 1 ]; \Aurora\App\Permission::set([ 'edit_users' => 1 ], 1); - \Aurora\App\Permission::addMethod('editUser', function ($subject) use (&$user) { - if (!\Aurora\App\Permission::can('edit_users')) { - return false; - } - - if (($subject['id'] ?? null) == ($user['id'] ?? null)) { - return true; - } - - $user_role = (int) ($user['role'] ?? 0); - $subject_role = (int) ($subject['role'] ?? 0); - - if (($user['role_slug'] ?? '') === 'owner') { - return $subject_role <= $user_role; - } - - return $subject_role < $user_role; - }); $this->assertEquals([ 'slug' => 'Invalid value. Slug may only contain alpha-numeric characters, underscores, and dashes', From b991138dc3f419826d67e6f25aaea6a3e735bd18 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 05:10:35 +0200 Subject: [PATCH 309/334] Fix impersonate condition --- app/bootstrap/index.php | 3 --- app/bootstrap/routes.php | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/bootstrap/index.php b/app/bootstrap/index.php index ee7f37c..adea48e 100644 --- a/app/bootstrap/index.php +++ b/app/bootstrap/index.php @@ -43,9 +43,6 @@ function setting(?string $key = null): mixed $user = &$GLOBALS['user']; \Aurora\App\Permission::set($db->query('SELECT permission, role_level FROM roles_permissions ORDER BY permission')->fetchAll(\PDO::FETCH_KEY_PAIR), $user['role'] ?? 0); - \Aurora\App\Permission::addMethod('impersonate', function ($subject) use (&$user) { - return ($subject['status'] ?? false) && $subject['role'] <= ($user['role'] ?? 0) && \Aurora\App\Permission::can('impersonate'); - }); \Aurora\App\Permission::addMethod('editUser', function ($subject) use (&$user) { return \Aurora\App\Modules\User::canEdit($user, $subject); }); diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index f87b832..7468102 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -426,7 +426,7 @@ 'status' => 1, ]); - if (!\Aurora\App\Permission::can('impersonate') || empty($subject) || $subject['role'] > ($user['role'] ?? 0)) { + if (!\Aurora\App\Permission::can('impersonate') || empty($subject) || (int) ($subject['role'] ?? 0) >= (int) ($user['role'] ?? 0)) { http_response_code(403); exit; } From e491f08fca66d3e3033e5fc17b7ceeb50c00450b Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 05:21:40 +0200 Subject: [PATCH 310/334] Add dockerignore file --- .dockerignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..16a9eb0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +.vscode +app/react/node_modules +app/public/content +vendor +*.log +.DS_Store From 9c2e42647d68f2f43f1dc0847a6631dcf95e21a8 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 05:22:57 +0200 Subject: [PATCH 311/334] Fix select --- app/react/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/index.js b/app/react/src/index.js index c975465..806f40a 100644 --- a/app/react/src/index.js +++ b/app/react/src/index.js @@ -78,8 +78,8 @@ const AdminPages = () => {
{theme == 'light' ? : }
- changeLanguage(e.target.value)}> + {getLanguages().map((lang) => )}
From 7ec762797f1a52fae81471c9a27eadd87cb6651f Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 05:24:46 +0200 Subject: [PATCH 312/334] Refactor roles code --- app/react/src/pages/tables/Users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/react/src/pages/tables/Users.js b/app/react/src/pages/tables/Users.js index 877ccbe..21ae911 100644 --- a/app/react/src/pages/tables/Users.js +++ b/app/react/src/pages/tables/Users.js @@ -16,11 +16,11 @@ export default function Users() { url: '/api/roles', }); const roles_options = useMemo(() => { - let roles = roles_req?.data ?? {}; + const roles = Array.isArray(roles_req?.data) ? roles_req.data : []; return [ { key: '', title: t('all') }, - ...Object.keys(roles).map(key => ({ key: roles[key].level, title: getRoleTitle(roles[key].slug) })), + ...roles.map(role => ({ key: role.level, title: getRoleTitle(role.slug) })), ]; }, [ roles_req, t ]); From 42536973599ba628edbfc5cddcb8928b3a47b509 Mon Sep 17 00:00:00 2001 From: Usbac Date: Sun, 31 May 2026 05:25:29 +0200 Subject: [PATCH 313/334] Improve role selector code --- app/react/src/pages/User.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index bb44543..f0f8b10 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -63,7 +63,7 @@ export default function User() { const navigate = useNavigate(); const params = new URLSearchParams(location.search); const [ id, setId ] = useState(params.get('id')); - const roles = roles_req?.data ?? {}; + const roles = Array.isArray(roles_req?.data) ? roles_req.data : []; const sessions = sessions_req?.data ?? []; const is_current_user = id && user?.id == id; const { t } = useI18n(); @@ -194,11 +194,14 @@ export default function User() {
- setData({ ...data, role: parseInt(e.target.value, 10) })} + > + {roles.map(role => )}
From 539c609e33c7f77a876bf004a1c605fc871c7939 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 1 Jun 2026 21:05:07 +0200 Subject: [PATCH 314/334] Refactor selectors code --- app/react/src/pages/Page.js | 4 ++-- app/react/src/pages/Post.js | 4 ++-- app/react/src/pages/Settings.js | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/react/src/pages/Page.js b/app/react/src/pages/Page.js index 84c82d0..d8ee415 100644 --- a/app/react/src/pages/Page.js +++ b/app/react/src/pages/Page.js @@ -123,9 +123,9 @@ export default function Page() {
- setData({ ...data, static_file: e.target.value })}> - {view_files.map(file => )} + {view_files.map(file => )}
diff --git a/app/react/src/pages/Post.js b/app/react/src/pages/Post.js index 0524375..19c9f76 100644 --- a/app/react/src/pages/Post.js +++ b/app/react/src/pages/Post.js @@ -144,9 +144,9 @@ export default function Post() {
- setData({ ...data, user_id: e.target.value })}> - {Object.values(users).map(user => )} + {Object.values(users).map(user => )}
diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 1febffa..5301666 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -32,8 +32,8 @@ const General = ({ data, setData }) => {
- setData({ ...data, theme: e.target.value })}> + {Object.values(data.meta.themes).map(theme => )}
@@ -45,8 +45,8 @@ const General = ({ data, setData }) => {
{t('website_language_description')} - setData({ ...data, language: e.target.value })}> + {data.meta.languages.map(lang => )}
@@ -58,8 +58,8 @@ const General = ({ data, setData }) => {
- setData({ ...data, timezone: e.target.value })}> + {data.meta.timezones.map(tz => )}
From 9c4a0e9d853952ace83f9e45b1b1ba86a6e6addb Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 1 Jun 2026 21:09:24 +0200 Subject: [PATCH 315/334] Add assets directory and update Dockerfile for frontend assets --- docker/Dockerfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8a1f726..cf5edc6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,6 +4,8 @@ WORKDIR /app COPY app/react/package*.json ./ +RUN mkdir -p /public/assets/js + RUN npm install COPY app/react . @@ -47,4 +49,9 @@ COPY composer.json composer.lock ./ RUN composer install --no-interaction --prefer-dist +COPY . . + +COPY --from=frontend /public/assets/js/admin.js app/public/assets/js/admin.js +COPY --from=frontend /public/assets/js/tinymce app/public/assets/js/tinymce + EXPOSE 80 From faab519278aafc8a38ba35ef69bc363c176fb264 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 1 Jun 2026 21:32:04 +0200 Subject: [PATCH 316/334] Fix User component indentation --- app/react/src/pages/User.js | 436 ++++++++++++++++++------------------ 1 file changed, 218 insertions(+), 218 deletions(-) diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index f0f8b10..634cee0 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -5,236 +5,236 @@ import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useI18n } from '../providers/I18nProvider'; const Session = ({ id, userAgent, ip, createdAt, updatedAt, current, fetchSessions, t }) => { - const [ revoking, setRevoking ] = useState(false); - const { request } = useApi(); - const { theme } = useOutletContext(); - const device = getDeviceInfo(userAgent); - const type = getDeviceType(userAgent); - let Icon = IconKey; + const [ revoking, setRevoking ] = useState(false); + const { request } = useApi(); + const { theme } = useOutletContext(); + const device = getDeviceInfo(userAgent); + const type = getDeviceType(userAgent); + let Icon = IconKey; - if (!userAgent) { - Icon = IconKey; - } else if (type == 'Desktop') { - Icon = IconDesktop; - } else if (type == 'Mobile' || type == 'Tablet') { - Icon = IconMobile; - } + if (!userAgent) { + Icon = IconKey; + } else if (type == 'Desktop') { + Icon = IconDesktop; + } else if (type == 'Mobile' || type == 'Tablet') { + Icon = IconMobile; + } - const revoke = () => { - setRevoking(true); - request({ - method: 'DELETE', - url: '/api/me/sessions/' + id, - }).then(res => { - alert(t(res?.data?.success ? 'session_deleted_successfully' : 'error_occurred')); - return fetchSessions(); - }).finally(() => setRevoking(false)); - }; + const revoke = () => { + setRevoking(true); + request({ + method: 'DELETE', + url: '/api/me/sessions/' + id, + }).then(res => { + alert(t(res?.data?.success ? 'session_deleted_successfully' : 'error_occurred')); + return fetchSessions(); + }).finally(() => setRevoking(false)); + }; - return (
- -
- - {device.os}{device.version ? ` (${device.version})` : ''} - {current && {t('current_session')}} - {userAgent && } panelClassName="dropdown-content" align="center" children={{userAgent}}/>} - -

{t('registered')}: {formatDate(createdAt)}

-

{t('last_active')}: {formatDate(updatedAt)}

- {ip &&

IP: {ip}

} -
- {!current && } -
); + return (
+ +
+ + {device.os}{device.version ? ` (${device.version})` : ''} + {current && {t('current_session')}} + {userAgent && } panelClassName="dropdown-content" align="center" children={{userAgent}}/>} + +

{t('registered')}: {formatDate(createdAt)}

+

{t('last_active')}: {formatDate(updatedAt)}

+ {ip &&

IP: {ip}

} +
+ {!current && } +
); }; export default function User() { - const { user, settings, fetch_user } = useOutletContext(); - const [ data, setData ] = useState(undefined); - const [ open_image_dialog, setOpenImageDialog ] = useState(false); - const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ - method: 'GET', - url: '/api/roles', - }); - const { data: sessions_req, fetch: fetch_sessions } = useRequest({ - method: 'GET', - url: '/api/me/sessions', - }); - const location = useLocation(); - const navigate = useNavigate(); - const params = new URLSearchParams(location.search); - const [ id, setId ] = useState(params.get('id')); - const roles = Array.isArray(roles_req?.data) ? roles_req.data : []; - const sessions = sessions_req?.data ?? []; - const is_current_user = id && user?.id == id; - const { t } = useI18n(); - const { request } = useApi(); + const { user, settings, fetch_user } = useOutletContext(); + const [ data, setData ] = useState(undefined); + const [ open_image_dialog, setOpenImageDialog ] = useState(false); + const { data: roles_req, is_loading: is_loading_roles, fetch: fetch_roles } = useRequest({ + method: 'GET', + url: '/api/roles', + }); + const { data: sessions_req, fetch: fetch_sessions } = useRequest({ + method: 'GET', + url: '/api/me/sessions', + }); + const location = useLocation(); + const navigate = useNavigate(); + const params = new URLSearchParams(location.search); + const [ id, setId ] = useState(params.get('id')); + const roles = Array.isArray(roles_req?.data) ? roles_req.data : []; + const sessions = sessions_req?.data ?? []; + const is_current_user = id && user?.id == id; + const { t } = useI18n(); + const { request } = useApi(); - useEffect(() => { - fetch_roles(); - fetch_sessions(); + useEffect(() => { + fetch_roles(); + fetch_sessions(); - if (id) { - request({ - method: 'GET', - url: `/api/users?id=${id}`, - }).then(res => setData(res?.data?.data[0] ?? null)); - } else { - setData({}); - } - }, []); + if (id) { + request({ + method: 'GET', + url: `/api/users?id=${id}`, + }).then(res => setData(res?.data?.data[0] ?? null)); + } else { + setData({}); + } + }, []); - const remove = () => { - if (confirm(t('confirm_delete_user', data.name))) { - request({ - method: 'DELETE', - url: '/api/users', - data: { id: id }, - }).then(res => { - if (res?.data?.success) { - alert(t('user_deleted_successfully')); - navigate('/admin/users', { replace: true }); - } else { - alert(t('error_deleting_user')); - } - }); - } - }; + const remove = () => { + if (confirm(t('confirm_delete_user', data.name))) { + request({ + method: 'DELETE', + url: '/api/users', + data: { id: id }, + }).then(res => { + if (res?.data?.success) { + alert(t('user_deleted_successfully')); + navigate('/admin/users', { replace: true }); + } else { + alert(t('error_deleting_user')); + } + }); + } + }; - const impersonate = () => { - if (confirm(t('confirm_impersonate_user'))) { - request({ - method: 'POST', - url: '/api/users/impersonate', - data: { id: id }, - }).then(res => { - if (!res?.data?.success) { - alert(t('error_impersonating_user')); - } else { - fetch_user(); - } - }); - } - }; + const impersonate = () => { + if (confirm(t('confirm_impersonate_user'))) { + request({ + method: 'POST', + url: '/api/users/impersonate', + data: { id: id }, + }).then(res => { + if (!res?.data?.success) { + alert(t('error_impersonating_user')); + } else { + fetch_user(); + } + }); + } + }; - const submit = e => { - e.preventDefault(); - request({ - method: 'POST', - url: '/api/users' + (id ? `?id=${id}` : ''), - data: data, - }).then(res => { - if (res?.data?.success) { - alert(t('user_saved_successfully')); - if (res?.data?.id) { - navigate(`/admin/users/edit?id=${res.data.id}`, { replace: true }); - setId(res.data.id); - } - } else { - const parts = (res?.data?.errors ?? []).map(c => t(c)); - alert(parts.length ? parts.join('\n') : t('error_generic')); - } - }); - }; + const submit = e => { + e.preventDefault(); + request({ + method: 'POST', + url: '/api/users' + (id ? `?id=${id}` : ''), + data: data, + }).then(res => { + if (res?.data?.success) { + alert(t('user_saved_successfully')); + if (res?.data?.id) { + navigate(`/admin/users/edit?id=${res.data.id}`, { replace: true }); + setId(res.data.id); + } + } else { + const parts = (res?.data?.errors ?? []).map(c => t(c)); + alert(parts.length ? parts.join('\n') : t('error_generic')); + } + }); + }; - if (data === undefined) { - return ; - } + if (data === undefined) { + return ; + } - if (!data) { - return <>{t('error')}; - } + if (!data) { + return <>{t('error')}; + } - return ( - {open_image_dialog && { setOpenImageDialog(false); setData({ ...data, image: path }); }} onClose={() => setOpenImageDialog(false)}/>} -
-
- -

{t('user')}

-
-
- {id && <> - {!is_current_user && } - {!is_current_user && user?.role > data.role && } - - } - -
-
-
-
-
setOpenImageDialog(true)}> - -
- {id &&
-

ID: {id}

-

{t('no_posts')}: {data.posts}

-

{t('last_active')}: {formatDate(data.last_active)}

-
} -
-
-
-
- - setData({ ...data, name: e.target.value })} charCount={true}/> -
-
- - setData({ ...data, slug: getSlug(e.target.value) })} charCount={true}/> - {getUrl(`/${settings.blog_url}/author/${data.slug}`)} -
-
- - setData({ ...data, email: e.target.value })}/> -
-
- -
- + Code here will be injected into the footer of all pages.
- + Code here will be injected at the bottom of all post pages. Useful for things like adding a comment system.
From 656fa3e1275c904b3e87e70028c263ced5e5884d Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 1 Jun 2026 21:40:24 +0200 Subject: [PATCH 319/334] Update translation function --- app/react/src/providers/I18nProvider.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/react/src/providers/I18nProvider.js b/app/react/src/providers/I18nProvider.js index 4fb42c5..a81b9eb 100644 --- a/app/react/src/providers/I18nProvider.js +++ b/app/react/src/providers/I18nProvider.js @@ -28,11 +28,9 @@ export const I18nProvider = ({ children, defaultLanguage = 'en' }) => { }, [ language ]); const t = (key, ...params) => { - const translation = translations[language]?.[key]; - - if (!translation) { - throw new Error(`Unknown translation key: "${key}" for language "${language}"`); - } + const translation = translations[language]?.[key] + ?? translations['en'][key] + ?? key; let i = 0; return translation.replace(/%s|%d|%f/g, e => params[i++] ?? e); @@ -67,6 +65,7 @@ export const I18nProvider = ({ children, defaultLanguage = 'en' }) => { * getLanguages: function (): string[] * }} * `t` looks up the key in the active locale and replaces `%s`, `%d`, and `%f` placeholders in order with the extra arguments. + * Falls back to English, then to the key itself when a translation is missing. * @throws {Error} When used outside an {@link I18nProvider}. */ export const useI18n = () => { From 1a5f0d0c7558aecbfbb4aa156217e74c1f9d4916 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 1 Jun 2026 21:40:39 +0200 Subject: [PATCH 320/334] Add fetch to useEffect in useElement --- app/react/src/utils/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/utils/utils.js b/app/react/src/utils/utils.js index bf1d648..b4b943a 100644 --- a/app/react/src/utils/utils.js +++ b/app/react/src/utils/utils.js @@ -172,7 +172,7 @@ export const useElement = (url) => { useEffect(() => { fetch(); - }, []); + }, [ fetch ]); return [ is_loading ? undefined : (data?.data && !is_error ? data.data : null), From e8c74f06d5040d91c8a9f95f197bbd1c9653cc74 Mon Sep 17 00:00:00 2001 From: Usbac Date: Mon, 1 Jun 2026 21:47:52 +0200 Subject: [PATCH 321/334] Add check for invalid ids --- app/bootstrap/routes.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 7468102..28446b9 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -834,6 +834,11 @@ exit; } + if (empty($ids)) { + http_response_code(400); + exit; + } + $success = match ($mod_str) { 'pages' => $page_mod->remove($ids), 'posts' => $post_mod->remove($ids), From df6f48e865b4399f9e13ef415a9d66c79dde9f11 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 00:20:59 +0200 Subject: [PATCH 322/334] Change reset_views_count endpoint to POST --- app/bootstrap/routes.php | 2 +- app/react/src/pages/Settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 28446b9..a662d45 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -683,7 +683,7 @@ return json_encode([ 'success' => !file_exists($path) || (is_writable($path) && unlink($path)) ]); }); - $router->get('json:api/reset_views_count', function() use ($db) { + $router->post('json:api/reset_views_count', function() use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { http_response_code(403); exit; diff --git a/app/react/src/pages/Settings.js b/app/react/src/pages/Settings.js index 30eab29..202fcc3 100644 --- a/app/react/src/pages/Settings.js +++ b/app/react/src/pages/Settings.js @@ -127,7 +127,7 @@ const Data = ({ data, setData, user }) => { const resetViewsCount = () => { if (confirm(t('confirm_reset_views'))) { request({ - method: 'GET', + method: 'POST', url: '/api/reset_views_count', }).then(res => alert(t(res?.data?.success ? 'views_reset_successfully' : 'error_resetting_views'))); } From 38bb83cd90bdc4c583d611a46375f33ce4ca94dd Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 00:23:18 +0200 Subject: [PATCH 323/334] Refactor token handling in login function. Delete tokens when resetting password. Do not return token in api/me endpoint --- app/bootstrap/routes.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index a662d45..6afb252 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -205,12 +205,12 @@ }); $login = function($user_id) use ($db, $setAuthToken) { - $data = [ 'token' => bin2hex(random_bytes(64)) ]; + $token = bin2hex(random_bytes(64)); try { - $data['success'] = (bool) $db->insert('tokens', [ + $success = (bool) $db->insert('tokens', [ 'user_id' => $user_id, - 'token' => $data['token'], + 'token' => $token, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'ip' => Helper::getUserIP(), 'created_at' => time(), @@ -223,19 +223,17 @@ ]; } - if ($data['success']) { + if ($success) { $total = (int) $db->query('SELECT COUNT(*) FROM tokens WHERE user_id = ?', $user_id)->fetchColumn(); $to_remove = (int) ($total - \Aurora\Core\Kernel::config('max_active_sessions')); if ($to_remove > 0) { $db->query('DELETE FROM tokens WHERE user_id = ? ORDER BY created_at ASC, token ASC LIMIT ?', $user_id, $to_remove); } - $setAuthToken($data['token'], time() + (60 * 60 * 24 * 30)); // 30 days - } else { - unset($data['token']); + $setAuthToken($token, time() + (60 * 60 * 24 * 30)); // 30 days } - return $data; + return [ 'success' => $success ]; }; /** @@ -318,6 +316,7 @@ } $db->delete('password_restores', $hash, 'hash'); + $db->query('DELETE FROM tokens WHERE user_id = ?', $user['id']); $db->update($user_mod->getTable(), [ 'password' => $user_mod->getPassword($password) ], $user['id']); return json_encode($login($user['id'])); } @@ -373,7 +372,7 @@ $me = $user; if ($me) { - unset($me['password']); + unset($me['password'], $me['token']); foreach (\Aurora\App\Permission::getPermissions() as $action) { $me['actions'][$action] = \Aurora\App\Permission::can($action); } From 3254ef15602b1640b89a742f41141f731e184ca6 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 01:50:14 +0200 Subject: [PATCH 324/334] Add id to useEffect in user page --- app/react/src/pages/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/src/pages/User.js b/app/react/src/pages/User.js index 634cee0..b6ffc24 100644 --- a/app/react/src/pages/User.js +++ b/app/react/src/pages/User.js @@ -81,7 +81,7 @@ export default function User() { } else { setData({}); } - }, []); + }, [ id ]); const remove = () => { if (confirm(t('confirm_delete_user', data.name))) { From b0a99c92fcc8b9fc5403e860fb7be5d237ee88e9 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 01:54:09 +0200 Subject: [PATCH 325/334] Change api/auth endpoint to POST --- app/bootstrap/routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 6afb252..c3e7e51 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -340,7 +340,7 @@ } }); - $router->any('json:api/auth', function($body) use ($user_mod, $login) { + $router->post('json:api/auth', function($body) use ($user_mod, $login) { $email = $body['email'] ?? ''; $password = $body['password'] ?? ''; $user = $user_mod->get([ From 8232901423d3f424310c3b19e108882b6a2e669f Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 01:56:34 +0200 Subject: [PATCH 326/334] Improve authorization header check --- app/bootstrap/routes.php | 4 +--- app/core/Helper.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index c3e7e51..9aa46c2 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -15,9 +15,7 @@ $router = $kernel->router; $getAuthToken = function() { - $headers = getallheaders(); - - return preg_match('/Bearer\s(\S+)/', $headers['Authorization'] ?? '', $matches) + return preg_match('/Bearer\s(\S+)/i', Helper::getAuthorizationHeader(), $matches) ? $matches[1] : ($_COOKIE['auth_token'] ?? false); }; diff --git a/app/core/Helper.php b/app/core/Helper.php index a8f9929..6843049 100755 --- a/app/core/Helper.php +++ b/app/core/Helper.php @@ -76,6 +76,37 @@ public static function getUserIP(): mixed return 'UNKNOWN'; } + /** + * Returns the Authorization header value from the current request + * @return string the Authorization header value, or an empty string if missing + */ + public static function getAuthorizationHeader(): string + { + foreach ([ 'HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION' ] as $key) { + if (!empty($_SERVER[$key])) { + return $_SERVER[$key]; + } + } + + $headers = function_exists('getallheaders') ? getallheaders() : []; + + if ($headers === false) { + $headers = []; + } + + if (function_exists('apache_request_headers')) { + $headers = array_merge($headers, apache_request_headers() ?: []); + } + + foreach ($headers as $name => $value) { + if (strcasecmp($name, 'Authorization') === 0) { + return $value; + } + } + + return ''; + } + /** * Copies the given source (file or directory) to the given destination * @param string $source the source From 559d6b21b8a83b1dd01ded0840514bea7868b33a Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 02:06:46 +0200 Subject: [PATCH 327/334] Fix tests --- tests/unit/controllers/modules/LinkTest.php | 6 +++--- tests/unit/controllers/modules/PageTest.php | 6 +++--- tests/unit/controllers/modules/PostTest.php | 22 ++++++++++----------- tests/unit/controllers/modules/TagTest.php | 8 ++++---- tests/unit/controllers/modules/UserTest.php | 14 ++++++------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/unit/controllers/modules/LinkTest.php b/tests/unit/controllers/modules/LinkTest.php index 715f9b6..f531629 100644 --- a/tests/unit/controllers/modules/LinkTest.php +++ b/tests/unit/controllers/modules/LinkTest.php @@ -100,15 +100,15 @@ public function testCheckFields(): void { \Aurora\App\Permission::set([ 'edit_links' => 1 ], 1); $this->assertEquals([ - 'title' => 'Invalid value', + 'invalid_title', ], $this->mod->checkFields([ 'title' => '' ], '', [])); $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Home' ], '', [])); \Aurora\App\Permission::set([ 'edit_links' => 2 ], 1); $this->assertEquals([ - 'You do not have permissions to perform this action', - 'title' => 'Invalid value', + 'invalid_title', + 'no_permission', ], $this->mod->checkFields([ 'title' => '' ], '', [])); } diff --git a/tests/unit/controllers/modules/PageTest.php b/tests/unit/controllers/modules/PageTest.php index 716ef7e..3aea1f6 100644 --- a/tests/unit/controllers/modules/PageTest.php +++ b/tests/unit/controllers/modules/PageTest.php @@ -124,7 +124,7 @@ public function testCheckFields(): void { \Aurora\App\Permission::set([ 'edit_pages' => 1 ], 1); $this->assertEquals([ - 'title' => 'Invalid value', + 'invalid_title', ], $this->mod->checkFields([ 'title' => '', 'slug' => '' ], 0, [])); $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => '' ], 1, [])); @@ -132,12 +132,12 @@ public function testCheckFields(): void $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech' ], 1, [])); $this->assertEquals([ - 'slug' => 'Slug already in use, try a different one', + 'repeated_slug', ], $this->mod->checkFields([ 'title' => 'Health & Wellness', 'slug' => 'home' ], 2, [])); \Aurora\App\Permission::set([ 'edit_pages' => 2 ], 1); $this->assertEquals([ - 'You do not have permissions to perform this action', + 'no_permission', ], $this->mod->checkFields([ 'title' => 'Travel', 'slug' => 'travel' ], 0, [])); } diff --git a/tests/unit/controllers/modules/PostTest.php b/tests/unit/controllers/modules/PostTest.php index aac8ea5..b36f606 100644 --- a/tests/unit/controllers/modules/PostTest.php +++ b/tests/unit/controllers/modules/PostTest.php @@ -56,7 +56,7 @@ public function testAdd(): void 'meta_title' => 'Traveling while working remotely', 'meta_description' => 'A post about how to travel while working remotely', 'canonical_url' => '/traveling-while-working-remotely', - 'published_at' => 1728677100, + 'published_at' => 10, ], [ 'id' => 2, @@ -64,14 +64,14 @@ public function testAdd(): void 'slug' => 'top-beaches', 'description' => 'The best beaches in the whole world', 'html' => '', - 'user_id' => null, + 'user_id' => 0, 'image' => null, 'image_alt' => '', 'status' => 0, 'meta_title' => 'Top 5 beaches', 'meta_description' => 'The best beaches', 'canonical_url' => '/top-beaches', - 'published_at' => 1728677100, + 'published_at' => 10, ], ], $this->db->query('SELECT * FROM posts')->fetchAll()); } @@ -109,7 +109,7 @@ public function testSave(): void 'meta_title' => 'Traveling while working remotely', 'meta_description' => 'A post about how to travel while working remotely', 'canonical_url' => '/traveling-while-working-remotely', - 'published_at' => 1728677100, + 'published_at' => 10, ], [ 'id' => 2, @@ -117,14 +117,14 @@ public function testSave(): void 'slug' => 'top-beaches', 'description' => 'The best beaches in the whole world', 'html' => '', - 'user_id' => null, + 'user_id' => 0, 'image' => null, 'image_alt' => '', 'status' => 0, 'meta_title' => 'Top 5 beaches', 'meta_description' => 'The best beaches', 'canonical_url' => '/top-beaches', - 'published_at' => 1728677100, + 'published_at' => 10, ], ], $this->db->query('SELECT * FROM posts')->fetchAll()); } @@ -136,8 +136,8 @@ public function testCheckFields(): void { \Aurora\App\Permission::set([ 'edit_posts' => 1, 'publish_posts' => 1 ], 1); $this->assertEquals([ - 'title' => 'Invalid value', - 'slug' => 'Invalid value. Slug may only contain alpha-numeric characters, underscores, and dashes', + 'invalid_title', + 'invalid_slug', ], $this->mod->checkFields([ 'title' => '', 'slug' => '' ], 0, [])); $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Top countries', 'slug' => 'top-countries' ], 1, [])); @@ -145,12 +145,12 @@ public function testCheckFields(): void $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech' ], 1, [])); $this->assertEquals([ - 'slug' => 'Slug already in use, try a different one', + 'repeated_slug', ], $this->mod->checkFields([ 'title' => 'Top beaches', 'slug' => 'top-beaches' ], 1, [])); \Aurora\App\Permission::set([ 'edit_posts' => 2 ], 1); $this->assertEquals([ - 'You do not have permissions to perform this action', + 'no_permission', ], $this->mod->checkFields([ 'title' => 'Travel', 'slug' => 'travel' ], 0, [])); \Aurora\App\Permission::set([ 'edit_posts' => 1, 'publish_posts' => 2 ], 1); @@ -158,7 +158,7 @@ public function testCheckFields(): void $this->assertEquals([], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech', 'status' => 0 ], 1, [])); $this->assertEquals([ - 'You are not allowed to handle published posts', + 'no_publish_permission', ], $this->mod->checkFields([ 'title' => 'Tech', 'slug' => 'tech', 'status' => 1 ], 1, [])); } diff --git a/tests/unit/controllers/modules/TagTest.php b/tests/unit/controllers/modules/TagTest.php index b8c2641..a91b7b1 100644 --- a/tests/unit/controllers/modules/TagTest.php +++ b/tests/unit/controllers/modules/TagTest.php @@ -69,19 +69,19 @@ public function testCheckFields(): void { \Aurora\App\Permission::set([ 'edit_tags' => 1 ], 1); $this->assertEquals([ - 'name' => 'Invalid value', - 'slug' => 'Invalid value. Slug may only contain alpha-numeric characters, underscores, and dashes', + 'invalid_name', + 'invalid_slug', ], $this->mod->checkFields([ 'name' => '', 'slug' => '' ], 0, [])); $this->assertEquals([], $this->mod->checkFields([ 'name' => 'Tech', 'slug' => 'tech' ], 1, [])); $this->assertEquals([ - 'slug' => 'Slug already in use, try a different one', + 'repeated_slug', ], $this->mod->checkFields([ 'name' => 'Health & Wellness', 'slug' => 'health-wellness' ], 10000, [])); \Aurora\App\Permission::set([ 'edit_tags' => 2 ], 1); $this->assertEquals([ - 'You do not have permissions to perform this action', + 'no_permission', ], $this->mod->checkFields([ 'name' => 'Travel', 'slug' => 'travel' ], 0, [])); } diff --git a/tests/unit/controllers/modules/UserTest.php b/tests/unit/controllers/modules/UserTest.php index 48af65f..f1f396b 100644 --- a/tests/unit/controllers/modules/UserTest.php +++ b/tests/unit/controllers/modules/UserTest.php @@ -117,13 +117,13 @@ public function testCheckFields(): void \Aurora\App\Permission::set([ 'edit_users' => 1 ], 1); $this->assertEquals([ - 'slug' => 'Invalid value. Slug may only contain alpha-numeric characters, underscores, and dashes', - 'password' => 'Password must be at least 8 characters long', - 'email' => 'Invalid value', + 'invalid_slug', + 'invalid_value', + 'bad_password', ], $this->mod->checkFields([ 'name' => 'John', 'slug' => '' ], '', $user)); $this->assertEquals([ - 'You do not have permissions to perform this action', + 'no_permission', ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com' ], 1, $user)); $user = [ 'id' => 3, 'role' => 3, 'role_slug' => 'admin' ]; @@ -140,15 +140,15 @@ public function testCheckFields(): void ], 2, $user)); $this->assertEquals([ - 'slug' => 'Slug already in use, try a different one', + 'repeated_slug', ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'leon-kennedy', 'email' => 'john@mail.com' ], 1, $user)); $this->assertEquals([ - 'password' => 'Password must be at least 8 characters long', + 'bad_password', ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123', 'password_confirm' => '123' ], 1, $user)); $this->assertEquals([ - 'password' => 'Password and its confirmation must match', + 'bad_password_confirm', ], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123456789', 'password_confirm' => '123' ], 1, $user)); $this->assertEquals([], $this->mod->checkFields([ 'name' => 'John', 'slug' => 'john', 'email' => 'john@mail.com', 'password' => '123456789', 'password_confirm' => '123456789' ], 1, $user)); From f3eb769e4ccff670b7d999654fbb86ea9bfee0e3 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 02:30:12 +0200 Subject: [PATCH 328/334] Add test script to composer file --- composer.json | 3 ++- docs/en/Testing.md | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5825432..cc52b37 100755 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "scripts": { "post-autoload-dump": [ "mkdir -p public/content" - ] + ], + "test": "vendor/bin/phpunit" }, "require": { "php": ">=8.0.0", diff --git a/docs/en/Testing.md b/docs/en/Testing.md index 93e2827..0b5b9d7 100644 --- a/docs/en/Testing.md +++ b/docs/en/Testing.md @@ -11,5 +11,11 @@ composer install Then, to run the automated tests, just execute the following command: ```bash -vendor/bin/phpunit tests -``` \ No newline at end of file +composer test +``` + +Or directly: + +```bash +vendor/bin/phpunit +``` From d93569d8fa1eba549c8f72069190988918ca068c Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 02:32:18 +0200 Subject: [PATCH 329/334] Remove runInSeparateProcess in tests --- tests/unit/core/KernelTest.php | 3 --- tests/unit/core/RouteTest.php | 3 --- 2 files changed, 6 deletions(-) diff --git a/tests/unit/core/KernelTest.php b/tests/unit/core/KernelTest.php index 46f070a..2a6c040 100644 --- a/tests/unit/core/KernelTest.php +++ b/tests/unit/core/KernelTest.php @@ -17,9 +17,6 @@ public function testConfig(): void $this->assertNull($kernel::config('another')); } - /** - * @runInSeparateProcess - */ public function testInit(): void { $kernel = new \Aurora\Core\Kernel([ diff --git a/tests/unit/core/RouteTest.php b/tests/unit/core/RouteTest.php index bc8c1ec..ee70755 100644 --- a/tests/unit/core/RouteTest.php +++ b/tests/unit/core/RouteTest.php @@ -26,9 +26,6 @@ public function testCode(): void ob_end_clean(); } - /** - * @runInSeparateProcess - */ public function testRouting(): void { $route = new \Aurora\Core\Route(); From caaeff8a1b5f7115eab4f14057c70b7d97b3652f Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 02:39:17 +0200 Subject: [PATCH 330/334] Update test command --- composer.json | 2 +- docs/en/Testing.md | 2 +- phpunit.xml.dist | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 phpunit.xml.dist diff --git a/composer.json b/composer.json index cc52b37..417499e 100755 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "post-autoload-dump": [ "mkdir -p public/content" ], - "test": "vendor/bin/phpunit" + "test": "vendor/bin/phpunit -c phpunit.xml.dist" }, "require": { "php": ">=8.0.0", diff --git a/docs/en/Testing.md b/docs/en/Testing.md index 0b5b9d7..1c29266 100644 --- a/docs/en/Testing.md +++ b/docs/en/Testing.md @@ -17,5 +17,5 @@ composer test Or directly: ```bash -vendor/bin/phpunit +vendor/bin/phpunit -c phpunit.xml.dist ``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..4260092 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,17 @@ + + + + + tests + + + + + + + + From 14dac8bc7513827ac7610955561a72e72b36c415 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 03:01:13 +0200 Subject: [PATCH 331/334] Move calls to http_response_code --- app/bootstrap/routes.php | 2 +- app/controllers/modules/Link.php | 1 - app/controllers/modules/Page.php | 1 - app/controllers/modules/Post.php | 2 -- app/controllers/modules/Tag.php | 1 - 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 9aa46c2..04781a8 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -801,7 +801,7 @@ $id = $_GET['id'] ?? ''; $errors = $mod->checkFields($body, $id, $user); - if (in_array('no_permission', $errors, true)) { + if (array_intersect([ 'no_permission', 'no_publish_permission' ], $errors)) { http_response_code(403); } diff --git a/app/controllers/modules/Link.php b/app/controllers/modules/Link.php index b9b2e5c..eb86262 100755 --- a/app/controllers/modules/Link.php +++ b/app/controllers/modules/Link.php @@ -61,7 +61,6 @@ public function checkFields(array $data, $id, $user): array } if (!\Aurora\App\Permission::can('edit_links')) { - http_response_code(403); $errors[] = 'no_permission'; } diff --git a/app/controllers/modules/Page.php b/app/controllers/modules/Page.php index 2eaffd9..9f99937 100755 --- a/app/controllers/modules/Page.php +++ b/app/controllers/modules/Page.php @@ -64,7 +64,6 @@ public function checkFields(array $data, $id, $user): array } if (!\Aurora\App\Permission::can('edit_pages')) { - http_response_code(403); $errors[] = 'no_permission'; } diff --git a/app/controllers/modules/Post.php b/app/controllers/modules/Post.php index 9c5bc88..b40c87b 100755 --- a/app/controllers/modules/Post.php +++ b/app/controllers/modules/Post.php @@ -95,12 +95,10 @@ public function checkFields(array $data, $id, $user): array } if (!\Aurora\App\Permission::can('edit_posts')) { - http_response_code(403); $errors[] = 'no_permission'; } if (!empty($data['status']) && !\Aurora\App\Permission::can('publish_posts')) { - http_response_code(403); $errors[] = 'no_publish_permission'; } diff --git a/app/controllers/modules/Tag.php b/app/controllers/modules/Tag.php index 8491ace..be4e688 100755 --- a/app/controllers/modules/Tag.php +++ b/app/controllers/modules/Tag.php @@ -63,7 +63,6 @@ public function checkFields(array $data, $id, $user): array } if (!\Aurora\App\Permission::can('edit_tags')) { - http_response_code(403); $errors[] = 'no_permission'; } From 0c85e10a5d865fbf300d0550bfdbee5eea71ef0d Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 03:26:04 +0200 Subject: [PATCH 332/334] Allow response code definition --- app/core/Route.php | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/core/Route.php b/app/core/Route.php index 07f0fe0..1ff6e6f 100755 --- a/app/core/Route.php +++ b/app/core/Route.php @@ -108,7 +108,7 @@ public function code(int $code, \Closure $func): void public function handleRouteCode(int $code): void { if (array_key_exists($code, $this->codes)) { - echo $this->codes[$code](); + $this->outputResponse($this->codes[$code]()); } } @@ -142,8 +142,7 @@ public function handleRoute(string $url, array $request_body = []): void if ($this->matchesRoute($current, $len, $route)) { $this->mapParameters($current, $route); header('Content-Type: ' . $val['content_type']); - http_response_code($val['status'] ?? 200); - echo $val['action']($request_body); + $this->outputResponse($val['action']($request_body), $val['status'] ?? 200); return; } } @@ -242,4 +241,25 @@ private function isGet(string $str): bool { return preg_match(self::GET_FORMAT, $str); } + + /** + * Sends the route handler response. + * A string is used as the response body. + * A two-element list [body, statusCode] sets both body and HTTP status. + * @param mixed $response the handler return value + * @param int $default_status the HTTP status when the response is not a tuple + */ + private function outputResponse(mixed $response, int $default_status = 200): void + { + $body = $response; + $status = $default_status; + + if (is_array($response)) { + $body = $response[0]; + $status = (int) $response[1]; + } + + http_response_code($status); + echo $body; + } } From bf3c66164e4f83c97defccd69bef0329b7913ccf Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 03:32:55 +0200 Subject: [PATCH 333/334] Refactor response handling to return tuples for status codes and messages --- app/bootstrap/routes.php | 102 +++++++++++++++------------------------ app/core/Route.php | 18 ++++++- 2 files changed, 54 insertions(+), 66 deletions(-) diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php index 04781a8..04cf4c4 100644 --- a/app/bootstrap/routes.php +++ b/app/bootstrap/routes.php @@ -89,8 +89,7 @@ $per_page = \Aurora\App\Setting::get('per_page'); if (!$author) { - http_response_code(404); - return; + return [ '', 404 ]; } $where = implode(' AND ', [ @@ -114,8 +113,7 @@ $tag = $tag_mod->get([ 'slug' => $_GET['tag'] ]); if (!$tag) { - http_response_code(404); - return; + return [ '', 404 ]; } $where = implode(' AND ', [ @@ -142,8 +140,7 @@ ]); if (!$post) { - http_response_code(404); - return; + return [ '', 404 ]; } if (\Aurora\App\Setting::get('views_count')) { @@ -179,8 +176,7 @@ ]); if (!$page) { - http_response_code(404); - return; + return [ '', 404 ]; } if (\Aurora\App\Setting::get('views_count')) { @@ -327,8 +323,7 @@ $router->middleware('api/*', function() use ($db, &$user) { if (empty($user) && !in_array(Helper::getCurrentPath(), [ 'api/auth', 'api/password-reset/request', 'api/password-reset/confirm', 'api/logout' ])) { - http_response_code(401); - exit; + return [ '', 401 ]; } if (!empty($user['id'])) { @@ -424,8 +419,7 @@ ]); if (!\Aurora\App\Permission::can('impersonate') || empty($subject) || (int) ($subject['role'] ?? 0) >= (int) ($user['role'] ?? 0)) { - http_response_code(403); - exit; + return [ '', 403 ]; } return json_encode($login($subject['id'])); @@ -433,8 +427,7 @@ $router->post('json:api/media/create_folder', function($body) { if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - exit; + return [ '', 403 ]; } try { @@ -455,8 +448,7 @@ } if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - exit; + return [ '', 403 ]; } try { @@ -474,8 +466,7 @@ } if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - exit; + return [ '', 403 ]; } try { @@ -489,8 +480,7 @@ $router->post('json:api/media/move', function($body) { if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - exit; + return [ '', 403 ]; } try { @@ -504,8 +494,7 @@ $router->post('json:api/media/upload', function() { if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - exit; + return [ '', 403 ]; } $success = true; @@ -536,8 +525,7 @@ $path = Helper::getPath(Kernel::config('content') . '/' . ltrim($_GET['path'] ?? '', '/')); if (!\Aurora\App\Media::isValidPath($path)) { - http_response_code(403); - exit; + return [ '', 403 ]; } $zip = new ZipArchive(); @@ -585,8 +573,7 @@ $router->post('json:api/media', function() { if (!\Aurora\App\Permission::can('edit_media')) { - http_response_code(403); - exit; + return [ '', 403 ]; } $success = true; @@ -614,8 +601,7 @@ $router->get('json:api/db', function() use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { - http_response_code(403); - exit; + return [ '', 403 ]; } return json_encode([ @@ -629,8 +615,7 @@ $router->post('json:api/db', function() use ($db, $lang) { if (!\Aurora\App\Permission::can('edit_settings')) { - http_response_code(403); - exit; + return [ '', 403 ]; } $error = false; @@ -661,8 +646,7 @@ $router->get('api/logs', function() { if (!\Aurora\App\Permission::can('edit_settings')) { - http_response_code(403); - exit; + return [ '', 403 ]; } $path = \Aurora\Core\Helper::getPath(\Aurora\App\Setting::get('log_file')); @@ -671,8 +655,7 @@ $router->delete('json:api/logs', function() { if (!\Aurora\App\Permission::can('edit_settings')) { - http_response_code(403); - exit; + return [ '', 403 ]; } $path = Helper::getPath(\Aurora\App\Setting::get('log_file')); @@ -682,8 +665,7 @@ $router->post('json:api/reset_views_count', function() use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { - http_response_code(403); - exit; + return [ '', 403 ]; } return json_encode([ 'success' => $db->delete('views') ]); @@ -691,8 +673,7 @@ $router->post('json:api/settings', function($body) use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { - http_response_code(403); - exit; + return [ '', 403 ]; } try { @@ -714,8 +695,7 @@ $router->get('json:api/server', function() use ($db) { if (!\Aurora\App\Permission::can('edit_settings')) { - http_response_code(403); - exit; + return [ '', 403 ]; } return json_encode([ @@ -731,8 +711,7 @@ $router->get('json:api/update_version', function() { if (!\Aurora\App\Permission::can('update')) { - http_response_code(403); - exit; + return [ '', 403 ]; } return json_encode((new \Aurora\App\Update())->getLatestRelease()); @@ -740,8 +719,7 @@ $router->post('json:api/update', function($body) { if (!\Aurora\App\Permission::can('update')) { - http_response_code(403); - exit; + return [ '', 403 ]; } $result = (new \Aurora\App\Update())->run($body['zip'] ?? '', fn($line) => Helper::log($line)); @@ -794,22 +772,20 @@ case 'tags': $mod = $tag_mod; break; case 'links': $mod = $link_mod; break; default: - http_response_code(404); - return; + return [ '', 404 ]; } $id = $_GET['id'] ?? ''; $errors = $mod->checkFields($body, $id, $user); - if (array_intersect([ 'no_permission', 'no_publish_permission' ], $errors)) { - http_response_code(403); - } - if (!empty($errors)) { - return json_encode([ - 'success' => false, - 'errors' => $errors, - ]); + return [ + json_encode([ + 'success' => false, + 'errors' => $errors, + ]), + array_intersect([ 'no_permission', 'no_publish_permission' ], $errors) ? 403 : 200, + ]; } return json_encode([ @@ -827,13 +803,11 @@ $mod_str = $_GET['mod'] ?? ''; if (!\Aurora\App\Permission::can("edit_$mod_str")) { - http_response_code(403); - exit; + return [ '', 403 ]; } if (empty($ids)) { - http_response_code(400); - exit; + return [ '', 400 ]; } $success = match ($mod_str) { @@ -868,12 +842,13 @@ return $success; })(), - default => (function() { - http_response_code(404); - exit; - })(), + default => null, }; + if ($success === null) { + return [ '', 404 ]; + } + return json_encode([ 'success' => $success ]); }); @@ -929,8 +904,7 @@ ], ]); default: - http_response_code(404); - return; + return [ '', 404 ]; } $page = (int) max($_GET['page'] ?? 1, 1); diff --git a/app/core/Route.php b/app/core/Route.php index 1ff6e6f..9a05200 100755 --- a/app/core/Route.php +++ b/app/core/Route.php @@ -127,7 +127,12 @@ public function handleRoute(string $url, array $request_body = []): void if ($this->matchesRoute($current, $len, $route)) { $this->mapParameters($current, $route); - $middleware['action']($request_body); + $response = $middleware['action']($request_body); + + if ($this->isResponseTuple($response)) { + $this->outputResponse($response); + return; + } } } @@ -254,7 +259,7 @@ private function outputResponse(mixed $response, int $default_status = 200): voi $body = $response; $status = $default_status; - if (is_array($response)) { + if ($this->isResponseTuple($response)) { $body = $response[0]; $status = (int) $response[1]; } @@ -262,4 +267,13 @@ private function outputResponse(mixed $response, int $default_status = 200): voi http_response_code($status); echo $body; } + + /** + * Returns true if the value is a [body, statusCode] response tuple + * @param mixed $response the handler return value + */ + private function isResponseTuple(mixed $response): bool + { + return is_array($response) && array_keys($response) === [ 0, 1 ]; + } } From 0759d372a3b78c5e0e66d264aaa0c9923e967334 Mon Sep 17 00:00:00 2001 From: Usbac Date: Thu, 4 Jun 2026 03:43:31 +0200 Subject: [PATCH 334/334] Fix output code in route class and update phpunit config --- app/core/Route.php | 9 ++++++--- phpunit.xml.dist | 13 ++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/core/Route.php b/app/core/Route.php index 9a05200..7253dd2 100755 --- a/app/core/Route.php +++ b/app/core/Route.php @@ -252,9 +252,9 @@ private function isGet(string $str): bool * A string is used as the response body. * A two-element list [body, statusCode] sets both body and HTTP status. * @param mixed $response the handler return value - * @param int $default_status the HTTP status when the response is not a tuple + * @param int|null $default_status the HTTP status when the response is not a tuple */ - private function outputResponse(mixed $response, int $default_status = 200): void + private function outputResponse(mixed $response, ?int $default_status = null): void { $body = $response; $status = $default_status; @@ -264,7 +264,10 @@ private function outputResponse(mixed $response, int $default_status = 200): voi $status = (int) $response[1]; } - http_response_code($status); + if (isset($status)) { + http_response_code($status); + } + echo $body; } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4260092..8bffde5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,9 +1,12 @@ - + tests