Created simple login page, and User model

This commit is contained in:
Martin Berg Alstad 2023-07-20 14:47:13 +02:00
parent 745f292eee
commit 5e21947870
16 changed files with 183 additions and 32 deletions

View File

@ -0,0 +1,6 @@
namespace BackendTests.Controllers;
public class PlayerControllerTests
{
// TODO
}

View File

@ -7,8 +7,4 @@
<RootNamespace>DAL</RootNamespace> <RootNamespace>DAL</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\pac-man-board-game\pac-man-board-game.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,8 @@
namespace DAL.Database.Models;
public class User
{
public required string Username { get; init; }
public required string Password { get; init; } // TODO use hashing and salt
public string? Colour { get; init; }
}

View File

@ -1,26 +1,27 @@
using pacMan.GameStuff; using DAL.Database.Models;
using pacMan.GameStuff.Items;
namespace DAL.Database.Service; namespace DAL.Database.Service;
public class UserService public class UserService
{ {
private readonly List<Player> _users = new() private readonly List<User> _users = new()
{ {
new Player new User
{ {
Username = "admin", Username = "Firefox",
Colour = "red", Password = "Firefox",
PacMan = new Character Colour = "red"
{ },
Colour = "red", new User
Type = CharacterType.PacMan {
} Username = "Chrome",
Password = "Chrome",
Colour = "blue"
} }
}; };
public async Task<IPlayer?> Login(string username, string password) public async Task<User?> Login(string username, string password)
{ {
return await Task.Run(() => _users.FirstOrDefault(x => x.Username == username && password == "admin")); return await Task.Run(() => _users.FirstOrDefault(x => x.Username == username && x.Password == password));
} }
} }

View File

@ -1,5 +1,5 @@
PORT=44435 PORT=44435
HTTPS=true HTTPS=true
VITE_API_URI=localhost:3000/api/game VITE_API_URI=localhost:3000/api
VITE_API_HTTP=https://$VITE_API_URI VITE_API_HTTP=https://$VITE_API_URI
VITE_API_WS=wss://$VITE_API_URI/connect VITE_API_WS=wss://$VITE_API_URI/game/connect

View File

@ -3,6 +3,7 @@ import {Counter} from "./pages/counter";
import Home from "./pages/home"; import Home from "./pages/home";
import Game from "./pages/game"; import Game from "./pages/game";
import LobbyPage from "./pages/lobby"; import LobbyPage from "./pages/lobby";
import Login from "./pages/login";
const AppRoutes = [ const AppRoutes = [
{ {
@ -20,6 +21,10 @@ const AppRoutes = [
{ {
path: "/lobby", path: "/lobby",
element: <LobbyPage/>, element: <LobbyPage/>,
},
{
path: "/login",
element: <Login/>
} }
]; ];

View File

@ -7,10 +7,12 @@ const Input: FRComponent<InputProps, HTMLInputElement> = forwardRef((
id, id,
placeholder, placeholder,
required = false, required = false,
name,
}, ref) => ( }, ref) => (
<input type={type} <input type={type}
ref={ref} ref={ref}
id={id} id={id}
name={name}
className={"border-2 border-gray-300 rounded-md p-1 " + className} className={"border-2 border-gray-300 rounded-md p-1 " + className}
placeholder={placeholder} placeholder={placeholder}
required={required}/> required={required}/>

View File

@ -2,11 +2,13 @@ import React, {FC, Suspense} from "react";
import {atom, useAtomValue} from "jotai"; import {atom, useAtomValue} from "jotai";
import {Button} from "../components/button"; import {Button} from "../components/button";
import {thisPlayerAtom} from "../utils/state"; import {thisPlayerAtom} from "../utils/state";
import {getData, postData} from "../utils/api";
const fetchAtom = atom(async () => { const fetchAtom = atom(async () => {
const response = await fetch(import.meta.env.VITE_API_HTTP + "/all"); const response = await getData("/game/all");
return await response.json() as Game[]; return await response.json() as Game[];
}); });
// TODO create game button // TODO create game button
const LobbyPage: FC = () => ( // TODO check if player is defined in storage, if not redirect to login const LobbyPage: FC = () => ( // TODO check if player is defined in storage, if not redirect to login
<Suspense fallback={"Please wait"}> <Suspense fallback={"Please wait"}>
@ -26,19 +28,13 @@ const GameTable: FC<ComponentProps> = ({className}) => {
console.debug("Joining game " + gameId); console.debug("Joining game " + gameId);
const result = await fetch(import.meta.env.VITE_API_HTTP + "/join/" + gameId, { const result = await postData("/game/join/" + gameId, {body: thisPlayer});
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(thisPlayer),
})
if (result.ok) { if (result.ok) {
console.debug("Joined game " + gameId, result.body); console.debug("Joined game " + gameId, await result.json());
// TODO redirect to game page // TODO redirect to game page
} else { } else {
console.error("Failed to join game " + gameId, result.body); console.error("Failed to join game " + gameId, await result.json());
// TODO show error message // TODO show error message
} }
} }
@ -47,10 +43,10 @@ const GameTable: FC<ComponentProps> = ({className}) => {
<table className={`rounded overflow-hidden ${className}`}> <table className={`rounded overflow-hidden ${className}`}>
<thead className={"bg-gray-500 text-white"}> <thead className={"bg-gray-500 text-white"}>
<tr className={"my-5"}> <tr className={"my-5"}>
<th>Id</th> <th className={"p-2"}>Id</th>
<th>Count</th> <th className={"p-2"}>Count</th>
<th className={"p-2"}>State</th> <th className={"p-2"}>State</th>
<th>Join</th> <th className={"p-2"}>Join</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -66,6 +62,12 @@ const GameTable: FC<ComponentProps> = ({className}) => {
</td> </td>
</tr> </tr>
)} )}
{
data?.length === 0 &&
<tr>
<td colSpan={4} className={"text-center"}>No games found</td>
</tr>
}
</tbody> </tbody>
</table> </table>
); );

