Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Some stuff are truely ugly in the architecture of the app. I know, I need to fix
- All errors should be catched and AT LEAST display a snackbar notification
- The appliance settings / onboarding components should be remade properly, those are ugly and prone to issues I don't like it. (Like they should get all the data they need by themself and not rely on passed props, they should be able to decide whether the user CAN submit or not the form, ...)
- Make an eventbus (e.g. instead of doing mercure sendstate + other stuff by hand just say "bus.trigger("backdrop_updated")" and each packages register their events so that it handles everything by itself)
- Maybe we should try to build a REST-er api but that would need a lot of change including probably moving to a query-builder (or ORM) such as [bun](https://bun.uptrace.dev/) having better collection filtering / pagination (with [this](https://github.com/webstradev/gin-pagination) ?), ... It would also need to check how we would handle PUT method as they should only overwrite updated elements not the whole item (This could be done using a [JSON MERGE PATCH](https://github.com/evanphx/json-patch) library similarly to Api Platform but we need to check out how this library handles null value vs unset)
- The appliance interface should be translated, maybe check out the system language to set the default then allow overriding as UserSettings ?

## Links

Expand Down
14 changes: 11 additions & 3 deletions admin/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,17 @@
"unexpected": "An unexpected error occured"
}
},
"onboarding": {
"not_onboarded": "This appliance was not setup",
"already_onboarded": "This appliance was already setup"
"create_admin": {
"title": "Welcome to PartyHall",
"p1": "The first step before using your appliance is to create an admin account.",
"display_name": "Display name",
"password_repeat": "Repeat password",
"passwords_mismatch": "Passwords do not match",
"register": "Create account",
"success": {
"title": "Account {{username}} created",
"description": "You can now login with your new account"
}
},
"login": {
"username": "Username",
Expand Down
14 changes: 11 additions & 3 deletions admin/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,17 @@
"unexpected": "Une erreur inconnue s'est produite"
}
},
"onboarding": {
"not_onboarded": "Cet appareil n'a pas été configuré",
"already_onboarded": "Cet appareil a déjà été configuré"
"create_admin": {
"title": "Bievenue sur PartyHall",
"p1": "La première étape avant d'utiliser votre appareil est de créer un compte administrateur.",
"display_name": "Nom d'affichage",
"password_repeat": "Répéter le mot de passe",
"passwords_mismatch": "Les mots de passe ne correspondent pas",
"register": "Créer le compte",
"success": {
"title": "Compte {{username}} créé",
"description": "Vous pouvez maintenant vous connecter avec votre nouveau compte"
}
},
"login": {
"username": "Pseudo",
Expand Down
20 changes: 19 additions & 1 deletion admin/src/hooks/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type SettingsProps = {
userSettings: PhUserSettings | null;

guestsAllowed: boolean;
adminCreated: boolean;

version: string;
commit: string;
Expand All @@ -37,6 +38,7 @@ type SettingsContextProps = SettingsProps & {
setPageName: (name: string, mercureTopics?: string[]) => void;
setUserSettings: (userSettings: PhUserSettings) => void;
setHardwareFlashBrightness: (b: number) => void;
setAdminCreated: (adminCreated: boolean) => void;
};

const defaultProps: SettingsProps = {
Expand All @@ -47,6 +49,7 @@ const defaultProps: SettingsProps = {
userSettings: null,

guestsAllowed: false,
adminCreated: true,

version: 'INDEV',
commit: 'XXXXXX',
Expand All @@ -58,6 +61,7 @@ const SettingsContext = createContext<SettingsContextProps>({
setPageName: () => {},
setUserSettings: () => {},
setHardwareFlashBrightness: () => {},
setAdminCreated: () => {},
});

export default function SettingsProvider({ children }: { children: ReactNode }) {
Expand All @@ -75,6 +79,7 @@ export default function SettingsProvider({ children }: { children: ReactNode })
loaded: true,
userSettings: state.userSettings,
guestsAllowed: state.guestsAllowed,
adminCreated: state.adminCreated,
version: state.version,
commit: state.commit,
}));
Expand Down Expand Up @@ -109,11 +114,24 @@ export default function SettingsProvider({ children }: { children: ReactNode })
setUserSettings(settings);
};

const setAdminCreated = (adminCreated: boolean) =>
setCtx((oldCtx) => ({
...oldCtx,
adminCreated,
}));

useAsyncEffect(fetchStatus, []);

return (
<SettingsContext.Provider
value={{ ...ctx, fetch: fetchStatus, setPageName, setUserSettings, setHardwareFlashBrightness }}
value={{
...ctx,
fetch: fetchStatus,
setPageName,
setUserSettings,
setHardwareFlashBrightness,
setAdminCreated,
}}
>
<Loader loading={!ctx.loaded}>{children}</Loader>
</SettingsContext.Provider>
Expand Down
5 changes: 5 additions & 0 deletions admin/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RouterProvider, createHashRouter } from 'react-router-dom';
import AuthProvider from './hooks/auth';
import AuthedLayout from './pages/layout/authed_layout';
import Backend from 'i18next-http-backend';
import CreateAdminPage from './pages/createAdmin';
import EditEventPage from './pages/admin/edit_event';
import EventsPage from './pages/admin/events';
import Index from './pages';
Expand Down Expand Up @@ -45,6 +46,10 @@ i18n.use(Backend)
});

