9 Commits

Author SHA1 Message Date
7e5c065001 fix: eslint issues
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2026-04-02 21:03:43 +02:00
30486b3847 fix: auto-resolvable eslint issues 2026-04-02 20:38:43 +02:00
3813b89119 feat: make the front and the back bundled in a single binary
Some checks failed
continuous-integration/drone/push Build is failing
2026-04-02 20:36:42 +02:00
16c9dadc64 fix: build issues
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 19:15:43 +02:00
5c453dc093 chore: improve dev docker compose
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 19:05:22 +02:00
e018a8b172 chore: update backend dependencies 2026-04-02 18:37:37 +02:00
fb3f95bdab chore: update frontend dependencies 2026-04-02 18:14:15 +02:00
ec2b687107 Merge pull request 'Update Rust crate sha2 to 0.11.0' (#757) from renovate/sha2-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2026-04-02 00:25:02 +00:00
232bfe2fcb Update Rust crate sha2 to 0.11.0
Some checks failed
continuous-integration/drone/push Build is failing
renovate/stability-days Updates have met minimum release age requirement
continuous-integration/drone/pr Build is failing
2026-04-01 00:25:55 +00:00
73 changed files with 2542 additions and 2646 deletions

View File

@@ -4,32 +4,69 @@ type: docker
name: default
steps:
- name: web_build
image: node:25
volumes:
- name: web_app
path: /tmp/web_build
commands:
- cd geneit_app
- npm install
- npm run lint
- npm run build
- mv dist /tmp/web_build
- name: backend_check
image: rust
volumes:
- name: rust_registry
path: /usr/local/cargo/registry
commands:
- apt update && apt install -y cmake
- rustup component add clippy
- cd geneit_backend
- cargo clippy -- -D warnings
- cargo test
- name: app_deploy
image: node:24
environment:
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY:
from_secret: AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION: us-east-1
- name: backend_compile
image: rust
volumes:
- name: rust_registry
path: /usr/local/cargo/registry
- name: web_app
path: /tmp/web_build
- name: release
path: /tmp/release
depends_on:
- backend_check
- web_build
commands:
# Build website
- cd geneit_app
- npm install
- GENERATE_SOURCEMAP=false npm run build
# Install AWS
- curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
- unzip awscliv2.zip
- ./aws/install
- aws configure set default.s3.signature_version s3v4
# Upload to bucket
- bash upload_bucket.sh
- cd geneit_backend
- mv /tmp/web_build/dist static
- cargo build --release
- cp target/release/geneit_backend /tmp/release
- name: gitea_release
image: plugins/gitea-release
depends_on:
- backend_compile
when:
event:
- tag
volumes:
- name: release
path: /tmp/release
environment:
PLUGIN_API_KEY:
from_secret: API_KEY # needs repository read & write access
settings:
base_url: https://gitea.communiquons.org
files: /tmp/release/*
checksum: sha512
volumes:
- name: rust_registry
temp: {}
- name: web_app
temp: {}
- name: release
temp: {}

15
Makefile Normal file
View File

@@ -0,0 +1,15 @@
all: app backend
app:
cd geneit_app && npm run build && cd ..
rm -rf geneit_backend/static
mv geneit_app/dist geneit_backend/static
backend: app
cd geneit_backend && cargo clippy -- -D warnings && cargo build --release
DOCKER_TEMP_DIR := $(shell mktemp -d)
backend_docker: backend
cp geneit_backend/target/release/geneit_backend $(DOCKER_TEMP_DIR)
docker build -t pierre42100/geneit_backend -f geneit_backend/Dockerfile "$(DOCKER_TEMP_DIR)"
rm -rf $(DOCKER_TEMP_DIR)

View File

@@ -10,7 +10,8 @@
2. Start services:
```bash
cd geneit_backend
docker-compose up
mkdir -p storage/{rustfs,db,redis-data,redis-conf}
docker compose up
```
@@ -22,7 +23,7 @@ cargo install diesel_cli --no-default-features --features postgres
```
4. Initialize database:
4. Initialize database manually (or it will be done automatically when the backend is started):
```bash
diesel migration run
@@ -34,19 +35,9 @@ diesel migration run
> PGPASSWORD=pass psql -h localhost -p 5432 -U user -d geneit
> ```
## Test OIDC credentials
Emails:
```
harley@qlik.example
barb@qlik.example
quinn@qlik.example
sim@qlik.example
phillie@qlik.example
peta@qlik.example
sibylla@qlik.example
evan@qlik.example
franklin@qlik.example
```
Useful links:
Password: `Password1!`
* Mailcatcher: http://localhost:1080/
* Rustfs console: http://localhost:9090 (credentials: `topsecret` / `topsecret`)
* Dex OpenID configuration: http://127.0.0.1:9001/dex/.well-known/openid-configuration

View File

@@ -1 +1 @@
VITE_APP_BACKEND=http://localhost:8000
VITE_APP_BACKEND=http://localhost:8000/api

View File

@@ -1 +1 @@
VITE_APP_BACKEND=https://geneit-backend.communiquons.org
VITE_APP_BACKEND=/api

File diff suppressed because it is too large Load Diff

View File

@@ -21,17 +21,17 @@
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.3.9",
"@mui/lab": "^7.0.0-beta.17",
"@mui/lab": "^7.0.1-beta.23",
"@mui/material": "^7.3.9",
"@mui/x-data-grid": "^8.28.1",
"@mui/x-data-grid": "^8.28.2",
"@mui/x-date-pickers": "^8.27.2",
"@mui/x-tree-view": "^8.27.2",
"date-and-time": "^3.6.0",
"date-and-time": "^4.4.0",
"dayjs": "^1.11.20",
"email-validator": "^2.0.4",
"filesize": "^11.0.15",
"jspdf": "^3.0.4",
"mui-color-input": "^7.0.0",
"jspdf": "^4.2.1",
"mui-color-input": "^8.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-easy-crop": "^5.5.7",
@@ -41,16 +41,16 @@
"svg2pdf.js": "^2.7.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@eslint/js": "^10.0.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^5.2.0",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.1.0",
"eslint-plugin-react-hooks": "0.0.0-experimental-80b1cab3-20260331",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^16.5.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.1",
"vite": "^7.3.1"
"globals": "^17.4.0",
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.3"
}
}

View File

@@ -152,8 +152,8 @@ export function App(): React.ReactElement {
<Route path="*" element={<NotFoundRoute />} />
</Route>
)}
</>
)
</>,
),
);
return (
@@ -163,6 +163,7 @@ export function App(): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useAuth(): AuthContext {
return React.useContext(AuthContextK)!;
}

View File

@@ -1,12 +1,16 @@
import { AuthApi } from "./AuthApi";
interface APIResponse {
data: any;
data: unknown;
status: number;
}
export class ApiError extends Error {
constructor(message: string, public code: number, public data: any) {
constructor(
message: string,
public code: number,
public data: unknown,
) {
super(message);
}
}
@@ -36,11 +40,11 @@ export class APIClient {
uri: string;
method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
allowFail?: boolean;
jsonData?: any;
jsonData?: unknown;
formData?: FormData;
}): Promise<APIResponse> {
let body = undefined;
let headers: any = {
const headers: { [k: string]: string } = {
"X-auth-token": AuthApi.SignedIn ? AuthApi.AuthToken : "none",
};

View File

@@ -42,7 +42,7 @@ export class AuthApi {
*/
static async CreateAccount(
name: string,
mail: string
mail: string,
): Promise<CreateAccountResult> {
const res = await APIClient.exec({
uri: "/auth/create_account",
@@ -77,7 +77,7 @@ export class AuthApi {
*/
static async LoginWithPassword(
mail: string,
password: string
password: string,
): Promise<PasswordLoginResult> {
const res = await APIClient.exec({
uri: "/auth/password_login",
@@ -96,7 +96,10 @@ export class AuthApi {
return PasswordLoginResult.InvalidCredentials;
case 200:
case 201:
localStorage.setItem(TokenStateKey, res.data.token);
localStorage.setItem(
TokenStateKey,
(res.data as { token: string }).token,
);
return PasswordLoginResult.Success;
default:
return PasswordLoginResult.Error;
@@ -115,20 +118,20 @@ export class AuthApi {
method: "POST",
jsonData: { provider: id },
})
).data;
).data as { url: string };
}
/**
* Finish OpenID login
*/
static async FinishOpenIDLogin(code: string, state: string): Promise<void> {
const res: { user_id: number; token: string } = (
const res = (
await APIClient.exec({
uri: "/auth/finish_openid_login",
method: "POST",
jsonData: { code: code, state: state },
})
).data;
).data as { user_id: number; token: string };
localStorage.setItem(TokenStateKey, res.token);
}
@@ -167,7 +170,7 @@ export class AuthApi {
* Check reset password token
*/
static async CheckResetPasswordToken(
token: string
token: string,
): Promise<CheckResetTokenResponse> {
return (
await APIClient.exec({
@@ -175,7 +178,7 @@ export class AuthApi {
method: "POST",
jsonData: { token: token },
})
).data;
).data as CheckResetTokenResponse;
}
/**
@@ -183,7 +186,7 @@ export class AuthApi {
*/
static async ResetPassword(
token: string,
newPassword: string
newPassword: string,
): Promise<void> {
await APIClient.exec({
uri: "/auth/reset_password",

View File

@@ -85,11 +85,17 @@ export class Family implements FamilyAPI {
}
}
interface ExtendedFamilyInfoAPI extends FamilyAPI {
disable_couple_photos: boolean;
enable_genealogy: boolean;
enable_accommodations: boolean;
}
export class ExtendedFamilyInfo extends Family {
public disable_couple_photos: boolean;
public enable_genealogy: boolean;
public enable_accommodations: boolean;
constructor(p: any) {
constructor(p: ExtendedFamilyInfoAPI) {
super(p);
this.disable_couple_photos = p.disable_couple_photos;
this.enable_genealogy = p.enable_genealogy;
@@ -155,12 +161,13 @@ export class FamilyApi {
* Get the list of families
*/
static async GetList(): Promise<Family[]> {
return (
const res = (
await APIClient.exec({
method: "GET",
uri: "/family/list",
})
).data.map((f: FamilyAPI) => new Family(f));
).data as FamilyAPI[];
return res.map((f: FamilyAPI) => new Family(f));
}
/**
@@ -172,7 +179,7 @@ export class FamilyApi {
uri: `/family/${id}`,
});
return new ExtendedFamilyInfo(res.data);
return new ExtendedFamilyInfo(res.data as ExtendedFamilyInfoAPI);
}
/**
@@ -204,7 +211,7 @@ export class FamilyApi {
method: "GET",
uri: `/family/${id}/users`,
})
).data;
).data as FamilyUser[];
}
/**

View File

@@ -74,7 +74,7 @@ export class ServerApi {
uri: "/server/config",
method: "GET",
})
).data;
).data as ServerConfig;
}
/**

View File

@@ -33,7 +33,7 @@ export class UserApi {
uri: "/user/info",
method: "GET",
})
).data;
).data as User;
}
/**
@@ -54,7 +54,7 @@ export class UserApi {
*/
static async ReplacePassword(
oldPwd: string,
newPwd: string
newPwd: string,
): Promise<ReplacePasswordResponse> {
const res = await APIClient.exec({
uri: "/user/replace_password",
@@ -98,7 +98,7 @@ export class UserApi {
* Check delete account token
*/
static async CheckDeleteAccountToken(
token: string
token: string,
): Promise<DeleteAccountTokenInfo> {
return (
await APIClient.exec({
@@ -106,7 +106,7 @@ export class UserApi {
method: "POST",
jsonData: { token: token },
})
).data;
).data as DeleteAccountTokenInfo;
}
/**

View File

@@ -26,7 +26,7 @@ export class AccommodationsList {
}
this.list.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase())
a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase()),
);
}
@@ -73,7 +73,7 @@ export class AccommodationListApi {
method: "GET",
uri: `/family/${family.family_id}/accommodations/list/list`,
})
).data;
).data as Accommodation[];
return new AccommodationsList(data);
}
@@ -83,7 +83,7 @@ export class AccommodationListApi {
*/
static async Create(
family: Family,
accommodation: UpdateAccommodation
accommodation: UpdateAccommodation,
): Promise<Accommodation> {
return (
await APIClient.exec({
@@ -91,7 +91,7 @@ export class AccommodationListApi {
uri: `/family/${family.family_id}/accommodations/list/create`,
jsonData: accommodation,
})
).data;
).data as Accommodation;
}
/**
@@ -99,7 +99,7 @@ export class AccommodationListApi {
*/
static async Update(
accommodation: Accommodation,
update: UpdateAccommodation
update: UpdateAccommodation,
): Promise<Accommodation> {
return (
await APIClient.exec({
@@ -107,7 +107,7 @@ export class AccommodationListApi {
uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`,
jsonData: update,
})
).data;
).data as Accommodation;
}
/**
@@ -119,6 +119,6 @@ export class AccommodationListApi {
method: "DELETE",
uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`,
})
).data;
).data as Accommodation;
}
}

View File

@@ -23,7 +23,7 @@ export class AccommodationsCalendarURLApi {
*/
static async Create(
family: Family,
calendar: NewCalendarURL
calendar: NewCalendarURL,
): Promise<AccommodationCalendarURL> {
return (
await APIClient.exec({
@@ -31,7 +31,7 @@ export class AccommodationsCalendarURLApi {
uri: `/family/${family.family_id}/accommodations/reservations_calendars/create`,
jsonData: calendar,
})
).data;
).data as AccommodationCalendarURL;
}
/**
@@ -50,20 +50,20 @@ export class AccommodationsCalendarURLApi {
method: "GET",
uri: `/family/${family.family_id}/accommodations/reservations_calendars/list`,
})
).data;
).data as AccommodationCalendarURL[];
}
/**
* Delete an accommodation calendar
*/
static async Delete(
calendar: AccommodationCalendarURL
calendar: AccommodationCalendarURL,
): Promise<AccommodationCalendarURL> {
return (
await APIClient.exec({
method: "DELETE",
uri: `/family/${calendar.family_id}/accommodations/reservations_calendars/${calendar.id}`,
})
).data;
).data as AccommodationCalendarURL;
}
}

View File

@@ -48,7 +48,7 @@ export class AccommodationsReservationsList {
}
filter(
predicate: (m: AccommodationReservation) => boolean
predicate: (m: AccommodationReservation) => boolean,
): AccommodationReservation[] {
return this.list.filter(predicate);
}
@@ -75,7 +75,7 @@ export class AccommodationsReservationsApi {
*/
static async Create(
family: Family,
reservation: UpdateAccommodationReservation
reservation: UpdateAccommodationReservation,
): Promise<AccommodationReservation> {
return (
await APIClient.exec({
@@ -86,21 +86,21 @@ export class AccommodationsReservationsApi {
end: reservation.end,
},
})
).data;
).data as AccommodationReservation;
}
/**
* Get the entire list of accommodations of a family
*/
static async FullListOfFamily(
family: Family
family: Family,
): Promise<AccommodationsReservationsList> {
const data = (
await APIClient.exec({
method: "GET",
uri: `/family/${family.family_id}/accommodations/reservations/full_list`,
})
).data;
).data as AccommodationReservation[];
return new AccommodationsReservationsList(data);
}
@@ -112,14 +112,14 @@ export class AccommodationsReservationsApi {
family: Family,
accommodation: Accommodation,
start: number,
end: number
end: number,
): Promise<AccommodationsReservationsList> {
const data = (
await APIClient.exec({
method: "GET",
uri: `/family/${family.family_id}/accommodations/reservations/accommodation/${accommodation.id}/for_interval?start=${start}&end=${end}`,
})
).data;
).data as AccommodationReservation[];
return new AccommodationsReservationsList(data);
}
@@ -129,7 +129,7 @@ export class AccommodationsReservationsApi {
*/
static async Update(
family: Family,
r: UpdateAccommodationReservation
r: UpdateAccommodationReservation,
): Promise<void> {
await APIClient.exec({
method: "PATCH",
@@ -156,7 +156,7 @@ export class AccommodationsReservationsApi {
*/
static async Validate(
r: AccommodationReservation,
accept: boolean
accept: boolean,
): Promise<ValidateResaResult> {
const res = await APIClient.exec({
method: "POST",

View File

@@ -166,7 +166,7 @@ export class CoupleApi {
jsonData: m,
});
return new Couple(res.data);
return new Couple(res.data as CoupleApiInterface);
}
/**
@@ -174,26 +174,28 @@ export class CoupleApi {
*/
static async GetSingle(
family_id: number,
couple_id: number
couple_id: number,
): Promise<Couple> {
const res = await APIClient.exec({
uri: `/family/${family_id}/genealogy/couple/${couple_id}`,
method: "GET",
});
return new Couple(res.data);
return new Couple(res.data as CoupleApiInterface);
}
/**
* Get the entire list of couples of a family
*/
static async GetEntireList(family_id: number): Promise<CouplesList> {
const res = await APIClient.exec({
const res = (
await APIClient.exec({
uri: `/family/${family_id}/genealogy/couples`,
method: "GET",
});
})
).data as CoupleApiInterface[];
return new CouplesList(res.data.map((d: any) => new Couple(d)));
return new CouplesList(res.map((d) => new Couple(d)));
}
/**

View File

@@ -12,7 +12,7 @@ export class DataApi {
uri: `/family/${family_id}/genealogy/data/export`,
method: "GET",
});
return res.data;
return res.data as Blob;
}
/**
@@ -26,6 +26,6 @@ export class DataApi {
method: "PUT",
formData: fd,
});
return res.data;
return res.data as Blob;
}
}

View File

@@ -115,7 +115,7 @@ export class Member implements MemberDataApi {
const firstName = this.first_name ?? "";
return firstName.length === 0
? this.last_name ?? ""
? (this.last_name ?? "")
: `${firstName} ${this.last_name?.toUpperCase() ?? ""}`;
}
@@ -123,7 +123,7 @@ export class Member implements MemberDataApi {
const lastName = this.last_name ?? "";
return lastName.length === 0
? this.last_name ?? ""
? (this.last_name ?? "")
: `${lastName} ${this.first_name ?? ""}`;
}
@@ -164,14 +164,14 @@ export class Member implements MemberDataApi {
}
get displayBirthDeath(): string {
let birthDeath = [];
const birthDeath = [];
if (this.dateOfBirth) birthDeath.push(fmtDate(this.dateOfBirth));
if (this.dateOfDeath) birthDeath.push(fmtDate(this.dateOfDeath));
return birthDeath.join(" - ");
}
get displayBirthDeathShort(): string {
let birthDeath = [];
const birthDeath = [];
if (this.birth_year) birthDeath.push(this.birth_year.toString());
if (this.death_year) birthDeath.push(this.death_year.toString());
return birthDeath.join(" - ");
@@ -226,7 +226,7 @@ export class MembersList {
this.list.sort((a, b) =>
a.invertedFullName
.toLowerCase()
.localeCompare(b.invertedFullName.toLocaleLowerCase())
.localeCompare(b.invertedFullName.toLocaleLowerCase()),
);
}
@@ -257,7 +257,7 @@ export class MembersList {
childrenOfCouple(c: Couple): Member[] {
if (!c.husband && !c.wife) return [];
return this.list.filter(
(m) => m.mother === c.wife && m.father === c.husband
(m) => m.mother === c.wife && m.father === c.husband,
);
}
@@ -267,7 +267,7 @@ export class MembersList {
(m) =>
m.id !== p?.id &&
((m.mother && m.mother === p?.mother) ||
(m.father && m.father === p?.father))
(m.father && m.father === p?.father)),
);
}
}
@@ -277,13 +277,15 @@ export class MemberApi {
* Create a new member
*/
static async Create(m: Member): Promise<Member> {
const res = await APIClient.exec({
const res = (
await APIClient.exec({
uri: `/family/${m.family_id}/genealogy/member/create`,
method: "POST",
jsonData: m,
});
})
).data as MemberDataApi;
return new Member(res.data);
return new Member(res);
}
/**
@@ -291,26 +293,30 @@ export class MemberApi {
*/
static async GetSingle(
family_id: number,
member_id: number
member_id: number,
): Promise<Member> {
const res = await APIClient.exec({
const res = (
await APIClient.exec({
uri: `/family/${family_id}/genealogy/member/${member_id}`,
method: "GET",
});
})
).data as MemberDataApi;
return new Member(res.data);
return new Member(res);
}
/**
* Get the entire list of family members of a family
*/
static async GetEntireList(family_id: number): Promise<MembersList> {
const res = await APIClient.exec({
const res = (
await APIClient.exec({
uri: `/family/${family_id}/genealogy/members`,
method: "GET",
});
})
).data as MemberDataApi[];
return new MembersList(res.data.map((d: any) => new Member(d)));
return new MembersList(res.map((d) => new Member(d)));
}
/**

View File

@@ -27,11 +27,11 @@ export function UpdateAccommodationDialog(p: {
const nameErr = checkConstraint(
ServerApi.Config.constraints.accommodation_name_len,
accommodation?.name
accommodation?.name,
);
const descriptionErr = checkConstraint(
ServerApi.Config.constraints.accommodation_description_len,
accommodation?.description
accommodation?.description,
);
const clearForm = () => {
@@ -49,7 +49,9 @@ export function UpdateAccommodationDialog(p: {
};
React.useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (!accommodation) setAccommodation(p.accommodation);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [p.open, p.accommodation]);
return (

View File

@@ -54,10 +54,13 @@ export function UpdateReservationDialog(p: {
};
React.useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (!reservation) setReservation(p.reservation);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [p.open, p.reservation]);
React.useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setConflicts(undefined);
(async () => {
try {
@@ -77,20 +80,21 @@ export function UpdateReservationDialog(p: {
family.family,
accommodations.accommodations.get(reservation.accommodation_id)!,
reservation.start,
reservation.end
reservation.end,
)
).filter(
(r) =>
r.id !== p.reservation?.reservation_id && r.validated !== false
)
r.id !== p.reservation?.reservation_id && r.validated !== false,
),
);
} catch (e) {
console.error(e);
alert(
"Echec de la vérification de la présence de conflits de calendrier !"
"Echec de la vérification de la présence de conflits de calendrier !",
);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
p.open,
reservation?.accommodation_id,
@@ -118,7 +122,7 @@ export function UpdateReservationDialog(p: {
options={accommodations.accommodations.openToReservationList.map(
(a) => {
return { label: a.name, value: a.id.toString() };
}
},
)}
value={
reservation?.accommodation_id === -1

View File

@@ -63,6 +63,7 @@ export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useAlert(): AlertContext {
return React.useContext(AlertContextK)!;
}

View File

@@ -11,20 +11,20 @@ import React, { PropsWithChildren } from "react";
type ConfirmContext = (
message: string,
title?: string,
confirmButton?: string
confirmButton?: string,
) => Promise<boolean>;
const ConfirmContextK = React.createContext<ConfirmContext | null>(null);
export function ConfirmDialogProvider(
p: PropsWithChildren
p: PropsWithChildren,
): React.ReactElement {
const [open, setOpen] = React.useState(false);
const [title, setTitle] = React.useState<string | undefined>(undefined);
const [message, setMessage] = React.useState("");
const [confirmButton, setConfirmButton] = React.useState<string | undefined>(
undefined
undefined,
);
const cb = React.useRef<null | ((a: boolean) => void)>(null);
@@ -78,6 +78,7 @@ export function ConfirmDialogProvider(
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useConfirm(): ConfirmContext {
return React.useContext(ConfirmContextK)!;
}

View File

@@ -11,7 +11,7 @@ const darkTheme = createTheme(
mode: "dark",
},
},
dataGridFr
dataGridFr,
);
const lightTheme = createTheme(
@@ -20,7 +20,7 @@ const lightTheme = createTheme(
mode: "light",
},
},
dataGridFr
dataGridFr,
);
interface DarkThemeContext {
@@ -32,7 +32,7 @@ const DarkThemeContextK = React.createContext<DarkThemeContext | null>(null);
export function DarkThemeProvider(p: PropsWithChildren): React.ReactElement {
const [enabled, setEnabled] = React.useState(
localStorage.getItem(localStorageKey) === "true"
localStorage.getItem(localStorageKey) === "true",
);
return (
@@ -52,6 +52,7 @@ export function DarkThemeProvider(p: PropsWithChildren): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useDarkTheme(): DarkThemeContext {
return React.useContext(DarkThemeContextK)!;
}

View File

@@ -15,7 +15,7 @@ const LoadingMessageContextK =
React.createContext<LoadingMessageContext | null>(null);
export function LoadingMessageProvider(
p: PropsWithChildren
p: PropsWithChildren,
): React.ReactElement {
const [open, setOpen] = React.useState(false);
@@ -59,6 +59,7 @@ export function LoadingMessageProvider(
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useLoadingMessage(): LoadingMessageContext {
return React.useContext(LoadingMessageContextK)!;
}

View File

@@ -38,6 +38,7 @@ export function SnackbarProvider(p: PropsWithChildren): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useSnackbar(): SnackbarContext {
return React.useContext(SnackbarContextK)!;
}

View File

@@ -7,12 +7,12 @@ type DialogContext = () => Promise<NewCalendarURL | undefined>;
const DialogContextK = React.createContext<DialogContext | null>(null);
export function CreateAccommodationCalendarURLDialogProvider(
p: PropsWithChildren
p: PropsWithChildren,
): React.ReactElement {
const [open, setOpen] = React.useState(false);
const cb = React.useRef<null | ((a: NewCalendarURL | undefined) => void)>(
null
null,
);
const handleClose = (res?: NewCalendarURL) => {
@@ -47,6 +47,7 @@ export function CreateAccommodationCalendarURLDialogProvider(
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useCreateAccommodationCalendarURL(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@@ -7,7 +7,7 @@ type DialogContext = (cal: AccommodationCalendarURL) => Promise<void>;
const DialogContextK = React.createContext<DialogContext | null>(null);
export function InstallCalendarDialogProvider(
p: PropsWithChildren
p: PropsWithChildren,
): React.ReactElement {
const [cal, setCal] = React.useState<AccommodationCalendarURL | undefined>();
@@ -39,6 +39,7 @@ export function InstallCalendarDialogProvider(
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useInstallCalendarDialog(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@@ -4,13 +4,13 @@ import { UpdateAccommodationDialog } from "../../../dialogs/accommodations/Updat
type DialogContext = (
accommodation: UpdateAccommodation,
create: boolean
create: boolean,
) => Promise<UpdateAccommodation | undefined>;
const DialogContextK = React.createContext<DialogContext | null>(null);
export function UpdateAccommodationDialogProvider(
p: PropsWithChildren
p: PropsWithChildren,
): React.ReactElement {
const [open, setOpen] = React.useState(false);
@@ -59,6 +59,7 @@ export function UpdateAccommodationDialogProvider(
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useUpdateAccommodation(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@@ -4,13 +4,13 @@ import { UpdateReservationDialog } from "../../../dialogs/accommodations/UpdateR
type DialogContext = (
reservation: UpdateAccommodationReservation,
create: boolean
create: boolean,
) => Promise<UpdateAccommodationReservation | undefined>;
const DialogContextK = React.createContext<DialogContext | null>(null);
export function UpdateReservationDialogProvider(
p: PropsWithChildren
p: PropsWithChildren,
): React.ReactElement {
const [open, setOpen] = React.useState(false);
@@ -59,6 +59,7 @@ export function UpdateReservationDialogProvider(
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useUpdateAccommodationReservation(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@@ -48,12 +48,13 @@ export function FamiliesListRoute(): React.ReactElement {
return (
<AsyncWidget
ready={families !== null}
// eslint-disable-next-line react-hooks/refs
loadKey={`families-list-${loadKey.current}`}
load={load}
errMsg="Echec du chargement de la liste des familles"
build={() => (
<>
{families!!.length === 0 ? (
{families!.length === 0 ? (
<NoFamilyWidget
onRequestCreateFamily={onRequestCreateFamily}
onRequestJoinFamily={onRequestJoinFamily}
@@ -190,7 +191,7 @@ function FamilyCard(p: {
if (
!p.f.CanLeave ||
!(await confirm(
"Voulez-vous vraiment quitter la famille " + p.f.name + " ?"
"Voulez-vous vraiment quitter la famille " + p.f.name + " ?",
))
)
return;
@@ -200,7 +201,7 @@ function FamilyCard(p: {
p.onFamilyLeft();
alert(
`La famille ${p.f.name} a été retirée de votre liste de familles !`
`La famille ${p.f.name} a été retirée de votre liste de familles !`,
);
} catch (e) {
console.error(e);

View File

@@ -14,10 +14,10 @@ import React from "react";
import { ServerApi } from "../api/ServerApi";
import { ReplacePasswordResponse, User, UserApi } from "../api/UserApi";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { useUser } from "../widgets/BaseAuthenticatedPage";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { formatDate } from "../utils/time_utils";
import { useUser } from "../widgets/BaseAuthenticatedPage";
import { PasswordInput } from "../widgets/PasswordInput";
import { formatDate } from "../widgets/TimeWidget";
export function ProfileRoute(): React.ReactElement {
const user = useUser();
@@ -172,7 +172,7 @@ function ChangePasswordCard(): React.ReactElement {
break;
case ReplacePasswordResponse.TooManyRequests:
setError(
"Trop de tentatives de changement de mot de passe, veuillez réessayer ultérieurement !"
"Trop de tentatives de changement de mot de passe, veuillez réessayer ultérieurement !",
);
break;
}
@@ -257,7 +257,7 @@ function DeleteAccountButton(): React.ReactElement {
if (
!(await confirm(
"Voulez-vous initier la suppression de votre compte ?",
"Suppression de compte"
"Suppression de compte",
))
)
return;
@@ -265,7 +265,7 @@ function DeleteAccountButton(): React.ReactElement {
await UserApi.RequestAccountDeletion();
await alert(
"Demande de suppression de compte enregistrée avec succès. Veuillez consulter votre boîte mail."
"Demande de suppression de compte enregistrée avec succès. Veuillez consulter votre boîte mail.",
);
} catch (e) {
console.error(e);

View File

@@ -40,7 +40,7 @@ export function LoginRoute(): React.ReactElement {
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleMouseDownPassword = (
event: React.MouseEvent<HTMLButtonElement>
event: React.MouseEvent<HTMLButtonElement>,
) => {
event.preventDefault();
};
@@ -55,7 +55,7 @@ export function LoginRoute(): React.ReactElement {
switch (res) {
case PasswordLoginResult.TooManyRequests:
setError(
"Trop de tentatives de connection. Veuillez réessayer ultérieurement."
"Trop de tentatives de connection. Veuillez réessayer ultérieurement.",
);
break;
@@ -84,6 +84,7 @@ export function LoginRoute(): React.ReactElement {
setLoading(true);
const res = await AuthApi.StartOpenIDLogin(id);
// eslint-disable-next-line react-hooks/immutability
window.location.href = res.url;
} catch (e) {
console.error(e);

View File

@@ -14,9 +14,9 @@ import { FamilyApi } from "../../api/FamilyApi";
import { ServerApi } from "../../api/ServerApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
import { formatDate } from "../../utils/time_utils";
import { useFamily } from "../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../widgets/FamilyCard";
import { formatDate } from "../../widgets/TimeWidget";
export function FamilySettingsRoute(): React.ReactElement {
const alert = useAlert();
@@ -29,7 +29,7 @@ export function FamilySettingsRoute(): React.ReactElement {
try {
if (
!(await confirm(
"Voulez-vous vraiment supprimer cette famille, et toute les données qui s'y rattachent ? Cette opération est absolument irréversible !"
"Voulez-vous vraiment supprimer cette famille, et toute les données qui s'y rattachent ? Cette opération est absolument irréversible !",
))
)
return;
@@ -69,10 +69,10 @@ function FamilySettingsCard(): React.ReactElement {
const [newName, setNewName] = React.useState(family.family.name);
const [enableGenealogy, setEnableGenealogy] = React.useState(
family.family.enable_genealogy
family.family.enable_genealogy,
);
const [enableAccommodations, setEnableAccommodations] = React.useState(
family.family.enable_accommodations
family.family.enable_accommodations,
);
const canEdit = family.family.is_admin;

View File

@@ -38,6 +38,7 @@ export function FamilyUsersListRoute(): React.ReactElement {
return (
<AsyncWidget
// eslint-disable-next-line react-hooks/refs
loadKey={`${family.family.family_id}-${key.current}`}
errMsg="Echec du chargement de la liste des utilisateurs de la famille !"
load={load}
@@ -62,7 +63,7 @@ function UsersTable(p: {
const family = useFamily();
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
{}
{},
);
const handleEditClick = (id: GridRowId) => () => {
@@ -79,7 +80,7 @@ function UsersTable(p: {
const user = p.users.find((u) => u.user_id === id)!;
if (
!(await confirm(
`Voulez-vous vraiment retirer ${user.user_name} de cette famille ?`
`Voulez-vous vraiment retirer ${user.user_name} de cette famille ?`,
))
)
return;
@@ -139,7 +140,7 @@ function UsersTable(p: {
label="Save"
material={{
sx: {
color: 'primary.main',
color: "primary.main",
},
}}
onClick={handleSaveClick(id)}

View File

@@ -74,7 +74,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
const [showPending, setShowPending] = React.useState(true);
const [hiddenPeople, setHiddenPeople] = React.useState<Set<number>>(
new Set()
new Set(),
);
const [hiddenAccommodations, setHiddenAccommodations] = React.useState<
Set<number>
@@ -100,7 +100,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
const load = async () => {
setReservations(
await AccommodationsReservationsApi.FullListOfFamily(family.family)
await AccommodationsReservationsApi.FullListOfFamily(family.family),
);
setUsers(await FamilyApi.GetUsersList(family.family.family_id));
};
@@ -136,7 +136,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
start: Math.floor(d.start.getTime() / 1000),
end: Math.floor(d.end.getTime() / 1000),
},
true
true,
);
if (!resa) return;
@@ -157,7 +157,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
const onEventClick = (ev: EventClickArg) => {
const id: number = ev.event.extendedProps.id;
const resa = reservations?.get(id)!;
const resa = reservations!.get(id)!;
const acc = accommodations.accommodations.get(resa.accommodation_id)!;
const user = users?.find((u) => u.user_id === resa.user_id);
@@ -182,7 +182,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
const respondToResaRequest = async (
r: AccommodationReservation,
validate: boolean
validate: boolean,
) => {
try {
loadingMessage.show("Validation de la réservation en cours...");
@@ -194,7 +194,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
if (res === ValidateResaResult.Conflict) {
throw new Error(
"The reservation is in conflict with other reservations!"
"The reservation is in conflict with other reservations!",
);
} else if (res === ValidateResaResult.Error) {
throw new Error("Failed to validate the reservation!");
@@ -217,7 +217,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
const rejectReservation = async (r: AccommodationReservation) => {
if (
!(await confirm(
"Voulez-vous vraiment rejeter cette demande de réservation ?"
"Voulez-vous vraiment rejeter cette demande de réservation ?",
))
)
return;
@@ -230,7 +230,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
if (
ac?.need_validation &&
!(await confirm(
"Voulez-vous vraiment changer cette réservation ? Celle-ci devra être de nouveau validée !"
"Voulez-vous vraiment changer cette réservation ? Celle-ci devra être de nouveau validée !",
))
)
return;
@@ -242,7 +242,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
start: r.reservation_start,
end: r.reservation_end,
},
false
false,
);
if (!newResa) return;
@@ -266,7 +266,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
try {
if (
!(await confirm(
"Voulez-vous vraiment supprimer cette réservation ? L'opération n'est pas réversible !"
"Voulez-vous vraiment supprimer cette réservation ? L'opération n'est pas réversible !",
))
)
return;
@@ -290,6 +290,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
<>
<FamilyPageTitle title="Réservations" />
<AsyncWidget
// eslint-disable-next-line react-hooks/refs
loadKey={loadKey.current}
load={load}
errMsg="Echec du chargement de la liste des réservations !"
@@ -365,7 +366,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
if (v) hiddenAccommodations.delete(a.id);
else hiddenAccommodations.add(a.id);
setHiddenAccommodations(
new Set(hiddenAccommodations)
new Set(hiddenAccommodations),
);
}}
/>
@@ -422,7 +423,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
eventClick={onEventClick}
events={visibleReservations?.map((r) => {
const a = accommodations.accommodations.get(
r.accommodation_id
r.accommodation_id,
)!;
const u = users?.find((u) => u.user_id === r.user_id);
return {
@@ -493,12 +494,12 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
<p>
Du{" "}
{fmtUnixDate(
activeEvent?.reservation.reservation_start ?? 0
activeEvent?.reservation.reservation_start ?? 0,
)}{" "}
<br />
Au{" "}
{fmtUnixDate(
activeEvent?.reservation.reservation_end ?? 0
activeEvent?.reservation.reservation_end ?? 0,
)}
</p>
<p>

View File

@@ -66,7 +66,7 @@ function AccommodationsListCard(): React.ReactElement {
need_validation: false,
color: "2196f3",
},
true
true,
);
if (!accommodation) return;
@@ -112,7 +112,7 @@ function AccommodationsListCard(): React.ReactElement {
try {
if (
!(await confirm(
`Voulez-vous vraiment supprimer le logement '${a.name}' ? Cette opération est définitive !`
`Voulez-vous vraiment supprimer le logement '${a.name}' ? Cette opération est définitive !`,
))
)
return;
@@ -258,7 +258,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
try {
if (
!(await confirm(
`Voulez-vous vraiment supprimer le calendrier '${c.name}' ? Cette opération est définitive !`
`Voulez-vous vraiment supprimer le calendrier '${c.name}' ? Cette opération est définitive !`,
))
)
return;
@@ -287,7 +287,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
const cal = await AccommodationsCalendarURLApi.Create(
family.family,
newCal
newCal,
);
setSuccess("Le calendrier a été créé avec succès !");
@@ -337,6 +337,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
<AsyncWidget
ready={list !== undefined}
// eslint-disable-next-line react-hooks/refs
loadKey={key.current}
load={load}
errMsg="Echec du chargement de la liste des calendriers !"

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/immutability */
import ClearIcon from "@mui/icons-material/Clear";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
@@ -111,7 +112,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
try {
if (
!(await confirm(
"Voulez-vous vraiment supprimer cette fiche de couple ? L'opération n'est pas réversible !"
"Voulez-vous vraiment supprimer cette fiche de couple ? L'opération n'est pas réversible !",
))
)
return;
@@ -130,6 +131,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
return (
<AsyncWidget
// eslint-disable-next-line react-hooks/refs
loadKey={`${coupleId}-${count.current}`}
load={load}
ready={couple !== undefined}
@@ -230,7 +232,7 @@ export function CouplePage(p: {
const [changed, setChanged] = React.useState(false);
const [couple, setCouple] = React.useState(
new Couple(structuredClone(p.couple))
new Couple(structuredClone(p.couple)),
);
const updatedCouple = () => {
@@ -240,7 +242,7 @@ export function CouplePage(p: {
const save = async () => {
loadingMessage.show(
"Enregistrement des informations du couple en cours..."
"Enregistrement des informations du couple en cours...",
);
await p.onSave!(couple);
loadingMessage.hide();
@@ -250,7 +252,7 @@ export function CouplePage(p: {
if (
changed &&
!(await confirm(
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !"
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !",
))
)
return;
@@ -491,7 +493,7 @@ export function CouplePage(p: {
<div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink
to={family.family.URL(
`genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`
`genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`,
)}
>
<Button>Nouveau</Button>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/immutability */
import { mdiFamilyTree } from "@mdi/js";
import Icon from "@mdi/react";
import { Icon } from "@mdi/react";
import ClearIcon from "@mui/icons-material/Clear";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
@@ -125,7 +126,7 @@ export function FamilyMemberRoute(): React.ReactElement {
try {
if (
!(await confirm(
"Voulez-vous vraiment supprimer cette fiche membre ? L'opération n'est pas réversible !"
"Voulez-vous vraiment supprimer cette fiche membre ? L'opération n'est pas réversible !",
))
)
return;
@@ -144,6 +145,7 @@ export function FamilyMemberRoute(): React.ReactElement {
return (
<AsyncWidget
// eslint-disable-next-line react-hooks/refs
loadKey={`${memberId}-${count.current}`}
load={load}
ready={member !== undefined}
@@ -250,7 +252,7 @@ export function MemberPage(p: {
const [changed, setChanged] = React.useState(false);
const [member, setMember] = React.useState(
new Member(structuredClone(p.member))
new Member(structuredClone(p.member)),
);
const updatedMember = () => {
@@ -260,7 +262,7 @@ export function MemberPage(p: {
const save = async () => {
loadingMessage.show(
"Enregistrement des informations du membre en cours..."
"Enregistrement des informations du membre en cours...",
);
await p.onSave!(member);
loadingMessage.hide();
@@ -270,7 +272,7 @@ export function MemberPage(p: {
if (
changed &&
!(await confirm(
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !"
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !",
))
)
return;
@@ -666,7 +668,7 @@ export function MemberPage(p: {
to={family.family.URL(
`genealogy/couple/create?${
member.sex === "F" ? "wife" : "husband"
}=${member.id}`
}=${member.id}`,
)}
>
<Button>Nouveau</Button>
@@ -695,7 +697,7 @@ export function MemberPage(p: {
to={family.family.URL(
`genealogy/member/create?${
member.sex === "F" ? "mother" : "father"
}=${member.id}`
}=${member.id}`,
)}
>
<Button>Nouveau</Button>
@@ -723,7 +725,7 @@ export function MemberPage(p: {
<div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink
to={family.family.URL(
`genealogy/member/create?mother=${member.mother}&father=${member.father}`
`genealogy/member/create?mother=${member.mother}&father=${member.father}`,
)}
>
<Button>Nouveau</Button>
@@ -748,7 +750,7 @@ function CoupleItem(p: {
const genealogy = useGenealogy();
const statusStr = ServerApi.Config.couples_states.find(
(c) => c.code === p.couple.state
(c) => c.code === p.couple.state,
)?.fr;
const status = [];

View File

@@ -20,7 +20,7 @@ export function getRadianAngle(degreeValue: number): number {
export function rotateSize(
width: number,
height: number,
rotation: number
rotation: number,
): { width: number; height: number } {
const rotRad = getRadianAngle(rotation);
@@ -39,7 +39,7 @@ export default async function getCroppedImg(
imageSrc: string,
pixelCrop: Area,
rotation = 0,
flip = { horizontal: false, vertical: false }
flip = { horizontal: false, vertical: false },
): Promise<Blob> {
const image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
@@ -55,7 +55,7 @@ export default async function getCroppedImg(
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
image.width,
image.height,
rotation
rotation,
);
// set canvas size to match the bounding box
@@ -93,14 +93,14 @@ export default async function getCroppedImg(
0,
0,
pixelCrop.width,
pixelCrop.height
pixelCrop.height,
);
// As Base64 string
// return croppedCanvas.toDataURL('image/jpeg');
// As a blob
return await new Promise((resolve, _reject) => {
return await new Promise((resolve) => {
croppedCanvas.toBlob((file) => {
resolve(file!);
}, "image/jpeg");

View File

@@ -14,8 +14,8 @@ export async function selectFileToUpload(p: {
fileEl.click();
// Wait for a file to be chosen
await new Promise((res, _rej) =>
fileEl.addEventListener("change", () => res(null))
await new Promise((res) =>
fileEl.addEventListener("change", () => res(null)),
);
if ((fileEl.files?.length ?? 0) === 0) return null;
@@ -25,8 +25,8 @@ export async function selectFileToUpload(p: {
if (p.maxSize && file.size > p.maxSize) {
throw new Error(
`Le fichier sélectionné est trop lourd ! (taille maximale acceptée : ${filesize(
p.maxSize
)})`
p.maxSize,
)})`,
);
}

View File

@@ -1,5 +1,5 @@
let canvas: HTMLCanvasElement = document.createElement("canvas");
let charLen: Map<string, Map<string, number>> = new Map();
const canvas: HTMLCanvasElement = document.createElement("canvas");
const charLen: Map<string, Map<string, number>> = new Map();
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.

View File

@@ -1,6 +1,6 @@
export function getAllIndexes(s: string, val: string) {
var indexes = [],
i = -1;
export function getAllIndexes(s: string, val: string): number[] {
const indexes = [];
let i = -1;
while ((i = s.indexOf(val, i + 1)) !== -1) {
indexes.push(i);
}

View File

@@ -1,3 +1,5 @@
import { format } from "date-and-time";
/**
* Get formatted UNIX date
*/
@@ -10,7 +12,7 @@ export function fmtUnixDate(time: number): string {
*/
export function fmtUnixDateFullCalendar(
time: number,
correctEnd: boolean
correctEnd: boolean,
): string {
let d = new Date(time * 1000);
@@ -29,3 +31,65 @@ export function fmtUnixDateFullCalendar(
return s;
}
/**
* Format a date into a human-readable form
*/
export function formatDate(time: number): string {
const t = new Date();
t.setTime(1000 * time);
return format(t, "DD/MM/YYYY HH:mm:ss");
}
/**
* Get human readable time between to dates
*/
export function timeDiff(a: number, b: number): string {
let diff = b - a;
if (diff === 0) return "maintenant";
if (diff === 1) return "1 seconde";
if (diff < 60) {
return `${diff} secondes`;
}
diff = Math.floor(diff / 60);
if (diff === 1) return "1 minute";
if (diff < 24) {
return `${diff} minutes`;
}
diff = Math.floor(diff / 60);
if (diff === 1) return "1 heure";
if (diff < 24) {
return `${diff} heures`;
}
const diffDays = Math.floor(diff / 24);
if (diffDays === 1) return "1 jour";
if (diffDays < 31) {
return `${diffDays} jours`;
}
diff = Math.floor(diffDays / 31);
if (diff < 12) {
return `${diff} mois`;
}
const diffYears = Math.floor(diffDays / 365);
if (diffYears === 1) return "1 an";
return `${diffYears} ans`;
}
/**
* Get human readable time diff from a given timestamp to today
*/
export function timeDiffFromNow(time: number): string {
return timeDiff(time, Math.floor(new Date().getTime() / 1000));
}

View File

@@ -1,23 +1,23 @@
import { Alert, Box, Button, CircularProgress } from "@mui/material";
import { useEffect, useRef, useState } from "react";
import React from "react";
enum State {
Loading,
Ready,
Error,
}
const State = {
Loading: 0,
Ready: 1,
Error: 2,
} as const;
type State = keyof typeof State;
export function AsyncWidget(p: {
loadKey: any;
loadKey: unknown;
load: () => Promise<void>;
errMsg: string;
build: () => React.ReactElement;
ready?: boolean;
errAdditionalElement?: () => React.ReactElement;
}): React.ReactElement {
const [state, setState] = useState(State.Loading);
const counter = useRef<any | null>(null);
const [state, setState] = React.useState<number>(State.Loading);
const load = async () => {
try {
@@ -30,12 +30,11 @@ export function AsyncWidget(p: {
}
};
useEffect(() => {
if (counter.current === p.loadKey) return;
counter.current = p.loadKey;
React.useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load();
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [p.loadKey]);
if (state === State.Error)
return (
@@ -48,10 +47,6 @@ export function AsyncWidget(p: {
height: "100%",
flex: "1",
flexDirection: "column",
backgroundColor: (theme) =>
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[900],
}}
>
<Alert
@@ -64,7 +59,7 @@ export function AsyncWidget(p: {
<Button onClick={load}>Réessayer</Button>
{p.errAdditionalElement && p.errAdditionalElement()}
{p.errAdditionalElement?.()}
</Box>
);
@@ -78,10 +73,6 @@ export function AsyncWidget(p: {
alignItems: "center",
height: "100%",
flex: "1",
backgroundColor: (theme) =>
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[900],
}}
>
<CircularProgress />

View File

@@ -1,5 +1,5 @@
import { mdiFamilyTree } from "@mdi/js";
import Icon from "@mdi/react";
import { Icon } from "@mdi/react";
import SettingsIcon from "@mui/icons-material/Settings";
import { Box, Button } from "@mui/material";
import AppBar from "@mui/material/AppBar";
@@ -150,6 +150,7 @@ export function BaseAuthenticatedPage(): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useUser(): UserContext {
return React.useContext(UserContextK)!;
}

View File

@@ -11,7 +11,7 @@ import {
mdiPlus,
mdiRefresh,
} from "@mdi/js";
import Icon from "@mdi/react";
import { Icon } from "@mdi/react";
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
import HomeIcon from "@mui/icons-material/Home";
import {
@@ -65,7 +65,7 @@ export function BaseFamilyRoute(): React.ReactElement {
loadKey.current += 1;
setFamily(null);
return new Promise<void>((res, _rej) => {
return new Promise<void>((res) => {
loadPromise.current = () => res();
});
};
@@ -79,7 +79,7 @@ export function BaseFamilyRoute(): React.ReactElement {
try {
if (
!(await confirm(
"Voulez-vous vraiment générer un nouveau code d'invitation pour cette famille ? Cette action aura pour effet d'invalider l'ancien code !"
"Voulez-vous vraiment générer un nouveau code d'invitation pour cette famille ? Cette action aura pour effet d'invalider l'ancien code !",
))
)
return;
@@ -98,6 +98,7 @@ export function BaseFamilyRoute(): React.ReactElement {
return (
<AsyncWidget
ready={family !== null}
// eslint-disable-next-line react-hooks/refs
loadKey={`${familyId}-${loadKey.current}`}
load={load}
errMsg="Échec du chargement des informations de la famille !"
@@ -287,6 +288,7 @@ export function BaseFamilyRoute(): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useFamily(): FamilyContext {
return React.useContext(FamilyContextK)!;
}

View File

@@ -1,5 +1,5 @@
import { mdiFamilyTree } from "@mdi/js";
import Icon from "@mdi/react";
import { Icon } from "@mdi/react";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import CssBaseline from "@mui/material/CssBaseline";
@@ -9,14 +9,14 @@ import Typography from "@mui/material/Typography";
import { Link, Outlet } from "react-router-dom";
import { DarkThemeButton } from "./DarkThemeButton";
function Copyright(props: any) {
function Copyright(): React.ReactElement {
return (
<Typography
variant="body2"
color="text.secondary"
align="center"
style={{ marginTop: "20px" }}
{...props}
sx={{ mt: 5 }}
>
{"Copyright © "}
<a
@@ -90,7 +90,7 @@ export function BaseLoginPage() {
{/* inner page */}
<Outlet />
<Copyright sx={{ mt: 5 }} />
<Copyright />
</Box>
</Grid>
</Grid>

View File

@@ -1,5 +1,5 @@
import { mdiBabyCarriage, mdiCross } from "@mdi/js";
import Icon from "@mdi/react";
import { Icon } from "@mdi/react";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { TreeItem, SimpleTreeView } from "@mui/x-tree-view";

View File

@@ -16,7 +16,7 @@ import { useBlocker } from "react-router-dom";
export function ConfirmLeaveWithoutSaveDialog(p: {
shouldBlock: boolean;
}): React.ReactElement {
let blocker = useBlocker(p.shouldBlock);
const blocker = useBlocker(p.shouldBlock);
React.useEffect(() => {
if (blocker.state === "blocked" && !p.shouldBlock) {
@@ -49,8 +49,8 @@ export function ConfirmLeaveWithoutSaveDialog(p: {
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={confirmNavigation as any}>Quitter la page</Button>
<Button onClick={cancelNavigation as any} autoFocus>
<Button onClick={confirmNavigation}>Quitter la page</Button>
<Button onClick={cancelNavigation} autoFocus>
Rester sur la page
</Button>
</DialogActions>

View File

@@ -7,7 +7,7 @@ import {
} from "@mui/material";
import { Member, fmtDate } from "../api/genealogy/MemberApi";
import { MemberPhoto } from "./MemberPhoto";
import Icon from "@mdi/react";
import { Icon } from "@mdi/react";
import FemaleIcon from "@mui/icons-material/Female";
import MaleIcon from "@mui/icons-material/Male";
@@ -37,7 +37,7 @@ export function MemberItem(p: {
secondary={
p.member?.dead
? `${fmtDate(p.member?.dateOfBirth)} - ${fmtDate(
p.member?.dateOfDeath
p.member?.dateOfDeath,
)}`
: fmtDate(p.member?.dateOfBirth)
}

View File

@@ -1,58 +1,5 @@
import { Tooltip } from "@mui/material";
import date from "date-and-time";
export function formatDate(time: number): string {
const t = new Date();
t.setTime(1000 * time);
return date.format(t, "DD/MM/YYYY HH:mm:ss");
}
export function timeDiff(a: number, b: number): string {
let diff = b - a;
if (diff === 0) return "maintenant";
if (diff === 1) return "1 seconde";
if (diff < 60) {
return `${diff} secondes`;
}
diff = Math.floor(diff / 60);
if (diff === 1) return "1 minute";
if (diff < 24) {
return `${diff} minutes`;
}
diff = Math.floor(diff / 60);
if (diff === 1) return "1 heure";
if (diff < 24) {
return `${diff} heures`;
}
const diffDays = Math.floor(diff / 24);
if (diffDays === 1) return "1 jour";
if (diffDays < 31) {
return `${diffDays} jours`;
}
diff = Math.floor(diffDays / 31);
if (diff < 12) {
return `${diff} mois`;
}
const diffYears = Math.floor(diffDays / 365);
if (diffYears === 1) return "1 an";
return `${diffYears} ans`;
}
export function timeDiffFromNow(time: number): string {
return timeDiff(time, Math.floor(new Date().getTime() / 1000));
}
import { formatDate, timeDiffFromNow } from "../utils/time_utils";
export function TimeWidget(p: { time: number }): React.ReactElement {
return (

View File

@@ -31,7 +31,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
const load = async () => {
setAccommodations(
await AccommodationListApi.GetListOfFamily(family.family)
await AccommodationListApi.GetListOfFamily(family.family),
);
};
@@ -39,7 +39,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
loadKey.current += 1;
setAccommodations(null);
return new Promise<void>((res, _rej) => {
return new Promise<void>((res) => {
loadPromise.current = () => res();
});
};
@@ -47,6 +47,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
return (
<AsyncWidget
ready={accommodations !== null}
// eslint-disable-next-line react-hooks/refs
loadKey={`${family.familyId}-${loadKey.current}`}
load={load}
errMsg="Échec du chargement des informations sur les logements de la famille !"
@@ -79,6 +80,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useAccommodations(): AccommodationsContext {
return React.useContext(AccommodationsContextK)!;
}

View File

@@ -58,7 +58,7 @@ export function MemberInput(p: {
return (
<Autocomplete
value={p.current ? genealogy.members.get(p.current) : undefined}
onChange={(_event: any, newValue: Member | null | undefined) => {
onChange={(_event: unknown, newValue: Member | null | undefined) => {
p.onValueChange(newValue?.id);
}}
inputValue={inputValue}
@@ -69,14 +69,14 @@ export function MemberInput(p: {
sx={{ width: "100%" }}
filterOptions={(options, state) => {
const res = options.filter((m) =>
m?.fullName.toLowerCase().includes(state.inputValue)
m?.fullName.toLowerCase().includes(state.inputValue),
);
res.length = Math.min(20, res.length);
return res;
}}
getOptionLabel={(o) => o?.fullName ?? ""}
renderInput={(params) => <TextField {...params} label={p.label} />}
renderOption={(_props, option, _state) => (
renderOption={(_props, option) => (
<MemberItem
member={option}
onClick={() => p.onValueChange(option?.id)}

View File

@@ -34,7 +34,7 @@ export function BaseGenealogyRoute(): React.ReactElement {
setMembers(null);
setCouples(null);
return new Promise<void>((res, _rej) => {
return new Promise<void>((res) => {
loadPromise.current = () => res();
});
};
@@ -42,6 +42,7 @@ export function BaseGenealogyRoute(): React.ReactElement {
return (
<AsyncWidget
ready={members !== null && couples !== null}
// eslint-disable-next-line react-hooks/refs
loadKey={`${family.familyId}-${loadKey.current}`}
load={load}
errMsg="Échec du chargement des informations de généalogie de la famille !"
@@ -68,6 +69,7 @@ export function BaseGenealogyRoute(): React.ReactElement {
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useGenealogy(): GenealogyContext {
return React.useContext(GenealogyContextK)!;
}

View File

@@ -1,5 +1,5 @@
import { mdiXml } from "@mdi/js";
import Icon from "@mdi/react";
import { Icon } from "@mdi/react";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import { IconButton, Tooltip } from "@mui/material";
import jsPDF from "jspdf";
@@ -89,7 +89,7 @@ function center(container_width: number, el_width: number): number {
function buildSimpleTreeNode(
tree: FamilyTreeNode,
depth: number
depth: number,
): SimpleTreeNode {
if (depth === 0) throw new Error("Too much recursion reached!");
@@ -100,7 +100,7 @@ function buildSimpleTreeNode(
let childrenToProcess = tree.down;
if (depth > 1)
tree.couples?.forEach(
(c) => (childrenToProcess = childrenToProcess?.concat(c.down))
(c) => (childrenToProcess = childrenToProcess?.concat(c.down)),
);
else childrenToProcess = [];
@@ -156,7 +156,7 @@ export function SimpleFamilyTree(p: {
const tree = React.useMemo(
() => buildSimpleTreeNode(p.tree, p.depth),
[p.tree, p.depth]
[p.tree, p.depth],
);
const height = p.depth * (CARD_HEIGHT + LEVEL_SPACING) - LEVEL_SPACING;
@@ -250,44 +250,42 @@ function NodeArea(p: {
childrenLinkDestY?: number;
node: SimpleTreeNode;
}): React.ReactElement {
let parent_x_offset: number;
let pers1 = p.node.member;
let pers2 = p.node.spouse?.member;
let didSwap = false;
// Show male of the left (all the time)
if (pers2?.sex === "M") {
let s = pers1;
const s = pers1;
pers1 = pers2;
pers2 = s;
didSwap = true;
}
parent_x_offset = p.x + center(p.node.width, p.node.parentWidth);
const parent_x_offset = p.x + center(p.node.width, p.node.parentWidth);
let unusedChildrenWidth = p.node.width - p.node.childrenWidth;
const unusedChildrenWidth = p.node.width - p.node.childrenWidth;
let downXOffset = p.x + Math.floor(unusedChildrenWidth / 2);
let endFirstFaceX =
const endFirstFaceX =
parent_x_offset +
Math.floor((memberCardWidth(pers1) - FACE_WIDTH) / 2) +
FACE_WIDTH;
let beginingOfSecondCardX =
const beginingOfSecondCardX =
parent_x_offset + p.node.parentWidth - memberCardWidth(pers2);
let beginSecondFaceX =
const beginSecondFaceX =
p.node.spouse &&
beginingOfSecondCardX + (memberCardWidth(pers2) - FACE_WIDTH) / 2;
let middleParentFaceY = p.y + Math.floor(FACE_HEIGHT / 2);
const middleParentFaceY = p.y + Math.floor(FACE_HEIGHT / 2);
// Compute points for link between children and parent
let parentLinkX = didSwap
? beginSecondFaceX! + Math.floor(FACE_WIDTH / 2)
: parent_x_offset + Math.floor(memberCardWidth(pers1) / 2);
let parentLinkY = p.y;
const parentLinkY = p.y;
// Remove ugly little shifts
if (Math.abs(parentLinkX - (p.childrenLinkDestX ?? 0)) < 10)
@@ -301,7 +299,7 @@ function NodeArea(p: {
if (
pers2 &&
p.node.down.every(
(n) => n.member.father === pers1.id && n.member.mother !== pers2!.id
(n) => n.member.father === pers1.id && n.member.mother !== pers2!.id,
)
) {
childrenLinkX = parent_x_offset + Math.floor(memberCardWidth(pers1) / 2);
@@ -312,7 +310,7 @@ function NodeArea(p: {
else if (
pers2 &&
p.node.down.every(
(n) => n.member.father !== pers1.id && n.member.mother === pers2!.id
(n) => n.member.father !== pers1.id && n.member.mother === pers2!.id,
)
) {
childrenLinkX = beginSecondFaceX! + Math.floor(memberCardWidth(pers2) / 2);
@@ -371,6 +369,7 @@ function NodeArea(p: {
node={n}
/>
);
// eslint-disable-next-line react-hooks/immutability
downXOffset += n.width + SIBLINGS_SPACING;
return el;
})}
@@ -419,7 +418,7 @@ function MemberCard(p: {
<tspan
x={center(
w,
getTextWidth(p.member.lastNameUpperCase ?? "", NAME_FONT)
getTextWidth(p.member.lastNameUpperCase ?? "", NAME_FONT),
)}
dy="14"
font-size="13"
@@ -430,7 +429,7 @@ function MemberCard(p: {
<tspan
x={center(
w,
getTextWidth(p.member.displayBirthDeathShort, BIRTH_FONT)
getTextWidth(p.member.displayBirthDeathShort, BIRTH_FONT),
)}
dy="14"
font-size="10"

View File

@@ -1,3 +1,4 @@
target/
storage/
.idea
static/

1608
geneit_backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,14 +15,14 @@ anyhow = "1.0.102"
actix-web = "4.13.0"
actix-cors = "0.7.1"
actix-multipart = "0.7.2"
actix-remote-ip = "0.1.0"
actix-remote-ip = "1.0.0"
futures-util = "0.3.32"
diesel = { version = "2.3.7", features = ["postgres"] }
diesel_migrations = "2.3.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
mailchecker = "6.0.20"
redis = "0.32.7"
redis = "1.1.0"
lettre = "0.11.20"
rand = "0.10.0"
bcrypt = "0.19.0"
@@ -31,13 +31,16 @@ thiserror = "2.0.18"
serde_with = "3.18.0"
rust_iso3166 = "0.1.14"
rust-s3 = "0.37.1"
sha2 = "0.10.9"
sha2 = "0.11.0"
image = "0.25.10"
uuid = { version = "1.17.0", features = ["v4"] }
uuid = { version = "1.23.0", features = ["v4"] }
httpdate = "1.0.3"
zip = "4.6.1"
zip = "8.5.0"
mime_guess = "2.0.5"
tempfile = "3.27.0"
base64 = "0.22.1"
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
chrono = "0.4.44"
hex = "0.4.3"
rust-embed = { version = "8.11.0", features = ["mime-guess"] }
build-time = "0.1.3"

View File

@@ -1,10 +0,0 @@
#!/bin/bash
cargo build --release
TEMP_DIR=$(mktemp -d)
cp target/release/geneit_backend "$TEMP_DIR"
docker build -f Dockerfile "$TEMP_DIR" -t pierre42100/geneit_backend
rm -r $TEMP_DIR

View File

@@ -1,51 +1,150 @@
services:
minio:
image: minio/minio
rustfs:
image: rustfs/rustfs:1.0.0-alpha.90
user: "1000"
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment:
- MINIO_ROOT_USER=topsecret
- MINIO_ROOT_PASSWORD=topsecret
- RUSTFS_ACCESS_KEY=topsecret
- RUSTFS_SECRET_KEY=topsecret
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9090
- RUSTFS_VOLUMES=/data
volumes:
- ./storage/minio:/data
command: ["minio", "server", "/data", "--console-address", ":9090"]
- ./storage/rustfs:/data
ports:
- 9000:9000
- 9090:9090
- "9000:9000"
- "9090:9090"
expose:
- 9000
read_only: true
healthcheck:
test:
[
"CMD",
"sh", "-c",
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9090/rustfs/console/health"
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres
image: postgres:18
user: "1000"
restart: unless-stopped
ports:
- "5432:5432"
expose:
- 5432
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=geneit
- PGDATA=/data
read_only: true
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U user" ]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ./storage/db:/var/lib/postgresql/data
- ./storage/db:/data
tmpfs:
- /tmp
- /run/postgresql
mailcatcher:
image: dockage/mailcatcher:0.9.0
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
ports:
- 1080:1080
- 1025:1025
- "1080:1080"
- "1025:1025"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://127.0.0.1:1080 || exit 1
interval: 5m
timeout: 3s
retries: 3
start_period: 2m
redis:
image: redis:alpine
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
command: redis-server --requirepass ${REDIS_PASS:-secretredis}
ports:
- 6379:6379
- "6379:6379"
healthcheck:
test: [ "CMD-SHELL", "redis-cli --no-auth-warning --pass ${REDIS_PASS:-secretredis} ping | grep PONG" ]
interval: 1s
timeout: 3s
retries: 5
volumes:
- ./storage/redis-data:/data
- ./storage/redis-conf:/usr/local/etc/redis/redis.conf
oidc:
image: qlik/simple-oidc-provider
environment:
- REDIRECTS=http://localhost:3000/oidc_cb
- PORT=9001
image: dexidp/dex
user: "1000"
restart: unless-stopped
ports:
- 9001:9001
- "9001:9001"
command: [ "dex", "serve", "/conf/dex.config.yaml" ]
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
configs:
- source: dex-config
target: /conf/dex.config.yaml
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:9001/dex/healthz || exit 1
interval: 5m
timeout: 3s
retries: 3
start_period: 2m
configs:
dex-config:
content: |
issuer: http://127.0.0.1:9001/dex
storage:
type: memory
web:
http: 0.0.0.0:9001
oauth2:
skipApprovalScreen: false
connectors:
- type: mockCallback
id: mock
name: Example
staticClients:
- id: foo
secret: bar
redirectURIs:
- http://localhost:5173/oidc_cb
name: Project

View File

@@ -1,4 +1,5 @@
use clap::Parser;
use redis::{IntoConnectionInfo, RedisConnectionInfo};
use s3::creds::Credentials;
use s3::{Bucket, Region};
@@ -11,7 +12,7 @@ pub struct AppConfig {
pub listen_address: String,
/// Website origin
#[clap(short, long, env, default_value = "http://localhost:3000")]
#[clap(short, long, env, default_value = "http://localhost:5173")]
pub website_origin: String,
/// Proxy IP, might end with a star "*"
@@ -98,7 +99,7 @@ pub struct AppConfig {
#[arg(
long,
env,
default_value = "http://localhost:9001/.well-known/openid-configuration"
default_value = "http://localhost:9001/dex/.well-known/openid-configuration"
)]
pub oidc_configuration_url: String,
@@ -188,15 +189,16 @@ impl AppConfig {
/// Get Redis connection configuration
pub fn redis_connection_config(&self) -> redis::ConnectionInfo {
redis::ConnectionInfo {
addr: redis::ConnectionAddr::Tcp(self.redis_hostname.clone(), self.redis_port),
redis: redis::RedisConnectionInfo {
db: self.redis_db_number,
username: self.redis_username.clone(),
password: Some(self.redis_password.clone()),
protocol: Default::default(),
},
let mut settings = RedisConnectionInfo::default().set_db(self.redis_db_number);
if let Some(username) = self.redis_username.as_ref() {
settings = settings.set_username(username);
}
settings = settings.set_password(&self.redis_password);
redis::ConnectionAddr::Tcp(self.redis_hostname.clone(), self.redis_port)
.into_connection_info()
.expect("could not parse redis connection info!")
.set_redis_settings(settings)
}
/// Get password reset URL

View File

@@ -35,13 +35,15 @@ impl AccommodationRequest {
accommodation.name = self.name;
if let Some(d) = &self.description
&& !c.accommodation_description_len.validate(d) {
&& !c.accommodation_description_len.validate(d)
{
return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
}
accommodation.description.clone_from(&self.description);
if let Some(c) = &self.color
&& !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
&& !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c)
{
return Err(AccommodationListControllerErr::MalformedColor.into());
}
accommodation.color.clone_from(&self.color);

View File

@@ -49,17 +49,20 @@ impl CoupleRequest {
}
if let Some(husband) = self.husband
&& !members_service::exists(couple.family_id(), husband).await? {
&& !members_service::exists(couple.family_id(), husband).await?
{
return Err(CoupleControllerErr::HusbandNotExisting.into());
}
if let Some(d) = &self.wedding
&& !d.check() {
&& !d.check()
{
return Err(CoupleControllerErr::MalformedDateOfWedding.into());
}
if let Some(d) = &self.divorce
&& !d.check() {
&& !d.check()
{
return Err(CoupleControllerErr::MalformedDateOfDivorce.into());
}

View File

@@ -96,7 +96,8 @@ fn check_opt_str_val(
err: MemberControllerErr,
) -> anyhow::Result<()> {
if let Some(v) = val
&& !c.validate(v) {
&& !c.validate(v)
{
return Err(err.into());
}
Ok(())
@@ -151,7 +152,8 @@ impl MemberRequest {
)?;
if let Some(mail) = &self.email
&& !mailchecker::is_valid(mail) {
&& !mailchecker::is_valid(mail)
{
return Err(MemberControllerErr::InvalidEmailAddress.into());
}
@@ -186,17 +188,20 @@ impl MemberRequest {
)?;
if let Some(c) = &self.country
&& !countries_utils::is_code_valid(c) {
&& !countries_utils::is_code_valid(c)
{
return Err(MemberControllerErr::InvalidCountryCode.into());
}
if let Some(d) = &self.birth
&& !d.check() {
&& !d.check()
{
return Err(MemberControllerErr::MalformedDateOfBirth.into());
}
if let Some(d) = &self.death
&& !d.check() {
&& !d.check()
{
return Err(MemberControllerErr::MalformedDateOfDeath.into());
}
@@ -217,7 +222,8 @@ impl MemberRequest {
}
if let Some(father) = self.father
&& !members_service::exists(member.family_id(), father).await? {
&& !members_service::exists(member.family_id(), father).await?
{
return Err(MemberControllerErr::FatherNotExisting.into());
}

View File

@@ -16,6 +16,7 @@ pub mod members_controller;
pub mod photos_controller;
pub mod server_controller;
pub mod users_controller;
pub mod web_app_controller;
/// Custom error to ease controller writing
#[derive(Debug)]

View File

@@ -37,7 +37,8 @@ async fn get_photo(id: &PhotoIdPath, full_size: bool, req: HttpRequest) -> HttpR
// Check if an upload is un-necessary
if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
&& c.to_str().unwrap_or("") == hash {
&& c.to_str().unwrap_or("") == hash
{
return Ok(HttpResponse::NotModified().finish());
}

View File

@@ -5,11 +5,6 @@ use crate::utils::countries_utils;
use crate::utils::countries_utils::CountryCode;
use actix_web::{HttpResponse, Responder};
/// Default hello route
pub async fn home() -> impl Responder {
HttpResponse::Ok().json("GeneIT API service.")
}
#[derive(Debug, Clone, serde::Serialize)]
struct ServerConfig<'a> {
constraints: StaticConstraints,

View File

@@ -0,0 +1,75 @@
pub use serve_static::{root_index, serve_static_content};
/// Web asset hosting placeholder in debug mode
#[cfg(debug_assertions)]
mod serve_static {
use actix_web::{HttpResponse, Responder};
pub async fn root_index() -> impl Responder {
HttpResponse::Ok().body("Hello world! Debug=on for GeneIT!")
}
pub async fn serve_static_content() -> impl Responder {
HttpResponse::NotFound().body("Hello world! Static assets are not served in debug mode")
}
}
/// Web asset hosting in release mode
#[cfg(not(debug_assertions))]
mod serve_static {
use crate::utils::crypt_utils::sha256;
use crate::utils::time_utils;
use actix_web::http::header;
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use rust_embed::RustEmbed;
use std::cmp::max;
use std::ops::Add;
use std::time::Duration;
#[derive(RustEmbed)]
#[folder = "static/"]
struct WebAsset;
fn handle_embedded_file(path: &str, can_fallback: bool, req: HttpRequest) -> HttpResponse {
match (WebAsset::get(path), can_fallback) {
(Some(content), _) => {
let digest = hex::encode(sha256(content.data.as_ref()));
let file_time = max(time_utils::time_start_of_day(), time_utils::build_time());
// Check if the browser already knows the file by date
if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) {
let date_str = c.to_str().unwrap_or("");
if let Ok(date) = httpdate::parse_http_date(date_str)
&& date.add(Duration::from_secs(1))
>= time_utils::unix_to_system_time(file_time)
{
return HttpResponse::NotModified().finish();
}
}
// Check if the browser already knows the etag
if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
&& c.to_str().unwrap_or("") == digest
{
return HttpResponse::NotModified().finish();
}
HttpResponse::Ok()
.content_type(content.metadata.mimetype())
.insert_header(("etag", digest))
.insert_header(("last-modified", time_utils::unix_to_http_date(file_time)))
.body(content.data.into_owned())
}
(None, false) => HttpResponse::NotFound().body("404 Not Found"),
(None, true) => handle_embedded_file("index.html", false, req),
}
}
pub async fn root_index(req: HttpRequest) -> impl Responder {
handle_embedded_file("index.html", false, req)
}
pub async fn serve_static_content(req: HttpRequest, path: web::Path<String>) -> impl Responder {
handle_embedded_file(path.as_ref(), !path.as_ref().starts_with("static/"), req)
}
}

View File

@@ -9,7 +9,7 @@ use geneit_backend::controllers::{
accommodations_list_controller, accommodations_reservations_calendars_controller,
accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
families_controller, members_controller, photos_controller, server_controller,
users_controller,
users_controller, web_app_controller,
};
#[actix_web::main]
@@ -40,254 +40,259 @@ async fn main() -> std::io::Result<()> {
.max_age(3600),
)
.wrap(Logger::default())
.app_data(web::Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(),
}))
.app_data(web::Data::new(RemoteIPConfig::parse_opt(
AppConfig::get().proxy_ip.clone(),
)))
// Uploaded files
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
// Config controller
.route("/", web::get().to(server_controller::home))
.route(
"/server/config",
"/api/server/config",
web::get().to(server_controller::server_config),
)
// Auth controller
.route(
"/auth/create_account",
"/api/auth/create_account",
web::post().to(auth_controller::create_account),
)
.route(
"/auth/request_reset_password",
"/api/auth/request_reset_password",
web::post().to(auth_controller::request_reset_password),
)
.route(
"/auth/check_reset_password_token",
"/api/auth/check_reset_password_token",
web::post().to(auth_controller::check_reset_password_token),
)
.route(
"/auth/reset_password",
"/api/auth/reset_password",
web::post().to(auth_controller::reset_password),
)
.route(
"/auth/password_login",
"/api/auth/password_login",
web::post().to(auth_controller::password_login),
)
.route(
"/auth/start_openid_login",
"/api/auth/start_openid_login",
web::post().to(auth_controller::start_openid_login),
)
.route(
"/auth/finish_openid_login",
"/api/auth/finish_openid_login",
web::post().to(auth_controller::finish_openid_login),
)
.route("/auth/logout", web::get().to(auth_controller::logout))
.route("/api/auth/logout", web::get().to(auth_controller::logout))
// User controller
.route("/user/info", web::get().to(users_controller::auth_info))
.route("/api/user/info", web::get().to(users_controller::auth_info))
.route(
"/user/update_profile",
"/api/user/update_profile",
web::post().to(users_controller::update_profile),
)
.route(
"/user/replace_password",
"/api/user/replace_password",
web::post().to(users_controller::replace_password),
)
.route(
"/user/request_delete",
"/api/user/request_delete",
web::get().to(users_controller::request_delete_account),
)
.route(
"/user/check_delete_token",
"/api/user/check_delete_token",
web::post().to(users_controller::check_delete_token),
)
.route(
"/user/delete_account",
"/api/user/delete_account",
web::post().to(users_controller::delete_account),
)
// Families controller
.route(
"/family/create",
"/api/family/create",
web::post().to(families_controller::create),
)
.route("/family/join", web::post().to(families_controller::join))
.route("/family/list", web::get().to(families_controller::list))
.route("/api/family/join", web::post().to(families_controller::join))
.route("/api/family/list", web::get().to(families_controller::list))
.route(
"/family/{id}",
"/api/family/{id}",
web::get().to(families_controller::single_info),
)
.route(
"/family/{id}/leave",
"/api/family/{id}/leave",
web::post().to(families_controller::leave),
)
.route("/family/{id}", web::patch().to(families_controller::update))
.route("/api/family/{id}", web::patch().to(families_controller::update))
.route(
"/family/{id}",
"/api/family/{id}",
web::delete().to(families_controller::delete),
)
.route(
"/family/{id}/renew_invitation_code",
"/api/family/{id}/renew_invitation_code",
web::post().to(families_controller::renew_invitation_code),
)
.route(
"/family/{id}/users",
"/api/family/{id}/users",
web::get().to(families_controller::users),
)
.route(
"/family/{id}/user/{user_id}",
"/api/family/{id}/user/{user_id}",
web::patch().to(families_controller::update_membership),
)
.route(
"/family/{id}/user/{user_id}",
"/api/family/{id}/user/{user_id}",
web::delete().to(families_controller::delete_membership),
)
// [GENEALOGY] Members controller
.route(
"/family/{id}/genealogy/member/create",
"/api/family/{id}/genealogy/member/create",
web::post().to(members_controller::create),
)
.route(
"/family/{id}/genealogy/members",
"/api/family/{id}/genealogy/members",
web::get().to(members_controller::get_all),
)
.route(
"/family/{id}/genealogy/member/{member_id}",
"/api/family/{id}/genealogy/member/{member_id}",
web::get().to(members_controller::get_single),
)
.route(
"/family/{id}/genealogy/member/{member_id}",
"/api/family/{id}/genealogy/member/{member_id}",
web::put().to(members_controller::update),
)
.route(
"/family/{id}/genealogy/member/{member_id}",
"/api/family/{id}/genealogy/member/{member_id}",
web::delete().to(members_controller::delete),
)
.route(
"/family/{id}/genealogy/member/{member_id}/photo",
"/api/family/{id}/genealogy/member/{member_id}/photo",
web::put().to(members_controller::set_photo),
)
.route(
"/family/{id}/genealogy/member/{member_id}/photo",
"/api/family/{id}/genealogy/member/{member_id}/photo",
web::delete().to(members_controller::remove_photo),
)
// [GENEALOGY] Couples controller
.route(
"/family/{id}/genealogy/couple/create",
"/api/family/{id}/genealogy/couple/create",
web::post().to(couples_controller::create),
)
.route(
"/family/{id}/genealogy/couples",
"/api/family/{id}/genealogy/couples",
web::get().to(couples_controller::get_all),
)
.route(
"/family/{id}/genealogy/couple/{couple_id}",
"/api/family/{id}/genealogy/couple/{couple_id}",
web::get().to(couples_controller::get_single),
)
.route(
"/family/{id}/genealogy/couple/{couple_id}",
"/api/family/{id}/genealogy/couple/{couple_id}",
web::put().to(couples_controller::update),
)
.route(
"/family/{id}/genealogy/couple/{couple_id}",
"/api/family/{id}/genealogy/couple/{couple_id}",
web::delete().to(couples_controller::delete),
)
.route(
"/family/{id}/genealogy/couple/{couple_id}/photo",
"/api/family/{id}/genealogy/couple/{couple_id}/photo",
web::put().to(couples_controller::set_photo),
)
.route(
"/family/{id}/genealogy/couple/{couple_id}/photo",
"/api/family/{id}/genealogy/couple/{couple_id}/photo",
web::delete().to(couples_controller::remove_photo),
)
// [GENEALOGY] Data controller
.route(
"/family/{id}/genealogy/data/export",
"/api/family/{id}/genealogy/data/export",
web::get().to(data_controller::export_family),
)
.route(
"/family/{id}/genealogy/data/import",
"/api/family/{id}/genealogy/data/import",
web::put().to(data_controller::import_family),
)
// [ACCOMODATIONS] List controller
.route(
"/family/{id}/accommodations/list/create",
"/api/family/{id}/accommodations/list/create",
web::post().to(accommodations_list_controller::create),
)
.route(
"/family/{id}/accommodations/list/list",
"/api/family/{id}/accommodations/list/list",
web::get().to(accommodations_list_controller::get_full_list),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
"/api/family/{id}/accommodations/list/{accommodation_id}",
web::get().to(accommodations_list_controller::get_single),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
"/api/family/{id}/accommodations/list/{accommodation_id}",
web::put().to(accommodations_list_controller::update),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
"/api/family/{id}/accommodations/list/{accommodation_id}",
web::delete().to(accommodations_list_controller::delete),
)
// [ACCOMODATIONS] Reservations controller
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
"/api/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
web::get()
.to(accommodations_reservations_controller::get_accommodation_reservations),
)
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
"/api/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
web::get()
.to(accommodations_reservations_controller::get_accommodation_reservations_for_interval),
)
.route(
"/family/{id}/accommodations/reservations/full_list",
"/api/family/{id}/accommodations/reservations/full_list",
web::get().to(accommodations_reservations_controller::full_list),
)
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/create",
"/api/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/create",
web::post().to(accommodations_reservations_controller::create_reservation),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
"/api/family/{id}/accommodations/reservation/{reservation_id}",
web::get().to(accommodations_reservations_controller::get_single),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
"/api/family/{id}/accommodations/reservation/{reservation_id}",
web::patch().to(accommodations_reservations_controller::update_single),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
"/api/family/{id}/accommodations/reservation/{reservation_id}",
web::delete().to(accommodations_reservations_controller::delete),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}/validate",
"/api/family/{id}/accommodations/reservation/{reservation_id}/validate",
web::post().to(accommodations_reservations_controller::validate_or_reject),
)
// [ACCOMMODATIONS] Calendars controller
.route(
"/family/{id}/accommodations/reservations_calendars/create",
"/api/family/{id}/accommodations/reservations_calendars/create",
web::post().to(accommodations_reservations_calendars_controller::create),
)
.route(
"/family/{id}/accommodations/reservations_calendars/list",
"/api/family/{id}/accommodations/reservations_calendars/list",
web::get().to(accommodations_reservations_calendars_controller::get_list),
)
.route(
"/family/{id}/accommodations/reservations_calendars/{cal_id}",
"/api/family/{id}/accommodations/reservations_calendars/{cal_id}",
web::delete().to(accommodations_reservations_calendars_controller::delete),
)
.route(
"/acccommodations_calendar/{token}",
"/api/acccommodations_calendar/{token}",
web::get().to(accommodations_reservations_calendars_controller::anonymous_access),
)
// Photos controller
.route(
"/photo/{id}",
"/api/photo/{id}",
web::get().to(photos_controller::get_full_size),
)
.route(
"/photo/{id}/thumbnail",
"/api/photo/{id}/thumbnail",
web::get().to(photos_controller::get_thumbnail),
)
// Static web app controller
.route("/", web::get().to(web_app_controller::root_index))
.route(
"/{tail:.*}",
web::get().to(web_app_controller::serve_static_content),
)
})
.bind(AppConfig::get().listen_address.as_str())?
.run()

View File

@@ -150,7 +150,8 @@ pub mod loop_detection {
impl LoopStack<'_> {
pub fn contains(&self, id: MemberID) -> bool {
if let Some(ls) = &self.prev
&& ls.contains(id) {
&& ls.contains(id)
{
return true;
}

View File

@@ -1,17 +1,11 @@
use sha2::{Digest, Sha256, Sha512};
/// Compute hash of a slice of bytes
pub fn sha256(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let h = hasher.finalize();
format!("{h:x}")
/// Compute SHA256sum of a given slice of bytes
pub fn sha256(input: &[u8]) -> String {
hex::encode(Sha256::digest(input))
}
/// Compute hash of a slice of bytes (sha512)
pub fn sha512(bytes: &[u8]) -> String {
let mut hasher = Sha512::new();
hasher.update(bytes);
let h = hasher.finalize();
format!("{h:x}")
/// Compute SHA512sum of a given slice of bytes
pub fn sha512(input: &[u8]) -> String {
hex::encode(Sha512::digest(input))
}

View File

@@ -1,6 +1,7 @@
//! # Time utilities
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::{DateTime, Local, NaiveTime};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Get the current time since epoch
pub fn time() -> u64 {
@@ -9,3 +10,29 @@ pub fn time() -> u64 {
.unwrap()
.as_secs()
}
/// Convert UNIX time to system time
pub fn unix_to_system_time(time: u64) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(time)
}
/// Get build time in UNIX format
pub fn build_time() -> u64 {
let build_time = build_time::build_time_local!();
let date =
chrono::DateTime::parse_from_rfc3339(build_time).expect("Failed to parse compile date");
date.timestamp() as u64
}
/// Get the first second of the day (local time)
pub fn time_start_of_day() -> u64 {
let local: DateTime<Local> = Local::now()
.with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
.unwrap();
local.timestamp() as u64
}
/// Format UNIX time to HTTP date
pub fn unix_to_http_date(time: u64) -> String {
httpdate::fmt_http_date(unix_to_system_time(time))
}