initial refactoring to use a pure html/js login screen

This commit is contained in:
Joey Hines 2025-05-25 22:05:35 -06:00
parent 457d6cee96
commit 2ba48354ac
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
6 changed files with 264 additions and 70 deletions

View File

@ -6,8 +6,8 @@ pub type NewUserState {
} }
pub type NewUserClientMessages { pub type NewUserClientMessages {
Register(game_code: String, username: String) JoinAsPlayer(token: String, user_id: Int, game_id: String)
CreateRoom(game_code: String, username: String) JoinAsHost(token: String, user_id: Int, game_id: String)
} }
pub fn new_user_client_messages_decoder() -> decode.Decoder( pub fn new_user_client_messages_decoder() -> decode.Decoder(
@ -15,40 +15,32 @@ pub fn new_user_client_messages_decoder() -> decode.Decoder(
) { ) {
use variant <- decode.field("type", decode.string) use variant <- decode.field("type", decode.string)
case variant { case variant {
"Register" -> { "JoinAsPlayer" -> {
use game_code <- decode.field("game_code", decode.string) use token <- decode.field("token", decode.string)
use username <- decode.field("username", decode.string) use user_id <- decode.field("user_id", decode.int)
decode.success(Register(game_code:, username:)) use game_id <- decode.field("game_id", decode.string)
decode.success(JoinAsPlayer(token:, user_id:, game_id:))
} }
"CreateRoom" -> { "JoinAsHost" -> {
use game_code <- decode.field("game_code", decode.string) use token <- decode.field("token", decode.string)
use username <- decode.field("username", decode.string) use user_id <- decode.field("user_id", decode.int)
decode.success(CreateRoom(game_code:, username:)) use game_id <- decode.field("game_id", decode.string)
decode.success(JoinAsHost(token:, user_id:, game_id:))
} }
_ -> decode.failure(Register("", ""), "NewUserClientMessages") _ -> decode.failure(JoinAsPlayer("", 0, ""), "NewUserClientMessages")
} }
} }
pub type NewUserServerMessages { pub type NewUserServerMessages {
RegisterResponse(username: String, token: String) RegisterResponse
HostRegisterResponse(username: String, token: String) HostRegisterResponse
} }
pub fn encode_new_user_server_messages( pub fn encode_new_user_server_messages(
new_user_server_messages: NewUserServerMessages, new_user_server_messages: NewUserServerMessages,
) -> json.Json { ) -> json.Json {
case new_user_server_messages { case new_user_server_messages {
RegisterResponse(..) -> RegisterResponse -> json.string("register_response")
json.object([ HostRegisterResponse -> json.string("host_register_response")
#("type", json.string("RegisterResponse")),
#("username", json.string(new_user_server_messages.username)),
#("token", json.string(new_user_server_messages.token)),
])
HostRegisterResponse(..) ->
json.object([
#("type", json.string("HostRegisterResponse")),
#("username", json.string(new_user_server_messages.username)),
#("token", json.string(new_user_server_messages.token)),
])
} }
} }

View File

@ -1,17 +1,30 @@
import app/clients/new_user_client import app/clients/new_user_client
import app/web import app/web
import gleam/bit_array
import gleam/bytes_tree import gleam/bytes_tree
import gleam/dict
import gleam/erlang/process import gleam/erlang/process
import gleam/http/cookie
import gleam/http/request.{type Request} import gleam/http/request.{type Request}
import gleam/http/response.{type Response} import gleam/http/response.{type Response}
import gleam/int import gleam/int
import gleam/io import gleam/io
import gleam/json
import gleam/option.{None, Some} import gleam/option.{None, Some}
import gleam/otp/actor import gleam/otp/actor
import gleam/result import gleam/result
import gleam/string import gleam/string
import login
import mist.{type Connection, type ResponseData} import mist.{type Connection, type ResponseData}
import player
import session
import socket_manager import socket_manager
import storail
type JoinAs {
JoinAsHost
JoinAsPlayer
}
pub fn handle_request( pub fn handle_request(
req: Request(Connection), req: Request(Connection),
@ -25,10 +38,75 @@ pub fn handle_request(
["static", "frontend_bg.wasm"] -> ["static", "frontend_bg.wasm"] ->
serve_wasm(ctx, "frontend_bg.wasm", "application/wasm") serve_wasm(ctx, "frontend_bg.wasm", "application/wasm")
["ws"] -> serve_websocket(ctx, req) ["ws"] -> serve_websocket(ctx, req)
["login", "host"] -> server_login(ctx, req, JoinAsHost)
["login", "join"] -> server_login(ctx, req, JoinAsPlayer)
_ -> response.set_body(response.new(404), mist.Bytes(bytes_tree.new())) _ -> response.set_body(response.new(404), mist.Bytes(bytes_tree.new()))
} }
} }
fn server_login(
ctx: web.Context,
req: Request(Connection),
join_type: JoinAs,
) -> Response(ResponseData) {
let assert Ok(req) = mist.read_body(req, 1000)
let assert Ok(body) = bit_array.to_string(req.body)
let assert Ok(login_req) = json.parse(body, login.login_request_decoder())
let #(token, user_id) =
web.register_user(ctx, login_req.username, login_req.game_code)
let _ = case join_type {
JoinAsHost -> {
let key = storail.key(ctx.sessions, login_req.game_code)
let session = session.Session(login_req.game_code, user_id, dict.new())
let assert Ok(Nil) = storail.write(key, session)
}
JoinAsPlayer -> {
let key = storail.key(ctx.sessions, login_req.game_code)
let assert Ok(session) = storail.read(key)
let player =
player.Player(
name: login_req.username,
id: user_id,
score: 0,
buzz_in_time: None,
)
let session =
session.Session(
..session,
players: dict.insert(session.players, player.id, player),
)
let assert Ok(Nil) = storail.write(key, session)
}
}
let join_type_name = case join_type {
JoinAsHost -> "host"
JoinAsPlayer -> "player"
}
response.new(200)
|> response.set_cookie(
"play_of_the_game/token",
token,
cookie.defaults(req.scheme),
)
|> response.set_cookie(
"play_of_the_game/client_type",
join_type_name,
cookie.defaults(req.scheme),
)
|> response.set_cookie(
"play_of_the_game/username",
login_req.username,
cookie.defaults(req.scheme),
)
|> response.set_body(mist.Bytes(bytes_tree.new()))
}
fn serve_wasm( fn serve_wasm(
ctx: web.Context, ctx: web.Context,
file: String, file: String,

View File

@ -19,6 +19,10 @@ import session
import socket_manager import socket_manager
import storail import storail
pub type Error {
InvalidToken
}
pub type Context { pub type Context {
Context( Context(
config: config.Config, config: config.Config,
@ -109,8 +113,8 @@ fn handle_player(
} }
} }
fn register_user( pub fn register_user(
state: SocketState, ctx: Context,
username: String, username: String,
game_code: String, game_code: String,
) -> #(String, Int) { ) -> #(String, Int) {
@ -127,7 +131,7 @@ fn register_user(
let assert <<user_id:int-size(64)>> = crypto.strong_random_bytes(8) let assert <<user_id:int-size(64)>> = crypto.strong_random_bytes(8)
let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id)) let key = storail.key(ctx.player_sessions, int.to_base16(user_id))
let assert Ok(Nil) = let assert Ok(Nil) =
storail.write( storail.write(
@ -136,13 +140,40 @@ fn register_user(
id: user_id, id: user_id,
token_hash:, token_hash:,
username:, username:,
socket_id: state.id, socket_id: None,
), ),
) )
#(token, user_id) #(token, user_id)
} }
pub fn attach_socket_to_user(
state: SocketState,
user_id: Int,
token: String,
) -> Result(Nil, Error) {
let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id))
let assert Ok(player_state) = storail.read(key)
let hashed_token = player_session.hash_token(token)
case hashed_token == player_state.token_hash {
False -> Error(InvalidToken)
True -> {
let assert Ok(Nil) =
storail.write(
key,
player_session.PlayerSession(
..player_state,
socket_id: Some(state.id),
),
)
Ok(Nil)
}
}
}
fn handle_new_user( fn handle_new_user(
socket_state: SocketState, socket_state: SocketState,
_new_user_state: new_user_client.NewUserState, _new_user_state: new_user_client.NewUserState,
@ -152,49 +183,25 @@ fn handle_new_user(
json.parse(msg, new_user_client.new_user_client_messages_decoder()) json.parse(msg, new_user_client.new_user_client_messages_decoder())
let #(socket_state, new_user_msg) = case msg { let #(socket_state, new_user_msg) = case msg {
new_user_client.CreateRoom(game_code, username) -> { new_user_client.JoinAsHost(token, user_id, game_id) -> {
let #(token, user_id) = register_user(socket_state, username, game_code) let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)
let key = storail.key(socket_state.ctx.sessions, game_code)
let session = session.Session(game_code, user_id, dict.new())
let assert Ok(Nil) = storail.write(key, session)
#( #(
SocketState( SocketState(
..socket_state, ..socket_state,
state: Host(host_client.HostState(user_id, game_code)), state: Player(player_client.PlayerState(user_id:, game_id:)),
), ),
new_user_client.HostRegisterResponse(username, token), new_user_client.HostRegisterResponse,
) )
} }
new_user_client.Register(game_code, username) -> { new_user_client.JoinAsPlayer(token, user_id, game_id) -> {
let #(token, user_id) = register_user(socket_state, username, game_code) let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)
let key = storail.key(socket_state.ctx.sessions, game_code)
let assert Ok(session) = storail.read(key)
let player =
player.Player(name: username, id: user_id, score: 0, buzz_in_time: None)
let session =
session.Session(
..session,
players: dict.insert(session.players, player.id, player),
)
send_message_to_host(
socket_state,
session.host_user_id,
host_client.UpdatePlayerStates(session.players),
)
let assert Ok(Nil) = storail.write(key, session)
#( #(
SocketState( SocketState(
..socket_state, ..socket_state,
state: Player(player_client.PlayerState(user_id, game_code)), state: Host(host_client.HostState(user_id:, game_id:)),
), ),
new_user_client.RegisterResponse(username, token), new_user_client.HostRegisterResponse,
) )
} }
} }
@ -218,8 +225,9 @@ fn get_player_session_subject(
ctx: Context, ctx: Context,
player: player_session.PlayerSession, player: player_session.PlayerSession,
) -> process.Subject(String) { ) -> process.Subject(String) {
let assert Some(id) = player.socket_id
let assert Ok(subject) = let assert Ok(subject) =
socket_manager.get_socket_subj_by_id(ctx.socket_manager, player.socket_id) socket_manager.get_socket_subj_by_id(ctx.socket_manager, id)
subject subject
} }

