Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ output/**/*.*

.env
.history/

# Local JWT secret used only for development
bbbeasy-backend/tmp/jwt.secret
18 changes: 2 additions & 16 deletions bbbeasy-backend/app/src/Core/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ public function exists($key): bool
public function set($key, $value): void
{
$this->runtimeValues[$key] = $value;
$this->f3->set('SESSION.' . $key, $value);
}

/**
Expand Down Expand Up @@ -278,20 +277,7 @@ private function serializeUser(User $user): array

private function syncSessionState(): void
{
if (!$this->currentUser instanceof User) {
return;
}

$user = $this->serializeUser($this->currentUser);
$user['loggedIn'] = true;

$this->f3->set('SESSION.user', $user);
$this->f3->set('SESSION.user.loggedIn', true);
$this->f3->set('SESSION.user.id', $user['id']);
$this->f3->set('SESSION.user.role', $user['role']);
$this->f3->set('SESSION.user.roleId', $this->currentUser->role->id);
$this->f3->set('SESSION.user.username', $user['username']);
$this->f3->set('SESSION.user.email', $user['email']);
// JWT-only mode keeps the authenticated user in request memory only.
}

/**
Expand Down Expand Up @@ -386,7 +372,7 @@ private function isRevoked(string $jti): bool
return true;
}

return null !== \Cache::instance()->get($this->revokedTokenCacheKey($jti));
return false !== \Cache::instance()->get($this->revokedTokenCacheKey($jti));
}

private function revokedTokenCacheKey(string $jti): string
Expand Down
4 changes: 2 additions & 2 deletions bbbeasy-backend/tests/src/Actions/Account/LoginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function testAuthenticateExistingUser($f3)
$test->expect(isset($payload['sub']) && (int) $payload['sub'] === (int) $user->id, 'JWT subject matches the authenticated user');
$test->expect(isset($payload['exp']) && \is_string($expiresAt) && strtotime($expiresAt) > $now, 'JWT expiry is set in the future');
$test->expect(isset($responseBody['session']['expiresAt']) && $responseBody['session']['expiresAt'] === $expiresAt, 'Login returns a concrete token expiration date');
$test->expect($f3->exists('SESSION.user'), 'Sessions is aware that the user us logged in');
$test->expect(null !== \Registry::get('session')->get('user'), 'JWT session keeps the authenticated user in memory');

UserFaker::logout();

Expand All @@ -179,7 +179,7 @@ public function testValidAuthentication($f3)
$user = UserFaker::create(UserRole::ADMINISTRATOR);
$data = ['email' => $user->email, 'password' => UserRole::ADMINISTRATOR . UserRole::ADMINISTRATOR];
$f3->mock(self::LOGIN_ROUTE, null, null, $this->postJsonData($data));
$test->expect($f3->exists('SESSION.user'), 'User with id "' . $user->id . '" is now logged in');
$test->expect(null !== \Registry::get('session')->get('user'), 'User with id "' . $user->id . '" is now logged in');

return $test->results();
}
Expand Down
4 changes: 3 additions & 1 deletion bbbeasy-backend/tests/src/Actions/Rooms/StartTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Fake\PresetFaker;
use Fake\RoomFaker;
use Models\User;
use Registry;
use Test\Scenario;

/**
Expand Down Expand Up @@ -66,7 +67,8 @@ public function testValidRoom($f3)
$test = $this->newTest();

$loggedUser = new User();
$loggedUser->load(['id = ?', [$f3->get('SESSION.user.id')]]);
$currentUser = Registry::get('session')->get('user');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Null session user indexing 🐞 Bug ☼ Reliability

StartTest/ViewTest now index into $currentUser['id'] without verifying the session returned an
array, which can trigger warnings/failures when no user is loaded into the session.
Session::get('user') explicitly returns null when currentUser is not set.
Agent Prompt
## Issue description
Room tests assume `Registry::get('session')->get('user')` is always a non-null array and immediately access `$currentUser['id']`. When no authenticated user is present, this becomes an unsafe array-offset access.

## Issue Context
`Core\Session::get('user')` returns `null` if there is no authenticated `currentUser`, so tests should either (a) ensure login happens in their own setup, or (b) assert/fail clearly when `currentUser` is missing.

## Fix Focus Areas
- bbbeasy-backend/tests/src/Actions/Rooms/StartTest.php[69-72]
- bbbeasy-backend/tests/src/Actions/Rooms/ViewTest.php[69-72]
- bbbeasy-backend/app/src/Core/Session.php[99-111]

## Implementation notes
- In each test, add an explicit login/setup step before reading session user.
- Or add a guard:
  - if `$currentUser === null` (or not array / missing `id`), fail the test with a clear message.
  - then proceed to load the user.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

$loggedUser->load(['id = ?', [$currentUser['id']]]);
$preset = PresetFaker::create($loggedUser);
$room = RoomFaker::create($loggedUser, $preset);
$f3->mock(self::START_ROOM_ROUTE . $room->id, null, null);
Expand Down
4 changes: 3 additions & 1 deletion bbbeasy-backend/tests/src/Actions/Rooms/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Fake\PresetFaker;
use Fake\RoomFaker;
use Models\User;
use Registry;
use Test\Scenario;

/**
Expand Down Expand Up @@ -66,7 +67,8 @@ public function testValidLink($f3)
$test = $this->newTest();

$loggedUser = new User();
$loggedUser->load(['id = ?', [$f3->get('SESSION.user.id')]]);
$currentUser = Registry::get('session')->get('user');
$loggedUser->load(['id = ?', [$currentUser['id']]]);
$preset = PresetFaker::create($loggedUser);
$room = RoomFaker::create($loggedUser, $preset, 'abcdef-123456');
$f3->mock(self::VIEW_ROOM_ROUTE . $room->short_link, null, null);
Expand Down
2 changes: 1 addition & 1 deletion bbbeasy-frontend/.yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ approvedGitRepositories:

enableScripts: true

nodeLinker: node-modules
nodeLinker: pnp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Yarn runtime path not tracked 🐞 Bug ☼ Reliability

Frontend config switches to nodeLinker: pnp while yarnPath points to
.yarn/releases/yarn-4.16.0.cjs, but .gitignore ignores .yarn/*, so fresh clones/CI can fail to
run Yarn unless that runtime is provisioned out-of-band. This PR increases the likelihood of
install/build failures by changing the linker mode without ensuring the configured Yarn runtime path
is available.
Agent Prompt
## Issue description
The repo configures Yarn via `yarnPath: .yarn/releases/yarn-4.16.0.cjs` and now switches to `nodeLinker: pnp`. However, `.gitignore` ignores `.yarn/*`, which prevents the configured Yarn runtime from being committed by default; if the runtime isn’t present, Yarn execution/install can fail on clean environments.

## Issue Context
- Yarn Berry expects the `yarnPath` target to exist when Yarn starts.
- Ignoring `.yarn/*` means the critical `releases/` file is easy to omit from the repo.

## Fix Focus Areas
- bbbeasy-frontend/.yarnrc.yml[6-10]
- bbbeasy-frontend/.gitignore[30-31]

## Implementation notes
Choose one:
1) Commit the Yarn release file and narrow the ignore rules (e.g., ignore caches but keep `.yarn/releases/**`).
2) Remove `yarnPath` and rely on Corepack-managed Yarn, documenting the required version.
3) If CI provisions Yarn runtime separately, document that requirement explicitly and ensure pipelines install/copy the runtime before running `yarn`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


npmMinimalAgeGate: 0

Expand Down
14 changes: 8 additions & 6 deletions bbbeasy-frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ interface IProps {
}

const App: React.FC<IProps> = ({ routes, isSider, logs }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`App` has a cyclomatic complexity of 6 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

const [currentUser, setCurrentUser] = React.useState<UserType | null>(null);
const [currentSession, setCurrentSession] = React.useState<SessionType | null>(null);
const [isLogged, setIsLogged] = React.useState<boolean>(false);
const [currentUser, setCurrentUser] = React.useState<UserType | null>(() => AuthService.getCurrentUser());
const [currentSession, setCurrentSession] = React.useState<SessionType | null>(() => AuthService.getCurrentSession());
const [isLogged, setIsLogged] = React.useState<boolean>(() => Boolean(AuthService.getCurrentUser() && AuthService.getCurrentSession()));
const isAuthenticated = Boolean(AuthService.getCurrentUser() && AuthService.getCurrentSession());

const [dataRooms, setDataRooms] = React.useState<RoomType[]>([]);
const [dataLabels, setDataLabels] = React.useState<LabelType[]>([]);
Expand All @@ -70,6 +71,7 @@ const App: React.FC<IProps> = ({ routes, isSider, logs }) => {
() => ({ isLogged, setIsLogged, currentUser, setCurrentUser, currentSession, setCurrentSession }),
[isLogged, currentUser, currentSession]
);
const authViewKey = isLogged ? 'authenticated' : 'anonymous';

const customTheme = {
token: {
Expand Down Expand Up @@ -125,7 +127,7 @@ const App: React.FC<IProps> = ({ routes, isSider, logs }) => {
setCurrentSession(session);
setIsLogged(true);

const allowedGroups = Object.keys(user.permissions);
const allowedGroups = Object.keys(user.permissions ?? {});
if (allowedGroups.length > 0) {
if (AuthService.isAllowedGroup(allowedGroups, 'logs')) {
Logger.info(logs);
Expand All @@ -152,9 +154,9 @@ const App: React.FC<IProps> = ({ routes, isSider, logs }) => {
direction={LocaleService.direction}
componentSize="large"
>
<UserContext.Provider value={userProvider}>
<UserContext.Provider key={authViewKey} value={userProvider}>
<DataContext.Provider value={dataProvider}>
{isLogged && isSider && <AppSider presets={dataPresets} />}
{isAuthenticated && isSider && <AppSider presets={dataPresets} />}
<Layout className="page-layout-body">
<AppHeader />
<Content className="site-content">
Expand Down
14 changes: 13 additions & 1 deletion bbbeasy-frontend/src/components/Presets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,19 @@
const [isModalVisible, setIsModalVisible] = React.useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [errorsEdit, setErrorsEdit] = React.useState({});
const [originalPreset, setOriginalPreset] = React.useState<MyPresetType | null>(null);

Check warning on line 114 in bbbeasy-frontend/src/components/Presets.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "originalPreset".

See more on https://sonarcloud.io/project/issues?id=riadvice_hivelvet&issues=AZ9_8J-IltpcPPRUa8yV&open=AZ9_8J-IltpcPPRUa8yV&pullRequest=1072

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'originalPreset' is assigned a value but never used


Unused variables are generally considered a code smell and should be avoided.

const isDefault = preset['name'] == 'default';
const deleteEnabled = deleteClickHandler != null && !isDefault;
const { token } = theme.useToken();

const clonePreset = (presetToClone: MyPresetType): MyPresetType => ({
...presetToClone,
categories: presetToClone.categories.map((category) => ({
...category,
subcategories: category.subcategories.map((subCategory) => ({ ...subCategory })),
})),
});

const props = {
beforeUpload: (file) => {
const isPNG = file.type === 'image/png' || file.type === 'image/jpeg' || file.type === 'image/jpg';
Expand Down Expand Up @@ -153,7 +162,10 @@
const showModal = (title: string, _titleTrans: string, content: SubCategoryType[]) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Preset changes still ignored 🐞 Bug ≡ Correctness

Preset edits can still incorrectly show “no_changes” because editPreset returns early when the
preset name is unchanged, ignoring category/subcategory config updates. The new originalPreset
snapshot added in showModal is never used to fix this comparison, so the user-facing bug this PR
targets remains reproducible.
Agent Prompt
## Issue description
Preset editing still treats successful parameter updates as “no changes” because the comparison logic only checks `newPreset.name == oldPreset.name`. The PR adds an `originalPreset` snapshot but it is not used anywhere, so it doesn’t improve change detection.

## Issue Context
- `showModal` stores a cloned snapshot (`originalPreset`) but the save flow still passes `preset` as the “old” preset and `editPreset` only compares names.
- Subcategory changes typically do not rename presets, so the UI will continue to display the info toast and skip updating the presets state.

## Fix Focus Areas
- bbbeasy-frontend/src/components/Presets.tsx[162-173]
- bbbeasy-frontend/src/components/Presets.tsx[659-676]

## Implementation notes
- Update `editPreset` to compare the full preset configuration (e.g., deep-equal `categories` + `subcategories`, or compare a stable serialized representation).
- Alternatively, since the backend returned an updated preset, remove the name-only early-return and always update state + show success when the save API call succeeds.
- If keeping `originalPreset`, pass it as `oldPreset` from `PresetsCol` when invoking `editClickHandler` so the comparison is against an immutable snapshot.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

setIsModalVisible(true);
setModalTitle(title);
setModalContent(content);
setOriginalPreset(clonePreset(preset));
setModalContent(content.map((item) => ({ ...item })));
setFile(null);
setFileList(null);

const indexLogo = content.findIndex((item) => item.type === 'file');
if (indexLogo > -1 && content[indexLogo].value != '') {
Expand Down
2 changes: 1 addition & 1 deletion bbbeasy-frontend/src/components/PublicRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const PublicRoute = ({ children, restricted }) => {

// restricted = true meaning restricted route else public route
if (currentUser != null && currentSession != null && restricted) {
const menuSider = MenuService.getMenuSider(currentUser.permissions);
const menuSider = MenuService.getMenuSider(currentUser.permissions ?? {});
const defaultRoute = menuSider.defaultRoute;
if (defaultRoute != '') {
return <Navigate to={defaultRoute} />;
Expand Down
34 changes: 19 additions & 15 deletions bbbeasy-frontend/src/components/layout/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

// eslint-disable-next-line complexity
const AppHeader = () => {
const { isLogged, setIsLogged, currentUser, setCurrentUser, setCurrentSession } = React.useContext(UserContext);

Check warning on line 66 in bbbeasy-frontend/src/components/layout/AppHeader.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "isLogged".

See more on https://sonarcloud.io/project/issues?id=riadvice_hivelvet&issues=AZ9_8J6jltpcPPRUa8yU&open=AZ9_8J6jltpcPPRUa8yU&pullRequest=1072
const currentLocale = LocaleService.language;
const result: LanguageType[] = Languages.filter((item) => item.value == currentLocale);
const language: string = result[0].name;
Expand All @@ -78,18 +78,21 @@
const isRoomsSearch = location.pathname.includes('rooms');
const [logo, setLogo] = React.useState<string>('');
const isLoginPage = location.pathname.includes('login');
if (isLoginPage) {
setIsLogged(false);
}
settingsService
.collect_settings()
.then((response) => {
const settings: SettingsType = response.data;
setLogo(settings.logo);
})
.catch((error) => {
console.log(error);
});
const storedUser = AuthService.getCurrentUser();
const storedSession = AuthService.getCurrentSession();
const isAuthenticated = Boolean(storedUser && storedSession);

useEffect(() => {
settingsService
.collect_settings()
.then((response) => {
const settings: SettingsType = response.data;
setLogo(settings.logo);
})
.catch((error) => {
console.log(error);
});
}, []);
const logout = () => {
AuthService.logout()
.catch((error) => {
Expand Down Expand Up @@ -187,16 +190,17 @@
</Dropdown>
);

const activeUser = currentUser ?? storedUser;
const menuProfile = {
items: [
{
key: '1',
className: 'username-item',
label: (
<>
<Trans i18nKey="signed_as" /> {currentUser?.username}
<Trans i18nKey="signed_as" /> {activeUser?.username}
<br />
<Text>{currentUser?.email}</Text>
<Text>{activeUser?.email}</Text>
</>
),
},
Expand All @@ -220,7 +224,7 @@
return (
<Header className="site-header">
<>
{!isLogged ? (
{!isAuthenticated || isLoginPage ? (
<Paragraph className="site-header-inner">
<Link to={'/'}>
<img
Expand Down
2 changes: 1 addition & 1 deletion bbbeasy-frontend/src/components/layout/AppSider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const AppSider = (props: Props) => {

useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function has a cyclomatic complexity of 6 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

const user: UserType = AuthService.getCurrentUser();
const menuSider = MenuService.getMenuSider(user.permissions);
const menuSider = MenuService.getMenuSider(user?.permissions ?? {});

setMenuItems(menuSider.items);
setNewMenuItems(menuSider.news);
Expand Down
26 changes: 9 additions & 17 deletions bbbeasy-frontend/src/lib/AxiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,22 @@
*/

import axios, { AxiosRequestHeaders } from 'axios';
import AuthService from '../services/auth.service';

export const axiosInstance = axios.create();

axiosInstance.interceptors.request.use((config) => {
try {
const sessionStr = localStorage.getItem('session');
if (sessionStr) {
const session = JSON.parse(sessionStr);
if (session?.expiresAt && Date.parse(session.expiresAt) < Date.now()) {
localStorage.removeItem('user');
localStorage.removeItem('session');
return config;
}

if (session?.accessToken && session?.tokenType) {
config.headers = (config.headers || {}) as AxiosRequestHeaders;
Object.assign(config.headers, {
Authorization: `${session.tokenType} ${session.accessToken}`,
});
}
const session = AuthService.getCurrentSession();
if (session?.accessToken) {
config.headers = (config.headers || {}) as AxiosRequestHeaders;
Object.assign(config.headers, {
Authorization: `${session.tokenType ?? 'Bearer'} ${session.accessToken}`,
});
}
} catch (error) {
console.warn('Failed to process session from localStorage:', error);
localStorage.removeItem('session');
console.warn('Failed to process auth state from localStorage:', error);
AuthService.clearAuth();
}

return config;
Expand Down
Loading