From d82e150eaa9bdf1cad2fdda60d4d0003e962594f Mon Sep 17 00:00:00 2001 From: Martin Dimitrov Date: Sun, 25 Feb 2024 08:55:35 -0800 Subject: [PATCH] init from lnurld-db --- .dockerignore | 2 + .gitea/workflows/docker-image.yaml | 21 +++ .gitignore | 178 ++++++++++++++++++ Dockerfile | 21 +++ README.md | 15 ++ bun.lockb | Bin 0 -> 91209 bytes docker-compose.yml | 14 ++ doorman.code-workspace | 13 ++ package.json | 12 ++ packages/client/README.md | 46 +++++ packages/client/index.html | 34 ++++ packages/client/package.json | 49 +++++ packages/client/public/manifest.json | 8 + packages/client/public/robots.txt | 3 + packages/client/src/App.tsx | 32 ++++ packages/client/src/components/AuthFlow.tsx | 141 ++++++++++++++ packages/client/src/contexts/AlertContext.tsx | 12 ++ .../src/handlers/DownloadFileHandler.ts | 24 +++ .../client/src/handlers/HandlerRegistry.ts | 12 ++ packages/client/src/handlers/NoopHandler.ts | 15 ++ .../client/src/handlers/RepsonseHandler.ts | 7 + packages/client/src/index.tsx | 32 ++++ packages/client/src/pages/AuthPage.tsx | 103 ++++++++++ packages/client/src/types/Action.ts | 5 + packages/client/src/utils/EnumUtils.ts | 3 + packages/client/tsconfig.json | 21 +++ packages/client/vite-env.d.ts | 1 + packages/client/vite.config.ts | 23 +++ packages/server/package.json | 27 +++ .../server/src/clients/db/AbstractDbClient.ts | 46 +++++ .../server/src/clients/db/RedisDbClient.ts | 82 ++++++++ .../server/src/clients/db/RedisDbProvider.ts | 14 ++ packages/server/src/routers/ActionRouter.ts | 65 +++++++ packages/server/src/routers/LnurlRouter.ts | 62 ++++++ packages/server/src/server.ts | 27 +++ packages/server/src/types/Environment.ts | 7 + packages/server/src/types/IAccessControl.ts | 4 + .../server/src/types/IAuthCallbackQuery.ts | 7 + packages/server/src/types/RedisKeys.ts | 3 + packages/server/src/util/ExpireChallenges.ts | 6 + packages/server/src/util/RateLimits.ts | 14 ++ tsconfig.json | 22 +++ 42 files changed, 1233 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/docker-image.yaml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 docker-compose.yml create mode 100644 doorman.code-workspace create mode 100644 package.json create mode 100644 packages/client/README.md create mode 100644 packages/client/index.html create mode 100644 packages/client/package.json create mode 100644 packages/client/public/manifest.json create mode 100644 packages/client/public/robots.txt create mode 100644 packages/client/src/App.tsx create mode 100644 packages/client/src/components/AuthFlow.tsx create mode 100644 packages/client/src/contexts/AlertContext.tsx create mode 100644 packages/client/src/handlers/DownloadFileHandler.ts create mode 100644 packages/client/src/handlers/HandlerRegistry.ts create mode 100644 packages/client/src/handlers/NoopHandler.ts create mode 100644 packages/client/src/handlers/RepsonseHandler.ts create mode 100644 packages/client/src/index.tsx create mode 100644 packages/client/src/pages/AuthPage.tsx create mode 100644 packages/client/src/types/Action.ts create mode 100644 packages/client/src/utils/EnumUtils.ts create mode 100644 packages/client/tsconfig.json create mode 100644 packages/client/vite-env.d.ts create mode 100644 packages/client/vite.config.ts create mode 100644 packages/server/package.json create mode 100644 packages/server/src/clients/db/AbstractDbClient.ts create mode 100644 packages/server/src/clients/db/RedisDbClient.ts create mode 100644 packages/server/src/clients/db/RedisDbProvider.ts create mode 100644 packages/server/src/routers/ActionRouter.ts create mode 100644 packages/server/src/routers/LnurlRouter.ts create mode 100644 packages/server/src/server.ts create mode 100644 packages/server/src/types/Environment.ts create mode 100644 packages/server/src/types/IAccessControl.ts create mode 100644 packages/server/src/types/IAuthCallbackQuery.ts create mode 100644 packages/server/src/types/RedisKeys.ts create mode 100644 packages/server/src/util/ExpireChallenges.ts create mode 100644 packages/server/src/util/RateLimits.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..901696c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +**/node_modules +.env* \ No newline at end of file diff --git a/.gitea/workflows/docker-image.yaml b/.gitea/workflows/docker-image.yaml new file mode 100644 index 0000000..253e7c8 --- /dev/null +++ b/.gitea/workflows/docker-image.yaml @@ -0,0 +1,21 @@ +name: Build and push image for doorman + +on: + push: + branches: [main] + +jobs: + docker: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + name: Check out code + + - name: Build Docker image + run: docker build . -t gitea.chromart.dedyn.io/martin/doorman:v1 -t gitea.chromart.dedyn.io/martin/doorman:latest + + - name: Login to Docker + run: echo -n '${{ secrets.PASSWORD }}' | docker login gitea.chromart.dedyn.io --username ${{ secrets.USERNAME }} --password-stdin + + - name: Push Docker image + run: docker image push --all-tags gitea.chromart.dedyn.io/martin/doorman \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d285efb --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + + +build \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2fd4124 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM oven/bun + +ADD packages packages +ADD bun.lockb bun.lockb +ADD package.json package.json +ADD tsconfig.json tsconfig.json + + +# install all deps +RUN bun install + +# client build +WORKDIR /home/bun/app/packages/client +RUN bun run build + +# move built client to server +RUN mv dist ../server/ +WORKDIR /home/bun/app/packages/server + +# start server +CMD bun run ./src/server.ts \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0102c10 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# lnurl-db + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.0.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..4a20bbf4ec2a774ac7b5ef3123d54fc64e780f2c GIT binary patch literal 91209 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 K`y&69zyAaM^75Df literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3942417 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3" + +services: + lnurl_db: + container_name: lnurl-db + image: gitea.chromart.dynv6.net/martin/lnurl-db:latest + environment: + - CHALLENGE_EXPIRE_MS=105000 + - BASE_DOMAIN=chromart.dynv6.net + - REDIS_CONNECT_URL=redis://@redis:6379 + depends_on: + - redis + redis: + image: redis:latest \ No newline at end of file diff --git a/doorman.code-workspace b/doorman.code-workspace new file mode 100644 index 0000000..deca794 --- /dev/null +++ b/doorman.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "packages/client" + }, + { + "path": "packages/server" + }, + { + "path": "." + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..741e73f --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "lnurl-db", + "module": "index.ts", + "type": "module", + "workspaces": ["packages/*"], + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/client/README.md b/packages/client/README.md new file mode 100644 index 0000000..b58e0af --- /dev/null +++ b/packages/client/README.md @@ -0,0 +1,46 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/packages/client/index.html b/packages/client/index.html new file mode 100644 index 0000000..30c493e --- /dev/null +++ b/packages/client/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + React App + + + +
+ + + + diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 0000000..7f9f6f9 --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,49 @@ +{ + "name": "lnurl-db-client", + "version": "0.1.0", + "private": true, + "dependencies": { + "@cloudscape-design/components": "^3.0.375", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^13.0.0", + "@testing-library/user-event": "^13.2.1", + "@types/jest": "^27.0.1", + "@types/node": "^16.7.13", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^4.0.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-qr-code": "^2.0.12", + "react-router-dom": "^6.15.0", + "typescript": "^4.4.2", + "vite": "^4.4.9", + "vite-plugin-svgr": "^4.0.0", + "vite-tsconfig-paths": "^4.2.1", + "web-vitals": "^2.1.0" + }, + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "proxy": "http://localhost:5000" +} diff --git a/packages/client/public/manifest.json b/packages/client/public/manifest.json new file mode 100644 index 0000000..f9051fe --- /dev/null +++ b/packages/client/public/manifest.json @@ -0,0 +1,8 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/client/public/robots.txt b/packages/client/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/packages/client/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx new file mode 100644 index 0000000..33ea27d --- /dev/null +++ b/packages/client/src/App.tsx @@ -0,0 +1,32 @@ +import { Alert, Button } from '@cloudscape-design/components'; +import React, { useState } from 'react'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { Action } from './types/Action'; +import { AlertContent, AlertContext } from './contexts/AlertContext'; + +function App() { + const navigate = useNavigate(); + const location = useLocation(); + + const [ content, setContent ] = useState({}); + + return ( +
+ +

homepage

+ {Object.values(Action).map(value => { + return + })} + {content.message && setContent({})} type={content.type || "info"}>{content.message}} + +
+
+ ); +} + +export default App; diff --git a/packages/client/src/components/AuthFlow.tsx b/packages/client/src/components/AuthFlow.tsx new file mode 100644 index 0000000..7c56b4c --- /dev/null +++ b/packages/client/src/components/AuthFlow.tsx @@ -0,0 +1,141 @@ +import { ColumnLayout, Container, Header, ProgressBar, Spinner } from "@cloudscape-design/components"; +import { useContext, useEffect, useState } from "react"; +import QRCode from "react-qr-code"; +import { Action } from "../types/Action"; +import { ResponseHandler } from "../handlers/RepsonseHandler"; +import { useNavigate } from "react-router-dom"; +import { AlertContext } from "../contexts/AlertContext"; + +export async function loader({ params }: {params: {action: string}}) { + return { action: params.action }; +} + +const WAIT_MS: number = 3000; +const COUNTDOWN_TIME_SECONDS = 100; + +export interface IAuthPageProps { + subdomain: string; + action: Action; + options?: RequestInit; + responseHandler: ResponseHandler; + handlerMetadata?: any; +}; + +export default function AuthFlow(props: IAuthPageProps) { + const { subdomain, action, options, responseHandler, handlerMetadata } = props; + const [ loading, setLoading ] = useState(true); + const [ qrContent, setQrContent ] = useState(""); + const [ challenge, setChallenge ] = useState(""); + const [ countdown, setCountdown ] = useState(COUNTDOWN_TIME_SECONDS); + + const { setContent } = useContext(AlertContext); + + const navigate = useNavigate(); + + useEffect(() => { + fetch(`/api/lnurl/login?subdomain=${subdomain}`) + .then(res => { + if (res.status === 429) { + throw new Error("Too many requests, try again later"); + } + return res; + }) + .then(res => res.json()) + .then(res => { + setQrContent(res.lnurl); + setLoading(false); + setChallenge(res.challenge); + setCountdown(COUNTDOWN_TIME_SECONDS); + }) + .catch((err) => { + console.log(err); + setContent({ + type: "error", + message: err?.message || "Unknown error occurred", + }); + navigate("/"); + }); + }, [action]); + + useEffect(() => { + if (challenge === "") { + return; + } + + const timer = setInterval(() => { + fetch(`/api/actions/${action}/${challenge}`, options) + .then(res => { + if (res.status === 401) { + throw new Error("Challenge not met"); + } + + return res; + }) + .then((res) => responseHandler(res, (err) => { + if (err) { + setContent({ + message: err.message, + type: "error", + }) + } else { + setContent({ + type: "success", + message: `${action} success!` + }) + } + setChallenge(""); + navigate("/"); + }, handlerMetadata)) + .catch((err) => console.error(err)); + }, WAIT_MS); + + return () => { + clearInterval(timer); + } + }, [challenge]); + + useEffect(() => { + if (countdown === 0) { + setContent({ + type: "error", + message: "Authentication timed out after 100s, try again", + }); + navigate("/"); + return; + } + const countdownTimer = setTimeout(() => { + setCountdown(countdown - 1); + }, 1000); + + return () => { + clearTimeout(countdownTimer); + } + }, [countdown]); + + return ( + +
+ + +
Authentication for {action} on {subdomain}
+ {loading && } + {!loading && ( + <> + +
+ +
+ + + )} +
+
+
+ + ) +} \ No newline at end of file diff --git a/packages/client/src/contexts/AlertContext.tsx b/packages/client/src/contexts/AlertContext.tsx new file mode 100644 index 0000000..39fb14a --- /dev/null +++ b/packages/client/src/contexts/AlertContext.tsx @@ -0,0 +1,12 @@ +import { AlertProps } from "@cloudscape-design/components"; +import React from "react"; + +export interface AlertContent { + type?: AlertProps.Type; + message?: string; +} + +export const AlertContext = React.createContext<{ content: AlertContent, setContent: (content: AlertContent) => void }>({ + content: {}, + setContent: () => null, +}); \ No newline at end of file diff --git a/packages/client/src/handlers/DownloadFileHandler.ts b/packages/client/src/handlers/DownloadFileHandler.ts new file mode 100644 index 0000000..8744a00 --- /dev/null +++ b/packages/client/src/handlers/DownloadFileHandler.ts @@ -0,0 +1,24 @@ +import { ResponseHandler } from "./RepsonseHandler"; + +export interface DownloadMetadata { + name?: string; + type?: string; +} + +const DownloadFileHandler: ResponseHandler = (res, next, extra) => { + if (res.status === 404) { + console.log("Nothing to download"); + return res.json().then(next); + } + return res.blob() + .then(data => { + let a = document.createElement("a"); + a.href = window.URL.createObjectURL(data); + a.download = `${extra?.name || "file"}.${extra?.type || "txt"}`; + a.click(); + next() + }) + .catch(next); +} + +export default DownloadFileHandler; \ No newline at end of file diff --git a/packages/client/src/handlers/HandlerRegistry.ts b/packages/client/src/handlers/HandlerRegistry.ts new file mode 100644 index 0000000..bd07001 --- /dev/null +++ b/packages/client/src/handlers/HandlerRegistry.ts @@ -0,0 +1,12 @@ +import { Action } from "../types/Action"; +import DownloadFileHandler from "./DownloadFileHandler"; +import NoopHandler from "./NoopHandler"; +import { ResponseHandler } from "./RepsonseHandler"; + +export function getHandler(action: Action): ResponseHandler { + switch(action) { + case Action.DOWNLOAD: return DownloadFileHandler; + case Action.UPLOAD: return NoopHandler; + case Action.DELETE: return NoopHandler; + } +} \ No newline at end of file diff --git a/packages/client/src/handlers/NoopHandler.ts b/packages/client/src/handlers/NoopHandler.ts new file mode 100644 index 0000000..36344ed --- /dev/null +++ b/packages/client/src/handlers/NoopHandler.ts @@ -0,0 +1,15 @@ +import { ResponseHandler } from "./RepsonseHandler"; + +const NoopHandler: ResponseHandler = (res, next) => { + return new Promise(async () => { + if (res.status >= 500) { + res = { message: res.statusText } as any; + } + else if (res.status !== 200) { + res = await res.json(); + } + next(res.status !== 200 ? res: undefined); + }); +} + +export default NoopHandler; diff --git a/packages/client/src/handlers/RepsonseHandler.ts b/packages/client/src/handlers/RepsonseHandler.ts new file mode 100644 index 0000000..1f046a3 --- /dev/null +++ b/packages/client/src/handlers/RepsonseHandler.ts @@ -0,0 +1,7 @@ +export interface Callback { + (err?: any): void; +} + +export interface ResponseHandler { + (res: Response, next: Callback, options?: ExtraMetadata): Promise; +} \ No newline at end of file diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx new file mode 100644 index 0000000..d47c0cd --- /dev/null +++ b/packages/client/src/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import { RouterProvider, createBrowserRouter } from 'react-router-dom'; +import { AuthPage, loader as authpageloader } from './pages/AuthPage'; + + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement +); + +const router = createBrowserRouter([ + { + path: "/", + element: , + errorElement:

error

, + children: [ + { + path: "action/:action", + loader: authpageloader, + element: + } + ] + } +]) + +root.render( + + + +); + diff --git a/packages/client/src/pages/AuthPage.tsx b/packages/client/src/pages/AuthPage.tsx new file mode 100644 index 0000000..54393e8 --- /dev/null +++ b/packages/client/src/pages/AuthPage.tsx @@ -0,0 +1,103 @@ +import React, { useState } from "react"; +import { useLoaderData } from "react-router-dom"; +import { Action } from "../types/Action"; +import { isInEnum } from "../utils/EnumUtils"; +import AuthFlow from "../components/AuthFlow"; +import { ColumnLayout, Container, FileUpload, FormField, Select } from "@cloudscape-design/components"; +import { getHandler } from "../handlers/HandlerRegistry"; +import { DownloadMetadata } from "../handlers/DownloadFileHandler"; + +export interface IAuthPageLoader { + action: Action; +} + +export async function loader({ params }: any) { + if (!isInEnum(Action, params.action)) { + throw new Error("Not a valid action"); + } + return { action: params.action }; +} + +interface SelectOption { + label?: string; + value?: string; +} + +const selectOptions: SelectOption[] = [ + { label: "backup", value: "backup" }, + { label: "key", value: "key" }, +]; + +export function AuthPage() { + const { action } = useLoaderData() as IAuthPageLoader; + const [selectedOption, setSelectedOption] = useState({value: ""}); + + const [files, setFiles] = useState([]); + + const fileReady = action !== Action.UPLOAD || files.length > 0; + + let options: RequestInit = {}; + let handlerMetadata: DownloadMetadata = { + name: selectedOption.value || "file", + type: "dat", + } + + if (action === Action.DELETE) { + options = { + method: 'delete' + } + } + + if (action === Action.UPLOAD) { + if (files.length > 0) { + const formData = new FormData(); + formData.append("file", files[0]); + + options = { + method: 'post', + body: formData, + } + console.log(options); + } + } + + return ( + + + +