72
frontend/index/index.html Normal file
View File

@ -0,0 +1,72 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Play of the Game</title>
<style>
html,
body,
canvas {
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
overflow: hidden;
position: absolute;
z-index: 0;
}
</style>
</head>
<body style="margin: 0; padding: 0; height: 100vh; width: 100vw">
<canvas id="glcanvas" tabindex="1" hidden></canvas>
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
<script type="module">
import init, { set_wasm } from "./static/frontend.js";
async function impl_run() {
let wbg = await init();
miniquad_add_plugin({
register_plugin: (a) => (a.wbg = wbg),
on_init: () => set_wasm(wasm_exports),
version: "0.0.1",
name: "wbg",
});
load("./static/frontend_bg.wasm");
}
window.run = function (join_type) {
console.log("Joining as: ", join_type);
var xhr = new XMLHttpRequest();
xhr.open("POST", "./login/" + join_type);
xhr.onload = function (event) {
console.log("Starting game...", event.target);
document.getElementById("run-container").remove();
document
.getElementById("glcanvas")
.removeAttribute("hidden");
document.getElementById("glcanvas").focus();
impl_run();
};
// or onerror, onabort
var formData = new FormData(document.getElementById("login"));
xhr.send(JSON.stringify(Object.fromEntries(formData));
};
</script>
<div
id="run-container"
style="
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
"
>
<form id="login">
<label for="username">Username:</label><br />
<input type="text" id="username" name="username" /><br />
<label for="gamecode">Gamecode:</label><br />
<input type="text" id="gamecode" name="gamecode" /><br />
</form>
<button onclick="run('join')">Join Game</button>
<button onclick="run('host')">Host Game</button>
</div>
</body>
</html>

36
shared/src/login.gleam Normal file
View File

@ -0,0 +1,36 @@
import gleam/dynamic/decode
import gleam/json
pub type LoginRequest {
LoginRequest(username: String, game_code: String)
}
pub fn encode_login_request(login_request: LoginRequest) -> json.Json {
json.object([
#("username", json.string(login_request.username)),
#("game_code", json.string(login_request.game_code)),
])
}
pub fn login_request_decoder() -> decode.Decoder(LoginRequest) {
use username <- decode.field("username", decode.string)
use game_code <- decode.field("game_code", decode.string)
decode.success(LoginRequest(username:, game_code:))
}
pub type LoginResponse {
LoginResponse(username: String, token: String)
}
pub fn encode_login_response(login_response: LoginResponse) -> json.Json {
json.object([
#("username", json.string(login_response.username)),
#("token", json.string(login_response.token)),
])
}
pub fn login_response_decoder() -> decode.Decoder(LoginResponse) {
use username <- decode.field("username", decode.string)
use token <- decode.field("token", decode.string)
decode.success(LoginResponse(username:, token:))
}

View File

@ -2,9 +2,15 @@ import gleam/bit_array
import gleam/crypto import gleam/crypto
import gleam/dynamic/decode import gleam/dynamic/decode
import gleam/json import gleam/json
import gleam/option.{type Option}
pub type PlayerSession { pub type PlayerSession {
PlayerSession(id: Int, token_hash: String, username: String, socket_id: Int) PlayerSession(
id: Int,
token_hash: String,
username: String,
socket_id: Option(Int),
)
} }
pub fn encode_player_session(player_session: PlayerSession) -> json.Json { pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
@ -12,7 +18,10 @@ pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
#("id", json.int(player_session.id)), #("id", json.int(player_session.id)),
#("token_hash", json.string(player_session.token_hash)), #("token_hash", json.string(player_session.token_hash)),
#("username", json.string(player_session.username)), #("username", json.string(player_session.username)),
#("socket_id", json.int(player_session.socket_id)), #("socket_id", case player_session.socket_id {
option.None -> json.null()
option.Some(value) -> json.int(value)
}),
]) ])
} }
@ -20,12 +29,11 @@ pub fn player_session_decoder() -> decode.Decoder(PlayerSession) {
use id <- decode.field("id", decode.int) use id <- decode.field("id", decode.int)
use token_hash <- decode.field("token_hash", decode.string) use token_hash <- decode.field("token_hash", decode.string)
use username <- decode.field("username", decode.string) use username <- decode.field("username", decode.string)
use socket_id <- decode.field("socket_id", decode.int) use socket_id <- decode.field("socket_id", decode.optional(decode.int))
decode.success(PlayerSession(id:, token_hash:, username:, socket_id:)) decode.success(PlayerSession(id:, token_hash:, username:, socket_id:))
} }
pub fn hash_token(token: String) -> String { pub fn hash_token(token: String) -> String {
let token =
crypto.new_hasher(crypto.Sha512) crypto.new_hasher(crypto.Sha512)
|> crypto.hash_chunk(bit_array.from_string(token)) |> crypto.hash_chunk(bit_array.from_string(token))
|> crypto.digest |> crypto.digest