const router = createHashRouter([
{
path: '/create-admin',
element: <CreateAdminPage />,
},
{
path: '/login',
element: <LoginPage />,
Expand Down
195 changes: 195 additions & 0 deletions admin/src/pages/createAdmin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { Button, Card, Flex, Form, Input, Typography, notification } from 'antd';
import { Controller, useForm, useWatch } from 'react-hook-form';
import PhLogo from '../assets/ph_logo_sd.webp';

import { useAuth } from '../hooks/auth';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSettings } from '../hooks/settings';
import { useTranslation } from 'react-i18next';

type FormType = {
username?: string;
display_name?: string;
password?: string;
password2?: string;
};

export default function CreateAdminPage() {
const { adminCreated, setAdminCreated } = useSettings();
const { t } = useTranslation();
const [notifApi, notifCtx] = notification.useNotification();
const { api, isLoggedIn } = useAuth();
const navigate = useNavigate();

const {
control,
handleSubmit,
formState: { errors, isSubmitting, isValid },
trigger,
clearErrors,
} = useForm<FormType>({ mode: 'onChange', reValidateMode: 'onChange' });

const password = useWatch({ control, name: 'password' });
const password2 = useWatch({ control, name: 'password2' });

useEffect(() => {
if (!password || !password2) {
clearErrors('password2');
return;
}

trigger('password2');
}, [password, password2, trigger, clearErrors]);

const onSubmit = async (values: FormType) => {
if (!values.username || !values.password || !values.password2) {
return;
}

try {
const resp = await api.admin.createAdmin(values.display_name || null, values.username, values.password);

if (!resp) {
throw new Error('generic.error.unexpected');
}

notifApi.success({
message: t('create_admin.success.title', { username: resp.username }),
description: t('create_admin.success.description'),
});

setAdminCreated(true);
} catch (e: any) {
console.error(e);

let description = 'generic.error.unexpected';
if (e.message?.type === 'bad-login') {
description = 'login.bad_login';
}

notifApi.error({
message: t('generic.error.title'),
description: t(description),
});
}
};

const onError = (errorInfo: any) => {
notifApi.error({
message: t('generic.error.title'),
description: JSON.stringify(errorInfo),
});
};

useEffect(() => {
// If the user is already logged in
// no need to check for admin creation
if (isLoggedIn()) {
navigate('/');

return;
}

if (adminCreated) {
navigate('/login');

return;
}
}, [api, adminCreated]);

return (
<Card>
<Flex gap={4} align="center" justify="stretch" vertical>
<img src={PhLogo} alt="PartyHall logo" style={{ display: 'block', maxHeight: '4em' }} />

<Typography.Title level={2} style={{ margin: 0 }}>
{t('create_admin.title')}
</Typography.Title>

<Typography.Paragraph>{t('create_admin.p1')}</Typography.Paragraph>

<Form
onFinish={handleSubmit(onSubmit)}
onFinishFailed={onError}
layout="horizontal"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
className="hide-asterisk"
style={{ maxWidth: 800 }}
autoComplete="off"
>
<Flex vertical>
<Form.Item
label={t('login.username')}
validateStatus={errors.username ? 'error' : ''}
help={errors.username?.message}
>
<Controller
name="username"
control={control}
rules={{ required: t('login.username_required') }}
render={({ field }) => <Input {...field} disabled={isSubmitting} />}
/>
</Form.Item>

<Form.Item label={t('create_admin.display_name')}>
<Controller
name="display_name"
control={control}
render={({ field }) => <Input {...field} disabled={isSubmitting} />}
/>
</Form.Item>

<Form.Item
label={t('login.password')}
validateStatus={errors.password ? 'error' : ''}
help={errors.password?.message}
>
<Controller
name="password"
control={control}
rules={{ required: t('login.password_required') }}
render={({ field }) => <Input.Password {...field} disabled={isSubmitting} />}
/>
</Form.Item>

<Form.Item
label={t('create_admin.password_repeat')}
validateStatus={errors.password2 ? 'error' : ''}
help={errors.password2?.message}
>
<Controller
name="password2"
control={control}
rules={{
required: t('login.password_required'),
validate: (value) => value === password || t('create_admin.passwords_mismatch'),
}}
render={({ field }) => <Input.Password {...field} disabled={isSubmitting} />}
/>
</Form.Item>
</Flex>

<Flex
gap="middle"
style={{
alignItems: 'center',
justifyContent: 'space-around',
}}
>
<Button
type="primary"
htmlType="submit"
loading={isSubmitting}
disabled={!isValid || isSubmitting}
>
{t('create_admin.register')}
</Button>
</Flex>
</Form>
</Flex>
{notifCtx}
</Card>
);
}
7 changes: 6 additions & 1 deletion admin/src/pages/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type FormType = {
};

export default function LoginPage() {
const { adminCreated } = useSettings();
const { t } = useTranslation();
const { guestsAllowed } = useSettings();
const [admin, setAdmin] = useState<boolean>(!guestsAllowed);
Expand Down Expand Up @@ -62,7 +63,11 @@ export default function LoginPage() {
if (isLoggedIn()) {
navigate('/');
}
}, [api]);

if (!adminCreated) {
navigate('/create-admin');
}
}, [api, adminCreated]);

