- add: frontend webpage for login/register

This commit is contained in:
2024-12-17 17:03:55 -05:00
parent c67cdd5b2a
commit 9c61b1b3f2
20 changed files with 29819 additions and 0 deletions

21
.gitignore vendored
View File

@@ -111,3 +111,24 @@ compile_commands.json
CTestTestfile.cmake CTestTestfile.cmake
_deps _deps
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -1,4 +1,17 @@
services: services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: frontend
ports:
- "3000:80"
depends_on:
- api-service
- consul
environment:
- REACT_APP_API_URL=http://api-service:8080
auth-service: auth-service:
build: build:
context: ./ context: ./

28
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Step 1: Build the React app
FROM node:20 AS build
# Set working directory
WORKDIR /app
# Copy package.json and install dependencies
COPY package*.json ./
RUN npm install
# Copy the source code
COPY . .
# Build the React app
RUN npm run build
# Step 2: Serve the React app using nginx
FROM nginx:1.25
# Copy the built React files to the nginx html folder
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,23 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
# Root directory for the built React app
root /usr/share/nginx/html;
# Serve index.html for all routes
location / {
try_files $uri /index.html;
}
# Serve static files directly
location ~* \.(?:ico|css|js|gif|jpg|jpeg|png|svg|woff2?|eot|ttf|otf)$ {
expires max;
add_header Cache-Control "public";
}
# Optional: Log errors
error_log /var/log/nginx/error.log warn;
access_log /var/log/nginx/access.log;
}

17331
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
frontend/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "login",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.7.9",
"cra-template": "1.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.2",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"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"
]
}
}

11997
frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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`.
-->
<title>OsIROSE</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
frontend/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
frontend/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "OsIROSE",
"name": "OsIROSE Frontend",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
frontend/src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

18
frontend/src/App.js Normal file
View File

@@ -0,0 +1,18 @@
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import LoginPage from "./components/LoginPage";
import RegisterPage from "./components/RegisterPage";
const App = () => {
return (
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/" element={<h1>Welcome to the Game!</h1>} />
</Routes>
</Router>
);
};
export default App;

View File

@@ -0,0 +1,80 @@
import React, {useEffect, useState} from "react";
import axios from "axios";
import {getServiceAddress} from "../utils/consul";
import { Link } from "react-router-dom";
const LoginPage = () => {
const [apiUrl, setApiUrl] = useState(null);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
useEffect(() => {
// Fetch the API address from Consul
const fetchApiUrl = async () => {
try {
const { ServiceAddress, ServicePort } = await getServiceAddress("api-service");
setApiUrl(`http://${ServiceAddress}:${ServicePort}/api/login`);
} catch (error) {
setMessage("Failed to retrieve API information.");
}
};
fetchApiUrl();
}, []);
const handleLogin = async (e) => {
e.preventDefault();
if (!apiUrl) {
setMessage("API URL not available");
return;
}
try {
const response = await axios.post(apiUrl, { username, password });
// Extract token and server info from response
const { token, serverIp } = response.data;
setMessage("Login successful! Launching game...");
const { ServiceAddress, ServicePort } = await getServiceAddress("packet-service");
window.location.href = `osirose-launcher://launch?otp=${encodeURIComponent(token)}&ip=${encodeURIComponent(ServiceAddress)}&port=${encodeURIComponent(ServicePort)}&username=${encodeURIComponent(username)}`;
} catch (error) {
setMessage("Login failed: " + error.response?.data?.error || error.message);
}
};
return (
<div className={"register-container"}>
<h1>Login</h1>
<form onSubmit={handleLogin}>
<div>
<label>Username:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Login</button>
</form>
<p>{message}</p>
<p>
Don't have an account? <Link to="/register">Register here</Link>.
</p>
</div>
);
};
export default LoginPage;

View File

@@ -0,0 +1,70 @@
import React, { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import {getServiceAddress} from "../utils/consul";
const RegisterPage = () => {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const navigate = useNavigate();
const handleRegister = async (e) => {
e.preventDefault();
try {
const { ServiceAddress, ServicePort } = await getServiceAddress("api-service");
const response = await axios.post(`http://${ServiceAddress}:${ServicePort}/api/register`, {
username,
email,
password,
});
alert("Registration successful! Please log in.");
navigate("/login"); // Redirect to login page
} catch (error) {
console.error("Registration failed:", error);
setErrorMessage("Registration failed. Please try again.");
}
};
return (
<div className="register-container">
<h1>Register</h1>
<form onSubmit={handleRegister}>
<div>
<label>Username:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{errorMessage && <p className="error-message">{errorMessage}</p>}
<button type="submit">Register</button>
</form>
</div>
);
};
export default RegisterPage;

58
frontend/src/index.css Normal file
View File

@@ -0,0 +1,58 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.register-container {
width: 300px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.register-container h1 {
text-align: center;
}
.register-container label {
display: block;
margin-bottom: 8px;
}
.register-container input {
width: 94%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 4px;
}
.error-message {
color: red;
font-size: 14px;
}
.register-container button {
width: 100%;
padding: 10px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.register-container button:hover {
background-color: #45a049;
}

11
frontend/src/index.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,23 @@
import axios from "axios";
const CONSUL_URL = process.env.REACT_APP_CONSUL_URL;
/**
* Fetch the IP and port of the service from Consul.
* @param {string} serviceName - The name of the registered service.
* @returns {Promise<{Address: string, ServicePort: number}>}
*/
export async function getServiceAddress(serviceName) {
try {
const response = await axios.get(`${CONSUL_URL}/v1/catalog/service/${serviceName}`);
if (response.data && response.data.length > 0) {
const { ServiceAddress, ServicePort } = response.data[0]; // Get the first instance
return { ServiceAddress, ServicePort };
} else {
throw new Error("Service not found in Consul");
}
} catch (error) {
console.error("Failed to fetch service address from Consul:", error);
throw error;
}
}