From 2ba48354acf886cb89ecab52cd02b0ce0877dc37 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 25 May 2025 22:05:35 -0600 Subject: [PATCH] initial refactoring to use a pure html/js login screen --- backend/src/app/clients/new_user_client.gleam | 42 ++++------ backend/src/app/router.gleam | 78 ++++++++++++++++++ backend/src/app/web.gleam | 82 ++++++++++--------- frontend/index/index.html | 72 ++++++++++++++++ shared/src/login.gleam | 36 ++++++++ shared/src/player_session.gleam | 24 ++++-- 6 files changed, 264 insertions(+), 70 deletions(-) create mode 100644 frontend/index/index.html create mode 100644 shared/src/login.gleam diff --git a/backend/src/app/clients/new_user_client.gleam b/backend/src/app/clients/new_user_client.gleam index 788e466..ba5d121 100644 --- a/backend/src/app/clients/new_user_client.gleam +++ b/backend/src/app/clients/new_user_client.gleam @@ -6,8 +6,8 @@ pub type NewUserState { } pub type NewUserClientMessages { - Register(game_code: String, username: String) - CreateRoom(game_code: String, username: String) + JoinAsPlayer(token: String, user_id: Int, game_id: String) + JoinAsHost(token: String, user_id: Int, game_id: String) } 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) case variant { - "Register" -> { - use game_code <- decode.field("game_code", decode.string) - use username <- decode.field("username", decode.string) - decode.success(Register(game_code:, username:)) + "JoinAsPlayer" -> { + use token <- decode.field("token", decode.string) + use user_id <- decode.field("user_id", decode.int) + use game_id <- decode.field("game_id", decode.string) + decode.success(JoinAsPlayer(token:, user_id:, game_id:)) } - "CreateRoom" -> { - use game_code <- decode.field("game_code", decode.string) - use username <- decode.field("username", decode.string) - decode.success(CreateRoom(game_code:, username:)) + "JoinAsHost" -> { + use token <- decode.field("token", decode.string) + use user_id <- decode.field("user_id", decode.int) + 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 { - RegisterResponse(username: String, token: String) - HostRegisterResponse(username: String, token: String) + RegisterResponse + HostRegisterResponse } pub fn encode_new_user_server_messages( new_user_server_messages: NewUserServerMessages, ) -> json.Json { case new_user_server_messages { - RegisterResponse(..) -> - json.object([ - #("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)), - ]) + RegisterResponse -> json.string("register_response") + HostRegisterResponse -> json.string("host_register_response") } } diff --git a/backend/src/app/router.gleam b/backend/src/app/router.gleam index 778d970..9354abf 100644 --- a/backend/src/app/router.gleam +++ b/backend/src/app/router.gleam @@ -1,17 +1,30 @@ import app/clients/new_user_client import app/web +import gleam/bit_array import gleam/bytes_tree +import gleam/dict import gleam/erlang/process +import gleam/http/cookie import gleam/http/request.{type Request} import gleam/http/response.{type Response} import gleam/int import gleam/io +import gleam/json import gleam/option.{None, Some} import gleam/otp/actor import gleam/result import gleam/string +import login import mist.{type Connection, type ResponseData} +import player +import session import socket_manager +import storail + +type JoinAs { + JoinAsHost + JoinAsPlayer +} pub fn handle_request( req: Request(Connection), @@ -25,10 +38,75 @@ pub fn handle_request( ["static", "frontend_bg.wasm"] -> serve_wasm(ctx, "frontend_bg.wasm", "application/wasm") ["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())) } } +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( ctx: web.Context, file: String, diff --git a/backend/src/app/web.gleam b/backend/src/app/web.gleam index ec76291..af2ee28 100644 --- a/backend/src/app/web.gleam +++ b/backend/src/app/web.gleam @@ -19,6 +19,10 @@ import session import socket_manager import storail +pub type Error { + InvalidToken +} + pub type Context { Context( config: config.Config, @@ -109,8 +113,8 @@ fn handle_player( } } -fn register_user( - state: SocketState, +pub fn register_user( + ctx: Context, username: String, game_code: String, ) -> #(String, Int) { @@ -127,7 +131,7 @@ fn register_user( let assert <> = 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) = storail.write( @@ -136,13 +140,40 @@ fn register_user( id: user_id, token_hash:, username:, - socket_id: state.id, + socket_id: None, ), ) #(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( socket_state: SocketState, _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()) let #(socket_state, new_user_msg) = case msg { - new_user_client.CreateRoom(game_code, username) -> { - let #(token, user_id) = register_user(socket_state, username, game_code) - - 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) + new_user_client.JoinAsHost(token, user_id, game_id) -> { + let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token) #( SocketState( ..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) -> { - let #(token, user_id) = register_user(socket_state, username, game_code) - - 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) - + new_user_client.JoinAsPlayer(token, user_id, game_id) -> { + let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token) #( SocketState( ..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, player: player_session.PlayerSession, ) -> process.Subject(String) { + let assert Some(id) = player.socket_id 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 } diff --git a/frontend/index/index.html b/frontend/index/index.html new file mode 100644 index 0000000..59c1e58 --- /dev/null +++ b/frontend/index/index.html @@ -0,0 +1,72 @@ + + + + Play of the Game + + + + + + +
+
+
+
+
+
+
+ + +
+ + diff --git a/shared/src/login.gleam b/shared/src/login.gleam new file mode 100644 index 0000000..7609875 --- /dev/null +++ b/shared/src/login.gleam @@ -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:)) +} diff --git a/shared/src/player_session.gleam b/shared/src/player_session.gleam index fc1a632..a9cf8e3 100644 --- a/shared/src/player_session.gleam +++ b/shared/src/player_session.gleam @@ -2,9 +2,15 @@ import gleam/bit_array import gleam/crypto import gleam/dynamic/decode import gleam/json +import gleam/option.{type Option} 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 { @@ -12,7 +18,10 @@ pub fn encode_player_session(player_session: PlayerSession) -> json.Json { #("id", json.int(player_session.id)), #("token_hash", json.string(player_session.token_hash)), #("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,14 +29,13 @@ pub fn player_session_decoder() -> decode.Decoder(PlayerSession) { use id <- decode.field("id", decode.int) use token_hash <- decode.field("token_hash", 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:)) } pub fn hash_token(token: String) -> String { - let token = - crypto.new_hasher(crypto.Sha512) - |> crypto.hash_chunk(bit_array.from_string(token)) - |> crypto.digest - |> bit_array.base64_encode(True) + crypto.new_hasher(crypto.Sha512) + |> crypto.hash_chunk(bit_array.from_string(token)) + |> crypto.digest + |> bit_array.base64_encode(True) }