extract secrets to env, use docker compose

This commit is contained in:
Lett Osprey 2026-04-15 20:04:56 +02:00
parent 14ba148284
commit c8300f5978
16 changed files with 186 additions and 133 deletions

View file

@ -12,4 +12,5 @@ lustre = ">= 5.3.5 and < 6.0.0"
gleam_crypto = ">= 1.5.1 and < 2.0.0"
group_registry = ">= 1.0.0 and < 2.0.0"
wisp = ">= 2.2.1 and < 3.0.0"
envoy = ">= 1.1.0 and < 2.0.0"
shared = { path = "../shared" }

View file

@ -28,6 +28,7 @@ packages = [
]
[requirements]
envoy = { version = ">= 1.1.0 and < 2.0.0" }
gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" }
gleam_erlang = { version = ">= 1.0.0 and < 2.0.0" }
gleam_http = { version = ">= 3.7.2 and < 5.0.0" }

View file

@ -17,7 +17,7 @@ type State {
State(
question_number: Int,
// id, (name (question#, answer_attempt)
slow_answers: List(#(String, #(String, List(#(String, String))))),
single_answers: List(#(String, #(String, List(#(String, String))))),
// int in #pair: ping counted since response back.
name_answers: List(#(String, #(Int, AnswerStatus))),
hide_answers: Bool,
@ -71,16 +71,16 @@ pub fn initialize(
GiveSingleAnswer(id, question, answer) -> {
State(
..state,
slow_answers: case list.key_find(state.slow_answers, id) {
single_answers: case list.key_find(state.single_answers, id) {
Ok(value) -> {
let #(name, list) = value
list.key_set(state.slow_answers, id, #(
list.key_set(state.single_answers, id, #(
name,
list.key_set(list, question, answer),
))
}
Error(_) -> {
state.slow_answers
state.single_answers
}
},
)
@ -91,11 +91,11 @@ pub fn initialize(
// Switch from "Wait for next question" to "Answer next question" mode
AnswerQuiz -> answer_quiz(state, registry)
message.FetchPlayers(subject:) -> {
fetch_players(state.slow_answers, subject)
fetch_players(state.single_answers, subject)
state
}
message.AddPlayer(name) ->
State(..state, slow_answers: add_player(name, state.slow_answers))
State(..state, single_answers: add_player(name, state.single_answers))
}
|> actor.continue()
})
@ -241,7 +241,7 @@ fn combine_lists(state: State) {
}),
// Second list require a bit more work Iterate over each payers answers,
// creating user objects where question number match current question number.
list.flat_map(state.slow_answers, fn(name_answers) {
list.flat_map(state.single_answers, fn(name_answers) {
let #(_, #(name, answers)) = name_answers
list.filter_map(answers, fn(number_answer) {
let #(answer_number, answer) = number_answer

View file

@ -34,7 +34,7 @@ pub fn serve(
}
}
pub fn serve_slow(
pub fn serve_single(
request: Request(Connection),
component: lustre.App(
#(List(#(String, String)), message.ClientsServer),

View file

@ -18,10 +18,14 @@ import web/components/control
import web/router
import wisp
import wisp/wisp_mist
import envoy
pub fn main() {
wisp.configure_logger()
let assert Ok(sha_api_key) = envoy.get("SHAED_API_KEY")
let assert Ok(secret) = envoy.get("QTERM_SECRET")
let assert Ok(state_handler) = statehandler.initialize()
let assert Ok(room_handler) = roomhandler.initialize(state_handler)
@ -48,7 +52,7 @@ pub fn main() {
room_handler,
)
["socket", "single", id, pin] ->
sockethandler.serve_slow(
sockethandler.serve_single(
req,
answerlist.component(),
id,
@ -58,8 +62,8 @@ pub fn main() {
)
_ ->
wisp_mist.handler(
router.handle_request(room_handler, state_handler, _),
"very_secret",
router.handle_request(sha_api_key, room_handler, state_handler, _),
secret,
)(req)
}
}

View file

@ -190,83 +190,94 @@ fn view(model: Model) -> Element(Msg) {
])
}
},
html.div([class("terminal-section")], case lobby {
[] -> []
lobby -> {
let answered =
list.filter(lobby, fn(x) {
case x.answer {
message.IDontKnow | message.HasAnswered | message.GivenAnswer(_) ->
True
_ -> False
case model.state {
Answer(_) | WaitForQuiz(_) ->
element.fragment([
html.div([class("terminal-section")], case lobby {
[] -> []
lobby -> {
let answered =
list.filter(lobby, fn(x) {
case x.answer {
message.IDontKnow
| message.HasAnswered
| message.GivenAnswer(_) -> True
_ -> False
}
})
|> list.length
|> int.to_string
let size = lobby |> list.length |> int.to_string
[
html.div([attribute.class("terminal-box")], [
html.span([attribute.class("terminal-label")], [
html.text("[PROGRESS] "),
]),
html.text("Answered: "),
case answered == size {
True -> html.text("Everyone!")
False -> html.text(answered <> "/" <> size)
},
]),
]
}
})
|> list.length
|> int.to_string
let size = lobby |> list.length |> int.to_string
[
html.div([attribute.class("terminal-box")], [
html.span([attribute.class("terminal-label")], [
html.text("[PROGRESS] "),
]),
html.text("Answered: "),
case answered == size {
True -> html.text("Everyone!")
False -> html.text(answered <> "/" <> size)
}),
terminal_section(
lobby,
"[ACTIVE TRANSMISSIONS]",
fn(x) {
case x.answer {
message.GivenAnswer(_) | message.HasAnswered -> True
_ -> False
}
},
]),
]
}
}),
terminal_section(
lobby,
"[ACTIVE TRANSMISSIONS]",
fn(x) {
case x.answer {
message.GivenAnswer(_) | message.HasAnswered -> True
_ -> False
}
},
fn(user) {
let User(name, ping_time, answer) = user
case answer {
message.GivenAnswer(answer) -> answer
message.HasAnswered -> "Answer Given"
_ -> "Odd State..."
}
|> content_cell(name, ping_time, _)
},
),
terminal_section(
lobby,
"[P A S S]",
fn(x) {
case x.answer {
message.IDontKnow -> True
_ -> False
}
},
fn(user) {
let User(name, ping_time, _) = user
content_cell(name, ping_time, "P.A.S.S :(")
},
),
terminal_section(
lobby,
"[AWAITING RESPONSE]",
fn(x) {
case x.answer {
message.NotAnswered -> True
_ -> False
}
},
fn(user) {
case user {
User(name, ping_time, _) ->
content_cell(name, ping_time, "Not Answered")
}
},
),
fn(user) {
let User(name, ping_time, answer) = user
case answer {
message.GivenAnswer(answer) -> answer
message.HasAnswered -> "Answer Given"
_ -> "Odd State..."
}
|> content_cell(name, ping_time, _)
},
),
terminal_section(
lobby,
"[P A S S]",
fn(x) {
case x.answer {
message.IDontKnow -> True
_ -> False
}
},
fn(user) {
let User(name, ping_time, _) = user
content_cell(name, ping_time, "P.A.S.S :(")
},
),
terminal_section(
lobby,
"[AWAITING RESPONSE]",
fn(x) {
case x.answer {
message.NotAnswered -> True
_ -> False
}
},
fn(user) {
case user {
User(name, ping_time, _) ->
content_cell(name, ping_time, "Not Answered")
}
},
),
server_component.element(
[server_component.route("/socket/control/TMA/PINA")],
[],
),
])
_ -> element.none()
},
])
}

View file

@ -110,6 +110,7 @@ pub fn view_players(
[click_cell_pair(Some("ENTER NEW PLAYER"), None, True, handler)],
),
),
])
}

View file

@ -1,5 +1,6 @@
import gleam/bit_array
import gleam/crypto
import gleam/dynamic
import gleam/dynamic/decode
import gleam/erlang/process.{type Subject}
import gleam/http
@ -11,6 +12,7 @@ import web/handlers/serve.{html_404}
import wisp.{type Request, type Response}
pub fn handle_request(
sha_api_key: String,
room_handler: Started(Subject(RoomControl)),
state_handler: Started(Subject(StateControl)),
req: Request,
@ -18,19 +20,54 @@ pub fn handle_request(
use req <- middleware(req)
case wisp.path_segments(req) {
[] | ["index.html"] -> serve.main_html(fetch_rooms(room_handler))
["api", "room"] -> handle_room(room_handler, req)
["api", ..path] -> handle_admin_api(state_handler, req, path)
["api", ..path] ->
handle_api(sha_api_key, room_handler, state_handler, req, path)
_ -> html_404()
}
}
fn handle_room(room_handler: Started(Subject(RoomControl)), req: Request) {
use json <- wisp.require_json(req)
fn handle_room(
room_handler: Started(Subject(RoomControl)),
req: Request,
json: dynamic.Dynamic,
) {
case req.method {
http.Post -> add_room(room_handler, json)
_ -> #(404, "bad api path", "Resource not found")
}
}
fn handle_api(
sha_api_key: String,
room_handler: Started(Subject(RoomControl)),
state_handler: Started(Subject(StateControl)),
req: Request,
path: List(String),
) {
use json <- wisp.require_json(req)
case list.key_find(req.headers, "x-api-key") {
Ok(key) -> {
case
bit_array.base16_encode(crypto.hash(crypto.Sha256, <<key:utf8>>))
== sha_api_key
{
True ->
case path {
["api", "room"] -> handle_room(room_handler, req, json)
["api", ..path] -> handle_admin_api(state_handler, req, path, json)
_ -> #(404, "bad api path", "Resource not found")
}
False -> {
#(401, "invalid api key", "unauthorized")
}
}
}
Error(_) -> {
#(401, "missing api key", "unauthorized")
}
}
|> serve.create_json_response
}
@ -38,9 +75,8 @@ fn handle_admin_api(
actor: Started(Subject(StateControl)),
req: Request,
path: List(String),
json: dynamic.Dynamic,
) {
use json <- wisp.require_json(req)
case list.key_find(req.headers, "x-api-key") {
Ok(key) -> {
case
@ -65,7 +101,6 @@ fn handle_admin_api(
#(401, "missing api key", "unauthorized")
}
}
|> serve.create_json_response
}
fn fetch_rooms(