return (
<Card>
Expand Down
24 changes: 16 additions & 8 deletions backend/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ var rootCmd = &cobra.Command{
return
}

dal.DB = services.DB

hasAnAdmin, err := dal.USERS.HasAnAdmin()
if err != nil {
log.LOG.Error(err)

return
}

_, err = mercure_client.NewClient()
if err != nil {
log.LOG.Error(err)
return
}

us := config.GET.UserSettings

// If the wireless access point is configured
Expand All @@ -88,16 +103,8 @@ var rootCmd = &cobra.Command{
return
}

_, err = mercure_client.NewClient()
if err != nil {
log.LOG.Error(err)
return
}

state.STATE.SetMode(state.MODE_PHOTOBOOTH)

dal.DB = services.DB

event, err := dal.EVENTS.GetCurrent()
if err != nil {
log.LOG.Error(err)
Expand All @@ -122,6 +129,7 @@ var rootCmd = &cobra.Command{
utils.CURRENT_COMMIT = utils.CURRENT_COMMIT[:7]
}

state.STATE.AdminCreated = hasAnAdmin
state.STATE.UserSettings = config.GET.UserSettings
state.STATE.GuestsAllowed = config.GET.GuestsAllowed
state.STATE.Version = utils.CURRENT_VERSION
Expand Down
Loading