diff --git a/virtweb_frontend/public/index.html b/virtweb_frontend/public/index.html
index aa069f2..ecb1c9e 100644
--- a/virtweb_frontend/public/index.html
+++ b/virtweb_frontend/public/index.html
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
-
React App
+ VirtWeb
You need to enable JavaScript to run this app.
diff --git a/virtweb_frontend/public/manifest.json b/virtweb_frontend/public/manifest.json
index 080d6c7..fb5e09d 100644
--- a/virtweb_frontend/public/manifest.json
+++ b/virtweb_frontend/public/manifest.json
@@ -1,6 +1,6 @@
{
- "short_name": "React App",
- "name": "Create React App Sample",
+ "short_name": "VirtWeb",
+ "name": "Virtual machines management",
"icons": [
{
"src": "favicon.ico",
diff --git a/virtweb_frontend/src/api/AuthApi.ts b/virtweb_frontend/src/api/AuthApi.ts
index 0d117db..d29b9fe 100644
--- a/virtweb_frontend/src/api/AuthApi.ts
+++ b/virtweb_frontend/src/api/AuthApi.ts
@@ -37,7 +37,6 @@ export class AuthApi {
await APIClient.exec({
uri: "/auth/local",
method: "POST",
- allowFail: true,
jsonData: {
username: username,
password: password,
diff --git a/virtweb_frontend/src/routes/auth/LoginRoute.tsx b/virtweb_frontend/src/routes/auth/LoginRoute.tsx
index dc9c577..b38d1b9 100644
--- a/virtweb_frontend/src/routes/auth/LoginRoute.tsx
+++ b/virtweb_frontend/src/routes/auth/LoginRoute.tsx
@@ -1,3 +1,167 @@
-export function LoginRoute() {
- return <>>;
+import { Visibility, VisibilityOff } from "@mui/icons-material";
+import {
+ Alert,
+ CircularProgress,
+ FormControl,
+ IconButton,
+ InputAdornment,
+ InputLabel,
+ OutlinedInput,
+ Tooltip,
+} from "@mui/material";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Grid from "@mui/material/Grid";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+import * as React from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { useAuth } from "../../App";
+import { AuthApi } from "../../api/AuthApi";
+import { ServerApi } from "../../api/ServerApi";
+
+/**
+ * Login form
+ */
+export function LoginRoute(): React.ReactElement {
+ const [loading, setLoading] = React.useState(false);
+ const [error, setError] = React.useState(null);
+
+ const auth = useAuth();
+ const navigate = useNavigate();
+
+ const [username, setUsername] = React.useState("");
+ const [password, setPassword] = React.useState("");
+
+ const canSubmit = username.length > 0 && password.length > 0;
+
+ const [showPassword, setShowPassword] = React.useState(false);
+ const handleClickShowPassword = () => setShowPassword((show) => !show);
+
+ const handleMouseDownPassword = (
+ event: React.MouseEvent
+ ) => {
+ event.preventDefault();
+ };
+
+ const handleLoginSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ if (!canSubmit) return;
+
+ try {
+ await AuthApi.LoginWithPassword(username, password);
+
+ navigate("/");
+ auth.setSignedIn(true);
+ } catch (e) {
+ console.error(e);
+ setError("Auth failed!");
+ }
+ setLoading(false);
+ };
+
+ const authWithOpenID = async () => {
+ try {
+ setLoading(true);
+
+ const res = await AuthApi.StartOpenIDLogin();
+ window.location.href = res.url;
+ } catch (e) {
+ console.error(e);
+ setError("Failed to initialize OpenID login");
+ }
+ };
+
+ if (loading)
+ return (
+ <>
+
+ >
+ );
+
+ return (
+ <>
+ {error && (
+
+ {error}
+
+ )}
+ {ServerApi.Config.local_auth_enabled && (
+ <>
+
+ Local authentication
+
+
+ setUsername(e.target.value)}
+ autoComplete="username"
+ autoFocus
+ />
+
+
+ Password
+ setPassword(e.target.value)}
+ autoComplete="current-password"
+ endAdornment={
+
+
+
+ {showPassword ? : }
+
+
+
+ }
+ />
+
+
+
+ Login
+
+
+ >
+ )}
+
+
+ {ServerApi.Config.oidc_auth_enabled && (
+ authWithOpenID()}
+ >
+ Authenticate using OpenID
+
+ )}
+
+ >
+ );
}