Compare commits
	
		
			1 Commits
		
	
	
		
			master
			...
			e35bd966d2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e35bd966d2 | 
@@ -14,7 +14,7 @@ steps:
 | 
				
			|||||||
  - cargo test
 | 
					  - cargo test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: app_deploy
 | 
					- name: app_deploy
 | 
				
			||||||
  image: node:24
 | 
					  image: node:22
 | 
				
			||||||
  environment:
 | 
					  environment:
 | 
				
			||||||
    AWS_ACCESS_KEY_ID:
 | 
					    AWS_ACCESS_KEY_ID:
 | 
				
			||||||
      from_secret: AWS_ACCESS_KEY_ID
 | 
					      from_secret: AWS_ACCESS_KEY_ID
 | 
				
			||||||
@@ -24,7 +24,7 @@ steps:
 | 
				
			|||||||
  commands:
 | 
					  commands:
 | 
				
			||||||
  # Build website
 | 
					  # Build website
 | 
				
			||||||
  - cd geneit_app
 | 
					  - cd geneit_app
 | 
				
			||||||
  - npm install
 | 
					  - npm install 
 | 
				
			||||||
  - GENERATE_SOURCEMAP=false npm run build 
 | 
					  - GENERATE_SOURCEMAP=false npm run build 
 | 
				
			||||||
  # Install AWS
 | 
					  # Install AWS
 | 
				
			||||||
  - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
 | 
					  - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +0,0 @@
 | 
				
			|||||||
import js from '@eslint/js'
 | 
					 | 
				
			||||||
import globals from 'globals'
 | 
					 | 
				
			||||||
import reactHooks from 'eslint-plugin-react-hooks'
 | 
					 | 
				
			||||||
import reactRefresh from 'eslint-plugin-react-refresh'
 | 
					 | 
				
			||||||
