refactor: split app context and user context (#48)

* refactor: split app context and user context

* tests: fix api tests

* chore: rename dockerfiles

* fix: use correct forwardauth address
This commit is contained in:
Stavros
2025-03-14 20:38:09 +02:00
committed by GitHub
parent 7e39cb0dfe
commit 52f189563b
21 changed files with 320 additions and 143 deletions

30
.env.example Normal file
View File

@@ -0,0 +1,30 @@
PORT=3000
ADDRESS=0.0.0.0
SECRET=app_secret
SECRET_FILE=app_secret_file
APP_URL=http://localhost:3000
USERS=your_user_password_hash
USERS_FILE=users_file
COOKIE_SECURE=false
GITHUB_CLIENT_ID=github_client_id
GITHUB_CLIENT_SECRET=github_client_secret
GITHUB_CLIENT_SECRET_FILE=github_client_secret_file
GOOGLE_CLIENT_ID=google_client_id
GOOGLE_CLIENT_SECRET=google_client_secret
GOOGLE_CLIENT_SECRET_FILE=google_client_secret_file
TAILSCALE_CLIENT_ID=tailscale_client_id
TAILSCALE_CLIENT_SECRET=tailscale_client_secret
TAILSCALE_CLIENT_SECRET_FILE=tailscale__client_secret_file
GENERIC_CLIENT_ID=generic_client_id
GENERIC_CLIENT_SECRET=generic_client_secret
GENERIC_CLIENT_SECRET_FILE=generic_client_secret_file
GENERIC_SCOPES=generic_scopes
GENERIC_AUTH_URL=generic_auth_url
GENERIC_TOKEN_URL=generic_token_url
GENERIC_USER_URL=generic_user_url
DISABLE_CONTINUE=false
OAUTH_WHITELIST=
GENERIC_NAME=My OAuth
SESSION_EXPIRY=7200
LOG_LEVEL=0
APP_TITLE=Tinyauth SSO

6
.gitignore vendored
View File

@@ -19,3 +19,9 @@ secret_oauth.txt
# apple stuff # apple stuff
.DS_Store .DS_Store
# env
.env
# tmp directory
tmp

View File

@@ -8,7 +8,6 @@ Contributing is relatively easy, you just need to follow the steps carefully and
- Golang v1.23.2 and above - Golang v1.23.2 and above
- Git - Git
- Docker - Docker
- Make (not required but it will make your life easier)
## Cloning the repository ## Cloning the repository
@@ -21,55 +20,33 @@ cd tinyauth
## Install requirements ## Install requirements
To install the requirements simply run: Although you will not need the requirements in your machine since the development will happen in docker, I still recommend to install them because this way you will not have errors, to install the go requirements, run:
```sh ```sh
make requirements go mod tidy
``` ```
It will download all the node packages required by the frontend as well as all the go requirements. You also need to download the frontend requirements, this can be done like so:
## Developing locally
In order to develop the app you need to firstly compile the frontend and then the go app. To avoid running the same commands over and over again you can just run:
```sh ```sh
make run cd site/
bun install
``` ```
This is the equivalent of `go run main.go`, if you would like to build a binary run: ## Create your `.env` file
```sh In order to ocnfigure the app you need to create an environment file, this can be done by copying the `.env.example` file to `.env` and modifying the environment variables inside to suit your needs.
make build
```
To avoid rebuilding the frontend every time you can run: ## Developing
```sh I have designed the development workflow to be entirely in docker, this is because it will directly work with traefik and you will not need to do any building in your host machine. The recommended development setup is to have a subdomain pointing to your machine like this:
make run-no-web
```
And:
```sh
make build-no-web
```
For these commands to succeed you must have built the frontend at least once.
> [!WARNING]
> Make sure you have set the environment variables when running outside of docker else the app will fail.
## Developing in docker
My recommended development method is docker so I can test that both my image works and that the app responds correctly to traefik. In my setup I have set these two DNS records in my DNS server:
``` ```
*.dev.example.com -> 127.0.0.1 *.dev.example.com -> 127.0.0.1
dev.example.com -> 127.0.0.1 dev.example.com -> 127.0.0.1
``` ```
Then I can just make sure the domains are correct in the example docker compose file and do: Then you can just make sure the domains are correct in the example docker compose file and run:
```sh ```sh
docker compose -f docker-compose.dev.yml up --build docker compose -f docker-compose.dev.yml up --build

22
Dockerfile.dev Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:1.23-alpine3.21
WORKDIR /tinyauth
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY ./cmd ./cmd
COPY ./internal ./internal
COPY ./main.go ./
COPY ./air.toml ./
RUN mkdir -p ./internal/assets/dist && \
echo "app running" > ./internal/assets/dist/index.html
RUN go install github.com/air-verse/air@v1.61.7
EXPOSE 3000
ENTRYPOINT ["air", "-c", "air.toml"]

View File

@@ -1,34 +0,0 @@
# Build website
web:
cd site; bun run build
# Requirements
requirements:
cd site; bun install
go mod tidy
# Copy site assets
assets: web
rm -rf internal/assets/dist
mkdir -p internal/assets/dist
cp -r site/dist/* internal/assets/dist
# Run development binary
run: assets
go run main.go
# Run development binary without compiling the frontend
run-skip-web:
go run main.go
# Test
test:
go test ./...
# Build
build: assets
go build -o tinyauth
# Build the binary without compiling the frontend
build-skip-web:
go build -o tinyauth

24
air.toml Normal file
View File

@@ -0,0 +1,24 @@
root = "/tinyauth"
tmp_dir = "tmp"
[build]
pre_cmd = ["go mod tidy"]
cmd = "go build -o ./tmp/tinyauth ."
bin = "tmp/tinyauth"
include_ext = ["go"]
exclude_dir = ["internal/assets/dist"]
exclude_regex = [".*_test\\.go"]
stop_on_error = true
[color]
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
clean_on_exit = true
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@@ -13,21 +13,36 @@ services:
image: traefik/whoami:latest image: traefik/whoami:latest
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.nginx.rule: Host(`whoami.dev.example.com`) traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
traefik.http.services.nginx.loadbalancer.server.port: 80 traefik.http.services.nginx.loadbalancer.server.port: 80
traefik.http.routers.nginx.middlewares: tinyauth traefik.http.routers.nginx.middlewares: tinyauth
tinyauth: tinyauth-frontend:
container_name: tinyauth container_name: tinyauth-frontend
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: site/Dockerfile.dev
environment: volumes:
- SECRET=some-random-32-chars-string - ./site/src:/site/src
- APP_URL=http://tinyauth.dev.example.com ports:
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password - 5173:5173
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.example.com`) traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
traefik.http.services.tinyauth.loadbalancer.server.port: 3000 traefik.http.services.tinyauth.loadbalancer.server.port: 5173
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik
tinyauth-backend:
container_name: tinyauth-backend
build:
context: .
dockerfile: Dockerfile.dev
env_file: .env
volumes:
- ./internal:/tinyauth/internal
- ./cmd:/tinyauth/cmd
- ./main.go:/tinyauth/main.go
ports:
- 3000:3000
labels:
traefik.enable: true
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth-backend:3000/api/auth/traefik

View File

@@ -372,7 +372,7 @@ func (api *API) SetupRoutes() {
api.Router.POST("/api/totp", func(c *gin.Context) { api.Router.POST("/api/totp", func(c *gin.Context) {
// Create totp struct // Create totp struct
var totpReq types.Totp var totpReq types.TotpRequest
// Bind JSON // Bind JSON
err := c.BindJSON(&totpReq) err := c.BindJSON(&totpReq)
@@ -461,11 +461,8 @@ func (api *API) SetupRoutes() {
}) })
}) })
api.Router.GET("/api/status", func(c *gin.Context) { api.Router.GET("/api/app", func(c *gin.Context) {
log.Debug().Msg("Checking status") log.Debug().Msg("Getting app context")
// Get user context
userContext := api.Hooks.UseUserContext(c)
// Get configured providers // Get configured providers
configuredProviders := api.Providers.GetConfiguredProviders() configuredProviders := api.Providers.GetConfiguredProviders()
@@ -475,33 +472,48 @@ func (api *API) SetupRoutes() {
configuredProviders = append(configuredProviders, "username") configuredProviders = append(configuredProviders, "username")
} }
// Fill status struct with data from user context and api config // Create app context struct
status := types.Status{ appContext := types.AppContext{
Username: userContext.Username, Status: 200,
IsLoggedIn: userContext.IsLoggedIn, Message: "Ok",
Oauth: userContext.OAuth,
Provider: userContext.Provider,
ConfiguredProviders: configuredProviders, ConfiguredProviders: configuredProviders,
DisableContinue: api.Config.DisableContinue, DisableContinue: api.Config.DisableContinue,
Title: api.Config.Title, Title: api.Config.Title,
GenericName: api.Config.GenericName, GenericName: api.Config.GenericName,
TotpPending: userContext.TotpPending, }
// Return app context
c.JSON(200, appContext)
})
api.Router.GET("/api/user", func(c *gin.Context) {
log.Debug().Msg("Getting user context")
// Get user context
userContext := api.Hooks.UseUserContext(c)
// Create user context response
userContextResponse := types.UserContextResponse{
Status: 200,
IsLoggedIn: userContext.IsLoggedIn,
Username: userContext.Username,
Provider: userContext.Provider,
Oauth: userContext.OAuth,
TotpPending: userContext.TotpPending,
} }
// If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200 // If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200
if !userContext.IsLoggedIn { if !userContext.IsLoggedIn {
log.Debug().Msg("Unauthorized") log.Debug().Msg("Unauthorized")
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
status.Status = 401 userContextResponse.Message = "Unauthorized"
status.Message = "Unauthorized"
} else { } else {
log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated") log.Debug().Interface("userContext", userContext).Msg("Authenticated")
status.Status = 200 userContextResponse.Message = "Authenticated"
status.Message = "Authenticated"
} }
// Return data // Return user context
c.JSON(200, status) c.JSON(200, userContextResponse)
}) })
api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) { api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) {
@@ -710,7 +722,12 @@ func (api *API) Run() {
log.Info().Str("address", api.Config.Address).Int("port", api.Config.Port).Msg("Starting server") log.Info().Str("address", api.Config.Address).Int("port", api.Config.Port).Msg("Starting server")
// Run server // Run server
api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port)) err := api.Router.Run(fmt.Sprintf("%s:%d", api.Config.Address, api.Config.Port))
// Check error
if err != nil {
log.Fatal().Err(err).Msg("Failed to start server")
}
} }
// handleError logs the error and redirects to the error page (only meant for stuff the user may access does not apply for login paths) // handleError logs the error and redirects to the error page (only meant for stuff the user may access does not apply for login paths)

View File

@@ -2,6 +2,7 @@ package api_test
import ( import (
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -122,9 +123,9 @@ func TestLogin(t *testing.T) {
} }
} }
// Test status // Test user context
func TestStatus(t *testing.T) { func TestUserContext(t *testing.T) {
t.Log("Testing status") t.Log("Testing user context")
// Get API // Get API
api := getAPI(t) api := getAPI(t)
@@ -133,7 +134,7 @@ func TestStatus(t *testing.T) {
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
// Create request // Create request
req, err := http.NewRequest("GET", "/api/status", nil) req, err := http.NewRequest("GET", "/api/user", nil)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
@@ -152,11 +153,31 @@ func TestStatus(t *testing.T) {
// Assert // Assert
assert.Equal(t, recorder.Code, http.StatusOK) assert.Equal(t, recorder.Code, http.StatusOK)
// Parse the body // Read the body of the response
body := recorder.Body.String() body, bodyErr := io.ReadAll(recorder.Body)
if !strings.Contains(body, "user") { // Check if there was an error
t.Fatalf("Expected user in body") if bodyErr != nil {
t.Fatalf("Error getting body: %v", bodyErr)
}
// Unmarshal the body into the user struct
type User struct {
Username string `json:"username"`
}
var user User
jsonErr := json.Unmarshal(body, &user)
// Check if there was an error
if jsonErr != nil {
t.Fatalf("Error unmarshalling body: %v", jsonErr)
}
// We should get the username back
if user.Username != "user" {
t.Fatalf("Expected user, got %s", user.Username)
} }
} }

View File

@@ -55,6 +55,7 @@ type Config struct {
SessionExpiry int `mapstructure:"session-expiry"` SessionExpiry int `mapstructure:"session-expiry"`
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
Title string `mapstructure:"app-title"` Title string `mapstructure:"app-title"`
EnvFile string `mapstructure:"env-file"`
} }
// UserContext is the context for the user // UserContext is the context for the user
@@ -138,22 +139,28 @@ type Proxy struct {
Proxy string `uri:"proxy" binding:"required"` Proxy string `uri:"proxy" binding:"required"`
} }
// Status response // User Context response is the response for the user context endpoint
type Status struct { type UserContextResponse struct {
Status int `json:"status"`
Message string `json:"message"`
IsLoggedIn bool `json:"isLoggedIn"`
Username string `json:"username"`
Provider string `json:"provider"`
Oauth bool `json:"oauth"`
TotpPending bool `json:"totpPending"`
}
// App Context is the response for the app context endpoint
type AppContext struct {
Status int `json:"status"` Status int `json:"status"`
Message string `json:"message"` Message string `json:"message"`
IsLoggedIn bool `json:"isLoggedIn"`
Username string `json:"username"`
Provider string `json:"provider"`
Oauth bool `json:"oauth"`
ConfiguredProviders []string `json:"configuredProviders"` ConfiguredProviders []string `json:"configuredProviders"`
DisableContinue bool `json:"disableContinue"` DisableContinue bool `json:"disableContinue"`
Title string `json:"title"` Title string `json:"title"`
GenericName string `json:"genericName"` GenericName string `json:"genericName"`
TotpPending bool `json:"totpPending"`
} }
// Totp request // Totp request is the request for the totp endpoint
type Totp struct { type TotpRequest struct {
Code string `json:"code"` Code string `json:"code"`
} }

23
site/Dockerfile.dev Normal file
View File

@@ -0,0 +1,23 @@
FROM oven/bun:1.1.45-alpine
WORKDIR /site
COPY ./site/package.json ./
COPY ./site/bun.lockb ./
RUN bun install
COPY ./site/public ./public
COPY ./site/src ./src
COPY ./site/eslint.config.js ./
COPY ./site/index.html ./
COPY ./site/tsconfig.json ./
COPY ./site/tsconfig.app.json ./
COPY ./site/tsconfig.node.json ./
COPY ./site/vite.config.ts ./
COPY ./site/postcss.config.cjs ./
EXPOSE 5173
ENTRYPOINT ["bun", "run", "dev"]

View File

@@ -0,0 +1,42 @@
import { useQuery } from "@tanstack/react-query";
import React, { createContext, useContext } from "react";
import axios from "axios";
import { AppContextSchemaType } from "../schemas/app-context-schema";
const AppContext = createContext<AppContextSchemaType | null>(null);
export const AppContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const {
data: userContext,
isLoading,
error,
} = useQuery({
queryKey: ["appContext"],
queryFn: async () => {
const res = await axios.get("/api/app");
return res.data;
},
});
if (error && !isLoading) {
throw error;
}
return (
<AppContext.Provider value={userContext}>{children}</AppContext.Provider>
);
};
export const useAppContext = () => {
const context = useContext(AppContext);
if (context === null) {
throw new Error("useAppContext must be used within an AppContextProvider");
}
return context;
};

View File

@@ -17,7 +17,7 @@ export const UserContextProvider = ({
} = useQuery({ } = useQuery({
queryKey: ["userContext"], queryKey: ["userContext"],
queryFn: async () => { queryFn: async () => {
const res = await axios.get("/api/status"); const res = await axios.get("/api/user");
return res.data; return res.data;
}, },
}); });

View File

@@ -16,6 +16,7 @@ import { NotFoundPage } from "./pages/not-found-page.tsx";
import { UnauthorizedPage } from "./pages/unauthorized-page.tsx"; import { UnauthorizedPage } from "./pages/unauthorized-page.tsx";
import { InternalServerError } from "./pages/internal-server-error.tsx"; import { InternalServerError } from "./pages/internal-server-error.tsx";
import { TotpPage } from "./pages/totp-page.tsx"; import { TotpPage } from "./pages/totp-page.tsx";
import { AppContextProvider } from "./context/app-context.tsx";
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -30,20 +31,22 @@ createRoot(document.getElementById("root")!).render(
<MantineProvider forceColorScheme="dark"> <MantineProvider forceColorScheme="dark">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Notifications /> <Notifications />
<UserContextProvider> <AppContextProvider>
<BrowserRouter> <UserContextProvider>
<Routes> <BrowserRouter>
<Route path="/" element={<App />} /> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/" element={<App />} />
<Route path="/totp" element={<TotpPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} /> <Route path="/totp" element={<TotpPage />} />
<Route path="/continue" element={<ContinuePage />} /> <Route path="/logout" element={<LogoutPage />} />
<Route path="/unauthorized" element={<UnauthorizedPage />} /> <Route path="/continue" element={<ContinuePage />} />
<Route path="/error" element={<InternalServerError />} /> <Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="*" element={<NotFoundPage />} /> <Route path="/error" element={<InternalServerError />} />
</Routes> <Route path="*" element={<NotFoundPage />} />
</BrowserRouter> </Routes>
</UserContextProvider> </BrowserRouter>
</UserContextProvider>
</AppContextProvider>
</QueryClientProvider> </QueryClientProvider>
</MantineProvider> </MantineProvider>
</StrictMode>, </StrictMode>,

View File

@@ -5,13 +5,15 @@ import { useUserContext } from "../context/user-context";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { isQueryValid } from "../utils/utils"; import { isQueryValid } from "../utils/utils";
import { useAppContext } from "../context/app-context";
export const ContinuePage = () => { export const ContinuePage = () => {
const queryString = window.location.search; const queryString = window.location.search;
const params = new URLSearchParams(queryString); const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri") ?? ""; const redirectUri = params.get("redirect_uri") ?? "";
const { isLoggedIn, disableContinue } = useUserContext(); const { isLoggedIn } = useUserContext();
const { disableContinue } = useAppContext();
if (!isLoggedIn) { if (!isLoggedIn) {
return <Navigate to={`/login?redirect_uri=${redirectUri}`} />; return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;

View File

@@ -9,14 +9,15 @@ import { OAuthButtons } from "../components/auth/oauth-buttons";
import { LoginFormValues } from "../schemas/login-schema"; import { LoginFormValues } from "../schemas/login-schema";
import { LoginForm } from "../components/auth/login-forn"; import { LoginForm } from "../components/auth/login-forn";
import { isQueryValid } from "../utils/utils"; import { isQueryValid } from "../utils/utils";
import { useAppContext } from "../context/app-context";
export const LoginPage = () => { export const LoginPage = () => {
const queryString = window.location.search; const queryString = window.location.search;
const params = new URLSearchParams(queryString); const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri") ?? ""; const redirectUri = params.get("redirect_uri") ?? "";
const { isLoggedIn, configuredProviders, title, genericName } = const { isLoggedIn } = useUserContext();
useUserContext(); const { configuredProviders, title, genericName } = useAppContext();
const oauthProviders = configuredProviders.filter( const oauthProviders = configuredProviders.filter(
(value) => value !== "username", (value) => value !== "username",

View File

@@ -6,9 +6,11 @@ import { useUserContext } from "../context/user-context";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
import { capitalize } from "../utils/utils"; import { capitalize } from "../utils/utils";
import { useAppContext } from "../context/app-context";
export const LogoutPage = () => { export const LogoutPage = () => {
const { isLoggedIn, username, oauth, provider, genericName } = useUserContext(); const { isLoggedIn, username, oauth, provider } = useUserContext();
const { genericName } = useAppContext();
if (!isLoggedIn) { if (!isLoggedIn) {
return <Navigate to="/login" />; return <Navigate to="/login" />;
@@ -45,8 +47,9 @@ export const LogoutPage = () => {
</Text> </Text>
<Text> <Text>
You are currently logged in as <Code>{username}</Code> You are currently logged in as <Code>{username}</Code>
{oauth && ` using ${capitalize(provider === "generic" ? genericName : provider)} OAuth`}. Click the button {oauth &&
below to log out. ` using ${capitalize(provider === "generic" ? genericName : provider)} OAuth`}
. Click the button below to log out.
</Text> </Text>
<Button <Button
fullWidth fullWidth

View File

@@ -6,13 +6,15 @@ import { TotpForm } from "../components/auth/totp-form";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useAppContext } from "../context/app-context";
export const TotpPage = () => { export const TotpPage = () => {
const queryString = window.location.search; const queryString = window.location.search;
const params = new URLSearchParams(queryString); const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri") ?? ""; const redirectUri = params.get("redirect_uri") ?? "";
const { totpPending, isLoggedIn, title } = useUserContext(); const { totpPending, isLoggedIn } = useUserContext();
const { title } = useAppContext();
if (isLoggedIn) { if (isLoggedIn) {
return <Navigate to={`/logout`} />; return <Navigate to={`/logout`} />;

View File

@@ -0,0 +1,10 @@
import { z } from "zod";
export const appContextSchema = z.object({
configuredProviders: z.array(z.string()),
disableContinue: z.boolean(),
title: z.string(),
genericName: z.string(),
});
export type AppContextSchemaType = z.infer<typeof appContextSchema>;

View File

@@ -5,10 +5,6 @@ export const userContextSchema = z.object({
username: z.string(), username: z.string(),
oauth: z.boolean(), oauth: z.boolean(),
provider: z.string(), provider: z.string(),
configuredProviders: z.array(z.string()),
disableContinue: z.boolean(),
title: z.string(),
genericName: z.string(),
totpPending: z.boolean(), totpPending: z.boolean(),
}); });

View File

@@ -4,4 +4,14 @@ import react from "@vitejs/plugin-react-swc";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
host: "0.0.0.0",
proxy: {
"/api": {
target: "http://tinyauth-backend:3000/api",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
}
}
}); });