Compare commits
1 Commits
20260402
...
renovate/u
| Author | SHA1 | Date | |
|---|---|---|---|
| c08b348d56 |
77
.drone.yml
77
.drone.yml
@@ -4,69 +4,32 @@ type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: web_build
|
||||
image: node:25
|
||||
volumes:
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
commands:
|
||||
- cd geneit_app
|
||||
- npm install
|
||||
- npm run lint
|
||||
- npm run build
|
||||
- mv dist /tmp/web_build
|
||||
|
||||
- name: backend_check
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
commands:
|
||||
- apt update && apt install -y cmake
|
||||
- rustup component add clippy
|
||||
- cd geneit_backend
|
||||
- cargo clippy -- -D warnings
|
||||
- cargo test
|
||||
|
||||
- name: backend_compile
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
path: /usr/local/cargo/registry
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
- name: release
|
||||
path: /tmp/release
|
||||
depends_on:
|
||||
- backend_check
|
||||
- web_build
|
||||
commands:
|
||||
- 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
|
||||
- name: app_deploy
|
||||
image: node:24
|
||||
environment:
|
||||
PLUGIN_API_KEY:
|
||||
from_secret: API_KEY # needs repository read & write access
|
||||
settings:
|
||||
base_url: https://gitea.communiquons.org
|
||||
files: /tmp/release/*
|
||||
checksum: sha512
|
||||
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
temp: {}
|
||||
- name: web_app
|
||||
temp: {}
|
||||
- name: release
|
||||
temp: {}
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: AWS_SECRET_ACCESS_KEY
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
commands:
|
||||
# Build website
|
||||
- cd geneit_app
|
||||
- npm install
|
||||
- GENERATE_SOURCEMAP=false npm run build
|
||||
# Install AWS
|
||||
- curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
||||
- unzip awscliv2.zip
|
||||
- ./aws/install
|
||||
- aws configure set default.s3.signature_version s3v4
|
||||
# Upload to bucket
|
||||
- bash upload_bucket.sh
|
||||
|
||||
15
Makefile
15
Makefile
@@ -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)
|
||||
23
README.md
23
README.md
@@ -10,8 +10,7 @@
|
||||
2. Start services:
|
||||
```bash
|
||||
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
|
||||
diesel migration run
|
||||
@@ -35,9 +34,19 @@ diesel migration run
|
||||
> 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/
|
||||
* Rustfs console: http://localhost:9090 (credentials: `topsecret` / `topsecret`)
|
||||
* Dex OpenID configuration: http://127.0.0.1:9001/dex/.well-known/openid-configuration
|
||||
Password: `Password1!`
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_APP_BACKEND=http://localhost:8000/api
|
||||
VITE_APP_BACKEND=http://localhost:8000
|
||||
@@ -1 +1 @@
|
||||
VITE_APP_BACKEND=/api
|
||||
VITE_APP_BACKEND=https://geneit-backend.communiquons.org
|
||||
|
||||
2279
geneit_app/package-lock.json
generated
2279
geneit_app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,17 +21,17 @@
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@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/x-data-grid": "^8.28.2",
|
||||
"@mui/x-data-grid": "^8.28.1",
|
||||
"@mui/x-date-pickers": "^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",
|
||||
"email-validator": "^2.0.4",
|
||||
"filesize": "^11.0.15",
|
||||
"jspdf": "^4.2.1",
|
||||
"mui-color-input": "^8.0.0",
|
||||
"jspdf": "^3.0.4",
|
||||
"mui-color-input": "^7.0.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-easy-crop": "^5.5.7",
|
||||
@@ -41,16 +41,16 @@
|
||||
"svg2pdf.js": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-80b1cab3-20260331",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.3"
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.57.1",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +152,8 @@ export function App(): React.ReactElement {
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
)}
|
||||
</>,
|
||||
),
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -163,7 +163,6 @@ export function App(): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useAuth(): AuthContext {
|
||||
return React.useContext(AuthContextK)!;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { AuthApi } from "./AuthApi";
|
||||
|
||||
interface APIResponse {
|
||||
data: unknown;
|
||||
data: any;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: number,
|
||||
public data: unknown,
|
||||
) {
|
||||
constructor(message: string, public code: number, public data: any) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -40,11 +36,11 @@ export class APIClient {
|
||||
uri: string;
|
||||
method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
|
||||
allowFail?: boolean;
|
||||
jsonData?: unknown;
|
||||
jsonData?: any;
|
||||
formData?: FormData;
|
||||
}): Promise<APIResponse> {
|
||||
let body = undefined;
|
||||
const headers: { [k: string]: string } = {
|
||||
let headers: any = {
|
||||
"X-auth-token": AuthApi.SignedIn ? AuthApi.AuthToken : "none",
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export class AuthApi {
|
||||
*/
|
||||
static async CreateAccount(
|
||||
name: string,
|
||||
mail: string,
|
||||
mail: string
|
||||
): Promise<CreateAccountResult> {
|
||||
const res = await APIClient.exec({
|
||||
uri: "/auth/create_account",
|
||||
@@ -77,7 +77,7 @@ export class AuthApi {
|
||||
*/
|
||||
static async LoginWithPassword(
|
||||
mail: string,
|
||||
password: string,
|
||||
password: string
|
||||
): Promise<PasswordLoginResult> {
|
||||
const res = await APIClient.exec({
|
||||
uri: "/auth/password_login",
|
||||
@@ -96,10 +96,7 @@ export class AuthApi {
|
||||
return PasswordLoginResult.InvalidCredentials;
|
||||
case 200:
|
||||
case 201:
|
||||
localStorage.setItem(
|
||||
TokenStateKey,
|
||||
(res.data as { token: string }).token,
|
||||
);
|
||||
localStorage.setItem(TokenStateKey, res.data.token);
|
||||
return PasswordLoginResult.Success;
|
||||
default:
|
||||
return PasswordLoginResult.Error;
|
||||
@@ -118,20 +115,20 @@ export class AuthApi {
|
||||
method: "POST",
|
||||
jsonData: { provider: id },
|
||||
})
|
||||
).data as { url: string };
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish OpenID login
|
||||
*/
|
||||
static async FinishOpenIDLogin(code: string, state: string): Promise<void> {
|
||||
const res = (
|
||||
const res: { user_id: number; token: string } = (
|
||||
await APIClient.exec({
|
||||
uri: "/auth/finish_openid_login",
|
||||
method: "POST",
|
||||
jsonData: { code: code, state: state },
|
||||
})
|
||||
).data as { user_id: number; token: string };
|
||||
).data;
|
||||
|
||||
localStorage.setItem(TokenStateKey, res.token);
|
||||
}
|
||||
@@ -170,7 +167,7 @@ export class AuthApi {
|
||||
* Check reset password token
|
||||
*/
|
||||
static async CheckResetPasswordToken(
|
||||
token: string,
|
||||
token: string
|
||||
): Promise<CheckResetTokenResponse> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
@@ -178,7 +175,7 @@ export class AuthApi {
|
||||
method: "POST",
|
||||
jsonData: { token: token },
|
||||
})
|
||||
).data as CheckResetTokenResponse;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +183,7 @@ export class AuthApi {
|
||||
*/
|
||||
static async ResetPassword(
|
||||
token: string,
|
||||
newPassword: string,
|
||||
newPassword: string
|
||||
): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: "/auth/reset_password",
|
||||
|
||||
@@ -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 {
|
||||
public disable_couple_photos: boolean;
|
||||
public enable_genealogy: boolean;
|
||||
public enable_accommodations: boolean;
|
||||
constructor(p: ExtendedFamilyInfoAPI) {
|
||||
constructor(p: any) {
|
||||
super(p);
|
||||
this.disable_couple_photos = p.disable_couple_photos;
|
||||
this.enable_genealogy = p.enable_genealogy;
|
||||
@@ -161,13 +155,12 @@ export class FamilyApi {
|
||||
* Get the list of families
|
||||
*/
|
||||
static async GetList(): Promise<Family[]> {
|
||||
const res = (
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: "/family/list",
|
||||
})
|
||||
).data as FamilyAPI[];
|
||||
return res.map((f: FamilyAPI) => new Family(f));
|
||||
).data.map((f: FamilyAPI) => new Family(f));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +172,7 @@ export class FamilyApi {
|
||||
uri: `/family/${id}`,
|
||||
});
|
||||
|
||||
return new ExtendedFamilyInfo(res.data as ExtendedFamilyInfoAPI);
|
||||
return new ExtendedFamilyInfo(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,7 +204,7 @@ export class FamilyApi {
|
||||
method: "GET",
|
||||
uri: `/family/${id}/users`,
|
||||
})
|
||||
).data as FamilyUser[];
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,7 +74,7 @@ export class ServerApi {
|
||||
uri: "/server/config",
|
||||
method: "GET",
|
||||
})
|
||||
).data as ServerConfig;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,7 @@ export class UserApi {
|
||||
uri: "/user/info",
|
||||
method: "GET",
|
||||
})
|
||||
).data as User;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,7 @@ export class UserApi {
|
||||
*/
|
||||
static async ReplacePassword(
|
||||
oldPwd: string,
|
||||
newPwd: string,
|
||||
newPwd: string
|
||||
): Promise<ReplacePasswordResponse> {
|
||||
const res = await APIClient.exec({
|
||||
uri: "/user/replace_password",
|
||||
@@ -98,7 +98,7 @@ export class UserApi {
|
||||
* Check delete account token
|
||||
*/
|
||||
static async CheckDeleteAccountToken(
|
||||
token: string,
|
||||
token: string
|
||||
): Promise<DeleteAccountTokenInfo> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
@@ -106,7 +106,7 @@ export class UserApi {
|
||||
method: "POST",
|
||||
jsonData: { token: token },
|
||||
})
|
||||
).data as DeleteAccountTokenInfo;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,7 +26,7 @@ export class AccommodationsList {
|
||||
}
|
||||
|
||||
this.list.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase()),
|
||||
a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class AccommodationListApi {
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/list/list`,
|
||||
})
|
||||
).data as Accommodation[];
|
||||
).data;
|
||||
|
||||
return new AccommodationsList(data);
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export class AccommodationListApi {
|
||||
*/
|
||||
static async Create(
|
||||
family: Family,
|
||||
accommodation: UpdateAccommodation,
|
||||
accommodation: UpdateAccommodation
|
||||
): Promise<Accommodation> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
@@ -91,7 +91,7 @@ export class AccommodationListApi {
|
||||
uri: `/family/${family.family_id}/accommodations/list/create`,
|
||||
jsonData: accommodation,
|
||||
})
|
||||
).data as Accommodation;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +99,7 @@ export class AccommodationListApi {
|
||||
*/
|
||||
static async Update(
|
||||
accommodation: Accommodation,
|
||||
update: UpdateAccommodation,
|
||||
update: UpdateAccommodation
|
||||
): Promise<Accommodation> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
@@ -107,7 +107,7 @@ export class AccommodationListApi {
|
||||
uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`,
|
||||
jsonData: update,
|
||||
})
|
||||
).data as Accommodation;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +119,6 @@ export class AccommodationListApi {
|
||||
method: "DELETE",
|
||||
uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`,
|
||||
})
|
||||
).data as Accommodation;
|
||||
).data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class AccommodationsCalendarURLApi {
|
||||
*/
|
||||
static async Create(
|
||||
family: Family,
|
||||
calendar: NewCalendarURL,
|
||||
calendar: NewCalendarURL
|
||||
): Promise<AccommodationCalendarURL> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
@@ -31,7 +31,7 @@ export class AccommodationsCalendarURLApi {
|
||||
uri: `/family/${family.family_id}/accommodations/reservations_calendars/create`,
|
||||
jsonData: calendar,
|
||||
})
|
||||
).data as AccommodationCalendarURL;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,20 +50,20 @@ export class AccommodationsCalendarURLApi {
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations_calendars/list`,
|
||||
})
|
||||
).data as AccommodationCalendarURL[];
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an accommodation calendar
|
||||
*/
|
||||
static async Delete(
|
||||
calendar: AccommodationCalendarURL,
|
||||
calendar: AccommodationCalendarURL
|
||||
): Promise<AccommodationCalendarURL> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "DELETE",
|
||||
uri: `/family/${calendar.family_id}/accommodations/reservations_calendars/${calendar.id}`,
|
||||
})
|
||||
).data as AccommodationCalendarURL;
|
||||
).data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class AccommodationsReservationsList {
|
||||
}
|
||||
|
||||
filter(
|
||||
predicate: (m: AccommodationReservation) => boolean,
|
||||
predicate: (m: AccommodationReservation) => boolean
|
||||
): AccommodationReservation[] {
|
||||
return this.list.filter(predicate);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export class AccommodationsReservationsApi {
|
||||
*/
|
||||
static async Create(
|
||||
family: Family,
|
||||
reservation: UpdateAccommodationReservation,
|
||||
reservation: UpdateAccommodationReservation
|
||||
): Promise<AccommodationReservation> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
@@ -86,21 +86,21 @@ export class AccommodationsReservationsApi {
|
||||
end: reservation.end,
|
||||
},
|
||||
})
|
||||
).data as AccommodationReservation;
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entire list of accommodations of a family
|
||||
*/
|
||||
static async FullListOfFamily(
|
||||
family: Family,
|
||||
family: Family
|
||||
): Promise<AccommodationsReservationsList> {
|
||||
const data = (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations/full_list`,
|
||||
})
|
||||
).data as AccommodationReservation[];
|
||||
).data;
|
||||
|
||||
return new AccommodationsReservationsList(data);
|
||||
}
|
||||
@@ -112,14 +112,14 @@ export class AccommodationsReservationsApi {
|
||||
family: Family,
|
||||
accommodation: Accommodation,
|
||||
start: number,
|
||||
end: number,
|
||||
end: number
|
||||
): Promise<AccommodationsReservationsList> {
|
||||
const data = (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations/accommodation/${accommodation.id}/for_interval?start=${start}&end=${end}`,
|
||||
})
|
||||
).data as AccommodationReservation[];
|
||||
).data;
|
||||
|
||||
return new AccommodationsReservationsList(data);
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export class AccommodationsReservationsApi {
|
||||
*/
|
||||
static async Update(
|
||||
family: Family,
|
||||
r: UpdateAccommodationReservation,
|
||||
r: UpdateAccommodationReservation
|
||||
): Promise<void> {
|
||||
await APIClient.exec({
|
||||
method: "PATCH",
|
||||
@@ -156,7 +156,7 @@ export class AccommodationsReservationsApi {
|
||||
*/
|
||||
static async Validate(
|
||||
r: AccommodationReservation,
|
||||
accept: boolean,
|
||||
accept: boolean
|
||||
): Promise<ValidateResaResult> {
|
||||
const res = await APIClient.exec({
|
||||
method: "POST",
|
||||
|
||||
@@ -166,7 +166,7 @@ export class CoupleApi {
|
||||
jsonData: m,
|
||||
});
|
||||
|
||||
return new Couple(res.data as CoupleApiInterface);
|
||||
return new Couple(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,28 +174,26 @@ export class CoupleApi {
|
||||
*/
|
||||
static async GetSingle(
|
||||
family_id: number,
|
||||
couple_id: number,
|
||||
couple_id: number
|
||||
): Promise<Couple> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/couple/${couple_id}`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new Couple(res.data as CoupleApiInterface);
|
||||
return new Couple(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entire list of couples of a family
|
||||
*/
|
||||
static async GetEntireList(family_id: number): Promise<CouplesList> {
|
||||
const res = (
|
||||
await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/couples`,
|
||||
method: "GET",
|
||||
})
|
||||
).data as CoupleApiInterface[];
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/couples`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new CouplesList(res.map((d) => new Couple(d)));
|
||||
return new CouplesList(res.data.map((d: any) => new Couple(d)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ export class DataApi {
|
||||
uri: `/family/${family_id}/genealogy/data/export`,
|
||||
method: "GET",
|
||||
});
|
||||
return res.data as Blob;
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,6 +26,6 @@ export class DataApi {
|
||||
method: "PUT",
|
||||
formData: fd,
|
||||
});
|
||||
return res.data as Blob;
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export class Member implements MemberDataApi {
|
||||
const firstName = this.first_name ?? "";
|
||||
|
||||
return firstName.length === 0
|
||||
? (this.last_name ?? "")
|
||||
? this.last_name ?? ""
|
||||
: `${firstName} ${this.last_name?.toUpperCase() ?? ""}`;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export class Member implements MemberDataApi {
|
||||
const lastName = this.last_name ?? "";
|
||||
|
||||
return lastName.length === 0
|
||||
? (this.last_name ?? "")
|
||||
? this.last_name ?? ""
|
||||
: `${lastName} ${this.first_name ?? ""}`;
|
||||
}
|
||||
|
||||
@@ -164,14 +164,14 @@ export class Member implements MemberDataApi {
|
||||
}
|
||||
|
||||
get displayBirthDeath(): string {
|
||||
const birthDeath = [];
|
||||
let birthDeath = [];
|
||||
if (this.dateOfBirth) birthDeath.push(fmtDate(this.dateOfBirth));
|
||||
if (this.dateOfDeath) birthDeath.push(fmtDate(this.dateOfDeath));
|
||||
return birthDeath.join(" - ");
|
||||
}
|
||||
|
||||
get displayBirthDeathShort(): string {
|
||||
const birthDeath = [];
|
||||
let birthDeath = [];
|
||||
if (this.birth_year) birthDeath.push(this.birth_year.toString());
|
||||
if (this.death_year) birthDeath.push(this.death_year.toString());
|
||||
return birthDeath.join(" - ");
|
||||
@@ -226,7 +226,7 @@ export class MembersList {
|
||||
this.list.sort((a, b) =>
|
||||
a.invertedFullName
|
||||
.toLowerCase()
|
||||
.localeCompare(b.invertedFullName.toLocaleLowerCase()),
|
||||
.localeCompare(b.invertedFullName.toLocaleLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ export class MembersList {
|
||||
childrenOfCouple(c: Couple): Member[] {
|
||||
if (!c.husband && !c.wife) return [];
|
||||
return this.list.filter(
|
||||
(m) => m.mother === c.wife && m.father === c.husband,
|
||||
(m) => m.mother === c.wife && m.father === c.husband
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export class MembersList {
|
||||
(m) =>
|
||||
m.id !== p?.id &&
|
||||
((m.mother && m.mother === p?.mother) ||
|
||||
(m.father && m.father === p?.father)),
|
||||
(m.father && m.father === p?.father))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -277,15 +277,13 @@ export class MemberApi {
|
||||
* Create a new member
|
||||
*/
|
||||
static async Create(m: Member): Promise<Member> {
|
||||
const res = (
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/member/create`,
|
||||
method: "POST",
|
||||
jsonData: m,
|
||||
})
|
||||
).data as MemberDataApi;
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/member/create`,
|
||||
method: "POST",
|
||||
jsonData: m,
|
||||
});
|
||||
|
||||
return new Member(res);
|
||||
return new Member(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,30 +291,26 @@ export class MemberApi {
|
||||
*/
|
||||
static async GetSingle(
|
||||
family_id: number,
|
||||
member_id: number,
|
||||
member_id: number
|
||||
): Promise<Member> {
|
||||
const res = (
|
||||
await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/member/${member_id}`,
|
||||
method: "GET",
|
||||
})
|
||||
).data as MemberDataApi;
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/member/${member_id}`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new Member(res);
|
||||
return new Member(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entire list of family members of a family
|
||||
*/
|
||||
static async GetEntireList(family_id: number): Promise<MembersList> {
|
||||
const res = (
|
||||
await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/members`,
|
||||
method: "GET",
|
||||
})
|
||||
).data as MemberDataApi[];
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/members`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new MembersList(res.map((d) => new Member(d)));
|
||||
return new MembersList(res.data.map((d: any) => new Member(d)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,11 +27,11 @@ export function UpdateAccommodationDialog(p: {
|
||||
|
||||
const nameErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_name_len,
|
||||
accommodation?.name,
|
||||
accommodation?.name
|
||||
);
|
||||
const descriptionErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_description_len,
|
||||
accommodation?.description,
|
||||
accommodation?.description
|
||||
);
|
||||
|
||||
const clearForm = () => {
|
||||
@@ -49,9 +49,7 @@ export function UpdateAccommodationDialog(p: {
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (!accommodation) setAccommodation(p.accommodation);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [p.open, p.accommodation]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -54,13 +54,10 @@ export function UpdateReservationDialog(p: {
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (!reservation) setReservation(p.reservation);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [p.open, p.reservation]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setConflicts(undefined);
|
||||
(async () => {
|
||||
try {
|
||||
@@ -80,21 +77,20 @@ export function UpdateReservationDialog(p: {
|
||||
family.family,
|
||||
accommodations.accommodations.get(reservation.accommodation_id)!,
|
||||
reservation.start,
|
||||
reservation.end,
|
||||
reservation.end
|
||||
)
|
||||
).filter(
|
||||
(r) =>
|
||||
r.id !== p.reservation?.reservation_id && r.validated !== false,
|
||||
),
|
||||
r.id !== p.reservation?.reservation_id && r.validated !== false
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(
|
||||
"Echec de la vérification de la présence de conflits de calendrier !",
|
||||
"Echec de la vérification de la présence de conflits de calendrier !"
|
||||
);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
p.open,
|
||||
reservation?.accommodation_id,
|
||||
@@ -122,7 +118,7 @@ export function UpdateReservationDialog(p: {
|
||||
options={accommodations.accommodations.openToReservationList.map(
|
||||
(a) => {
|
||||
return { label: a.name, value: a.id.toString() };
|
||||
},
|
||||
}
|
||||
)}
|
||||
value={
|
||||
reservation?.accommodation_id === -1
|
||||
|
||||
@@ -63,7 +63,6 @@ export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useAlert(): AlertContext {
|
||||
return React.useContext(AlertContextK)!;
|
||||
}
|
||||
|
||||
@@ -11,20 +11,20 @@ import React, { PropsWithChildren } from "react";
|
||||
type ConfirmContext = (
|
||||
message: string,
|
||||
title?: string,
|
||||
confirmButton?: string,
|
||||
confirmButton?: string
|
||||
) => Promise<boolean>;
|
||||
|
||||
const ConfirmContextK = React.createContext<ConfirmContext | null>(null);
|
||||
|
||||
export function ConfirmDialogProvider(
|
||||
p: PropsWithChildren,
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const [title, setTitle] = React.useState<string | undefined>(undefined);
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [confirmButton, setConfirmButton] = React.useState<string | undefined>(
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
const cb = React.useRef<null | ((a: boolean) => void)>(null);
|
||||
@@ -78,7 +78,6 @@ export function ConfirmDialogProvider(
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useConfirm(): ConfirmContext {
|
||||
return React.useContext(ConfirmContextK)!;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const darkTheme = createTheme(
|
||||
mode: "dark",
|
||||
},
|
||||
},
|
||||
dataGridFr,
|
||||
dataGridFr
|
||||
);
|
||||
|
||||
const lightTheme = createTheme(
|
||||
@@ -20,7 +20,7 @@ const lightTheme = createTheme(
|
||||
mode: "light",
|
||||
},
|
||||
},
|
||||
dataGridFr,
|
||||
dataGridFr
|
||||
);
|
||||
|
||||
interface DarkThemeContext {
|
||||
@@ -32,7 +32,7 @@ const DarkThemeContextK = React.createContext<DarkThemeContext | null>(null);
|
||||
|
||||
export function DarkThemeProvider(p: PropsWithChildren): React.ReactElement {
|
||||
const [enabled, setEnabled] = React.useState(
|
||||
localStorage.getItem(localStorageKey) === "true",
|
||||
localStorage.getItem(localStorageKey) === "true"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -52,7 +52,6 @@ export function DarkThemeProvider(p: PropsWithChildren): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useDarkTheme(): DarkThemeContext {
|
||||
return React.useContext(DarkThemeContextK)!;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const LoadingMessageContextK =
|
||||
React.createContext<LoadingMessageContext | null>(null);
|
||||
|
||||
export function LoadingMessageProvider(
|
||||
p: PropsWithChildren,
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
@@ -59,7 +59,6 @@ export function LoadingMessageProvider(
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useLoadingMessage(): LoadingMessageContext {
|
||||
return React.useContext(LoadingMessageContextK)!;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ export function SnackbarProvider(p: PropsWithChildren): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useSnackbar(): SnackbarContext {
|
||||
return React.useContext(SnackbarContextK)!;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ type DialogContext = () => Promise<NewCalendarURL | undefined>;
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function CreateAccommodationCalendarURLDialogProvider(
|
||||
p: PropsWithChildren,
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const cb = React.useRef<null | ((a: NewCalendarURL | undefined) => void)>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
const handleClose = (res?: NewCalendarURL) => {
|
||||
@@ -47,7 +47,6 @@ export function CreateAccommodationCalendarURLDialogProvider(
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useCreateAccommodationCalendarURL(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ type DialogContext = (cal: AccommodationCalendarURL) => Promise<void>;
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function InstallCalendarDialogProvider(
|
||||
p: PropsWithChildren,
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [cal, setCal] = React.useState<AccommodationCalendarURL | undefined>();
|
||||
|
||||
@@ -39,7 +39,6 @@ export function InstallCalendarDialogProvider(
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useInstallCalendarDialog(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import { UpdateAccommodationDialog } from "../../../dialogs/accommodations/Updat
|
||||
|
||||
type DialogContext = (
|
||||
accommodation: UpdateAccommodation,
|
||||
create: boolean,
|
||||
create: boolean
|
||||
) => Promise<UpdateAccommodation | undefined>;
|
||||
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function UpdateAccommodationDialogProvider(
|
||||
p: PropsWithChildren,
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
@@ -59,7 +59,6 @@ export function UpdateAccommodationDialogProvider(
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useUpdateAccommodation(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import { UpdateReservationDialog } from "../../../dialogs/accommodations/UpdateR
|
||||
|
||||
type DialogContext = (
|
||||
reservation: UpdateAccommodationReservation,
|
||||
create: boolean,
|
||||
create: boolean
|
||||
) => Promise<UpdateAccommodationReservation | undefined>;
|
||||
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function UpdateReservationDialogProvider(
|
||||
p: PropsWithChildren,
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
@@ -59,7 +59,6 @@ export function UpdateReservationDialogProvider(
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useUpdateAccommodationReservation(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
|
||||
@@ -48,13 +48,12 @@ export function FamiliesListRoute(): React.ReactElement {
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={families !== null}
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={`families-list-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Echec du chargement de la liste des familles"
|
||||
build={() => (
|
||||
<>
|
||||
{families!.length === 0 ? (
|
||||
{families!!.length === 0 ? (
|
||||
<NoFamilyWidget
|
||||
onRequestCreateFamily={onRequestCreateFamily}
|
||||
onRequestJoinFamily={onRequestJoinFamily}
|
||||
@@ -191,7 +190,7 @@ function FamilyCard(p: {
|
||||
if (
|
||||
!p.f.CanLeave ||
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment quitter la famille " + p.f.name + " ?",
|
||||
"Voulez-vous vraiment quitter la famille " + p.f.name + " ?"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -201,7 +200,7 @@ function FamilyCard(p: {
|
||||
p.onFamilyLeft();
|
||||
|
||||
alert(
|
||||
`La famille ${p.f.name} a été retirée de votre liste de familles !`,
|
||||
`La famille ${p.f.name} a été retirée de votre liste de familles !`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -14,10 +14,10 @@ import React from "react";
|
||||
import { ServerApi } from "../api/ServerApi";
|
||||
import { ReplacePasswordResponse, User, UserApi } from "../api/UserApi";
|
||||
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { formatDate } from "../utils/time_utils";
|
||||
import { useUser } from "../widgets/BaseAuthenticatedPage";
|
||||
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { PasswordInput } from "../widgets/PasswordInput";
|
||||
import { formatDate } from "../widgets/TimeWidget";
|
||||
|
||||
export function ProfileRoute(): React.ReactElement {
|
||||
const user = useUser();
|
||||
@@ -172,7 +172,7 @@ function ChangePasswordCard(): React.ReactElement {
|
||||
break;
|
||||
case ReplacePasswordResponse.TooManyRequests:
|
||||
setError(
|
||||
"Trop de tentatives de changement de mot de passe, veuillez réessayer ultérieurement !",
|
||||
"Trop de tentatives de changement de mot de passe, veuillez réessayer ultérieurement !"
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -257,7 +257,7 @@ function DeleteAccountButton(): React.ReactElement {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous initier la suppression de votre compte ?",
|
||||
"Suppression de compte",
|
||||
"Suppression de compte"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -265,7 +265,7 @@ function DeleteAccountButton(): React.ReactElement {
|
||||
await UserApi.RequestAccountDeletion();
|
||||
|
||||
await alert(
|
||||
"Demande de suppression de compte enregistrée avec succès. Veuillez consulter votre boîte mail.",
|
||||
"Demande de suppression de compte enregistrée avec succès. Veuillez consulter votre boîte mail."
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -40,7 +40,7 @@ export function LoginRoute(): React.ReactElement {
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
|
||||
const handleMouseDownPassword = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
@@ -55,7 +55,7 @@ export function LoginRoute(): React.ReactElement {
|
||||
switch (res) {
|
||||
case PasswordLoginResult.TooManyRequests:
|
||||
setError(
|
||||
"Trop de tentatives de connection. Veuillez réessayer ultérieurement.",
|
||||
"Trop de tentatives de connection. Veuillez réessayer ultérieurement."
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -84,7 +84,6 @@ export function LoginRoute(): React.ReactElement {
|
||||
setLoading(true);
|
||||
|
||||
const res = await AuthApi.StartOpenIDLogin(id);
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
window.location.href = res.url;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -14,9 +14,9 @@ import { FamilyApi } from "../../api/FamilyApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { formatDate } from "../../utils/time_utils";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { FamilyCard } from "../../widgets/FamilyCard";
|
||||
import { formatDate } from "../../widgets/TimeWidget";
|
||||
|
||||
export function FamilySettingsRoute(): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
@@ -29,7 +29,7 @@ export function FamilySettingsRoute(): React.ReactElement {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment supprimer cette famille, et toute les données qui s'y rattachent ? Cette opération est absolument irréversible !",
|
||||
"Voulez-vous vraiment supprimer cette famille, et toute les données qui s'y rattachent ? Cette opération est absolument irréversible !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -69,10 +69,10 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
|
||||
const [newName, setNewName] = React.useState(family.family.name);
|
||||
const [enableGenealogy, setEnableGenealogy] = React.useState(
|
||||
family.family.enable_genealogy,
|
||||
family.family.enable_genealogy
|
||||
);
|
||||
const [enableAccommodations, setEnableAccommodations] = React.useState(
|
||||
family.family.enable_accommodations,
|
||||
family.family.enable_accommodations
|
||||
);
|
||||
|
||||
const canEdit = family.family.is_admin;
|
||||
|
||||
@@ -38,7 +38,6 @@ export function FamilyUsersListRoute(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={`${family.family.family_id}-${key.current}`}
|
||||
errMsg="Echec du chargement de la liste des utilisateurs de la famille !"
|
||||
load={load}
|
||||
@@ -63,7 +62,7 @@ function UsersTable(p: {
|
||||
const family = useFamily();
|
||||
|
||||
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
const handleEditClick = (id: GridRowId) => () => {
|
||||
@@ -80,7 +79,7 @@ function UsersTable(p: {
|
||||
const user = p.users.find((u) => u.user_id === id)!;
|
||||
if (
|
||||
!(await confirm(
|
||||
`Voulez-vous vraiment retirer ${user.user_name} de cette famille ?`,
|
||||
`Voulez-vous vraiment retirer ${user.user_name} de cette famille ?`
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -140,7 +139,7 @@ function UsersTable(p: {
|
||||
label="Save"
|
||||
material={{
|
||||
sx: {
|
||||
color: "primary.main",
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
onClick={handleSaveClick(id)}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
const [showPending, setShowPending] = React.useState(true);
|
||||
|
||||
const [hiddenPeople, setHiddenPeople] = React.useState<Set<number>>(
|
||||
new Set(),
|
||||
new Set()
|
||||
);
|
||||
const [hiddenAccommodations, setHiddenAccommodations] = React.useState<
|
||||
Set<number>
|
||||
@@ -100,7 +100,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
|
||||
const load = async () => {
|
||||
setReservations(
|
||||
await AccommodationsReservationsApi.FullListOfFamily(family.family),
|
||||
await AccommodationsReservationsApi.FullListOfFamily(family.family)
|
||||
);
|
||||
setUsers(await FamilyApi.GetUsersList(family.family.family_id));
|
||||
};
|
||||
@@ -136,7 +136,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
start: Math.floor(d.start.getTime() / 1000),
|
||||
end: Math.floor(d.end.getTime() / 1000),
|
||||
},
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
if (!resa) return;
|
||||
@@ -157,7 +157,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
|
||||
const onEventClick = (ev: EventClickArg) => {
|
||||
const id: number = ev.event.extendedProps.id;
|
||||
const resa = reservations!.get(id)!;
|
||||
const resa = reservations?.get(id)!;
|
||||
const acc = accommodations.accommodations.get(resa.accommodation_id)!;
|
||||
|
||||
const user = users?.find((u) => u.user_id === resa.user_id);
|
||||
@@ -182,7 +182,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
|
||||
const respondToResaRequest = async (
|
||||
r: AccommodationReservation,
|
||||
validate: boolean,
|
||||
validate: boolean
|
||||
) => {
|
||||
try {
|
||||
loadingMessage.show("Validation de la réservation en cours...");
|
||||
@@ -194,7 +194,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
|
||||
if (res === ValidateResaResult.Conflict) {
|
||||
throw new Error(
|
||||
"The reservation is in conflict with other reservations!",
|
||||
"The reservation is in conflict with other reservations!"
|
||||
);
|
||||
} else if (res === ValidateResaResult.Error) {
|
||||
throw new Error("Failed to validate the reservation!");
|
||||
@@ -217,7 +217,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
const rejectReservation = async (r: AccommodationReservation) => {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment rejeter cette demande de réservation ?",
|
||||
"Voulez-vous vraiment rejeter cette demande de réservation ?"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -230,7 +230,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
if (
|
||||
ac?.need_validation &&
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment changer cette réservation ? Celle-ci devra être de nouveau validée !",
|
||||
"Voulez-vous vraiment changer cette réservation ? Celle-ci devra être de nouveau validée !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -242,7 +242,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
start: r.reservation_start,
|
||||
end: r.reservation_end,
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
if (!newResa) return;
|
||||
@@ -266,7 +266,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment supprimer cette réservation ? L'opération n'est pas réversible !",
|
||||
"Voulez-vous vraiment supprimer cette réservation ? L'opération n'est pas réversible !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -290,7 +290,6 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
<>
|
||||
<FamilyPageTitle title="Réservations" />
|
||||
<AsyncWidget
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={loadKey.current}
|
||||
load={load}
|
||||
errMsg="Echec du chargement de la liste des réservations !"
|
||||
@@ -366,7 +365,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
if (v) hiddenAccommodations.delete(a.id);
|
||||
else hiddenAccommodations.add(a.id);
|
||||
setHiddenAccommodations(
|
||||
new Set(hiddenAccommodations),
|
||||
new Set(hiddenAccommodations)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -423,7 +422,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
eventClick={onEventClick}
|
||||
events={visibleReservations?.map((r) => {
|
||||
const a = accommodations.accommodations.get(
|
||||
r.accommodation_id,
|
||||
r.accommodation_id
|
||||
)!;
|
||||
const u = users?.find((u) => u.user_id === r.user_id);
|
||||
return {
|
||||
@@ -436,8 +435,8 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
r.validated === true
|
||||
? "green"
|
||||
: r.validated === false
|
||||
? "red"
|
||||
: "grey ",
|
||||
? "red"
|
||||
: "grey ",
|
||||
extendedProps: {
|
||||
id: r.id,
|
||||
},
|
||||
@@ -494,12 +493,12 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
<p>
|
||||
Du{" "}
|
||||
{fmtUnixDate(
|
||||
activeEvent?.reservation.reservation_start ?? 0,
|
||||
activeEvent?.reservation.reservation_start ?? 0
|
||||
)}{" "}
|
||||
<br />
|
||||
Au{" "}
|
||||
{fmtUnixDate(
|
||||
activeEvent?.reservation.reservation_end ?? 0,
|
||||
activeEvent?.reservation.reservation_end ?? 0
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -66,7 +66,7 @@ function AccommodationsListCard(): React.ReactElement {
|
||||
need_validation: false,
|
||||
color: "2196f3",
|
||||
},
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
if (!accommodation) return;
|
||||
@@ -112,7 +112,7 @@ function AccommodationsListCard(): React.ReactElement {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
`Voulez-vous vraiment supprimer le logement '${a.name}' ? Cette opération est définitive !`,
|
||||
`Voulez-vous vraiment supprimer le logement '${a.name}' ? Cette opération est définitive !`
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -258,7 +258,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
`Voulez-vous vraiment supprimer le calendrier '${c.name}' ? Cette opération est définitive !`,
|
||||
`Voulez-vous vraiment supprimer le calendrier '${c.name}' ? Cette opération est définitive !`
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -287,7 +287,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
|
||||
|
||||
const cal = await AccommodationsCalendarURLApi.Create(
|
||||
family.family,
|
||||
newCal,
|
||||
newCal
|
||||
);
|
||||
|
||||
setSuccess("Le calendrier a été créé avec succès !");
|
||||
@@ -337,7 +337,6 @@ function AccommodationsCalURLsCard(): React.ReactElement {
|
||||
|
||||
<AsyncWidget
|
||||
ready={list !== undefined}
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={key.current}
|
||||
load={load}
|
||||
errMsg="Echec du chargement de la liste des calendriers !"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react-hooks/immutability */
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
@@ -112,7 +111,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment supprimer cette fiche de couple ? L'opération n'est pas réversible !",
|
||||
"Voulez-vous vraiment supprimer cette fiche de couple ? L'opération n'est pas réversible !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -131,7 +130,6 @@ export function FamilyCoupleRoute(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={`${coupleId}-${count.current}`}
|
||||
load={load}
|
||||
ready={couple !== undefined}
|
||||
@@ -232,7 +230,7 @@ export function CouplePage(p: {
|
||||
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const [couple, setCouple] = React.useState(
|
||||
new Couple(structuredClone(p.couple)),
|
||||
new Couple(structuredClone(p.couple))
|
||||
);
|
||||
|
||||
const updatedCouple = () => {
|
||||
@@ -242,7 +240,7 @@ export function CouplePage(p: {
|
||||
|
||||
const save = async () => {
|
||||
loadingMessage.show(
|
||||
"Enregistrement des informations du couple en cours...",
|
||||
"Enregistrement des informations du couple en cours..."
|
||||
);
|
||||
await p.onSave!(couple);
|
||||
loadingMessage.hide();
|
||||
@@ -252,7 +250,7 @@ export function CouplePage(p: {
|
||||
if (
|
||||
changed &&
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !",
|
||||
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -493,7 +491,7 @@ export function CouplePage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`,
|
||||
`genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable react-hooks/immutability */
|
||||
import { mdiFamilyTree } from "@mdi/js";
|
||||
import { Icon } from "@mdi/react";
|
||||
import Icon from "@mdi/react";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
@@ -126,7 +125,7 @@ export function FamilyMemberRoute(): React.ReactElement {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment supprimer cette fiche membre ? L'opération n'est pas réversible !",
|
||||
"Voulez-vous vraiment supprimer cette fiche membre ? L'opération n'est pas réversible !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -145,7 +144,6 @@ export function FamilyMemberRoute(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={`${memberId}-${count.current}`}
|
||||
load={load}
|
||||
ready={member !== undefined}
|
||||
@@ -252,7 +250,7 @@ export function MemberPage(p: {
|
||||
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const [member, setMember] = React.useState(
|
||||
new Member(structuredClone(p.member)),
|
||||
new Member(structuredClone(p.member))
|
||||
);
|
||||
|
||||
const updatedMember = () => {
|
||||
@@ -262,7 +260,7 @@ export function MemberPage(p: {
|
||||
|
||||
const save = async () => {
|
||||
loadingMessage.show(
|
||||
"Enregistrement des informations du membre en cours...",
|
||||
"Enregistrement des informations du membre en cours..."
|
||||
);
|
||||
await p.onSave!(member);
|
||||
loadingMessage.hide();
|
||||
@@ -272,7 +270,7 @@ export function MemberPage(p: {
|
||||
if (
|
||||
changed &&
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !",
|
||||
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -668,7 +666,7 @@ export function MemberPage(p: {
|
||||
to={family.family.URL(
|
||||
`genealogy/couple/create?${
|
||||
member.sex === "F" ? "wife" : "husband"
|
||||
}=${member.id}`,
|
||||
}=${member.id}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@@ -697,7 +695,7 @@ export function MemberPage(p: {
|
||||
to={family.family.URL(
|
||||
`genealogy/member/create?${
|
||||
member.sex === "F" ? "mother" : "father"
|
||||
}=${member.id}`,
|
||||
}=${member.id}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@@ -725,7 +723,7 @@ export function MemberPage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`genealogy/member/create?mother=${member.mother}&father=${member.father}`,
|
||||
`genealogy/member/create?mother=${member.mother}&father=${member.father}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@@ -750,7 +748,7 @@ function CoupleItem(p: {
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const statusStr = ServerApi.Config.couples_states.find(
|
||||
(c) => c.code === p.couple.state,
|
||||
(c) => c.code === p.couple.state
|
||||
)?.fr;
|
||||
|
||||
const status = [];
|
||||
|
||||
@@ -20,7 +20,7 @@ export function getRadianAngle(degreeValue: number): number {
|
||||
export function rotateSize(
|
||||
width: number,
|
||||
height: number,
|
||||
rotation: number,
|
||||
rotation: number
|
||||
): { width: number; height: number } {
|
||||
const rotRad = getRadianAngle(rotation);
|
||||
|
||||
@@ -39,7 +39,7 @@ export default async function getCroppedImg(
|
||||
imageSrc: string,
|
||||
pixelCrop: Area,
|
||||
rotation = 0,
|
||||
flip = { horizontal: false, vertical: false },
|
||||
flip = { horizontal: false, vertical: false }
|
||||
): Promise<Blob> {
|
||||
const image = await createImage(imageSrc);
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -55,7 +55,7 @@ export default async function getCroppedImg(
|
||||
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
|
||||
image.width,
|
||||
image.height,
|
||||
rotation,
|
||||
rotation
|
||||
);
|
||||
|
||||
// set canvas size to match the bounding box
|
||||
@@ -93,14 +93,14 @@ export default async function getCroppedImg(
|
||||
0,
|
||||
0,
|
||||
pixelCrop.width,
|
||||
pixelCrop.height,
|
||||
pixelCrop.height
|
||||
);
|
||||
|
||||
// As Base64 string
|
||||
// return croppedCanvas.toDataURL('image/jpeg');
|
||||
|
||||
// As a blob
|
||||
return await new Promise((resolve) => {
|
||||
return await new Promise((resolve, _reject) => {
|
||||
croppedCanvas.toBlob((file) => {
|
||||
resolve(file!);
|
||||
}, "image/jpeg");
|
||||
|
||||
@@ -14,8 +14,8 @@ export async function selectFileToUpload(p: {
|
||||
fileEl.click();
|
||||
|
||||
// Wait for a file to be chosen
|
||||
await new Promise((res) =>
|
||||
fileEl.addEventListener("change", () => res(null)),
|
||||
await new Promise((res, _rej) =>
|
||||
fileEl.addEventListener("change", () => res(null))
|
||||
);
|
||||
|
||||
if ((fileEl.files?.length ?? 0) === 0) return null;
|
||||
@@ -25,8 +25,8 @@ export async function selectFileToUpload(p: {
|
||||
if (p.maxSize && file.size > p.maxSize) {
|
||||
throw new Error(
|
||||
`Le fichier sélectionné est trop lourd ! (taille maximale acceptée : ${filesize(
|
||||
p.maxSize,
|
||||
)})`,
|
||||
p.maxSize
|
||||
)})`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const canvas: HTMLCanvasElement = document.createElement("canvas");
|
||||
const charLen: Map<string, Map<string, number>> = new Map();
|
||||
let canvas: HTMLCanvasElement = document.createElement("canvas");
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function getAllIndexes(s: string, val: string): number[] {
|
||||
const indexes = [];
|
||||
let i = -1;
|
||||
export function getAllIndexes(s: string, val: string) {
|
||||
var indexes = [],
|
||||
i = -1;
|
||||
while ((i = s.indexOf(val, i + 1)) !== -1) {
|
||||
indexes.push(i);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { format } from "date-and-time";
|
||||
|
||||
/**
|
||||
* Get formatted UNIX date
|
||||
*/
|
||||
@@ -12,7 +10,7 @@ export function fmtUnixDate(time: number): string {
|
||||
*/
|
||||
export function fmtUnixDateFullCalendar(
|
||||
time: number,
|
||||
correctEnd: boolean,
|
||||
correctEnd: boolean
|
||||
): string {
|
||||
let d = new Date(time * 1000);
|
||||
|
||||
@@ -31,65 +29,3 @@ export function fmtUnixDateFullCalendar(
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date into a human-readable form
|
||||
*/
|
||||
export function formatDate(time: number): string {
|
||||
const t = new Date();
|
||||
t.setTime(1000 * time);
|
||||
return format(t, "DD/MM/YYYY HH:mm:ss");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human readable time between to dates
|
||||
*/
|
||||
export function timeDiff(a: number, b: number): string {
|
||||
let diff = b - a;
|
||||
|
||||
if (diff === 0) return "maintenant";
|
||||
if (diff === 1) return "1 seconde";
|
||||
|
||||
if (diff < 60) {
|
||||
return `${diff} secondes`;
|
||||
}
|
||||
|
||||
diff = Math.floor(diff / 60);
|
||||
|
||||
if (diff === 1) return "1 minute";
|
||||
if (diff < 24) {
|
||||
return `${diff} minutes`;
|
||||
}
|
||||
|
||||
diff = Math.floor(diff / 60);
|
||||
|
||||
if (diff === 1) return "1 heure";
|
||||
if (diff < 24) {
|
||||
return `${diff} heures`;
|
||||
}
|
||||
|
||||
const diffDays = Math.floor(diff / 24);
|
||||
|
||||
if (diffDays === 1) return "1 jour";
|
||||
if (diffDays < 31) {
|
||||
return `${diffDays} jours`;
|
||||
}
|
||||
|
||||
diff = Math.floor(diffDays / 31);
|
||||
|
||||
if (diff < 12) {
|
||||
return `${diff} mois`;
|
||||
}
|
||||
|
||||
const diffYears = Math.floor(diffDays / 365);
|
||||
|
||||
if (diffYears === 1) return "1 an";
|
||||
return `${diffYears} ans`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human readable time diff from a given timestamp to today
|
||||
*/
|
||||
export function timeDiffFromNow(time: number): string {
|
||||
return timeDiff(time, Math.floor(new Date().getTime() / 1000));
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const State = {
|
||||
Loading: 0,
|
||||
Ready: 1,
|
||||
Error: 2,
|
||||
} as const;
|
||||
|
||||
type State = keyof typeof State;
|
||||
enum State {
|
||||
Loading,
|
||||
Ready,
|
||||
Error,
|
||||
}
|
||||
|
||||
export function AsyncWidget(p: {
|
||||
loadKey: unknown;
|
||||
loadKey: any;
|
||||
load: () => Promise<void>;
|
||||
errMsg: string;
|
||||
build: () => React.ReactElement;
|
||||
ready?: boolean;
|
||||
errAdditionalElement?: () => 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 () => {
|
||||
try {
|
||||
@@ -30,11 +30,12 @@ export function AsyncWidget(p: {
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => {
|
||||
if (counter.current === p.loadKey) return;
|
||||
counter.current = p.loadKey;
|
||||
|
||||
load();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [p.loadKey]);
|
||||
});
|
||||
|
||||
if (state === State.Error)
|
||||
return (
|
||||
@@ -47,6 +48,10 @@ export function AsyncWidget(p: {
|
||||
height: "100%",
|
||||
flex: "1",
|
||||
flexDirection: "column",
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[900],
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
@@ -59,7 +64,7 @@ export function AsyncWidget(p: {
|
||||
|
||||
<Button onClick={load}>Réessayer</Button>
|
||||
|
||||
{p.errAdditionalElement?.()}
|
||||
{p.errAdditionalElement && p.errAdditionalElement()}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -73,6 +78,10 @@ export function AsyncWidget(p: {
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
flex: "1",
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[900],
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiFamilyTree } from "@mdi/js";
|
||||
import { Icon } from "@mdi/react";
|
||||
import Icon from "@mdi/react";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
@@ -150,7 +150,6 @@ export function BaseAuthenticatedPage(): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useUser(): UserContext {
|
||||
return React.useContext(UserContextK)!;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
mdiPlus,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import { Icon } from "@mdi/react";
|
||||
import Icon from "@mdi/react";
|
||||
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import {
|
||||
@@ -65,7 +65,7 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
loadKey.current += 1;
|
||||
setFamily(null);
|
||||
|
||||
return new Promise<void>((res) => {
|
||||
return new Promise<void>((res, _rej) => {
|
||||
loadPromise.current = () => res();
|
||||
});
|
||||
};
|
||||
@@ -79,7 +79,7 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment générer un nouveau code d'invitation pour cette famille ? Cette action aura pour effet d'invalider l'ancien code !",
|
||||
"Voulez-vous vraiment générer un nouveau code d'invitation pour cette famille ? Cette action aura pour effet d'invalider l'ancien code !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
@@ -98,7 +98,6 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={family !== null}
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={`${familyId}-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations de la famille !"
|
||||
@@ -288,7 +287,6 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useFamily(): FamilyContext {
|
||||
return React.useContext(FamilyContextK)!;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiFamilyTree } from "@mdi/js";
|
||||
import { Icon } from "@mdi/react";
|
||||
import Icon from "@mdi/react";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
@@ -9,14 +9,14 @@ import Typography from "@mui/material/Typography";
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import { DarkThemeButton } from "./DarkThemeButton";
|
||||
|
||||
function Copyright(): React.ReactElement {
|
||||
function Copyright(props: any) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
align="center"
|
||||
style={{ marginTop: "20px" }}
|
||||
sx={{ mt: 5 }}
|
||||
{...props}
|
||||
>
|
||||
{"Copyright © "}
|
||||
<a
|
||||
@@ -90,7 +90,7 @@ export function BaseLoginPage() {
|
||||
{/* inner page */}
|
||||
<Outlet />
|
||||
|
||||
<Copyright />
|
||||
<Copyright sx={{ mt: 5 }} />
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiBabyCarriage, mdiCross } from "@mdi/js";
|
||||
import { Icon } from "@mdi/react";
|
||||
import Icon from "@mdi/react";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { TreeItem, SimpleTreeView } from "@mui/x-tree-view";
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useBlocker } from "react-router-dom";
|
||||
export function ConfirmLeaveWithoutSaveDialog(p: {
|
||||
shouldBlock: boolean;
|
||||
}): React.ReactElement {
|
||||
const blocker = useBlocker(p.shouldBlock);
|
||||
let blocker = useBlocker(p.shouldBlock);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (blocker.state === "blocked" && !p.shouldBlock) {
|
||||
@@ -49,8 +49,8 @@ export function ConfirmLeaveWithoutSaveDialog(p: {
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={confirmNavigation}>Quitter la page</Button>
|
||||
<Button onClick={cancelNavigation} autoFocus>
|
||||
<Button onClick={confirmNavigation as any}>Quitter la page</Button>
|
||||
<Button onClick={cancelNavigation as any} autoFocus>
|
||||
Rester sur la page
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { Member, fmtDate } from "../api/genealogy/MemberApi";
|
||||
import { MemberPhoto } from "./MemberPhoto";
|
||||
import { Icon } from "@mdi/react";
|
||||
import Icon from "@mdi/react";
|
||||
import FemaleIcon from "@mui/icons-material/Female";
|
||||
import MaleIcon from "@mui/icons-material/Male";
|
||||
|
||||
@@ -37,7 +37,7 @@ export function MemberItem(p: {
|
||||
secondary={
|
||||
p.member?.dead
|
||||
? `${fmtDate(p.member?.dateOfBirth)} - ${fmtDate(
|
||||
p.member?.dateOfDeath,
|
||||
p.member?.dateOfDeath
|
||||
)}`
|
||||
: fmtDate(p.member?.dateOfBirth)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
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 {
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
|
||||
|
||||
const load = async () => {
|
||||
setAccommodations(
|
||||
await AccommodationListApi.GetListOfFamily(family.family),
|
||||
await AccommodationListApi.GetListOfFamily(family.family)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
|
||||
loadKey.current += 1;
|
||||
setAccommodations(null);
|
||||
|
||||
return new Promise<void>((res) => {
|
||||
return new Promise<void>((res, _rej) => {
|
||||
loadPromise.current = () => res();
|
||||
});
|
||||
};
|
||||
@@ -47,7 +47,6 @@ export function BaseAccommodationsRoute(): React.ReactElement {
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={accommodations !== null}
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={`${family.familyId}-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations sur les logements de la famille !"
|
||||
@@ -80,7 +79,6 @@ export function BaseAccommodationsRoute(): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useAccommodations(): AccommodationsContext {
|
||||
return React.useContext(AccommodationsContextK)!;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function MemberInput(p: {
|
||||
return (
|
||||
<Autocomplete
|
||||
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);
|
||||
}}
|
||||
inputValue={inputValue}
|
||||
@@ -69,14 +69,14 @@ export function MemberInput(p: {
|
||||
sx={{ width: "100%" }}
|
||||
filterOptions={(options, state) => {
|
||||
const res = options.filter((m) =>
|
||||
m?.fullName.toLowerCase().includes(state.inputValue),
|
||||
m?.fullName.toLowerCase().includes(state.inputValue)
|
||||
);
|
||||
res.length = Math.min(20, res.length);
|
||||
return res;
|
||||
}}
|
||||
getOptionLabel={(o) => o?.fullName ?? ""}
|
||||
renderInput={(params) => <TextField {...params} label={p.label} />}
|
||||
renderOption={(_props, option) => (
|
||||
renderOption={(_props, option, _state) => (
|
||||
<MemberItem
|
||||
member={option}
|
||||
onClick={() => p.onValueChange(option?.id)}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function BaseGenealogyRoute(): React.ReactElement {
|
||||
setMembers(null);
|
||||
setCouples(null);
|
||||
|
||||
return new Promise<void>((res) => {
|
||||
return new Promise<void>((res, _rej) => {
|
||||
loadPromise.current = () => res();
|
||||
});
|
||||
};
|
||||
@@ -42,7 +42,6 @@ export function BaseGenealogyRoute(): React.ReactElement {
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={members !== null && couples !== null}
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
loadKey={`${family.familyId}-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations de généalogie de la famille !"
|
||||
@@ -69,7 +68,6 @@ export function BaseGenealogyRoute(): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useGenealogy(): GenealogyContext {
|
||||
return React.useContext(GenealogyContextK)!;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiXml } from "@mdi/js";
|
||||
import { Icon } from "@mdi/react";
|
||||
import Icon from "@mdi/react";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import jsPDF from "jspdf";
|
||||
@@ -89,7 +89,7 @@ function center(container_width: number, el_width: number): number {
|
||||
|
||||
function buildSimpleTreeNode(
|
||||
tree: FamilyTreeNode,
|
||||
depth: number,
|
||||
depth: number
|
||||
): SimpleTreeNode {
|
||||
if (depth === 0) throw new Error("Too much recursion reached!");
|
||||
|
||||
@@ -100,7 +100,7 @@ function buildSimpleTreeNode(
|
||||
let childrenToProcess = tree.down;
|
||||
if (depth > 1)
|
||||
tree.couples?.forEach(
|
||||
(c) => (childrenToProcess = childrenToProcess?.concat(c.down)),
|
||||
(c) => (childrenToProcess = childrenToProcess?.concat(c.down))
|
||||
);
|
||||
else childrenToProcess = [];
|
||||
|
||||
@@ -156,7 +156,7 @@ export function SimpleFamilyTree(p: {
|
||||
|
||||
const tree = React.useMemo(
|
||||
() => buildSimpleTreeNode(p.tree, p.depth),
|
||||
[p.tree, p.depth],
|
||||
[p.tree, p.depth]
|
||||
);
|
||||
|
||||
const height = p.depth * (CARD_HEIGHT + LEVEL_SPACING) - LEVEL_SPACING;
|
||||
@@ -250,42 +250,44 @@ function NodeArea(p: {
|
||||
childrenLinkDestY?: number;
|
||||
node: SimpleTreeNode;
|
||||
}): React.ReactElement {
|
||||
let parent_x_offset: number;
|
||||
|
||||
let pers1 = p.node.member;
|
||||
let pers2 = p.node.spouse?.member;
|
||||
let didSwap = false;
|
||||
|
||||
// Show male of the left (all the time)
|
||||
if (pers2?.sex === "M") {
|
||||
const s = pers1;
|
||||
let s = pers1;
|
||||
pers1 = pers2;
|
||||
pers2 = s;
|
||||
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);
|
||||
|
||||
const endFirstFaceX =
|
||||
let endFirstFaceX =
|
||||
parent_x_offset +
|
||||
Math.floor((memberCardWidth(pers1) - FACE_WIDTH) / 2) +
|
||||
FACE_WIDTH;
|
||||
|
||||
const beginingOfSecondCardX =
|
||||
let beginingOfSecondCardX =
|
||||
parent_x_offset + p.node.parentWidth - memberCardWidth(pers2);
|
||||
|
||||
const beginSecondFaceX =
|
||||
let beginSecondFaceX =
|
||||
p.node.spouse &&
|
||||
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
|
||||
let parentLinkX = didSwap
|
||||
? beginSecondFaceX! + Math.floor(FACE_WIDTH / 2)
|
||||
: parent_x_offset + Math.floor(memberCardWidth(pers1) / 2);
|
||||
const parentLinkY = p.y;
|
||||
let parentLinkY = p.y;
|
||||
|
||||
// Remove ugly little shifts
|
||||
if (Math.abs(parentLinkX - (p.childrenLinkDestX ?? 0)) < 10)
|
||||
@@ -299,7 +301,7 @@ function NodeArea(p: {
|
||||
if (
|
||||
pers2 &&
|
||||
p.node.down.every(
|
||||
(n) => n.member.father === pers1.id && n.member.mother !== pers2!.id,
|
||||
(n) => n.member.father === pers1.id && n.member.mother !== pers2!.id
|
||||
)
|
||||
) {
|
||||
childrenLinkX = parent_x_offset + Math.floor(memberCardWidth(pers1) / 2);
|
||||
@@ -310,7 +312,7 @@ function NodeArea(p: {
|
||||
else if (
|
||||
pers2 &&
|
||||
p.node.down.every(
|
||||
(n) => n.member.father !== pers1.id && n.member.mother === pers2!.id,
|
||||
(n) => n.member.father !== pers1.id && n.member.mother === pers2!.id
|
||||
)
|
||||
) {
|
||||
childrenLinkX = beginSecondFaceX! + Math.floor(memberCardWidth(pers2) / 2);
|
||||
@@ -369,7 +371,6 @@ function NodeArea(p: {
|
||||
node={n}
|
||||
/>
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
downXOffset += n.width + SIBLINGS_SPACING;
|
||||
return el;
|
||||
})}
|
||||
@@ -418,7 +419,7 @@ function MemberCard(p: {
|
||||
<tspan
|
||||
x={center(
|
||||
w,
|
||||
getTextWidth(p.member.lastNameUpperCase ?? "", NAME_FONT),
|
||||
getTextWidth(p.member.lastNameUpperCase ?? "", NAME_FONT)
|
||||
)}
|
||||
dy="14"
|
||||
font-size="13"
|
||||
@@ -429,7 +430,7 @@ function MemberCard(p: {
|
||||
<tspan
|
||||
x={center(
|
||||
w,
|
||||
getTextWidth(p.member.displayBirthDeathShort, BIRTH_FONT),
|
||||
getTextWidth(p.member.displayBirthDeathShort, BIRTH_FONT)
|
||||
)}
|
||||
dy="14"
|
||||
font-size="10"
|
||||
|
||||
1
geneit_backend/.gitignore
vendored
1
geneit_backend/.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
target/
|
||||
storage/
|
||||
.idea
|
||||
static/
|
||||
1644
geneit_backend/Cargo.lock
generated
1644
geneit_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,14 +15,14 @@ anyhow = "1.0.102"
|
||||
actix-web = "4.13.0"
|
||||
actix-cors = "0.7.1"
|
||||
actix-multipart = "0.7.2"
|
||||
actix-remote-ip = "1.0.0"
|
||||
actix-remote-ip = "0.1.0"
|
||||
futures-util = "0.3.32"
|
||||
diesel = { version = "2.3.7", features = ["postgres"] }
|
||||
diesel_migrations = "2.3.1"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
mailchecker = "6.0.20"
|
||||
redis = "1.1.0"
|
||||
redis = "0.32.7"
|
||||
lettre = "0.11.20"
|
||||
rand = "0.10.0"
|
||||
bcrypt = "0.19.0"
|
||||
@@ -31,16 +31,13 @@ thiserror = "2.0.18"
|
||||
serde_with = "3.18.0"
|
||||
rust_iso3166 = "0.1.14"
|
||||
rust-s3 = "0.37.1"
|
||||
sha2 = "0.11.0"
|
||||
sha2 = "0.10.9"
|
||||
image = "0.25.10"
|
||||
uuid = { version = "1.23.0", features = ["v4"] }
|
||||
uuid = { version = "1.22.0", features = ["v4"] }
|
||||
httpdate = "1.0.3"
|
||||
zip = "8.5.0"
|
||||
zip = "4.3.0"
|
||||
mime_guess = "2.0.5"
|
||||
tempfile = "3.27.0"
|
||||
base64 = "0.22.1"
|
||||
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
|
||||
chrono = "0.4.44"
|
||||
hex = "0.4.3"
|
||||
rust-embed = { version = "8.11.0", features = ["mime-guess"] }
|
||||
build-time = "0.1.3"
|
||||
10
geneit_backend/build_docker_image.sh
Executable file
10
geneit_backend/build_docker_image.sh
Executable 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
|
||||
|
||||
@@ -1,150 +1,51 @@
|
||||
services:
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.0-alpha.90
|
||||
user: "1000"
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
minio:
|
||||
image: minio/minio
|
||||
environment:
|
||||
- RUSTFS_ACCESS_KEY=topsecret
|
||||
- RUSTFS_SECRET_KEY=topsecret
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9090
|
||||
- RUSTFS_VOLUMES=/data
|
||||
- MINIO_ROOT_USER=topsecret
|
||||
- MINIO_ROOT_PASSWORD=topsecret
|
||||
volumes:
|
||||
- ./storage/rustfs:/data
|
||||
- ./storage/minio:/data
|
||||
command: ["minio", "server", "/data", "--console-address", ":9090"]
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9090:9090"
|
||||
- 9000:9000
|
||||
- 9090:9090
|
||||
expose:
|
||||
- 9000
|
||||
read_only: true
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"sh", "-c",
|
||||
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9090/rustfs/console/health"
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
- 9000
|
||||
|
||||
db:
|
||||
image: postgres:18
|
||||
user: "1000"
|
||||
restart: unless-stopped
|
||||
image: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
expose:
|
||||
- 5432
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
environment:
|
||||
- POSTGRES_USER=user
|
||||
- POSTGRES_PASSWORD=pass
|
||||
- POSTGRES_DB=geneit
|
||||
- PGDATA=/data
|
||||
read_only: true
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U user" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./storage/db:/data
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /run/postgresql
|
||||
- ./storage/db:/var/lib/postgresql/data
|
||||
|
||||
mailcatcher:
|
||||
image: dockage/mailcatcher:0.9.0
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
read_only: true
|
||||
ports:
|
||||
- "1080:1080"
|
||||
- "1025:1025"
|
||||
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
|
||||
- 1080:1080
|
||||
- 1025:1025
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
read_only: true
|
||||
command: redis-server --requirepass ${REDIS_PASS:-secretredis}
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "redis-cli --no-auth-warning --pass ${REDIS_PASS:-secretredis} ping | grep PONG" ]
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- ./storage/redis-data:/data
|
||||
- ./storage/redis-conf:/usr/local/etc/redis/redis.conf
|
||||
|
||||
oidc:
|
||||
image: dexidp/dex
|
||||
user: "1000"
|
||||
restart: unless-stopped
|
||||
image: qlik/simple-oidc-provider
|
||||
environment:
|
||||
- REDIRECTS=http://localhost:3000/oidc_cb
|
||||
- PORT=9001
|
||||
ports:
|
||||
- "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
|
||||
- 9001:9001
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use clap::Parser;
|
||||
use redis::{IntoConnectionInfo, RedisConnectionInfo};
|
||||
use s3::creds::Credentials;
|
||||
use s3::{Bucket, Region};
|
||||
|
||||
@@ -12,7 +11,7 @@ pub struct AppConfig {
|
||||
pub listen_address: String,
|
||||
|
||||
/// Website origin
|
||||
#[clap(short, long, env, default_value = "http://localhost:5173")]
|
||||
#[clap(short, long, env, default_value = "http://localhost:3000")]
|
||||
pub website_origin: String,
|
||||
|
||||
/// Proxy IP, might end with a star "*"
|
||||
@@ -99,7 +98,7 @@ pub struct AppConfig {
|
||||
#[arg(
|
||||
long,
|
||||
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,
|
||||
|
||||
@@ -189,16 +188,15 @@ impl AppConfig {
|
||||
|
||||
/// Get Redis connection configuration
|
||||
pub fn redis_connection_config(&self) -> redis::ConnectionInfo {
|
||||
let mut settings = RedisConnectionInfo::default().set_db(self.redis_db_number);
|
||||
if let Some(username) = self.redis_username.as_ref() {
|
||||
settings = settings.set_username(username);
|
||||
redis::ConnectionInfo {
|
||||
addr: redis::ConnectionAddr::Tcp(self.redis_hostname.clone(), self.redis_port),
|
||||
redis: redis::RedisConnectionInfo {
|
||||
db: self.redis_db_number,
|
||||
username: self.redis_username.clone(),
|
||||
password: Some(self.redis_password.clone()),
|
||||
protocol: Default::default(),
|
||||
},
|
||||
}
|
||||
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
|
||||
|
||||
@@ -35,17 +35,15 @@ impl AccommodationRequest {
|
||||
accommodation.name = self.name;
|
||||
|
||||
if let Some(d) = &self.description
|
||||
&& !c.accommodation_description_len.validate(d)
|
||||
{
|
||||
return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
|
||||
}
|
||||
&& !c.accommodation_description_len.validate(d) {
|
||||
return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
|
||||
}
|
||||
accommodation.description.clone_from(&self.description);
|
||||
|
||||
if let Some(c) = &self.color
|
||||
&& !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c)
|
||||
{
|
||||
return Err(AccommodationListControllerErr::MalformedColor.into());
|
||||
}
|
||||
&& !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
|
||||
return Err(AccommodationListControllerErr::MalformedColor.into());
|
||||
}
|
||||
accommodation.color.clone_from(&self.color);
|
||||
|
||||
accommodation.need_validation = self.need_validation;
|
||||
|
||||
@@ -49,22 +49,19 @@ impl CoupleRequest {
|
||||
}
|
||||
|
||||
if let Some(husband) = self.husband
|
||||
&& !members_service::exists(couple.family_id(), husband).await?
|
||||
{
|
||||
return Err(CoupleControllerErr::HusbandNotExisting.into());
|
||||
}
|
||||
&& !members_service::exists(couple.family_id(), husband).await? {
|
||||
return Err(CoupleControllerErr::HusbandNotExisting.into());
|
||||
}
|
||||
|
||||
if let Some(d) = &self.wedding
|
||||
&& !d.check()
|
||||
{
|
||||
return Err(CoupleControllerErr::MalformedDateOfWedding.into());
|
||||
}
|
||||
&& !d.check() {
|
||||
return Err(CoupleControllerErr::MalformedDateOfWedding.into());
|
||||
}
|
||||
|
||||
if let Some(d) = &self.divorce
|
||||
&& !d.check()
|
||||
{
|
||||
return Err(CoupleControllerErr::MalformedDateOfDivorce.into());
|
||||
}
|
||||
&& !d.check() {
|
||||
return Err(CoupleControllerErr::MalformedDateOfDivorce.into());
|
||||
}
|
||||
|
||||
couple.set_wife(self.wife);
|
||||
couple.set_husband(self.husband);
|
||||
|
||||
@@ -96,10 +96,9 @@ fn check_opt_str_val(
|
||||
err: MemberControllerErr,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(v) = val
|
||||
&& !c.validate(v)
|
||||
{
|
||||
return Err(err.into());
|
||||
}
|
||||
&& !c.validate(v) {
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -152,10 +151,9 @@ impl MemberRequest {
|
||||
)?;
|
||||
|
||||
if let Some(mail) = &self.email
|
||||
&& !mailchecker::is_valid(mail)
|
||||
{
|
||||
return Err(MemberControllerErr::InvalidEmailAddress.into());
|
||||
}
|
||||
&& !mailchecker::is_valid(mail) {
|
||||
return Err(MemberControllerErr::InvalidEmailAddress.into());
|
||||
}
|
||||
|
||||
check_opt_str_val(
|
||||
&self.phone,
|
||||
@@ -188,22 +186,19 @@ impl MemberRequest {
|
||||
)?;
|
||||
|
||||
if let Some(c) = &self.country
|
||||
&& !countries_utils::is_code_valid(c)
|
||||
{
|
||||
return Err(MemberControllerErr::InvalidCountryCode.into());
|
||||
}
|
||||
&& !countries_utils::is_code_valid(c) {
|
||||
return Err(MemberControllerErr::InvalidCountryCode.into());
|
||||
}
|
||||
|
||||
if let Some(d) = &self.birth
|
||||
&& !d.check()
|
||||
{
|
||||
return Err(MemberControllerErr::MalformedDateOfBirth.into());
|
||||
}
|
||||
&& !d.check() {
|
||||
return Err(MemberControllerErr::MalformedDateOfBirth.into());
|
||||
}
|
||||
|
||||
if let Some(d) = &self.death
|
||||
&& !d.check()
|
||||
{
|
||||
return Err(MemberControllerErr::MalformedDateOfDeath.into());
|
||||
}
|
||||
&& !d.check() {
|
||||
return Err(MemberControllerErr::MalformedDateOfDeath.into());
|
||||
}
|
||||
|
||||
check_opt_str_val(
|
||||
&self.note,
|
||||
@@ -222,10 +217,9 @@ impl MemberRequest {
|
||||
}
|
||||
|
||||
if let Some(father) = self.father
|
||||
&& !members_service::exists(member.family_id(), father).await?
|
||||
{
|
||||
return Err(MemberControllerErr::FatherNotExisting.into());
|
||||
}
|
||||
&& !members_service::exists(member.family_id(), father).await? {
|
||||
return Err(MemberControllerErr::FatherNotExisting.into());
|
||||
}
|
||||
|
||||
member.first_name = self.first_name;
|
||||
member.last_name = self.last_name;
|
||||
|
||||
@@ -16,7 +16,6 @@ pub mod members_controller;
|
||||
pub mod photos_controller;
|
||||
pub mod server_controller;
|
||||
pub mod users_controller;
|
||||
pub mod web_app_controller;
|
||||
|
||||
/// Custom error to ease controller writing
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -37,10 +37,9 @@ async fn get_photo(id: &PhotoIdPath, full_size: bool, req: HttpRequest) -> HttpR
|
||||
|
||||
// Check if an upload is un-necessary
|
||||
if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
|
||||
&& c.to_str().unwrap_or("") == hash
|
||||
{
|
||||
return Ok(HttpResponse::NotModified().finish());
|
||||
}
|
||||
&& c.to_str().unwrap_or("") == hash {
|
||||
return Ok(HttpResponse::NotModified().finish());
|
||||
}
|
||||
|
||||
if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) {
|
||||
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()
|
||||
.as_secs()
|
||||
>= photo.time_create as u64
|
||||
{
|
||||
return Ok(HttpResponse::NotModified().finish());
|
||||
}
|
||||
{
|
||||
return Ok(HttpResponse::NotModified().finish());
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = s3_connection::get_file(&match full_size {
|
||||
|
||||
@@ -5,6 +5,11 @@ use crate::utils::countries_utils;
|
||||
use crate::utils::countries_utils::CountryCode;
|
||||
use actix_web::{HttpResponse, Responder};
|
||||
|
||||
/// Default hello route
|
||||
pub async fn home() -> impl Responder {
|
||||
HttpResponse::Ok().json("GeneIT API service.")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct ServerConfig<'a> {
|
||||
constraints: StaticConstraints,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use geneit_backend::controllers::{
|
||||
accommodations_list_controller, accommodations_reservations_calendars_controller,
|
||||
accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
|
||||
families_controller, members_controller, photos_controller, server_controller,
|
||||
users_controller, web_app_controller,
|
||||
users_controller,
|
||||
};
|
||||
|
||||
#[actix_web::main]
|
||||
@@ -40,259 +40,254 @@ async fn main() -> std::io::Result<()> {
|
||||
.max_age(3600),
|
||||
)
|
||||
.wrap(Logger::default())
|
||||
.app_data(web::Data::new(RemoteIPConfig::parse_opt(
|
||||
AppConfig::get().proxy_ip.clone(),
|
||||
)))
|
||||
.app_data(web::Data::new(RemoteIPConfig {
|
||||
proxy: AppConfig::get().proxy_ip.clone(),
|
||||
}))
|
||||
// Uploaded files
|
||||
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
||||
// Config controller
|
||||
.route("/", web::get().to(server_controller::home))
|
||||
.route(
|
||||
"/api/server/config",
|
||||
"/server/config",
|
||||
web::get().to(server_controller::server_config),
|
||||
)
|
||||
// Auth controller
|
||||
.route(
|
||||
"/api/auth/create_account",
|
||||
"/auth/create_account",
|
||||
web::post().to(auth_controller::create_account),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/request_reset_password",
|
||||
"/auth/request_reset_password",
|
||||
web::post().to(auth_controller::request_reset_password),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/check_reset_password_token",
|
||||
"/auth/check_reset_password_token",
|
||||
web::post().to(auth_controller::check_reset_password_token),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/reset_password",
|
||||
"/auth/reset_password",
|
||||
web::post().to(auth_controller::reset_password),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/password_login",
|
||||
"/auth/password_login",
|
||||
web::post().to(auth_controller::password_login),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/start_openid_login",
|
||||
"/auth/start_openid_login",
|
||||
web::post().to(auth_controller::start_openid_login),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/finish_openid_login",
|
||||
"/auth/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
|
||||
.route("/api/user/info", web::get().to(users_controller::auth_info))
|
||||
.route("/user/info", web::get().to(users_controller::auth_info))
|
||||
.route(
|
||||
"/api/user/update_profile",
|
||||
"/user/update_profile",
|
||||
web::post().to(users_controller::update_profile),
|
||||
)
|
||||
.route(
|
||||
"/api/user/replace_password",
|
||||
"/user/replace_password",
|
||||
web::post().to(users_controller::replace_password),
|
||||
)
|
||||
.route(
|
||||
"/api/user/request_delete",
|
||||
"/user/request_delete",
|
||||
web::get().to(users_controller::request_delete_account),
|
||||
)
|
||||
.route(
|
||||
"/api/user/check_delete_token",
|
||||
"/user/check_delete_token",
|
||||
web::post().to(users_controller::check_delete_token),
|
||||
)
|
||||
.route(
|
||||
"/api/user/delete_account",
|
||||
"/user/delete_account",
|
||||
web::post().to(users_controller::delete_account),
|
||||
)
|
||||
// Families controller
|
||||
.route(
|
||||
"/api/family/create",
|
||||
"/family/create",
|
||||
web::post().to(families_controller::create),
|
||||
)
|
||||
.route("/api/family/join", web::post().to(families_controller::join))
|
||||
.route("/api/family/list", web::get().to(families_controller::list))
|
||||
.route("/family/join", web::post().to(families_controller::join))
|
||||
.route("/family/list", web::get().to(families_controller::list))
|
||||
.route(
|
||||
"/api/family/{id}",
|
||||
"/family/{id}",
|
||||
web::get().to(families_controller::single_info),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/leave",
|
||||
"/family/{id}/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(
|
||||
"/api/family/{id}",
|
||||
"/family/{id}",
|
||||
web::delete().to(families_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/renew_invitation_code",
|
||||
"/family/{id}/renew_invitation_code",
|
||||
web::post().to(families_controller::renew_invitation_code),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/users",
|
||||
"/family/{id}/users",
|
||||
web::get().to(families_controller::users),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/user/{user_id}",
|
||||
"/family/{id}/user/{user_id}",
|
||||
web::patch().to(families_controller::update_membership),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/user/{user_id}",
|
||||
"/family/{id}/user/{user_id}",
|
||||
web::delete().to(families_controller::delete_membership),
|
||||
)
|
||||
// [GENEALOGY] Members controller
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/member/create",
|
||||
"/family/{id}/genealogy/member/create",
|
||||
web::post().to(members_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/members",
|
||||
"/family/{id}/genealogy/members",
|
||||
web::get().to(members_controller::get_all),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/member/{member_id}",
|
||||
"/family/{id}/genealogy/member/{member_id}",
|
||||
web::get().to(members_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/member/{member_id}",
|
||||
"/family/{id}/genealogy/member/{member_id}",
|
||||
web::put().to(members_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/member/{member_id}",
|
||||
"/family/{id}/genealogy/member/{member_id}",
|
||||
web::delete().to(members_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/member/{member_id}/photo",
|
||||
"/family/{id}/genealogy/member/{member_id}/photo",
|
||||
web::put().to(members_controller::set_photo),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/member/{member_id}/photo",
|
||||
"/family/{id}/genealogy/member/{member_id}/photo",
|
||||
web::delete().to(members_controller::remove_photo),
|
||||
)
|
||||
// [GENEALOGY] Couples controller
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/couple/create",
|
||||
"/family/{id}/genealogy/couple/create",
|
||||
web::post().to(couples_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/couples",
|
||||
"/family/{id}/genealogy/couples",
|
||||
web::get().to(couples_controller::get_all),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/couple/{couple_id}",
|
||||
"/family/{id}/genealogy/couple/{couple_id}",
|
||||
web::get().to(couples_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/couple/{couple_id}",
|
||||
"/family/{id}/genealogy/couple/{couple_id}",
|
||||
web::put().to(couples_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/couple/{couple_id}",
|
||||
"/family/{id}/genealogy/couple/{couple_id}",
|
||||
web::delete().to(couples_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/couple/{couple_id}/photo",
|
||||
"/family/{id}/genealogy/couple/{couple_id}/photo",
|
||||
web::put().to(couples_controller::set_photo),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/couple/{couple_id}/photo",
|
||||
"/family/{id}/genealogy/couple/{couple_id}/photo",
|
||||
web::delete().to(couples_controller::remove_photo),
|
||||
)
|
||||
// [GENEALOGY] Data controller
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/data/export",
|
||||
"/family/{id}/genealogy/data/export",
|
||||
web::get().to(data_controller::export_family),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/genealogy/data/import",
|
||||
"/family/{id}/genealogy/data/import",
|
||||
web::put().to(data_controller::import_family),
|
||||
)
|
||||
// [ACCOMODATIONS] List controller
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/list/create",
|
||||
"/family/{id}/accommodations/list/create",
|
||||
web::post().to(accommodations_list_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/list/list",
|
||||
"/family/{id}/accommodations/list/list",
|
||||
web::get().to(accommodations_list_controller::get_full_list),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/list/{accommodation_id}",
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::get().to(accommodations_list_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/list/{accommodation_id}",
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::put().to(accommodations_list_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/list/{accommodation_id}",
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::delete().to(accommodations_list_controller::delete),
|
||||
)
|
||||
// [ACCOMODATIONS] Reservations controller
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
|
||||
web::get()
|
||||
.to(accommodations_reservations_controller::get_accommodation_reservations),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
|
||||
web::get()
|
||||
.to(accommodations_reservations_controller::get_accommodation_reservations_for_interval),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservations/full_list",
|
||||
"/family/{id}/accommodations/reservations/full_list",
|
||||
web::get().to(accommodations_reservations_controller::full_list),
|
||||
)
|
||||
.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),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::get().to(accommodations_reservations_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::patch().to(accommodations_reservations_controller::update_single),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::delete().to(accommodations_reservations_controller::delete),
|
||||
)
|
||||
.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),
|
||||
)
|
||||
// [ACCOMMODATIONS] Calendars controller
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservations_calendars/create",
|
||||
"/family/{id}/accommodations/reservations_calendars/create",
|
||||
web::post().to(accommodations_reservations_calendars_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservations_calendars/list",
|
||||
"/family/{id}/accommodations/reservations_calendars/list",
|
||||
web::get().to(accommodations_reservations_calendars_controller::get_list),
|
||||
)
|
||||
.route(
|
||||
"/api/family/{id}/accommodations/reservations_calendars/{cal_id}",
|
||||
"/family/{id}/accommodations/reservations_calendars/{cal_id}",
|
||||
web::delete().to(accommodations_reservations_calendars_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/api/acccommodations_calendar/{token}",
|
||||
"/acccommodations_calendar/{token}",
|
||||
web::get().to(accommodations_reservations_calendars_controller::anonymous_access),
|
||||
)
|
||||
// Photos controller
|
||||
.route(
|
||||
"/api/photo/{id}",
|
||||
"/photo/{id}",
|
||||
web::get().to(photos_controller::get_full_size),
|
||||
)
|
||||
.route(
|
||||
"/api/photo/{id}/thumbnail",
|
||||
"/photo/{id}/thumbnail",
|
||||
web::get().to(photos_controller::get_thumbnail),
|
||||
)
|
||||
// Static web app controller
|
||||
.route("/", web::get().to(web_app_controller::root_index))
|
||||
.route(
|
||||
"/{tail:.*}",
|
||||
web::get().to(web_app_controller::serve_static_content),
|
||||
)
|
||||
})
|
||||
.bind(AppConfig::get().listen_address.as_str())?
|
||||
.run()
|
||||
|
||||
@@ -150,10 +150,9 @@ pub mod loop_detection {
|
||||
impl LoopStack<'_> {
|
||||
pub fn contains(&self, id: MemberID) -> bool {
|
||||
if let Some(ls) = &self.prev
|
||||
&& ls.contains(id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
&& ls.contains(id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.curr == id
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
|
||||
/// Compute SHA256sum of a given slice of bytes
|
||||
pub fn sha256(input: &[u8]) -> String {
|
||||
hex::encode(Sha256::digest(input))
|
||||
/// Compute hash of a slice of bytes
|
||||
pub fn sha256(bytes: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(bytes);
|
||||
let h = hasher.finalize();
|
||||
format!("{h:x}")
|
||||
}
|
||||
|
||||
/// Compute SHA512sum of a given slice of bytes
|
||||
pub fn sha512(input: &[u8]) -> String {
|
||||
hex::encode(Sha512::digest(input))
|
||||
/// Compute hash of a slice of bytes (sha512)
|
||||
pub fn sha512(bytes: &[u8]) -> String {
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(bytes);
|
||||
let h = hasher.finalize();
|
||||
format!("{h:x}")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! # Time utilities
|
||||
|
||||
use chrono::{DateTime, Local, NaiveTime};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Get the current time since epoch
|
||||
pub fn time() -> u64 {
|
||||
@@ -10,29 +9,3 @@ pub fn time() -> u64 {
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
/// Convert UNIX time to system time
|
||||
pub fn unix_to_system_time(time: u64) -> SystemTime {
|
||||
UNIX_EPOCH + Duration::from_secs(time)
|
||||
}
|
||||
|
||||
/// Get build time in UNIX format
|
||||
pub fn build_time() -> u64 {
|
||||
let build_time = build_time::build_time_local!();
|
||||
let date =
|
||||
chrono::DateTime::parse_from_rfc3339(build_time).expect("Failed to parse compile date");
|
||||
date.timestamp() as u64
|
||||
}
|
||||
|
||||
/// Get the first second of the day (local time)
|
||||
pub fn time_start_of_day() -> u64 {
|
||||
let local: DateTime<Local> = Local::now()
|
||||
.with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
|
||||
.unwrap();
|
||||
local.timestamp() as u64
|
||||
}
|
||||
|
||||
/// Format UNIX time to HTTP date
|
||||
pub fn unix_to_http_date(time: u64) -> String {
|
||||
httpdate::fmt_http_date(unix_to_system_time(time))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user