import tseslint from 'typescript-eslint'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default tseslint.config(
 | 
					 | 
				
			||||||
  { ignores: ['dist'] },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
 | 
					 | 
				
			||||||
    files: ['**/*.{ts,tsx}'],
 | 
					 | 
				
			||||||
    languageOptions: {
 | 
					 | 
				
			||||||
      ecmaVersion: 2020,
 | 
					 | 
				
			||||||
      globals: globals.browser,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    plugins: {
 | 
					 | 
				
			||||||
      'react-hooks': reactHooks,
 | 
					 | 
				
			||||||
      'react-refresh': reactRefresh,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    rules: {
 | 
					 | 
				
			||||||
      ...reactHooks.configs.recommended.rules,
 | 
					 | 
				
			||||||
      'react-refresh/only-export-components': [
 | 
					 | 
				
			||||||
        'warn',
 | 
					 | 
				
			||||||
        { allowConstantExport: true },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
							
								
								
									
										4936
									
								
								geneit_app/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4936
									
								
								geneit_app/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,54 +3,70 @@
 | 
				
			|||||||
  "version": "0.1.0",
 | 
					  "version": "0.1.0",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "type": "module",
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
 | 
				
			||||||
 | 
					    "@emotion/react": "^11.13.3",
 | 
				
			||||||
 | 
					    "@emotion/styled": "^11.13.0",
 | 
				
			||||||
 | 
					    "@fontsource/roboto": "^5.1.0",
 | 
				
			||||||
 | 
					    "@fullcalendar/core": "^6.1.15",
 | 
				
			||||||
 | 
					    "@fullcalendar/daygrid": "^6.1.15",
 | 
				
			||||||
 | 
					    "@fullcalendar/interaction": "^6.1.15",
 | 
				
			||||||
 | 
					    "@fullcalendar/list": "^6.1.15",
 | 
				
			||||||
 | 
					    "@fullcalendar/react": "^6.1.15",
 | 
				
			||||||
 | 
					    "@mdi/js": "^7.2.96",
 | 
				
			||||||
 | 
					    "@mdi/react": "^1.6.1",
 | 
				
			||||||
 | 
					    "@mui/icons-material": "^6.1.2",
 | 
				
			||||||
 | 
					    "@mui/lab": "^6.0.0-beta.10",
 | 
				
			||||||
 | 
					    "@mui/material": "^6.1.2",
 | 
				
			||||||
 | 
					    "@mui/x-data-grid": "^7.18.0",
 | 
				
			||||||
 | 
					    "@mui/x-date-pickers": "^7.18.0",
 | 
				
			||||||
 | 
					    "@mui/x-tree-view": "^7.18.0",
 | 
				
			||||||
 | 
					    "@testing-library/jest-dom": "^6.5.0",
 | 
				
			||||||
 | 
					    "@testing-library/react": "^16.0.1",
 | 
				
			||||||
 | 
					    "@testing-library/user-event": "^14.0.0",
 | 
				
			||||||
 | 
					    "@types/jest": "^29.5.13",
 | 
				
			||||||
 | 
					    "@types/react": "^18.3.11",
 | 
				
			||||||
 | 
					    "@types/react-dom": "^18.3.0",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-react": "^4.3.2",
 | 
				
			||||||
 | 
					    "date-and-time": "^3.6.0",
 | 
				
			||||||
 | 
					    "dayjs": "^1.11.13",
 | 
				
			||||||
 | 
					    "email-validator": "^2.0.4",
 | 
				
			||||||
 | 
					    "filesize": "^10.1.6",
 | 
				
			||||||
 | 
					    "jspdf": "^2.5.2",
 | 
				
			||||||
 | 
					    "mui-color-input": "^4.0.1",
 | 
				
			||||||
 | 
					    "react": "^18.3.1",
 | 
				
			||||||
 | 
					    "react-dom": "^18.3.1",
 | 
				
			||||||
 | 
					    "react-easy-crop": "^5.0.8",
 | 
				
			||||||
 | 
					    "react-qr-code": "^2.0.14",
 | 
				
			||||||
 | 
					    "react-router-dom": "^6.26.2",
 | 
				
			||||||
 | 
					    "react-zoom-pan-pinch": "^3.4.4",
 | 
				
			||||||
 | 
					    "svg2pdf.js": "^2.2.3",
 | 
				
			||||||
 | 
					    "typescript": "^5.6.2",
 | 
				
			||||||
 | 
					    "vite": "^5.4.8",
 | 
				
			||||||
 | 
					    "vite-tsconfig-paths": "^5.0.1",
 | 
				
			||||||
 | 
					    "web-vitals": "^3.5.2"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "dev": "vite",
 | 
					    "start": "vite",
 | 
				
			||||||
    "build": "tsc -b && vite build",
 | 
					    "build": "tsc && vite build",
 | 
				
			||||||
    "lint": "eslint .",
 | 
					 | 
				
			||||||
    "preview": "vite preview"
 | 
					    "preview": "vite preview"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "eslintConfig": {
 | 
				
			||||||
    "@emotion/react": "^11.14.0",
 | 
					    "extends": [
 | 
				
			||||||
    "@emotion/styled": "^11.14.1",
 | 
					      "react-app",
 | 
				
			||||||
    "@fontsource/roboto": "^5.2.8",
 | 
					      "react-app/jest"
 | 
				
			||||||
    "@fullcalendar/core": "^6.1.19",
 | 
					    ]
 | 
				
			||||||
    "@fullcalendar/daygrid": "^6.1.19",
 | 
					 | 
				
			||||||
    "@fullcalendar/interaction": "^6.1.19",
 | 
					 | 
				
			||||||
    "@fullcalendar/list": "^6.1.19",
 | 
					 | 
				
			||||||
    "@fullcalendar/react": "^6.1.19",
 | 
					 | 
				
			||||||
    "@mdi/js": "^7.4.47",
 | 
					 | 
				
			||||||
    "@mdi/react": "^1.6.1",
 | 
					 | 
				
			||||||
    "@mui/icons-material": "^7.2.0",
 | 
					 | 
				
			||||||
    "@mui/lab": "^7.0.0-beta.17",
 | 
					 | 
				
			||||||
    "@mui/material": "^7.2.0",
 | 
					 | 
				
			||||||
    "@mui/x-data-grid": "^8.16.0",
 | 
					 | 
				
			||||||
    "@mui/x-date-pickers": "^8.15.0",
 | 
					 | 
				
			||||||
    "@mui/x-tree-view": "^8.15.0",
 | 
					 | 
				
			||||||
    "date-and-time": "^3.6.0",
 | 
					 | 
				
			||||||
    "dayjs": "^1.11.19",
 | 
					 | 
				
			||||||
    "email-validator": "^2.0.4",
 | 
					 | 
				
			||||||
    "filesize": "^11.0.13",
 | 
					 | 
				
			||||||
    "jspdf": "^3.0.3",
 | 
					 | 
				
			||||||
    "mui-color-input": "^7.0.0",
 | 
					 | 
				
			||||||
    "react": "^19.2.0",
 | 
					 | 
				
			||||||
    "react-dom": "^19.2.0",
 | 
					 | 
				
			||||||
    "react-easy-crop": "^5.5.3",
 | 
					 | 
				
			||||||
    "react-qr-code": "^2.0.18",
 | 
					 | 
				
			||||||
    "react-router-dom": "^7.4.0",
 | 
					 | 
				
			||||||
    "react-zoom-pan-pinch": "^3.7.0",
 | 
					 | 
				
			||||||
    "svg2pdf.js": "^2.6.0"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "browserslist": {
 | 
				
			||||||
    "@eslint/js": "^9.38.0",
 | 
					    "production": [
 | 
				
			||||||
    "@types/react": "^19.2.2",
 | 
					      ">0.2%",
 | 
				
			||||||
    "@types/react-dom": "^19.2.2",
 | 
					      "not dead",
 | 
				
			||||||
    "@vitejs/plugin-react": "^4.7.0",
 | 
					      "not op_mini all"
 | 
				
			||||||
    "eslint": "^9.38.0",
 | 
					    ],
 | 
				
			||||||
    "eslint-plugin-react-hooks": "^5.2.0",
 | 
					    "development": [
 | 
				
			||||||
    "eslint-plugin-react-refresh": "^0.4.24",
 | 
					      "last 1 chrome version",
 | 
				
			||||||
    "globals": "^16.4.0",
 | 
					      "last 1 firefox version",
 | 
				
			||||||
    "typescript": "^5.9.3",
 | 
					      "last 1 safari version"
 | 
				
			||||||
    "typescript-eslint": "^8.46.2",
 | 
					    ]
 | 
				
			||||||
    "vite": "^7.0.6"
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,10 +59,7 @@ export function App(): React.ReactElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const context: AuthContext = {
 | 
					  const context: AuthContext = {
 | 
				
			||||||
    signedIn: signedIn,
 | 
					    signedIn: signedIn,
 | 
				
			||||||
    setSignedIn: (s) => {
 | 
					    setSignedIn: (s) => setSignedIn(s),
 | 
				
			||||||
      location.reload();
 | 
					 | 
				
			||||||
      setSignedIn(s);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const router = createBrowserRouter(
 | 
					  const router = createBrowserRouter(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
 | 
				
			|||||||
import { App } from "./App";
 | 
					import { App } from "./App";
 | 
				
			||||||
import { ServerApi } from "./api/ServerApi";
 | 
					import { ServerApi } from "./api/ServerApi";
 | 
				
			||||||
import "./index.css";
 | 
					import "./index.css";
 | 
				
			||||||
 | 
					import reportWebVitals from "./reportWebVitals";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Roboto font
 | 
					// Roboto font
 | 
				
			||||||
import "@fontsource/roboto/300.css";
 | 
					import "@fontsource/roboto/300.css";
 | 
				
			||||||
@@ -60,3 +61,8 @@ async function init() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
init();
 | 
					init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// If you want to start measuring performance in your app, pass a function
 | 
				
			||||||
 | 
					// to log results (for example: reportWebVitals(console.log))
 | 
				
			||||||
 | 
					// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
 | 
				
			||||||
 | 
					reportWebVitals();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								geneit_app/src/reportWebVitals.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								geneit_app/src/reportWebVitals.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { ReportHandler } from 'web-vitals';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const reportWebVitals = (onPerfEntry?: ReportHandler) => {
 | 
				
			||||||
 | 
					  if (onPerfEntry && onPerfEntry instanceof Function) {
 | 
				
			||||||
 | 
					    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
 | 
				
			||||||
 | 
					      getCLS(onPerfEntry);
 | 
				
			||||||
 | 
					      getFID(onPerfEntry);
 | 
				
			||||||
 | 
					      getFCP(onPerfEntry);
 | 
				
			||||||
 | 
					      getLCP(onPerfEntry);
 | 
				
			||||||
 | 
					      getTTFB(onPerfEntry);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default reportWebVitals;
 | 
				
			||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
 | 
					import { Visibility, VisibilityOff } from "@mui/icons-material";
 | 
				
			||||||
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Alert,
 | 
					  Alert,
 | 
				
			||||||
  CircularProgress,
 | 
					  CircularProgress,
 | 
				
			||||||
@@ -12,7 +11,7 @@ import {
 | 
				
			|||||||
} from "@mui/material";
 | 
					} from "@mui/material";
 | 
				
			||||||
import Box from "@mui/material/Box";
 | 
					import Box from "@mui/material/Box";
 | 
				
			||||||
import Button from "@mui/material/Button";
 | 
					import Button from "@mui/material/Button";
 | 
				
			||||||
import Grid from "@mui/material/Grid";
 | 
					import Grid from "@mui/material/Grid2";
 | 
				
			||||||
import TextField from "@mui/material/TextField";
 | 
					import TextField from "@mui/material/TextField";
 | 
				
			||||||
import Typography from "@mui/material/Typography";
 | 
					import Typography from "@mui/material/Typography";
 | 
				
			||||||
import * as React from "react";
 | 
					import * as React from "react";
 | 
				
			||||||
@@ -149,7 +148,7 @@ export function LoginRoute(): React.ReactElement {
 | 
				
			|||||||
                    onMouseDown={handleMouseDownPassword}
 | 
					                    onMouseDown={handleMouseDownPassword}
 | 
				
			||||||
                    edge="end"
 | 
					                    edge="end"
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    {showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
 | 
					                    {showPassword ? <VisibilityOff /> : <Visibility />}
 | 
				
			||||||
                  </IconButton>
 | 
					                  </IconButton>
 | 
				
			||||||
                </Tooltip>
 | 
					                </Tooltip>
 | 
				
			||||||
              </InputAdornment>
 | 
					              </InputAdornment>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,10 +137,8 @@ function UsersTable(p: {
 | 
				
			|||||||
            <GridActionsCellItem
 | 
					            <GridActionsCellItem
 | 
				
			||||||
              icon={<SaveIcon />}
 | 
					              icon={<SaveIcon />}
 | 
				
			||||||
              label="Save"
 | 
					              label="Save"
 | 
				
			||||||
              material={{
 | 
					              sx={{
 | 
				
			||||||
                sx: {
 | 
					                color: "primary.main",
 | 
				
			||||||
                  color: 'primary.main',
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
              onClick={handleSaveClick(id)}
 | 
					              onClick={handleSaveClick(id)}
 | 
				
			||||||
            />,
 | 
					            />,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ import EditIcon from "@mui/icons-material/Edit";
 | 
				
			|||||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
 | 
					import FileDownloadIcon from "@mui/icons-material/FileDownload";
 | 
				
			||||||
import SaveIcon from "@mui/icons-material/Save";
 | 
					import SaveIcon from "@mui/icons-material/Save";
 | 
				
			||||||
import { Button, Stack } from "@mui/material";
 | 
					import { Button, Stack } from "@mui/material";
 | 
				
			||||||
import Grid from "@mui/material/Grid";
 | 
					import Grid from "@mui/material/Grid2";
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { useNavigate, useParams } from "react-router-dom";
 | 
					import { useNavigate, useParams } from "react-router-dom";
 | 
				
			||||||
import { ServerApi } from "../../../api/ServerApi";
 | 
					import { ServerApi } from "../../../api/ServerApi";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import { useFamily } from "../../../widgets/BaseFamilyRoute";
 | 
				
			||||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
 | 
					import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function FamilyHomeRoute(): React.ReactElement {
 | 
					export function FamilyHomeRoute(): React.ReactElement {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ import {
 | 
				
			|||||||
  ListItemText,
 | 
					  ListItemText,
 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
} from "@mui/material";
 | 
					} from "@mui/material";
 | 
				
			||||||
import Grid from "@mui/material/Grid";
 | 
					import Grid from "@mui/material/Grid2";
 | 
				
			||||||
import * as EmailValidator from "email-validator";
 | 
					import * as EmailValidator from "email-validator";
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { useNavigate, useParams } from "react-router-dom";
 | 
					import { useNavigate, useParams } from "react-router-dom";
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								geneit_app/src/setupTests.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								geneit_app/src/setupTests.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					// jest-dom adds custom jest matchers for asserting on DOM nodes.
 | 
				
			||||||
 | 
					// allows you to do things like:
 | 
				
			||||||
 | 
					// expect(element).toHaveTextContent(/react/i)
 | 
				
			||||||
 | 
					// learn more: https://github.com/testing-library/jest-dom
 | 
				
			||||||
 | 
					import '@testing-library/jest-dom';
 | 
				
			||||||
@@ -43,13 +43,9 @@ export function BaseAuthenticatedPage(): React.ReactElement {
 | 
				
			|||||||
    setAnchorEl(null);
 | 
					    setAnchorEl(null);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const signOut = async () => {
 | 
					  const signOut = () => {
 | 
				
			||||||
    handleCloseMenu();
 | 
					    handleCloseMenu();
 | 
				
			||||||
    try {
 | 
					    AuthApi.SignOut();
 | 
				
			||||||
      await AuthApi.SignOut();
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.error(e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    navigate("/");
 | 
					    navigate("/");
 | 
				
			||||||
    auth.setSignedIn(false);
 | 
					    auth.setSignedIn(false);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,7 +54,7 @@ export function BaseFamilyRoute(): React.ReactElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const loadKey = React.useRef(1);
 | 
					  const loadKey = React.useRef(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const loadPromise = React.useRef<() => void>(null);
 | 
					  const loadPromise = React.useRef<() => void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const load = async () => {
 | 
					  const load = async () => {
 | 
				
			||||||
    const familyID = Number(familyId);
 | 
					    const familyID = Number(familyId);
 | 
				
			||||||
@@ -104,7 +104,7 @@ export function BaseFamilyRoute(): React.ReactElement {
 | 
				
			|||||||
      build={() => {
 | 
					      build={() => {
 | 
				
			||||||
        if (loadPromise.current != null) {
 | 
					        if (loadPromise.current != null) {
 | 
				
			||||||
          loadPromise.current?.();
 | 
					          loadPromise.current?.();
 | 
				
			||||||
          loadPromise.current = null;
 | 
					          loadPromise.current = undefined;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,9 +3,10 @@ import Icon from "@mdi/react";
 | 
				
			|||||||
import Avatar from "@mui/material/Avatar";
 | 
					import Avatar from "@mui/material/Avatar";
 | 
				
			||||||
import Box from "@mui/material/Box";
 | 
					import Box from "@mui/material/Box";
 | 
				
			||||||
import CssBaseline from "@mui/material/CssBaseline";
 | 
					import CssBaseline from "@mui/material/CssBaseline";
 | 
				
			||||||
import Grid from "@mui/material/Grid";
 | 
					import Grid from "@mui/material/Grid2";
 | 
				
			||||||
import Paper from "@mui/material/Paper";
 | 
					import Paper from "@mui/material/Paper";
 | 
				
			||||||
import Typography from "@mui/material/Typography";
 | 
					import Typography from "@mui/material/Typography";
 | 
				
			||||||
 | 
					import * as React from "react";
 | 
				
			||||||
import { Link, Outlet } from "react-router-dom";
 | 
					import { Link, Outlet } from "react-router-dom";
 | 
				
			||||||
import { DarkThemeButton } from "./DarkThemeButton";
 | 
					import { DarkThemeButton } from "./DarkThemeButton";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
 | 
					import { Visibility, VisibilityOff } from "@mui/icons-material";
 | 
				
			||||||
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  FormControl,
 | 
					  FormControl,
 | 
				
			||||||
  FormHelperText,
 | 
					  FormHelperText,
 | 
				
			||||||
@@ -51,7 +50,7 @@ export function PasswordInput(p: {
 | 
				
			|||||||
              onMouseDown={handleMouseDownPassword}
 | 
					              onMouseDown={handleMouseDownPassword}
 | 
				
			||||||
              edge="end"
 | 
					              edge="end"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              {showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
 | 
					              {showPassword ? <VisibilityOff /> : <Visibility />}
 | 
				
			||||||
            </IconButton>
 | 
					            </IconButton>
 | 
				
			||||||
          </InputAdornment>
 | 
					          </InputAdornment>
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const loadKey = React.useRef(1);
 | 
					  const loadKey = React.useRef(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const loadPromise = React.useRef<() => void>(null);
 | 
					  const loadPromise = React.useRef<() => void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const load = async () => {
 | 
					  const load = async () => {
 | 
				
			||||||
    setAccommodations(
 | 
					    setAccommodations(
 | 
				
			||||||
@@ -53,7 +53,7 @@ export function BaseAccommodationsRoute(): React.ReactElement {
 | 
				
			|||||||
      build={() => {
 | 
					      build={() => {
 | 
				
			||||||
        if (loadPromise.current != null) {
 | 
					        if (loadPromise.current != null) {
 | 
				
			||||||
          loadPromise.current?.();
 | 
					          loadPromise.current?.();
 | 
				
			||||||
          loadPromise.current = null;
 | 
					          loadPromise.current = undefined;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ export function BaseGenealogyRoute(): React.ReactElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const loadKey = React.useRef(1);
 | 
					  const loadKey = React.useRef(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const loadPromise = React.useRef<() => void>(null);
 | 
					  const loadPromise = React.useRef<() => void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const load = async () => {
 | 
					  const load = async () => {
 | 
				
			||||||
    setMembers(await MemberApi.GetEntireList(family.familyId));
 | 
					    setMembers(await MemberApi.GetEntireList(family.familyId));
 | 
				
			||||||
@@ -48,7 +48,7 @@ export function BaseGenealogyRoute(): React.ReactElement {
 | 
				
			|||||||
      build={() => {
 | 
					      build={() => {
 | 
				
			||||||
        if (loadPromise.current != null) {
 | 
					        if (loadPromise.current != null) {
 | 
				
			||||||
          loadPromise.current?.();
 | 
					          loadPromise.current?.();
 | 
				
			||||||
          loadPromise.current = null;
 | 
					          loadPromise.current = undefined;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "compilerOptions": {
 | 
					 | 
				
			||||||
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 | 
					 | 
				
			||||||
    "target": "ES2020",
 | 
					 | 
				
			||||||
    "useDefineForClassFields": true,
 | 
					 | 
				
			||||||
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
 | 
					 | 
				
			||||||
    "module": "ESNext",
 | 
					 | 
				
			||||||
    "skipLibCheck": true,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /* Bundler mode */
 | 
					 | 
				
			||||||
    "moduleResolution": "bundler",
 | 
					 | 
				
			||||||
    "allowImportingTsExtensions": true,
 | 
					 | 
				
			||||||
    "isolatedModules": true,
 | 
					 | 
				
			||||||
    "moduleDetection": "force",
 | 
					 | 
				
			||||||
    "noEmit": true,
 | 
					 | 
				
			||||||
    "jsx": "react-jsx",
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /* Linting */
 | 
					 | 
				
			||||||
    "strict": true,
 | 
					 | 
				
			||||||
    "noUnusedLocals": true,
 | 
					 | 
				
			||||||
    "noUnusedParameters": true,
 | 
					 | 
				
			||||||
    "noFallthroughCasesInSwitch": true,
 | 
					 | 
				
			||||||
    "noUncheckedSideEffectImports": true
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "include": ["src"]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,7 +1,21 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "files": [],
 | 
					  "compilerOptions": {
 | 
				
			||||||
  "references": [
 | 
					    "target": "ESNext",
 | 
				
			||||||
    { "path": "./tsconfig.app.json" },
 | 
					    "lib": ["dom", "dom.iterable", "esnext"],
 | 
				
			||||||
    { "path": "./tsconfig.node.json" }
 | 
					    "types": ["vite/client"],
 | 
				
			||||||
  ]
 | 
					    "allowJs": true,
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					    "esModuleInterop": true,
 | 
				
			||||||
 | 
					    "allowSyntheticDefaultImports": true,
 | 
				
			||||||
 | 
					    "strict": true,
 | 
				
			||||||
 | 
					    "forceConsistentCasingInFileNames": true,
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": true,
 | 
				
			||||||
 | 
					    "module": "esnext",
 | 
				
			||||||
 | 
					    "moduleResolution": "node",
 | 
				
			||||||
 | 
					    "resolveJsonModule": true,
 | 
				
			||||||
 | 
					    "isolatedModules": true,
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					    "jsx": "react-jsx"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["src"]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "compilerOptions": {
 | 
					 | 
				
			||||||
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 | 
					 | 
				
			||||||
    "target": "ES2022",
 | 
					 | 
				
			||||||
    "lib": ["ES2023"],
 | 
					 | 
				
			||||||
    "module": "ESNext",
 | 
					 | 
				
			||||||
    "skipLibCheck": true,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /* Bundler mode */
 | 
					 | 
				
			||||||
    "moduleResolution": "bundler",
 | 
					 | 
				
			||||||
    "allowImportingTsExtensions": true,
 | 
					 | 
				
			||||||
    "isolatedModules": true,
 | 
					 | 
				
			||||||
    "moduleDetection": "force",
 | 
					 | 
				
			||||||
    "noEmit": true,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /* Linting */
 | 
					 | 
				
			||||||
    "strict": true,
 | 
					 | 
				
			||||||
    "noUnusedLocals": true,
 | 
					 | 
				
			||||||
    "noUnusedParameters": true,
 | 
					 | 
				
			||||||
    "noFallthroughCasesInSwitch": true,
 | 
					 | 
				
			||||||
    "noUncheckedSideEffectImports": true
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "include": ["vite.config.ts"]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,7 +1,15 @@
 | 
				
			|||||||
import { defineConfig } from 'vite'
 | 
					import { defineConfig } from "vite";
 | 
				
			||||||
import react from '@vitejs/plugin-react'
 | 
					import react from "@vitejs/plugin-react";
 | 
				
			||||||
 | 
					import viteTsconfigPaths from "vite-tsconfig-paths";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// https://vite.dev/config/
 | 
					 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  plugins: [react()],
 | 
					  // depending on your application, base can also be "/"
 | 
				
			||||||
})
 | 
					  base: "/",
 | 
				
			||||||
 | 
					  plugins: [react(), viteTsconfigPaths()],
 | 
				
			||||||
 | 
					  server: {
 | 
				
			||||||
 | 
					    // this ensures that the browser opens upon server start
 | 
				
			||||||
 | 
					    open: true,
 | 
				
			||||||
 | 
					    // this sets a default port to 3000
 | 
				
			||||||
 | 
					    port: 3000,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1652
									
								
								geneit_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1652
									
								
								geneit_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,43 +1,43 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "geneit_backend"
 | 
					name = "geneit_backend"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
edition = "2024"
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
log = "0.4.28"
 | 
					log = "0.4.21"
 | 
				
			||||||
env_logger = "0.11.8"
 | 
					env_logger = "0.11.5"
 | 
				
			||||||
clap = { version = "4.5.51", features = ["derive", "env"] }
 | 
					clap = { version = "4.5.17", features = ["derive", "env"] }
 | 
				
			||||||
lazy_static = "1.5.0"
 | 
					lazy_static = "1.5.0"
 | 
				
			||||||
lazy-regex = "3.4.2"
 | 
					lazy-regex = "3.3.0"
 | 
				
			||||||
anyhow = "1.0.100"
 | 
					anyhow = "1.0.87"
 | 
				
			||||||
actix-web = "4.11.0"
 | 
					actix-web = "4.9.0"
 | 
				
			||||||
actix-cors = "0.7.1"
 | 
					actix-cors = "0.7.0"
 | 
				
			||||||
actix-multipart = "0.7.2"
 | 
					actix-multipart = "0.7.0"
 | 
				
			||||||
actix-remote-ip = "0.1.0"
 | 
					actix-remote-ip = "0.1.0"
 | 
				
			||||||
futures-util = "0.3.31"
 | 
					futures-util = "0.3.30"
 | 
				
			||||||
diesel = { version = "2.2.12", features = ["postgres"] }
 | 
					diesel = { version = "2.2.4", features = ["postgres"] }
 | 
				
			||||||
diesel_migrations = "2.2.0"
 | 
					diesel_migrations = "2.1.0"
 | 
				
			||||||
serde = { version = "1.0.228", features = ["derive"] }
 | 
					serde = { version = "1.0.210", features = ["derive"] }
 | 
				
			||||||
serde_json = "1.0.145"
 | 
					serde_json = "1.0.128"
 | 
				
			||||||
mailchecker = "6.0.19"
 | 
					mailchecker = "6.0.8"
 | 
				
			||||||
redis = "0.32.7"
 | 
					redis = "0.27.0"
 | 
				
			||||||
lettre = "0.11.19"
 | 
					lettre = "0.11.8"
 | 
				
			||||||
rand = "0.9.2"
 | 
					rand = "0.8.5"
 | 
				
			||||||
bcrypt = "0.17.1"
 | 
					bcrypt = "0.15.1"
 | 
				
			||||||
light-openid = "1.0.4"
 | 
					light-openid = "1.0.2"
 | 
				
			||||||
thiserror = "2.0.17"
 | 
					thiserror = "1.0.60"
 | 
				
			||||||
serde_with = "3.14.0"
 | 
					serde_with = "3.8.1"
 | 
				
			||||||
rust_iso3166 = "0.1.14"
 | 
					rust_iso3166 = "0.1.12"
 | 
				
			||||||
rust-s3 = "0.35.1"
 | 
					rust-s3 = "0.35.1"
 | 
				
			||||||
sha2 = "0.10.9"
 | 
					sha2 = "0.10.8"
 | 
				
			||||||
image = "0.25.8"
 | 
					image = "0.25.1"
 | 
				
			||||||
uuid = { version = "1.17.0", features = ["v4"] }
 | 
					uuid = { version = "1.8.0", features = ["v4"] }
 | 
				
			||||||
httpdate = "1.0.3"
 | 
					httpdate = "1.0.3"
 | 
				
			||||||
zip = "4.3.0"
 | 
					zip = "2.2.0"
 | 
				
			||||||
mime_guess = "2.0.5"
 | 
					mime_guess = "2.0.4"
 | 
				
			||||||
tempfile = "3.20.0"
 | 
					tempfile = "3.12.0"
 | 
				
			||||||
base64 = "0.22.1"
 | 
					base64 = "0.22.0"
 | 
				
			||||||
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
 | 
					ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
 | 
				
			||||||
chrono = "0.4.42"
 | 
					chrono = "0.4.38"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
use crate::app_config::AppConfig;
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
use diesel::result::{DatabaseErrorKind, Error};
 | 
					use diesel::result::{DatabaseErrorKind, Error};
 | 
				
			||||||
use diesel::{Connection, PgConnection};
 | 
					use diesel::{Connection, PgConnection};
 | 
				
			||||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
 | 
					use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
 | 
				
			||||||
use std::cell::RefCell;
 | 
					use std::cell::RefCell;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
 | 
					const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
 | 
				
			||||||
@@ -21,7 +21,7 @@ where
 | 
				
			|||||||
    if POSTGRES_CONNECTION.with(|i| i.borrow().is_none()) {
 | 
					    if POSTGRES_CONNECTION.with(|i| i.borrow().is_none()) {
 | 
				
			||||||
        let database_url = AppConfig::get().db_connection_chain();
 | 
					        let database_url = AppConfig::get().db_connection_chain();
 | 
				
			||||||
        let conn = PgConnection::establish(&database_url)
 | 
					        let conn = PgConnection::establish(&database_url)
 | 
				
			||||||
            .unwrap_or_else(|_| panic!("Error connecting to {database_url}"));
 | 
					            .unwrap_or_else(|_| panic!("Error connecting to {}", database_url));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        POSTGRES_CONNECTION.with(|i| *i.borrow_mut() = Some(conn))
 | 
					        POSTGRES_CONNECTION.with(|i| *i.borrow_mut() = Some(conn))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -38,7 +38,7 @@ where
 | 
				
			|||||||
                POSTGRES_CONNECTION.with(|i| *i.borrow_mut() = None)
 | 
					                POSTGRES_CONNECTION.with(|i| *i.borrow_mut() = None)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            log::error!("Database query error! {e:?}");
 | 
					            log::error!("Database query error! {:?}", e);
 | 
				
			||||||
            Err(e.into())
 | 
					            Err(e.into())
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ pub async fn create_bucket_if_required() -> anyhow::Result<()> {
 | 
				
			|||||||
            log::warn!("The bucket does not seem to exists, trying to create it!")
 | 
					            log::warn!("The bucket does not seem to exists, trying to create it!")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            log::error!("Got unexpected error when querying bucket info: {e}");
 | 
					            log::error!("Got unexpected error when querying bucket info: {}", e);
 | 
				
			||||||
            return Err(BucketServiceError::FailedFetchBucketInfo.into());
 | 
					            return Err(BucketServiceError::FailedFetchBucketInfo.into());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
 | 
				
			|||||||
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
 | 
					use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
 | 
				
			||||||
use crate::models::Accommodation;
 | 
					use crate::models::Accommodation;
 | 
				
			||||||
use crate::services::accommodations_list_service;
 | 
					use crate::services::accommodations_list_service;
 | 
				
			||||||
use actix_web::{HttpResponse, web};
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(thiserror::Error, Debug)]
 | 
					#[derive(thiserror::Error, Debug)]
 | 
				
			||||||
enum AccommodationListControllerErr {
 | 
					enum AccommodationListControllerErr {
 | 
				
			||||||
@@ -34,16 +34,18 @@ impl AccommodationRequest {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        accommodation.name = self.name;
 | 
					        accommodation.name = self.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(d) = &self.description
 | 
					        if let Some(d) = &self.description {
 | 
				
			||||||
            && !c.accommodation_description_len.validate(d) {
 | 
					            if !c.accommodation_description_len.validate(d) {
 | 
				
			||||||
                return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
 | 
					                return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        accommodation.description.clone_from(&self.description);
 | 
					        accommodation.description.clone_from(&self.description);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(c) = &self.color
 | 
					        if let Some(c) = &self.color {
 | 
				
			||||||
            && !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
 | 
					            if !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
 | 
				
			||||||
                return Err(AccommodationListControllerErr::MalformedColor.into());
 | 
					                return Err(AccommodationListControllerErr::MalformedColor.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        accommodation.color.clone_from(&self.color);
 | 
					        accommodation.color.clone_from(&self.color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        accommodation.need_validation = self.need_validation;
 | 
					        accommodation.need_validation = self.need_validation;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
use ical::{generator::*, *};
 | 
					use ical::{generator::*, *};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use actix_web::{HttpResponse, web};
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
use chrono::DateTime;
 | 
					use chrono::DateTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::StaticConstraints;
 | 
					use crate::constants::StaticConstraints;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ use crate::extractors::family_extractor::FamilyInPath;
 | 
				
			|||||||
use crate::models::{Accommodation, AccommodationReservationID, NewAccommodationReservation};
 | 
					use crate::models::{Accommodation, AccommodationReservationID, NewAccommodationReservation};
 | 
				
			||||||
use crate::services::accommodations_reservations_service;
 | 
					use crate::services::accommodations_reservations_service;
 | 
				
			||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
use actix_web::{HttpResponse, web};
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Deserialize)]
 | 
					#[derive(serde::Deserialize)]
 | 
				
			||||||
pub struct UpdateReservationQuery {
 | 
					pub struct UpdateReservationQuery {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ use crate::services::login_token_service::LoginTokenValue;
 | 
				
			|||||||
use crate::services::rate_limiter_service::RatedAction;
 | 
					use crate::services::rate_limiter_service::RatedAction;
 | 
				
			||||||
use crate::services::{login_token_service, openid_service, rate_limiter_service, users_service};
 | 
					use crate::services::{login_token_service, openid_service, rate_limiter_service, users_service};
 | 
				
			||||||
use actix_remote_ip::RemoteIP;
 | 
					use actix_remote_ip::RemoteIP;
 | 
				
			||||||
use actix_web::{HttpResponse, web};
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Deserialize)]
 | 
					#[derive(serde::Deserialize)]
 | 
				
			||||||
pub struct CreateAccountBody {
 | 
					pub struct CreateAccountBody {
 | 
				
			||||||
@@ -79,7 +79,10 @@ pub async fn request_reset_password(
 | 
				
			|||||||
    match users_service::get_by_mail(&req.mail).await {
 | 
					    match users_service::get_by_mail(&req.mail).await {
 | 
				
			||||||
        Ok(mut user) => users_service::request_reset_password(&mut user).await?,
 | 
					        Ok(mut user) => users_service::request_reset_password(&mut user).await?,
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            log::error!("Could not locate user account {e}! (error silently ignored)");
 | 
					            log::error!(
 | 
				
			||||||
 | 
					                "Could not locate user account {}! (error silently ignored)",
 | 
				
			||||||
 | 
					                e
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -119,7 +122,7 @@ pub async fn check_reset_password_token(
 | 
				
			|||||||
                RatedAction::CheckResetPasswordTokenFailed,
 | 
					                RatedAction::CheckResetPasswordTokenFailed,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
            log::error!("Password reset token could not be used: {e}");
 | 
					            log::error!("Password reset token could not be used: {}", e);
 | 
				
			||||||
            return Ok(HttpResponse::NotFound().finish());
 | 
					            return Ok(HttpResponse::NotFound().finish());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -153,7 +156,7 @@ pub async fn reset_password(remote_ip: RemoteIP, req: web::Json<ResetPasswordBod
 | 
				
			|||||||
                RatedAction::CheckResetPasswordTokenFailed,
 | 
					                RatedAction::CheckResetPasswordTokenFailed,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
            log::error!("Password reset token could not be used: {e}");
 | 
					            log::error!("Password reset token could not be used: {}", e);
 | 
				
			||||||
            return Ok(HttpResponse::NotFound().finish());
 | 
					            return Ok(HttpResponse::NotFound().finish());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -193,7 +196,7 @@ pub async fn password_login(remote_ip: RemoteIP, req: web::Json<PasswordLoginQue
 | 
				
			|||||||
    let user = match users_service::get_by_mail(&req.mail).await {
 | 
					    let user = match users_service::get_by_mail(&req.mail).await {
 | 
				
			||||||
        Ok(u) => u,
 | 
					        Ok(u) => u,
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            log::error!("Auth failed: could not find account by mail! {e}");
 | 
					            log::error!("Auth failed: could not find account by mail! {}", e);
 | 
				
			||||||
            rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin)
 | 
					            rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin)
 | 
				
			||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
            return Ok(HttpResponse::Unauthorized().json("Invalid credentials"));
 | 
					            return Ok(HttpResponse::Unauthorized().json("Invalid credentials"));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
use crate::controllers::HttpResult;
 | 
					 | 
				
			||||||
use crate::controllers::members_controller::RequestDate;
 | 
					use crate::controllers::members_controller::RequestDate;
 | 
				
			||||||
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
use crate::extractors::couple_extractor::FamilyAndCoupleInPath;
 | 
					use crate::extractors::couple_extractor::FamilyAndCoupleInPath;
 | 
				
			||||||
use crate::extractors::family_extractor::FamilyInPath;
 | 
					use crate::extractors::family_extractor::FamilyInPath;
 | 
				
			||||||
use crate::models::{Couple, CoupleState, MemberID, PhotoID};
 | 
					use crate::models::{Couple, CoupleState, MemberID, PhotoID};
 | 
				
			||||||
use crate::services::{couples_service, members_service, photos_service};
 | 
					use crate::services::{couples_service, members_service, photos_service};
 | 
				
			||||||
use actix_multipart::form::MultipartForm;
 | 
					 | 
				
			||||||
use actix_multipart::form::tempfile::TempFile;
 | 
					use actix_multipart::form::tempfile::TempFile;
 | 
				
			||||||
use actix_web::{HttpResponse, web};
 | 
					use actix_multipart::form::MultipartForm;
 | 
				
			||||||
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
serde_with::with_prefix!(prefix_wedding "wedding_");
 | 
					serde_with::with_prefix!(prefix_wedding "wedding_");
 | 
				
			||||||
serde_with::with_prefix!(prefix_divorce "divorce_");
 | 
					serde_with::with_prefix!(prefix_divorce "divorce_");
 | 
				
			||||||
@@ -48,20 +48,23 @@ impl CoupleRequest {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(husband) = self.husband
 | 
					        if let Some(husband) = self.husband {
 | 
				
			||||||
            && !members_service::exists(couple.family_id(), husband).await? {
 | 
					            if !members_service::exists(couple.family_id(), husband).await? {
 | 
				
			||||||
                return Err(CoupleControllerErr::HusbandNotExisting.into());
 | 
					                return Err(CoupleControllerErr::HusbandNotExisting.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(d) = &self.wedding
 | 
					        if let Some(d) = &self.wedding {
 | 
				
			||||||
            && !d.check() {
 | 
					            if !d.check() {
 | 
				
			||||||
                return Err(CoupleControllerErr::MalformedDateOfWedding.into());
 | 
					                return Err(CoupleControllerErr::MalformedDateOfWedding.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(d) = &self.divorce
 | 
					        if let Some(d) = &self.divorce {
 | 
				
			||||||
            && !d.check() {
 | 
					            if !d.check() {
 | 
				
			||||||
                return Err(CoupleControllerErr::MalformedDateOfDivorce.into());
 | 
					                return Err(CoupleControllerErr::MalformedDateOfDivorce.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        couple.set_wife(self.wife);
 | 
					        couple.set_wife(self.wife);
 | 
				
			||||||
        couple.set_husband(self.husband);
 | 
					        couple.set_husband(self.husband);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
use crate::connections::s3_connection;
 | 
					use crate::connections::s3_connection;
 | 
				
			||||||
use crate::constants;
 | 
					use crate::constants;
 | 
				
			||||||
use crate::controllers::HttpResult;
 | 
					 | 
				
			||||||
use crate::controllers::couples_controller::CoupleRequest;
 | 
					use crate::controllers::couples_controller::CoupleRequest;
 | 
				
			||||||
use crate::controllers::members_controller::MemberRequest;
 | 
					use crate::controllers::members_controller::MemberRequest;
 | 
				
			||||||
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
 | 
					use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
 | 
				
			||||||
use crate::models::{CoupleID, MemberID, PhotoID};
 | 
					use crate::models::{CoupleID, MemberID, PhotoID};
 | 
				
			||||||
use crate::services::photos_service::UploadedFile;
 | 
					use crate::services::photos_service::UploadedFile;
 | 
				
			||||||
use crate::services::{couples_service, members_service, photos_service};
 | 
					use crate::services::{couples_service, members_service, photos_service};
 | 
				
			||||||
use actix_multipart::form::MultipartForm;
 | 
					 | 
				
			||||||
use actix_multipart::form::tempfile::TempFile;
 | 
					use actix_multipart::form::tempfile::TempFile;
 | 
				
			||||||
 | 
					use actix_multipart::form::MultipartForm;
 | 
				
			||||||
use actix_web::HttpResponse;
 | 
					use actix_web::HttpResponse;
 | 
				
			||||||
use mime_guess::Mime;
 | 
					use mime_guess::Mime;
 | 
				
			||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
@@ -183,9 +183,9 @@ pub async fn import_family(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Err(e) = req_member_data.to_member(member).await {
 | 
					        if let Err(e) = req_member_data.to_member(member).await {
 | 
				
			||||||
            log::error!("Error while processing import (member {req_id:?}) - {e}");
 | 
					            log::error!("Error while processing import (member {:?}) - {e}", req_id);
 | 
				
			||||||
            return Ok(
 | 
					            return Ok(
 | 
				
			||||||
                HttpResponse::BadRequest().json(format!("Failed to validate member {req_id:?}!"))
 | 
					                HttpResponse::BadRequest().json(format!("Failed to validate member {:?}!", req_id))
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use crate::constants::{FAMILY_INVITATION_CODE_LEN, StaticConstraints};
 | 
					use crate::constants::{StaticConstraints, FAMILY_INVITATION_CODE_LEN};
 | 
				
			||||||
use crate::controllers::HttpResult;
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
 | 
					use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
 | 
				
			||||||
use crate::models::{FamilyMembership, UserID};
 | 
					use crate::models::{FamilyMembership, UserID};
 | 
				
			||||||
@@ -7,7 +7,7 @@ use crate::services::rate_limiter_service::RatedAction;
 | 
				
			|||||||
use crate::services::{families_service, rate_limiter_service};
 | 
					use crate::services::{families_service, rate_limiter_service};
 | 
				
			||||||
use crate::utils::string_utils::rand_str;
 | 
					use crate::utils::string_utils::rand_str;
 | 
				
			||||||
use actix_remote_ip::RemoteIP;
 | 
					use actix_remote_ip::RemoteIP;
 | 
				
			||||||
use actix_web::{HttpResponse, web};
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, serde::Deserialize)]
 | 
					#[derive(Debug, serde::Deserialize)]
 | 
				
			||||||
pub struct CreateFamilyReq {
 | 
					pub struct CreateFamilyReq {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,9 +5,9 @@ use crate::extractors::member_extractor::FamilyAndMemberInPath;
 | 
				
			|||||||
use crate::models::{Member, MemberID, PhotoID, Sex};
 | 
					use crate::models::{Member, MemberID, PhotoID, Sex};
 | 
				
			||||||
use crate::services::{members_service, photos_service};
 | 
					use crate::services::{members_service, photos_service};
 | 
				
			||||||
use crate::utils::countries_utils;
 | 
					use crate::utils::countries_utils;
 | 
				
			||||||
use actix_multipart::form::MultipartForm;
 | 
					 | 
				
			||||||
use actix_multipart::form::tempfile::TempFile;
 | 
					use actix_multipart::form::tempfile::TempFile;
 | 
				
			||||||
use actix_web::{HttpResponse, web};
 | 
					use actix_multipart::form::MultipartForm;
 | 
				
			||||||
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
serde_with::with_prefix!(prefix_birth "birth_");
 | 
					serde_with::with_prefix!(prefix_birth "birth_");
 | 
				
			||||||
serde_with::with_prefix!(prefix_death "death_");
 | 
					serde_with::with_prefix!(prefix_death "death_");
 | 
				
			||||||
@@ -95,10 +95,11 @@ fn check_opt_str_val(
 | 
				
			|||||||
    c: SizeConstraint,
 | 
					    c: SizeConstraint,
 | 
				
			||||||
    err: MemberControllerErr,
 | 
					    err: MemberControllerErr,
 | 
				
			||||||
) -> anyhow::Result<()> {
 | 
					) -> anyhow::Result<()> {
 | 
				
			||||||
    if let Some(v) = val
 | 
					    if let Some(v) = val {
 | 
				
			||||||
        && !c.validate(v) {
 | 
					        if !c.validate(v) {
 | 
				
			||||||
            return Err(err.into());
 | 
					            return Err(err.into());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -150,10 +151,11 @@ impl MemberRequest {
 | 
				
			|||||||
            MemberControllerErr::MalformedEmailAddress,
 | 
					            MemberControllerErr::MalformedEmailAddress,
 | 
				
			||||||
        )?;
 | 
					        )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(mail) = &self.email
 | 
					        if let Some(mail) = &self.email {
 | 
				
			||||||
            && !mailchecker::is_valid(mail) {
 | 
					            if !mailchecker::is_valid(mail) {
 | 
				
			||||||
                return Err(MemberControllerErr::InvalidEmailAddress.into());
 | 
					                return Err(MemberControllerErr::InvalidEmailAddress.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        check_opt_str_val(
 | 
					        check_opt_str_val(
 | 
				
			||||||
            &self.phone,
 | 
					            &self.phone,
 | 
				
			||||||
@@ -185,20 +187,23 @@ impl MemberRequest {
 | 
				
			|||||||
            MemberControllerErr::MalformedCountry,
 | 
					            MemberControllerErr::MalformedCountry,
 | 
				
			||||||
        )?;
 | 
					        )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(c) = &self.country
 | 
					        if let Some(c) = &self.country {
 | 
				
			||||||
            && !countries_utils::is_code_valid(c) {
 | 
					            if !countries_utils::is_code_valid(c) {
 | 
				
			||||||
                return Err(MemberControllerErr::InvalidCountryCode.into());
 | 
					                return Err(MemberControllerErr::InvalidCountryCode.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(d) = &self.birth
 | 
					        if let Some(d) = &self.birth {
 | 
				
			||||||
            && !d.check() {
 | 
					            if !d.check() {
 | 
				
			||||||
                return Err(MemberControllerErr::MalformedDateOfBirth.into());
 | 
					                return Err(MemberControllerErr::MalformedDateOfBirth.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(d) = &self.death
 | 
					        if let Some(d) = &self.death {
 | 
				
			||||||
            && !d.check() {
 | 
					            if !d.check() {
 | 
				
			||||||
                return Err(MemberControllerErr::MalformedDateOfDeath.into());
 | 
					                return Err(MemberControllerErr::MalformedDateOfDeath.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        check_opt_str_val(
 | 
					        check_opt_str_val(
 | 
				
			||||||
            &self.note,
 | 
					            &self.note,
 | 
				
			||||||
@@ -216,10 +221,11 @@ impl MemberRequest {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(father) = self.father
 | 
					        if let Some(father) = self.father {
 | 
				
			||||||
            && !members_service::exists(member.family_id(), father).await? {
 | 
					            if !members_service::exists(member.family_id(), father).await? {
 | 
				
			||||||
                return Err(MemberControllerErr::FatherNotExisting.into());
 | 
					                return Err(MemberControllerErr::FatherNotExisting.into());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        member.first_name = self.first_name;
 | 
					        member.first_name = self.first_name;
 | 
				
			||||||
        member.last_name = self.last_name;
 | 
					        member.last_name = self.last_name;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
//! # API controller
 | 
					//! # API controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use actix_web::HttpResponse;
 | 
					 | 
				
			||||||
use actix_web::body::BoxBody;
 | 
					use actix_web::body::BoxBody;
 | 
				
			||||||
 | 
					use actix_web::HttpResponse;
 | 
				
			||||||
use std::fmt::{Debug, Display, Formatter};
 | 
					use std::fmt::{Debug, Display, Formatter};
 | 
				
			||||||
use zip::result::ZipError;
 | 
					use zip::result::ZipError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,7 +31,7 @@ impl Display for HttpErr {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl actix_web::error::ResponseError for HttpErr {
 | 
					impl actix_web::error::ResponseError for HttpErr {
 | 
				
			||||||
    fn error_response(&self) -> HttpResponse<BoxBody> {
 | 
					    fn error_response(&self) -> HttpResponse<BoxBody> {
 | 
				
			||||||
        log::error!("Error while processing request! {self}");
 | 
					        log::error!("Error while processing request! {}", self);
 | 
				
			||||||
        HttpResponse::InternalServerError().body("Failed to execute request!")
 | 
					        HttpResponse::InternalServerError().body("Failed to execute request!")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ use crate::controllers::HttpResult;
 | 
				
			|||||||
use crate::models::PhotoID;
 | 
					use crate::models::PhotoID;
 | 
				
			||||||
use crate::services::photos_service;
 | 
					use crate::services::photos_service;
 | 
				
			||||||
use actix_web::http::header;
 | 
					use actix_web::http::header;
 | 
				
			||||||
use actix_web::{HttpRequest, HttpResponse, web};
 | 
					use actix_web::{web, HttpRequest, HttpResponse};
 | 
				
			||||||
use std::ops::Add;
 | 
					use std::ops::Add;
 | 
				
			||||||
use std::time::{Duration, UNIX_EPOCH};
 | 
					use std::time::{Duration, UNIX_EPOCH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,15 +36,16 @@ async fn get_photo(id: &PhotoIdPath, full_size: bool, req: HttpRequest) -> HttpR
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check if an upload is un-necessary
 | 
					    // Check if an upload is un-necessary
 | 
				
			||||||
    if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
 | 
					    if let Some(c) = req.headers().get(header::IF_NONE_MATCH) {
 | 
				
			||||||
        && c.to_str().unwrap_or("") == hash {
 | 
					        if c.to_str().unwrap_or("") == hash {
 | 
				
			||||||
            return Ok(HttpResponse::NotModified().finish());
 | 
					            return Ok(HttpResponse::NotModified().finish());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) {
 | 
					    if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) {
 | 
				
			||||||
        let date_str = c.to_str().unwrap_or("");
 | 
					        let date_str = c.to_str().unwrap_or("");
 | 
				
			||||||
        if let Ok(date) = httpdate::parse_http_date(date_str)
 | 
					        if let Ok(date) = httpdate::parse_http_date(date_str) {
 | 
				
			||||||
            && date
 | 
					            if date
 | 
				
			||||||
                .add(Duration::from_secs(1))
 | 
					                .add(Duration::from_secs(1))
 | 
				
			||||||
                .duration_since(UNIX_EPOCH)
 | 
					                .duration_since(UNIX_EPOCH)
 | 
				
			||||||
                .unwrap()
 | 
					                .unwrap()
 | 
				
			||||||
@@ -53,6 +54,7 @@ async fn get_photo(id: &PhotoIdPath, full_size: bool, req: HttpRequest) -> HttpR
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                return Ok(HttpResponse::NotModified().finish());
 | 
					                return Ok(HttpResponse::NotModified().finish());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let bytes = s3_connection::get_file(&match full_size {
 | 
					    let bytes = s3_connection::get_file(&match full_size {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,8 +9,8 @@ use crate::services::login_token_service::LoginToken;
 | 
				
			|||||||
use crate::services::rate_limiter_service::RatedAction;
 | 
					use crate::services::rate_limiter_service::RatedAction;
 | 
				
			||||||
use crate::services::{rate_limiter_service, users_service};
 | 
					use crate::services::{rate_limiter_service, users_service};
 | 
				
			||||||
use actix_remote_ip::RemoteIP;
 | 
					use actix_remote_ip::RemoteIP;
 | 
				
			||||||
use actix_web::HttpResponse;
 | 
					 | 
				
			||||||
use actix_web::web::Json;
 | 
					use actix_web::web::Json;
 | 
				
			||||||
 | 
					use actix_web::HttpResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Serialize)]
 | 
					#[derive(serde::Serialize)]
 | 
				
			||||||
struct UserAPI<'a> {
 | 
					struct UserAPI<'a> {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -75,7 +75,7 @@ impl FromRequest for FamilyAndAccommodationInPath {
 | 
				
			|||||||
            Self::load_accommodation_from_path(family, accommodation_id)
 | 
					            Self::load_accommodation_from_path(family, accommodation_id)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|e| {
 | 
					                .map_err(|e| {
 | 
				
			||||||
                    log::error!("Failed to extract accommodation ID from URL! {e}");
 | 
					                    log::error!("Failed to extract accommodation ID from URL! {}", e);
 | 
				
			||||||
                    actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
 | 
					                    actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -85,7 +85,7 @@ impl FromRequest for FamilyAndAccommodationReservationCalendarInPath {
 | 
				
			|||||||
            Self::load_calendar_from_path(family, accommodation_id)
 | 
					            Self::load_calendar_from_path(family, accommodation_id)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|e| {
 | 
					                .map_err(|e| {
 | 
				
			||||||
                    log::error!("Failed to extract calendar ID from URL! {e}");
 | 
					                    log::error!("Failed to extract calendar ID from URL! {}", e);
 | 
				
			||||||
                    actix_web::error::ErrorNotFound("Could not fetch calendar information!")
 | 
					                    actix_web::error::ErrorNotFound("Could not fetch calendar information!")
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -95,7 +95,7 @@ impl FromRequest for FamilyAndAccommodationReservationInPath {
 | 
				
			|||||||
            Self::load_accommodation_reservation_from_path(family, reservation_id)
 | 
					            Self::load_accommodation_reservation_from_path(family, reservation_id)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|e| {
 | 
					                .map_err(|e| {
 | 
				
			||||||
                    log::error!("Failed to extract accommodation ID from URL! {e}");
 | 
					                    log::error!("Failed to extract accommodation ID from URL! {}", e);
 | 
				
			||||||
                    actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
 | 
					                    actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,7 +71,7 @@ impl FromRequest for FamilyAndCoupleInPath {
 | 
				
			|||||||
            FamilyAndCoupleInPath::load_couple_from_path(family, couple_id)
 | 
					            FamilyAndCoupleInPath::load_couple_from_path(family, couple_id)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|e| {
 | 
					                .map_err(|e| {
 | 
				
			||||||
                    log::error!("Failed to extract couple ID from URL! {e}");
 | 
					                    log::error!("Failed to extract couple ID from URL! {}", e);
 | 
				
			||||||
                    actix_web::error::ErrorNotFound("Could not fetch couple information!")
 | 
					                    actix_web::error::ErrorNotFound("Could not fetch couple information!")
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -62,7 +62,7 @@ impl FromRequest for FamilyInPath {
 | 
				
			|||||||
            FamilyInPath::load_family_from_path(&token, family_id)
 | 
					            FamilyInPath::load_family_from_path(&token, family_id)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|e| {
 | 
					                .map_err(|e| {
 | 
				
			||||||
                    log::error!("Failed to extract family ID from URL! {e}");
 | 
					                    log::error!("Failed to extract family ID from URL! {}", e);
 | 
				
			||||||
                    actix_web::error::ErrorNotFound("Could not fetch family information!")
 | 
					                    actix_web::error::ErrorNotFound("Could not fetch family information!")
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,7 +71,7 @@ impl FromRequest for FamilyAndMemberInPath {
 | 
				
			|||||||
            FamilyAndMemberInPath::load_member_from_path(family, member_id)
 | 
					            FamilyAndMemberInPath::load_member_from_path(family, member_id)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|e| {
 | 
					                .map_err(|e| {
 | 
				
			||||||
                    log::error!("Failed to extract member ID from URL! {e}");
 | 
					                    log::error!("Failed to extract member ID from URL! {}", e);
 | 
				
			||||||
                    actix_web::error::ErrorNotFound("Could not fetch member information!")
 | 
					                    actix_web::error::ErrorNotFound("Could not fetch member information!")
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ use actix_cors::Cors;
 | 
				
			|||||||
use actix_multipart::form::tempfile::TempFileConfig;
 | 
					use actix_multipart::form::tempfile::TempFileConfig;
 | 
				
			||||||
use actix_remote_ip::RemoteIPConfig;
 | 
					use actix_remote_ip::RemoteIPConfig;
 | 
				
			||||||
use actix_web::middleware::Logger;
 | 
					use actix_web::middleware::Logger;
 | 
				
			||||||
use actix_web::{App, HttpServer, web};
 | 
					use actix_web::{web, App, HttpServer};
 | 
				
			||||||
use geneit_backend::app_config::AppConfig;
 | 
					use geneit_backend::app_config::AppConfig;
 | 
				
			||||||
use geneit_backend::connections::{db_connection, s3_connection};
 | 
					use geneit_backend::connections::{db_connection, s3_connection};
 | 
				
			||||||
use geneit_backend::controllers::{
 | 
					use geneit_backend::controllers::{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ impl User {
 | 
				
			|||||||
            .as_deref()
 | 
					            .as_deref()
 | 
				
			||||||
            .map(|hash| {
 | 
					            .map(|hash| {
 | 
				
			||||||
                bcrypt::verify(password, hash).unwrap_or_else(|e| {
 | 
					                bcrypt::verify(password, hash).unwrap_or_else(|e| {
 | 
				
			||||||
                    log::error!("Failed to validate password! {e}");
 | 
					                    log::error!("Failed to validate password! {}", e);
 | 
				
			||||||
                    false
 | 
					                    false
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ use crate::utils::string_utils::rand_str;
 | 
				
			|||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
use actix_web::dev::Payload;
 | 
					use actix_web::dev::Payload;
 | 
				
			||||||
use actix_web::{FromRequest, HttpRequest};
 | 
					use actix_web::{FromRequest, HttpRequest};
 | 
				
			||||||
use std::future::{Ready, ready};
 | 
					use std::future::{ready, Ready};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(thiserror::Error, Debug)]
 | 
					#[derive(thiserror::Error, Debug)]
 | 
				
			||||||
enum LoginTokenServiceError {
 | 
					enum LoginTokenServiceError {
 | 
				
			||||||
@@ -125,13 +125,13 @@ async fn load_token_info(token: &LoginTokenValue) -> anyhow::Result<Option<Login
 | 
				
			|||||||
    let token = match user_tokens.iter_mut().find(|t| t.key == key) {
 | 
					    let token = match user_tokens.iter_mut().find(|t| t.key == key) {
 | 
				
			||||||
        Some(t) => t,
 | 
					        Some(t) => t,
 | 
				
			||||||
        None => {
 | 
					        None => {
 | 
				
			||||||
            log::error!("Could not find token for key '{key}' (missing token)");
 | 
					            log::error!("Could not find token for key '{}' (missing token)", key);
 | 
				
			||||||
            return Ok(None);
 | 
					            return Ok(None);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if token.is_expired() {
 | 
					    if token.is_expired() {
 | 
				
			||||||
        log::error!("Could not find token for key '{key}' (token expired)");
 | 
					        log::error!("Could not find token for key '{}' (token expired)", key);
 | 
				
			||||||
        return Ok(None);
 | 
					        return Ok(None);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -169,7 +169,7 @@ impl FromRequest for LoginToken {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            let token = match load_token_info(&token).await {
 | 
					            let token = match load_token_info(&token).await {
 | 
				
			||||||
                Err(e) => {
 | 
					                Err(e) => {
 | 
				
			||||||
                    log::error!("Failed to load auth token! {e}");
 | 
					                    log::error!("Failed to load auth token! {}", e);
 | 
				
			||||||
                    return Err(actix_web::error::ErrorPreconditionFailed(
 | 
					                    return Err(actix_web::error::ErrorPreconditionFailed(
 | 
				
			||||||
                        "Failed to check auth token!",
 | 
					                        "Failed to check auth token!",
 | 
				
			||||||
                    ));
 | 
					                    ));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ pub async fn send_mail<D: Display>(to: &str, subject: &str, body: D) -> anyhow::
 | 
				
			|||||||
    let mailer = mailer.build();
 | 
					    let mailer = mailer.build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mailer.send(&email)?;
 | 
					    mailer.send(&email)?;
 | 
				
			||||||
    log::debug!("A mail was sent to {to} (subject = {subject})");
 | 
					    log::debug!("A mail was sent to {} (subject = {})", to, subject);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,8 @@ use crate::models::{FamilyID, Member, MemberID, NewMember};
 | 
				
			|||||||
use crate::schema::members;
 | 
					use crate::schema::members;
 | 
				
			||||||
use crate::services::{couples_service, photos_service};
 | 
					use crate::services::{couples_service, photos_service};
 | 
				
			||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
use diesel::RunQueryDsl;
 | 
					 | 
				
			||||||
use diesel::prelude::*;
 | 
					use diesel::prelude::*;
 | 
				
			||||||
 | 
					use diesel::RunQueryDsl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a new family member
 | 
					/// Create a new family member
 | 
				
			||||||
pub async fn create(family_id: FamilyID) -> anyhow::Result<Member> {
 | 
					pub async fn create(family_id: FamilyID) -> anyhow::Result<Member> {
 | 
				
			||||||
@@ -147,12 +147,13 @@ pub mod loop_detection {
 | 
				
			|||||||
        prev: Option<&'a LoopStack<'a>>,
 | 
					        prev: Option<&'a LoopStack<'a>>,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    impl LoopStack<'_> {
 | 
					    impl<'a> LoopStack<'a> {
 | 
				
			||||||
        pub fn contains(&self, id: MemberID) -> bool {
 | 
					        pub fn contains(&self, id: MemberID) -> bool {
 | 
				
			||||||
            if let Some(ls) = &self.prev
 | 
					            if let Some(ls) = &self.prev {
 | 
				
			||||||
                && ls.contains(id) {
 | 
					                if ls.contains(id) {
 | 
				
			||||||
                    return true;
 | 
					                    return true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.curr == id
 | 
					            self.curr == id
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -167,7 +168,7 @@ pub mod loop_detection {
 | 
				
			|||||||
            None => false,
 | 
					            None => false,
 | 
				
			||||||
            Some(id) => {
 | 
					            Some(id) => {
 | 
				
			||||||
                if curr_stack.contains(id) {
 | 
					                if curr_stack.contains(id) {
 | 
				
			||||||
                    log::debug!("Loop detected! {curr_stack:?}");
 | 
					                    log::debug!("Loop detected! {:?}", curr_stack);
 | 
				
			||||||
                    return true;
 | 
					                    return true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -187,10 +188,7 @@ pub mod loop_detection {
 | 
				
			|||||||
        let member = match members.get(&curr_stack.curr) {
 | 
					        let member = match members.get(&curr_stack.curr) {
 | 
				
			||||||
            Some(m) => m,
 | 
					            Some(m) => m,
 | 
				
			||||||
            None => {
 | 
					            None => {
 | 
				
			||||||
                log::warn!(
 | 
					                log::warn!("Member {:?} not found in the tree for loop detection, this should never happen!", curr_stack.curr);
 | 
				
			||||||
                    "Member {:?} not found in the tree for loop detection, this should never happen!",
 | 
					 | 
				
			||||||
                    curr_stack.curr
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@ fn redis_key(state: &str) -> String {
 | 
				
			|||||||
    format!("oidc-state-{state}")
 | 
					    format!("oidc-state-{state}")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn load_provider_info(prov_id: &str) -> anyhow::Result<OpenIDClient<'_>> {
 | 
					async fn load_provider_info(prov_id: &str) -> anyhow::Result<OpenIDClient> {
 | 
				
			||||||
    let prov = AppConfig::get()
 | 
					    let prov = AppConfig::get()
 | 
				
			||||||
        .openid_providers()
 | 
					        .openid_providers()
 | 
				
			||||||
        .into_iter()
 | 
					        .into_iter()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,8 @@ use crate::utils::crypt_utils::sha512;
 | 
				
			|||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
use actix_multipart::form::tempfile::TempFile;
 | 
					use actix_multipart::form::tempfile::TempFile;
 | 
				
			||||||
use diesel::prelude::*;
 | 
					use diesel::prelude::*;
 | 
				
			||||||
use image::ImageFormat;
 | 
					 | 
				
			||||||
use image::imageops::FilterType;
 | 
					use image::imageops::FilterType;
 | 
				
			||||||
 | 
					use image::ImageFormat;
 | 
				
			||||||
use mime_guess::Mime;
 | 
					use mime_guess::Mime;
 | 
				
			||||||
use std::fs::File;
 | 
					use std::fs::File;
 | 
				
			||||||
use std::io::{Cursor, Read, Seek, Write};
 | 
					use std::io::{Cursor, Read, Seek, Write};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ use crate::utils::string_utils::rand_str;
 | 
				
			|||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
use bcrypt::DEFAULT_COST;
 | 
					use bcrypt::DEFAULT_COST;
 | 
				
			||||||
use diesel::prelude::*;
 | 
					use diesel::prelude::*;
 | 
				
			||||||
 | 
					use std::io::ErrorKind;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get the information of a user, by its id
 | 
					/// Get the information of a user, by its id
 | 
				
			||||||
pub async fn get_by_id(id: UserID) -> anyhow::Result<User> {
 | 
					pub async fn get_by_id(id: UserID) -> anyhow::Result<User> {
 | 
				
			||||||
@@ -24,7 +25,8 @@ pub async fn get_by_mail(mail: &str) -> anyhow::Result<User> {
 | 
				
			|||||||
/// Get the information of a user, by its password reset token
 | 
					/// Get the information of a user, by its password reset token
 | 
				
			||||||
pub async fn get_by_pwd_reset_token(token: &str) -> anyhow::Result<User> {
 | 
					pub async fn get_by_pwd_reset_token(token: &str) -> anyhow::Result<User> {
 | 
				
			||||||
    if token.is_empty() {
 | 
					    if token.is_empty() {
 | 
				
			||||||
        return Err(anyhow::Error::from(std::io::Error::other(
 | 
					        return Err(anyhow::Error::from(std::io::Error::new(
 | 
				
			||||||
 | 
					            ErrorKind::Other,
 | 
				
			||||||
            "Token is empty!",
 | 
					            "Token is empty!",
 | 
				
			||||||
        )));
 | 
					        )));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -44,7 +46,8 @@ pub async fn get_by_pwd_reset_token(token: &str) -> anyhow::Result<User> {
 | 
				
			|||||||
/// Get the information of a user, by its account deletion token
 | 
					/// Get the information of a user, by its account deletion token
 | 
				
			||||||
pub async fn get_by_account_delete_token(token: &str) -> anyhow::Result<User> {
 | 
					pub async fn get_by_account_delete_token(token: &str) -> anyhow::Result<User> {
 | 
				
			||||||
    if token.is_empty() {
 | 
					    if token.is_empty() {
 | 
				
			||||||
        return Err(anyhow::Error::from(std::io::Error::other(
 | 
					        return Err(anyhow::Error::from(std::io::Error::new(
 | 
				
			||||||
 | 
					            ErrorKind::Other,
 | 
				
			||||||
            "Token is empty!",
 | 
					            "Token is empty!",
 | 
				
			||||||
        )));
 | 
					        )));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ pub fn sha256(bytes: &[u8]) -> String {
 | 
				
			|||||||
    let mut hasher = Sha256::new();
 | 
					    let mut hasher = Sha256::new();
 | 
				
			||||||
    hasher.update(bytes);
 | 
					    hasher.update(bytes);
 | 
				
			||||||
    let h = hasher.finalize();
 | 
					    let h = hasher.finalize();
 | 
				
			||||||
    format!("{h:x}")
 | 
					    format!("{:x}", h)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Compute hash of a slice of bytes (sha512)
 | 
					/// Compute hash of a slice of bytes (sha512)
 | 
				
			||||||
@@ -13,5 +13,5 @@ pub fn sha512(bytes: &[u8]) -> String {
 | 
				
			|||||||
    let mut hasher = Sha512::new();
 | 
					    let mut hasher = Sha512::new();
 | 
				
			||||||
    hasher.update(bytes);
 | 
					    hasher.update(bytes);
 | 
				
			||||||
    let h = hasher.finalize();
 | 
					    let h = hasher.finalize();
 | 
				
			||||||
    format!("{h:x}")
 | 
					    format!("{:x}", h)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use base64::{Engine as _, engine::general_purpose};
 | 
					use base64::{engine::general_purpose, Engine as _};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Encode a base64 string
 | 
					/// Encode a base64 string
 | 
				
			||||||
pub fn base64_enc<T: AsRef<[u8]>>(b: T) -> String {
 | 
					pub fn base64_enc<T: AsRef<[u8]>>(b: T) -> String {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,11 @@
 | 
				
			|||||||
use rand::distr::{Alphanumeric, SampleString};
 | 
					use rand::distributions::Alphanumeric;
 | 
				
			||||||
 | 
					use rand::Rng;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Generate a random string of a given length
 | 
					/// Generate a random string of a given size
 | 
				
			||||||
pub fn rand_str(len: usize) -> String {
 | 
					pub fn rand_str(len: usize) -> String {
 | 
				
			||||||
    Alphanumeric.sample_string(&mut rand::rng(), len)
 | 
					    rand::thread_rng()
 | 
				
			||||||
 | 
					        .sample_iter(&Alphanumeric)
 | 
				
			||||||
 | 
					        .map(char::from)
 | 
				
			||||||
 | 
					        .take(len)
 | 
				
			||||||
 | 
					        .collect()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,9 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "extends": ["local>renovate/presets"]
 | 
					  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 | 
				
			||||||
}
 | 
					  "packageRules": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "matchUpdateTypes": ["minor", "patch"],
 | 
				
			||||||
 | 
					      "automerge": true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user