1 Commits

Author SHA1 Message Date
c08b348d56 Update Rust crate uuid to 1.22.0
Some checks failed
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2026-04-02 00:26:05 +00:00
73 changed files with 2671 additions and 2567 deletions

View File

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

View File

@@ -1,15 +0,0 @@
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,8 +10,7 @@
2. Start services: 2. Start services:
```bash ```bash
cd geneit_backend cd geneit_backend
mkdir -p storage/{rustfs,db,redis-data,redis-conf} docker-compose up
docker compose up
``` ```
@@ -23,7 +22,7 @@ cargo install diesel_cli --no-default-features --features postgres
``` ```
4. Initialize database manually (or it will be done automatically when the backend is started): 4. Initialize database:
```bash ```bash
diesel migration run diesel migration run
@@ -35,9 +34,19 @@ diesel migration run
> PGPASSWORD=pass psql -h localhost -p 5432 -U user -d geneit > PGPASSWORD=pass psql -h localhost -p 5432 -U user -d geneit
> ``` > ```
## Test OIDC credentials
Emails:
Useful links: ```
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
```
* Mailcatcher: http://localhost:1080/ Password: `Password1!`
* 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/api VITE_APP_BACKEND=http://localhost:8000

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,10 +14,10 @@ import React from "react";
import { ServerApi } from "../api/ServerApi"; import { ServerApi } from "../api/ServerApi";
import { ReplacePasswordResponse, User, UserApi } from "../api/UserApi"; import { ReplacePasswordResponse, User, UserApi } from "../api/UserApi";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { formatDate } from "../utils/time_utils";
import { useUser } from "../widgets/BaseAuthenticatedPage"; import { useUser } from "../widgets/BaseAuthenticatedPage";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { PasswordInput } from "../widgets/PasswordInput"; import { PasswordInput } from "../widgets/PasswordInput";
import { formatDate } from "../widgets/TimeWidget";
export function ProfileRoute(): React.ReactElement { export function ProfileRoute(): React.ReactElement {
const user = useUser(); const user = useUser();
@@ -172,7 +172,7 @@ function ChangePasswordCard(): React.ReactElement {
break; break;
case ReplacePasswordResponse.TooManyRequests: case ReplacePasswordResponse.TooManyRequests:
setError( 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; break;
} }
@@ -257,7 +257,7 @@ function DeleteAccountButton(): React.ReactElement {
if ( if (
!(await confirm( !(await confirm(
"Voulez-vous initier la suppression de votre compte ?", "Voulez-vous initier la suppression de votre compte ?",
"Suppression de compte", "Suppression de compte"
)) ))
) )
return; return;
@@ -265,7 +265,7 @@ function DeleteAccountButton(): React.ReactElement {
await UserApi.RequestAccountDeletion(); await UserApi.RequestAccountDeletion();
await alert( 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) { } catch (e) {
console.error(e); console.error(e);

View File

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

View File

@@ -14,9 +14,9 @@ import { FamilyApi } from "../../api/FamilyApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../api/ServerApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
import { formatDate } from "../../utils/time_utils";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../widgets/FamilyCard"; import { FamilyCard } from "../../widgets/FamilyCard";
import { formatDate } from "../../widgets/TimeWidget";
export function FamilySettingsRoute(): React.ReactElement { export function FamilySettingsRoute(): React.ReactElement {
const alert = useAlert(); const alert = useAlert();
@@ -29,7 +29,7 @@ export function FamilySettingsRoute(): React.ReactElement {
try { try {
if ( if (
!(await confirm( !(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; return;
@@ -69,10 +69,10 @@ function FamilySettingsCard(): React.ReactElement {
const [newName, setNewName] = React.useState(family.family.name); const [newName, setNewName] = React.useState(family.family.name);
const [enableGenealogy, setEnableGenealogy] = React.useState( const [enableGenealogy, setEnableGenealogy] = React.useState(
family.family.enable_genealogy, family.family.enable_genealogy
); );
const [enableAccommodations, setEnableAccommodations] = React.useState( const [enableAccommodations, setEnableAccommodations] = React.useState(
family.family.enable_accommodations, family.family.enable_accommodations
); );
const canEdit = family.family.is_admin; const canEdit = family.family.is_admin;

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ function AccommodationsListCard(): React.ReactElement {
need_validation: false, need_validation: false,
color: "2196f3", color: "2196f3",
}, },
true, true
); );
if (!accommodation) return; if (!accommodation) return;
@@ -112,7 +112,7 @@ function AccommodationsListCard(): React.ReactElement {
try { try {
if ( if (
!(await confirm( !(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; return;
@@ -258,7 +258,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
try { try {
if ( if (
!(await confirm( !(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; return;
@@ -287,7 +287,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
const cal = await AccommodationsCalendarURLApi.Create( const cal = await AccommodationsCalendarURLApi.Create(
family.family, family.family,
newCal, newCal
); );
setSuccess("Le calendrier a été créé avec succès !"); setSuccess("Le calendrier a été créé avec succès !");
@@ -337,7 +337,6 @@ function AccommodationsCalURLsCard(): React.ReactElement {
<AsyncWidget <AsyncWidget
ready={list !== undefined} ready={list !== undefined}
// eslint-disable-next-line react-hooks/refs
loadKey={key.current} loadKey={key.current}
load={load} load={load}
errMsg="Echec du chargement de la liste des calendriers !" errMsg="Echec du chargement de la liste des calendriers !"

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/immutability */
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
@@ -112,7 +111,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
try { try {
if ( if (
!(await confirm( !(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; return;
@@ -131,7 +130,6 @@ export function FamilyCoupleRoute(): React.ReactElement {
return ( return (
<AsyncWidget <AsyncWidget
// eslint-disable-next-line react-hooks/refs
loadKey={`${coupleId}-${count.current}`} loadKey={`${coupleId}-${count.current}`}
load={load} load={load}
ready={couple !== undefined} ready={couple !== undefined}
@@ -232,7 +230,7 @@ export function CouplePage(p: {
const [changed, setChanged] = React.useState(false); const [changed, setChanged] = React.useState(false);
const [couple, setCouple] = React.useState( const [couple, setCouple] = React.useState(
new Couple(structuredClone(p.couple)), new Couple(structuredClone(p.couple))
); );
const updatedCouple = () => { const updatedCouple = () => {
@@ -242,7 +240,7 @@ export function CouplePage(p: {
const save = async () => { const save = async () => {
loadingMessage.show( loadingMessage.show(
"Enregistrement des informations du couple en cours...", "Enregistrement des informations du couple en cours..."
); );
await p.onSave!(couple); await p.onSave!(couple);
loadingMessage.hide(); loadingMessage.hide();
@@ -252,7 +250,7 @@ export function CouplePage(p: {
if ( if (
changed && changed &&
!(await confirm( !(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; return;
@@ -493,7 +491,7 @@ export function CouplePage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( 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> <Button>Nouveau</Button>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
const canvas: HTMLCanvasElement = document.createElement("canvas"); let canvas: HTMLCanvasElement = document.createElement("canvas");
const charLen: Map<string, Map<string, number>> = new Map(); let 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. * 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): number[] { export function getAllIndexes(s: string, val: string) {
const indexes = []; var indexes = [],
let i = -1; i = -1;
while ((i = s.indexOf(val, i + 1)) !== -1) { while ((i = s.indexOf(val, i + 1)) !== -1) {
indexes.push(i); indexes.push(i);
} }

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import {
mdiPlus, mdiPlus,
mdiRefresh, mdiRefresh,
} from "@mdi/js"; } from "@mdi/js";
import { Icon } from "@mdi/react"; import Icon from "@mdi/react";
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
import HomeIcon from "@mui/icons-material/Home"; import HomeIcon from "@mui/icons-material/Home";
import { import {
@@ -65,7 +65,7 @@ export function BaseFamilyRoute(): React.ReactElement {
loadKey.current += 1; loadKey.current += 1;
setFamily(null); setFamily(null);
return new Promise<void>((res) => { return new Promise<void>((res, _rej) => {
loadPromise.current = () => res(); loadPromise.current = () => res();
}); });
}; };
@@ -79,7 +79,7 @@ export function BaseFamilyRoute(): React.ReactElement {
try { try {
if ( if (
!(await confirm( !(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; return;
@@ -98,7 +98,6 @@ export function BaseFamilyRoute(): React.ReactElement {
return ( return (
<AsyncWidget <AsyncWidget
ready={family !== null} ready={family !== null}
// eslint-disable-next-line react-hooks/refs
loadKey={`${familyId}-${loadKey.current}`} loadKey={`${familyId}-${loadKey.current}`}
load={load} load={load}
errMsg="Échec du chargement des informations de la famille !" errMsg="Échec du chargement des informations de la famille !"
@@ -288,7 +287,6 @@ export function BaseFamilyRoute(): React.ReactElement {
); );
} }
// eslint-disable-next-line react-refresh/only-export-components
export function useFamily(): FamilyContext { export function useFamily(): FamilyContext {
return React.useContext(FamilyContextK)!; return React.useContext(FamilyContextK)!;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,58 @@
import { Tooltip } from "@mui/material"; import { Tooltip } from "@mui/material";
import { formatDate, timeDiffFromNow } from "../utils/time_utils"; 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));
}
export function TimeWidget(p: { time: number }): React.ReactElement { export function TimeWidget(p: { time: number }): React.ReactElement {
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,10 @@
#!/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,150 +1,51 @@
services: services:
rustfs: minio:
image: rustfs/rustfs:1.0.0-alpha.90 image: minio/minio
user: "1000"
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment: environment:
- RUSTFS_ACCESS_KEY=topsecret - MINIO_ROOT_USER=topsecret
- RUSTFS_SECRET_KEY=topsecret - MINIO_ROOT_PASSWORD=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: volumes:
- ./storage/rustfs:/data - ./storage/minio:/data
command: ["minio", "server", "/data", "--console-address", ":9090"]
ports: ports:
- "9000:9000" - 9000:9000
- "9090:9090" - 9090:9090
expose: expose:
- 9000 - 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: db:
image: postgres:18 image: postgres
user: "1000"
restart: unless-stopped
ports: ports:
- "5432:5432" - "5432:5432"
expose: expose:
- 5432 - 5432
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment: environment:
- POSTGRES_USER=user - POSTGRES_USER=user
- POSTGRES_PASSWORD=pass - POSTGRES_PASSWORD=pass
- POSTGRES_DB=geneit - POSTGRES_DB=geneit
- PGDATA=/data
read_only: true
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U user" ]
interval: 10s
timeout: 5s
retries: 5
volumes: volumes:
- ./storage/db:/data - ./storage/db:/var/lib/postgresql/data
tmpfs:
- /tmp
- /run/postgresql
mailcatcher: mailcatcher:
image: dockage/mailcatcher:0.9.0 image: dockage/mailcatcher:0.9.0
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
ports: ports:
- "1080:1080" - 1080:1080
- "1025:1025" - 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: redis:
image: redis:alpine 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} command: redis-server --requirepass ${REDIS_PASS:-secretredis}
ports: 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: volumes:
- ./storage/redis-data:/data - ./storage/redis-data:/data
- ./storage/redis-conf:/usr/local/etc/redis/redis.conf - ./storage/redis-conf:/usr/local/etc/redis/redis.conf
oidc: oidc:
image: dexidp/dex image: qlik/simple-oidc-provider
user: "1000" environment:
restart: unless-stopped - REDIRECTS=http://localhost:3000/oidc_cb
- PORT=9001
ports: 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,5 +1,4 @@
use clap::Parser; use clap::Parser;
use redis::{IntoConnectionInfo, RedisConnectionInfo};
use s3::creds::Credentials; use s3::creds::Credentials;
use s3::{Bucket, Region}; use s3::{Bucket, Region};
@@ -12,7 +11,7 @@ pub struct AppConfig {
pub listen_address: String, pub listen_address: String,
/// Website origin /// Website origin
#[clap(short, long, env, default_value = "http://localhost:5173")] #[clap(short, long, env, default_value = "http://localhost:3000")]
pub website_origin: String, pub website_origin: String,
/// Proxy IP, might end with a star "*" /// Proxy IP, might end with a star "*"
@@ -99,7 +98,7 @@ pub struct AppConfig {
#[arg( #[arg(
long, long,
env, env,
default_value = "http://localhost:9001/dex/.well-known/openid-configuration" default_value = "http://localhost:9001/.well-known/openid-configuration"
)] )]
pub oidc_configuration_url: String, pub oidc_configuration_url: String,
@@ -189,16 +188,15 @@ impl AppConfig {
/// Get Redis connection configuration /// Get Redis connection configuration
pub fn redis_connection_config(&self) -> redis::ConnectionInfo { pub fn redis_connection_config(&self) -> redis::ConnectionInfo {
let mut settings = RedisConnectionInfo::default().set_db(self.redis_db_number); redis::ConnectionInfo {
if let Some(username) = self.redis_username.as_ref() { addr: redis::ConnectionAddr::Tcp(self.redis_hostname.clone(), self.redis_port),
settings = settings.set_username(username); redis: redis::RedisConnectionInfo {
db: self.redis_db_number,
username: self.redis_username.clone(),
password: Some(self.redis_password.clone()),
protocol: Default::default(),
},
} }
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 /// Get password reset URL

View File

@@ -35,17 +35,15 @@ impl AccommodationRequest {
accommodation.name = self.name; accommodation.name = self.name;
if let Some(d) = &self.description if let Some(d) = &self.description
&& !c.accommodation_description_len.validate(d) && !c.accommodation_description_len.validate(d) {
{ return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
return Err(AccommodationListControllerErr::InvalidDescriptionLength.into()); }
}
accommodation.description.clone_from(&self.description); accommodation.description.clone_from(&self.description);
if let Some(c) = &self.color 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());
return Err(AccommodationListControllerErr::MalformedColor.into()); }
}
accommodation.color.clone_from(&self.color); accommodation.color.clone_from(&self.color);
accommodation.need_validation = self.need_validation; accommodation.need_validation = self.need_validation;

View File

@@ -49,22 +49,19 @@ impl CoupleRequest {
} }
if let Some(husband) = self.husband 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());
return Err(CoupleControllerErr::HusbandNotExisting.into()); }
}
if let Some(d) = &self.wedding if let Some(d) = &self.wedding
&& !d.check() && !d.check() {
{ return Err(CoupleControllerErr::MalformedDateOfWedding.into());
return Err(CoupleControllerErr::MalformedDateOfWedding.into()); }
}
if let Some(d) = &self.divorce if let Some(d) = &self.divorce
&& !d.check() && !d.check() {
{ return Err(CoupleControllerErr::MalformedDateOfDivorce.into());
return Err(CoupleControllerErr::MalformedDateOfDivorce.into()); }
}
couple.set_wife(self.wife); couple.set_wife(self.wife);
couple.set_husband(self.husband); couple.set_husband(self.husband);

View File

@@ -96,10 +96,9 @@ fn check_opt_str_val(
err: MemberControllerErr, err: MemberControllerErr,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let Some(v) = val if let Some(v) = val
&& !c.validate(v) && !c.validate(v) {
{ return Err(err.into());
return Err(err.into()); }
}
Ok(()) Ok(())
} }
@@ -152,10 +151,9 @@ impl MemberRequest {
)?; )?;
if let Some(mail) = &self.email if let Some(mail) = &self.email
&& !mailchecker::is_valid(mail) && !mailchecker::is_valid(mail) {
{ return Err(MemberControllerErr::InvalidEmailAddress.into());
return Err(MemberControllerErr::InvalidEmailAddress.into()); }
}
check_opt_str_val( check_opt_str_val(
&self.phone, &self.phone,
@@ -188,22 +186,19 @@ impl MemberRequest {
)?; )?;
if let Some(c) = &self.country if let Some(c) = &self.country
&& !countries_utils::is_code_valid(c) && !countries_utils::is_code_valid(c) {
{ return Err(MemberControllerErr::InvalidCountryCode.into());
return Err(MemberControllerErr::InvalidCountryCode.into()); }
}
if let Some(d) = &self.birth if let Some(d) = &self.birth
&& !d.check() && !d.check() {
{ return Err(MemberControllerErr::MalformedDateOfBirth.into());
return Err(MemberControllerErr::MalformedDateOfBirth.into()); }
}
if let Some(d) = &self.death if let Some(d) = &self.death
&& !d.check() && !d.check() {
{ return Err(MemberControllerErr::MalformedDateOfDeath.into());
return Err(MemberControllerErr::MalformedDateOfDeath.into()); }
}
check_opt_str_val( check_opt_str_val(
&self.note, &self.note,
@@ -222,10 +217,9 @@ impl MemberRequest {
} }
if let Some(father) = self.father 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());
return Err(MemberControllerErr::FatherNotExisting.into()); }
}
member.first_name = self.first_name; member.first_name = self.first_name;
member.last_name = self.last_name; member.last_name = self.last_name;

View File

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

View File

@@ -37,10 +37,9 @@ async fn get_photo(id: &PhotoIdPath, full_size: bool, req: HttpRequest) -> HttpR
// Check if an upload is un-necessary // Check if an upload is un-necessary
if let Some(c) = req.headers().get(header::IF_NONE_MATCH) 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());
return Ok(HttpResponse::NotModified().finish()); }
}
if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) { if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) {
let date_str = c.to_str().unwrap_or(""); let date_str = c.to_str().unwrap_or("");
@@ -51,9 +50,9 @@ async fn get_photo(id: &PhotoIdPath, full_size: bool, req: HttpRequest) -> HttpR
.unwrap() .unwrap()
.as_secs() .as_secs()
>= photo.time_create as u64 >= photo.time_create as u64
{ {
return Ok(HttpResponse::NotModified().finish()); return Ok(HttpResponse::NotModified().finish());
} }
} }
let bytes = s3_connection::get_file(&match full_size { let bytes = s3_connection::get_file(&match full_size {

View File

@@ -5,6 +5,11 @@ use crate::utils::countries_utils;
use crate::utils::countries_utils::CountryCode; use crate::utils::countries_utils::CountryCode;
use actix_web::{HttpResponse, Responder}; 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)] #[derive(Debug, Clone, serde::Serialize)]
struct ServerConfig<'a> { struct ServerConfig<'a> {
constraints: StaticConstraints, constraints: StaticConstraints,

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
//! # Time utilities //! # Time utilities
use chrono::{DateTime, Local, NaiveTime}; use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Get the current time since epoch /// Get the current time since epoch
pub fn time() -> u64 { pub fn time() -> u64 {
@@ -10,29 +9,3 @@ pub fn time() -> u64 {
.unwrap() .unwrap()
.as_secs() .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))
}