View File

@ -0,0 +1,54 @@
import React, {FormEvent} from "react";
import {Button} from "../components/button";
import Input from "../components/input";
import {useSetAtom} from "jotai";
import {thisPlayerAtom} from "../utils/state";
import Player from "../game/player";
import {useNavigate} from "react-router-dom";
import {postData} from "../utils/api";
const Login = () => {
const setThisPlayer = useSetAtom(thisPlayerAtom);
const navigate = useNavigate();
async function handleLogin(e: FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault();
const fields = e.currentTarget.querySelectorAll("input");
let user: User = {username: "", password: ""};
for (const field of fields) {
user = {
...user,
[field.name]: field.value,
}
}
const response = await postData("/player/login", {
body: {username: user.username, password: user.password} as User
})
const data = await response.json();
if (response.ok) {
console.debug("Login successful: ", data);
setThisPlayer(new Player(data as PlayerProps));
navigate("/lobby");
} else {
console.error("Error: ", data);
// TODO display error
}
}
return ( // TODO prettify
<form onSubmit={handleLogin}>
<h1>Login</h1>
<Input name={"username"} placeholder={"Username"}/>
<Input name={"password"} type={"password"} placeholder={"Password"}/>
<Button type={"submit"}>Login</Button>
</form>
);
}
export default Login;

View File

@ -21,6 +21,7 @@ interface InputProps extends ComponentProps {
type?: string, type?: string,
placeholder?: string, placeholder?: string,
required?: boolean, required?: boolean,
name?: string,
} }
interface CharacterProps { interface CharacterProps {

View File

@ -40,3 +40,16 @@ type Game = {
readonly count: number, readonly count: number,
readonly isGameStarted: boolean, readonly isGameStarted: boolean,
} }
type User = {
readonly username: string,
readonly password: string,
readonly colour?: import("../game/colour").Colour
}
type Api<T = ApiRequest> = (path: string, data?: ApiRequest & T) => Promise<Response>;
type ApiRequest = {
headers?: HeadersInit,
body?: any
}

View File

@ -0,0 +1,17 @@
export const getData: Api = async (path, {headers} = {}) => {
return await fetch(import.meta.env.VITE_API_HTTP + path, {
method: "GET",
headers: headers
});
}
export const postData: Api = async (path, {body, headers} = {}) => {
return await fetch(import.meta.env.VITE_API_HTTP + path, {
method: "POST",
headers: {
"Content-Type": "application/json",
...headers,
},
body: JSON.stringify(body),
})
}

View File

@ -0,0 +1,26 @@
using DAL.Database.Models;
using DAL.Database.Service;
using Microsoft.AspNetCore.Mvc;
using pacMan.GameStuff.Items;
namespace pacMan.Controllers;
[ApiController]
[Route("api/[controller]")]
public class PlayerController : ControllerBase
{
private readonly UserService _userService;
public PlayerController(UserService userService) => _userService = userService;
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] User user)
{
var result = await _userService.Login(user.Username, user.Password);
if (result is null) return Unauthorized("Invalid username or password");
return Ok((Player)result);
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] User user) => throw new NotSupportedException();
}

View File

@ -1,3 +1,5 @@
using DAL.Database.Models;
namespace pacMan.GameStuff.Items; namespace pacMan.GameStuff.Items;
public interface IPlayer public interface IPlayer
@ -40,4 +42,16 @@ public class Player : IPlayer, IEquatable<Player>
} }
public override int GetHashCode() => Username.GetHashCode(); public override int GetHashCode() => Username.GetHashCode();
public static explicit operator Player(User user) =>
new()
{
Username = user.Username,
PacMan = new Character
{
Colour = user.Colour,
Type = CharacterType.PacMan
},
Colour = user.Colour
};
} }

View File

@ -1,3 +1,4 @@
using DAL.Database.Service;
using pacMan.Interfaces; using pacMan.Interfaces;
using pacMan.Services; using pacMan.Services;
@ -9,6 +10,7 @@ builder.Services.AddControllersWithViews();
builder.Services builder.Services
.AddSingleton<IWebSocketService, WebSocketService>() .AddSingleton<IWebSocketService, WebSocketService>()
.AddSingleton<GameService>() .AddSingleton<GameService>()
.AddScoped<UserService>()
.AddTransient<IActionService, ActionService>(); .AddTransient<IActionService, ActionService>();
var app = builder.Build(); var app = builder.Build();

View File

@ -54,6 +54,10 @@
<Folder Include="ClientApp\tests\utils\" /> <Folder Include="ClientApp\tests\utils\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DataAccessLayer\DataAccessLayer.csproj" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed --> <!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true"> <Exec Command="node --version" ContinueOnError="true">