extract secrets to env, use docker compose
This commit is contained in:
parent
14ba148284
commit
c8300f5978
16 changed files with 186 additions and 133 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@
|
||||||
**/dist
|
**/dist
|
||||||
/server/priv/static/client.js
|
/server/priv/static/client.js
|
||||||
/server/priv/static/index.html
|
/server/priv/static/index.html
|
||||||
|
/quizterm.env
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ COPY ./server/src /quizterm/server/src
|
||||||
COPY ./server/gleam.toml /quizterm/server/
|
COPY ./server/gleam.toml /quizterm/server/
|
||||||
COPY ./client/src /quizterm/client/src
|
COPY ./client/src /quizterm/client/src
|
||||||
COPY ./client/gleam.toml /quizterm/client/
|
COPY ./client/gleam.toml /quizterm/client/
|
||||||
|
COPY ./shared/src /quizterm/shared/src
|
||||||
|
COPY ./shared/gleam.toml /quizterm/shared/gleam.toml
|
||||||
|
|
||||||
RUN cd /quizterm/server && gleam deps download
|
RUN cd /quizterm/server && gleam deps download
|
||||||
|
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 lettosprey
|
Copyright (c) 2026 Steinar Eliassen
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|
|
||||||
48
README.md
48
README.md
|
|
@ -1,27 +1,5 @@
|
||||||
### Welcome to QUIZTerm
|
### Welcome to QUIZTerm
|
||||||
|
## Building and running
|
||||||
QUIZTerm is a simple online "quiz answering" game. It provides a way for contestants to provide answers to questions,
|
|
||||||
and reveal the answers for everyone at the same time.
|
|
||||||
|
|
||||||
Cards showing who are playing, their answer status (have they answered or not?), and when revealed, what their answer
|
|
||||||
was, will show up on everyones screen.
|
|
||||||
|
|
||||||
Not quite finished yet, it is at a point where it is "usable" enough.
|
|
||||||
|
|
||||||
Endpoints explained
|
|
||||||
|
|
||||||
| Endpoint | Usage |
|
|
||||||
|--------------------------|--------------------------------------------------------------|
|
|
||||||
| /room/<room_id> | Create room with given room_id (max 200 rooms) |
|
|
||||||
| /board/<room_id> | Join a game with the given room_id |
|
|
||||||
| /board/<room_id>/control | Join a game with the given room_id with more control options |
|
|
||||||
|
|
||||||
|
|
||||||
| Ingame example | Idle player |
|
|
||||||
|--------------------------|--------------------------|
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
### Building and running
|
|
||||||
|
|
||||||
Docker, or a compatible container manager, like podman, is required to build and run
|
Docker, or a compatible container manager, like podman, is required to build and run
|
||||||
quizterm. The alternative is to install Gleam and Erlang/BEAM and run it dockerless.
|
quizterm. The alternative is to install Gleam and Erlang/BEAM and run it dockerless.
|
||||||
|
|
@ -44,3 +22,27 @@ is the port exposed outside the container. The latter can be set to whatever
|
||||||
port you want to use.
|
port you want to use.
|
||||||
|
|
||||||
Open web browser and access http://localhost:4321
|
Open web browser and access http://localhost:4321
|
||||||
|
|
||||||
|
## The rest of this readme is currently outdated and will be updated shortly
|
||||||
|
|
||||||
|
QUIZTerm is a simple online "quiz answering" game. It provides a way for contestants to provide answers to questions,
|
||||||
|
and reveal the answers for everyone at the same time.
|
||||||
|
|
||||||
|
Cards showing who are playing, their answer status (have they answered or not?), and when revealed, what their answer
|
||||||
|
was, will show up on everyones screen.
|
||||||
|
|
||||||
|
Not quite finished yet, it is at a point where it is "usable" enough.
|
||||||
|
|
||||||
|
Endpoints explained
|
||||||
|
|
||||||
|
| Endpoint | Usage |
|
||||||
|
|--------------------------|--------------------------------------------------------------|
|
||||||
|
| /room/<room_id> | Create room with given room_id (max 200 rooms) |
|
||||||
|
| /board/<room_id> | Join a game with the given room_id |
|
||||||
|
| /board/<room_id>/control | Join a game with the given room_id with more control options |
|
||||||
|
|
||||||
|
|
||||||
|
| Ingame example | Idle player |
|
||||||
|
|--------------------------|--------------------------|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,6 @@ fn view_join_live(room: String, pin: String) -> Element(Msg) {
|
||||||
[server_component.route("/socket/live/" <> room <> "/" <> pin)],
|
[server_component.route("/socket/live/" <> room <> "/" <> pin)],
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
server_component.element(
|
|
||||||
[server_component.route("/socket/control/" <> room <> "/" <> pin)],
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
services:
|
||||||
|
quizterm:
|
||||||
|
image: quizterm
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "1234:1234"
|
||||||
|
env_file:
|
||||||
|
- quizterm.env
|
||||||
2
quizterm.env.example
Normal file
2
quizterm.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
SHAED_API_KEY=d6722bd5f433b342bd51901e51c1bfebb9f5da2462be2212ea03b9597c88dbed
|
||||||
|
QTERM_SECRET=AVerySecretSentence
|
||||||
|
|
@ -12,4 +12,5 @@ lustre = ">= 5.3.5 and < 6.0.0"
|
||||||
gleam_crypto = ">= 1.5.1 and < 2.0.0"
|
gleam_crypto = ">= 1.5.1 and < 2.0.0"
|
||||||
group_registry = ">= 1.0.0 and < 2.0.0"
|
group_registry = ">= 1.0.0 and < 2.0.0"
|
||||||
wisp = ">= 2.2.1 and < 3.0.0"
|
wisp = ">= 2.2.1 and < 3.0.0"
|
||||||
|
envoy = ">= 1.1.0 and < 2.0.0"
|
||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ packages = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[requirements]
|
[requirements]
|
||||||
|
envoy = { version = ">= 1.1.0 and < 2.0.0" }
|
||||||
gleam_crypto = { version = ">= 1.5.1 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_erlang = { version = ">= 1.0.0 and < 2.0.0" }
|
||||||
gleam_http = { version = ">= 3.7.2 and < 5.0.0" }
|
gleam_http = { version = ">= 3.7.2 and < 5.0.0" }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ type State {
|
||||||
State(
|
State(
|
||||||
question_number: Int,
|
question_number: Int,
|
||||||
// id, (name (question#, answer_attempt)
|
// 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.
|
// int in #pair: ping counted since response back.
|
||||||
name_answers: List(#(String, #(Int, AnswerStatus))),
|
name_answers: List(#(String, #(Int, AnswerStatus))),
|
||||||
hide_answers: Bool,
|
hide_answers: Bool,
|
||||||
|
|
@ -71,16 +71,16 @@ pub fn initialize(
|
||||||
GiveSingleAnswer(id, question, answer) -> {
|
GiveSingleAnswer(id, question, answer) -> {
|
||||||
State(
|
State(
|
||||||
..state,
|
..state,
|
||||||
slow_answers: case list.key_find(state.slow_answers, id) {
|
single_answers: case list.key_find(state.single_answers, id) {
|
||||||
Ok(value) -> {
|
Ok(value) -> {
|
||||||
let #(name, list) = value
|
let #(name, list) = value
|
||||||
list.key_set(state.slow_answers, id, #(
|
list.key_set(state.single_answers, id, #(
|
||||||
name,
|
name,
|
||||||
list.key_set(list, question, answer),
|
list.key_set(list, question, answer),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Error(_) -> {
|
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
|
// Switch from "Wait for next question" to "Answer next question" mode
|
||||||
AnswerQuiz -> answer_quiz(state, registry)
|
AnswerQuiz -> answer_quiz(state, registry)
|
||||||
message.FetchPlayers(subject:) -> {
|
message.FetchPlayers(subject:) -> {
|
||||||
fetch_players(state.slow_answers, subject)
|
fetch_players(state.single_answers, subject)
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
message.AddPlayer(name) ->
|
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()
|
|> actor.continue()
|
||||||
})
|
})
|
||||||
|
|
@ -241,7 +241,7 @@ fn combine_lists(state: State) {
|
||||||
}),
|
}),
|
||||||
// Second list require a bit more work Iterate over each payers answers,
|
// Second list require a bit more work Iterate over each payers answers,
|
||||||
// creating user objects where question number match current question number.
|
// 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
|
let #(_, #(name, answers)) = name_answers
|
||||||
list.filter_map(answers, fn(number_answer) {
|
list.filter_map(answers, fn(number_answer) {
|
||||||
let #(answer_number, answer) = number_answer
|
let #(answer_number, answer) = number_answer
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ pub fn serve(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve_slow(
|
pub fn serve_single(
|
||||||
request: Request(Connection),
|
request: Request(Connection),
|
||||||
component: lustre.App(
|
component: lustre.App(
|
||||||
#(List(#(String, String)), message.ClientsServer),
|
#(List(#(String, String)), message.ClientsServer),
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,14 @@ import web/components/control
|
||||||
import web/router
|
import web/router
|
||||||
import wisp
|
import wisp
|
||||||
import wisp/wisp_mist
|
import wisp/wisp_mist
|
||||||
|
import envoy
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
wisp.configure_logger()
|
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(state_handler) = statehandler.initialize()
|
||||||
let assert Ok(room_handler) = roomhandler.initialize(state_handler)
|
let assert Ok(room_handler) = roomhandler.initialize(state_handler)
|
||||||
|
|
||||||
|
|
@ -48,7 +52,7 @@ pub fn main() {
|
||||||
room_handler,
|
room_handler,
|
||||||
)
|
)
|
||||||
["socket", "single", id, pin] ->
|
["socket", "single", id, pin] ->
|
||||||
sockethandler.serve_slow(
|
sockethandler.serve_single(
|
||||||
req,
|
req,
|
||||||
answerlist.component(),
|
answerlist.component(),
|
||||||
id,
|
id,
|
||||||
|
|
@ -58,8 +62,8 @@ pub fn main() {
|
||||||
)
|
)
|
||||||
_ ->
|
_ ->
|
||||||
wisp_mist.handler(
|
wisp_mist.handler(
|
||||||
router.handle_request(room_handler, state_handler, _),
|
router.handle_request(sha_api_key, room_handler, state_handler, _),
|
||||||
"very_secret",
|
secret,
|
||||||
)(req)
|
)(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,14 +190,18 @@ fn view(model: Model) -> Element(Msg) {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
case model.state {
|
||||||
|
Answer(_) | WaitForQuiz(_) ->
|
||||||
|
element.fragment([
|
||||||
html.div([class("terminal-section")], case lobby {
|
html.div([class("terminal-section")], case lobby {
|
||||||
[] -> []
|
[] -> []
|
||||||
lobby -> {
|
lobby -> {
|
||||||
let answered =
|
let answered =
|
||||||
list.filter(lobby, fn(x) {
|
list.filter(lobby, fn(x) {
|
||||||
case x.answer {
|
case x.answer {
|
||||||
message.IDontKnow | message.HasAnswered | message.GivenAnswer(_) ->
|
message.IDontKnow
|
||||||
True
|
| message.HasAnswered
|
||||||
|
| message.GivenAnswer(_) -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -267,6 +271,13 @@ fn view(model: Model) -> Element(Msg) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
server_component.element(
|
||||||
|
[server_component.route("/socket/control/TMA/PINA")],
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
])
|
||||||
|
_ -> element.none()
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ pub fn view_players(
|
||||||
[click_cell_pair(Some("ENTER NEW PLAYER"), None, True, handler)],
|
[click_cell_pair(Some("ENTER NEW PLAYER"), None, True, handler)],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import gleam/bit_array
|
import gleam/bit_array
|
||||||
import gleam/crypto
|
import gleam/crypto
|
||||||
|
import gleam/dynamic
|
||||||
import gleam/dynamic/decode
|
import gleam/dynamic/decode
|
||||||
import gleam/erlang/process.{type Subject}
|
import gleam/erlang/process.{type Subject}
|
||||||
import gleam/http
|
import gleam/http
|
||||||
|
|
@ -11,6 +12,7 @@ import web/handlers/serve.{html_404}
|
||||||
import wisp.{type Request, type Response}
|
import wisp.{type Request, type Response}
|
||||||
|
|
||||||
pub fn handle_request(
|
pub fn handle_request(
|
||||||
|
sha_api_key: String,
|
||||||
room_handler: Started(Subject(RoomControl)),
|
room_handler: Started(Subject(RoomControl)),
|
||||||
state_handler: Started(Subject(StateControl)),
|
state_handler: Started(Subject(StateControl)),
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|
@ -18,19 +20,54 @@ pub fn handle_request(
|
||||||
use req <- middleware(req)
|
use req <- middleware(req)
|
||||||
case wisp.path_segments(req) {
|
case wisp.path_segments(req) {
|
||||||
[] | ["index.html"] -> serve.main_html(fetch_rooms(room_handler))
|
[] | ["index.html"] -> serve.main_html(fetch_rooms(room_handler))
|
||||||
["api", "room"] -> handle_room(room_handler, req)
|
["api", ..path] ->
|
||||||
["api", ..path] -> handle_admin_api(state_handler, req, path)
|
handle_api(sha_api_key, room_handler, state_handler, req, path)
|
||||||
|
|
||||||
_ -> html_404()
|
_ -> html_404()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_room(room_handler: Started(Subject(RoomControl)), req: Request) {
|
fn handle_room(
|
||||||
use json <- wisp.require_json(req)
|
room_handler: Started(Subject(RoomControl)),
|
||||||
|
req: Request,
|
||||||
|
json: dynamic.Dynamic,
|
||||||
|
) {
|
||||||
case req.method {
|
case req.method {
|
||||||
http.Post -> add_room(room_handler, json)
|
http.Post -> add_room(room_handler, json)
|
||||||
_ -> #(404, "bad api path", "Resource not found")
|
_ -> #(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
|
|> serve.create_json_response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,9 +75,8 @@ fn handle_admin_api(
|
||||||
actor: Started(Subject(StateControl)),
|
actor: Started(Subject(StateControl)),
|
||||||
req: Request,
|
req: Request,
|
||||||
path: List(String),
|
path: List(String),
|
||||||
|
json: dynamic.Dynamic,
|
||||||
) {
|
) {
|
||||||
use json <- wisp.require_json(req)
|
|
||||||
|
|
||||||
case list.key_find(req.headers, "x-api-key") {
|
case list.key_find(req.headers, "x-api-key") {
|
||||||
Ok(key) -> {
|
Ok(key) -> {
|
||||||
case
|
case
|
||||||
|
|
@ -65,7 +101,6 @@ fn handle_admin_api(
|
||||||
#(401, "missing api key", "unauthorized")
|
#(401, "missing api key", "unauthorized")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> serve.create_json_response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_rooms(
|
fn fetch_rooms(
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
name = "shared"
|
name = "shared"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
|
||||||
# your project to the Hex package manager.
|
|
||||||
#
|
|
||||||
# description = ""
|
|
||||||
# licences = ["Apache-2.0"]
|
|
||||||
# repository = { type = "github", user = "", repo = "" }
|
|
||||||
# links = [{ title = "Website", href = "" }]
|
|
||||||
#
|
|
||||||
# For a full reference of all the available options, you can have a look at
|
|
||||||
# https://gleam.run/writing-gleam/gleam-toml/.
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
|
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
|
||||||
gleam_json = ">= 3.1.0 and < 4.0.0"
|
gleam_json = ">= 3.1.0 and < 4.0.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue