Add user-generated invitation codes.

This commit is contained in:
yukirij 2024-10-15 00:51:54 -07:00
parent ddff618361
commit b98c5055c5
17 changed files with 349 additions and 68 deletions

View File

@ -0,0 +1,7 @@
pub type InviteToken = [u8; 12];
#[derive(Clone, Copy)]
pub struct Invitation {
pub token:InviteToken,
pub parent:Option<u32>,
}

View File

@ -16,6 +16,7 @@ pub mod connection; use connection::Connection;
pub mod user; use user::User;
pub mod authentication; use authentication::Authentication;
pub mod session; use session::{Session, SessionToken};
pub mod invitation; pub use invitation::{Invitation, InviteToken};
pub mod context;
pub struct App {
@ -32,6 +33,9 @@ pub struct App {
pub auths:Trie<Authentication>,
pub sessions:Trie<Session>,
pub invite_tokens:Trie<u32>,
pub invites:Pool<Invitation>,
pub contests:Vec<u32>,
pub session_time:Chain<SessionToken>,
@ -124,6 +128,9 @@ impl App {
auths:Trie::new(),
sessions,
invite_tokens:Trie::new(),
invites:Pool::new(),
contests,
session_time,
@ -230,6 +237,18 @@ impl App {
)).await.ok();
}
QRPacketData::RInviteList(response) => {
socket.send(Message::Binary(
encode_response(CODE_INVITE_LIST, response.encode())
)).await.ok();
}
QRPacketData::RInviteAcquire(response) => {
socket.send(Message::Binary(
encode_response(CODE_INVITE_ACQUIRE, response.encode())
)).await.ok();
}
QRPacketData::TestResult(response) => {
socket.send(Message::Binary(
encode_response(CODE_TEST_RESULT, response.encode())

View File

@ -1,2 +1,2 @@
pub const VERSION :&str = env!("GIT_HASH");
pub const REGISTER_CODE :&str = "mountain";
//pub const REGISTER_CODE :&str = "mountain";

View File

@ -142,7 +142,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
app.log.log("Request: Register");
let mut is_valid = true;
if request.code != crate::config::REGISTER_CODE.as_bytes() { response.status = STATUS_BAD_CODE; is_valid = false; }
let invite = app.invite_tokens.get(&request.code).cloned();
if is_valid && invite.is_none() { response.status = STATUS_BAD_CODE; is_valid = false; }
if is_valid && request.handle.len() == 0 && request.handle.chars().count() <= 24 { response.status = STATUS_BAD_HANDLE; is_valid = false; }
if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; }
@ -207,6 +208,14 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
conn.auth = Some(response.token);
}
// Remove invitation from store.
if let Some(invite) = invite {
if let Some(invite) = app.invites.get(invite as usize).cloned() {
app.invite_tokens.unset(&invite.token);
}
app.invites.remove(invite as usize).ok();
}
}
}
Err(_) => { app.log.log("error: failed to generate salt.") }
@ -1000,6 +1009,95 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
Some(QRPacket::new(qr.id, QRPacketData::RUserList(response)))
}
// InviteAcquire
QRPacketData::QInviteAcquire => {
use crate::app::{Invitation, InviteToken};
app.log.log("Request: Invite Acquire");
let mut response = PacketInviteAcquireResponse::new();
if let Some(user_id) = user_id {
// Count number of invites acquired by user.
let mut count_invites = 0;
let invites = app.invites.list();
for id in invites {
if let Some(invite) = app.invites.get(id) {
if let Some(parent) = invite.parent {
if parent == user_id {
count_invites += 1;
}
}
}
}
// Create new invite if below threshold.
const MAX_INVITES :usize = 10;
if count_invites < MAX_INVITES {
let mut token = InviteToken::default();
loop {
rng.fill(&mut token).ok();
for byte in &mut token {
*byte = *byte % 62;
*byte += if *byte < 10 {
'0' as u8
} else if *byte < 36 {
'a' as u8 - 10
} else {
'A' as u8 - 36
};
}
if app.invite_tokens.get(&token).is_none() { break }
}
let invite = Invitation {
token,
parent:Some(user_id),
};
let id = app.invites.add(invite);
app.invite_tokens.set(&token, id as u32);
response.status = STATUS_OK;
} else {
response.status = STATUS_ERROR;
}
} else {
response.status = STATUS_NOAUTH;
}
Some(QRPacket::new(qr.id, QRPacketData::RInviteAcquire(response)))
}
// InviteList
QRPacketData::QInviteList => {
app.log.log("Request: Invite List");
let mut response = PacketInviteListResponse::new();
if let Some(user_id) = user_id {
// Get user's invitations.
let invites = app.invites.list();
for id in invites {
if let Some(invite) = app.invites.get(id) {
if let Some(parent) = invite.parent {
if parent == user_id {
response.records.push(invite.clone());
}
}
}
}
response.status = STATUS_OK;
} else {
response.status = STATUS_NOAUTH;
}
Some(QRPacket::new(qr.id, QRPacketData::RInviteList(response)))
}
_ => { Some(QRPacket::new(0, QRPacketData::None)) }
} {
Some(response) => {

View File

@ -198,6 +198,20 @@ pub async fn handle_ws(ws:WebSocketStream<TokioIo<Upgraded>>, args:HttpServiceAr
).ok();
}
CODE_INVITE_LIST => {
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QInviteList)
).ok();
}
CODE_INVITE_ACQUIRE => {
args.bus.send(
bus_ds,
QRPacket::new(conn_id, QRPacketData::QInviteAcquire)
).ok();
}
_ => { }
}
true

View File

@ -1,7 +1,6 @@
/*
** Status Codes
*/
pub const STATUS_OK :u16 = 0x0000;
pub const STATUS_ERROR :u16 = 0x0001;
@ -21,7 +20,6 @@ pub const STATUS_NOT_IMPL :u16 = 0x00FF;
/*
** Operation Codes
*/
pub const CODE_HELLO :u16 = 0x0001;
pub const CODE_REGISTER :u16 = 0x0010;
@ -46,15 +44,16 @@ pub const CODE_CHALLENGE_LIST :u16 = 0x0062;
pub const CODE_USER_LIST :u16 = 0x0100;
pub const CODE_USER_INFO :u16 = 0x0101;
//pub const CODE_USER_AWAIT_GET :u16 = 0x0110;
//pub const CODE_USER_AWAIT_SET :u16 = 0x0111;
pub const CODE_INVITE_LIST :u16 = 0x0180;
pub const CODE_INVITE_ACQUIRE :u16 = 0x0181;
pub const CODE_TEST_RESULT :u16 = 0xFFFF;
/*
** Game Messages
*/
pub const GMSG_ERROR :u8 = 0x00;
pub const GMSG_PLAY_MOVE :u8 = 0x01;
pub const GMSG_PLAY_DROP :u8 = 0x02;

View File

@ -55,6 +55,12 @@ pub enum QRPacketData {
QChallengeList,
RChallengeList(PacketChallengeListResponse),
QInviteAcquire,
RInviteAcquire(PacketInviteAcquireResponse),
QInviteList,
RInviteList(PacketInviteListResponse),
TestResult(PacketTestResult),
}

View File

@ -0,0 +1,32 @@
use crate::{
util::pack::pack_u16,
app::InviteToken,
};
use super::Packet;
#[derive(Clone)]
pub struct PacketInviteAcquireResponse {
pub status:u16,
pub token:InviteToken,
}
impl PacketInviteAcquireResponse {
pub fn new() -> Self
{
Self {
status:0,
token:InviteToken::default(),
}
}
}
impl Packet for PacketInviteAcquireResponse {
type Data = Self;
fn encode(&self) -> Vec<u8>
{
[
pack_u16(self.status),
self.token.to_vec(),
].concat()
}
}

View File

@ -0,0 +1,40 @@
use crate::{
util::pack::pack_u16,
app::Invitation,
};
use super::Packet;
#[derive(Clone)]
pub struct PacketInviteListResponse {
pub status:u16,
pub records:Vec<Invitation>,
}
impl PacketInviteListResponse {
pub fn new() -> Self
{
Self {
status:0,
records:Vec::new(),
}
}
}
impl Packet for PacketInviteListResponse {
type Data = Self;
fn encode(&self) -> Vec<u8>
{
let mut output = [
pack_u16(self.status),
pack_u16(self.records.len() as u16),
].concat();
for record in &self.records {
output.append(&mut [
record.token.to_vec(),
].concat());
}
output
}
}

View File

@ -8,13 +8,11 @@ mod resume; pub use resume::*;
mod summary; pub use summary::*;
mod session_list; pub use session_list::*;
//mod session_create; pub use session_create::*;
mod session_view; pub use session_view::*;
mod session_retire; pub use session_retire::*;
mod game_state; pub use game_state::*;
mod game_message; pub use game_message::*;
//mod game_history; pub use game_history::*;
mod challenge; pub use challenge::*;
mod challenge_answer; pub use challenge_answer::*;
@ -23,6 +21,9 @@ mod challenge_list; pub use challenge_list::*;
mod user_list; pub use user_list::*;
mod user_info; pub use user_info::*;
mod invite_acquire; pub use invite_acquire::*;
mod invite_list; pub use invite_list::*;
mod test_result; pub use test_result::*;
mod prelude {

View File

@ -1,52 +0,0 @@
use crate::{
app::session::SessionToken,
util::pack::pack_u16,
};
use super::Packet;
#[derive(Clone)]
pub struct PacketSessionCreate {
}
impl PacketSessionCreate {
pub fn new() -> Self
{
Self { }
}
}
impl Packet for PacketSessionCreate {
type Data = Self;
fn decode(_data:&Vec<u8>, _index:&mut usize) -> Result<Self::Data, ()>
{
Ok(Self::new())
}
}
#[derive(Clone)]
pub struct PacketSessionCreateResponse {
pub status:u16,
pub token:SessionToken,
}
impl PacketSessionCreateResponse {
pub fn new() -> Self
{
Self {
status:0,
token:SessionToken::default(),
}
}
}
impl Packet for PacketSessionCreateResponse {
type Data = Self;
fn encode(&self) -> Vec<u8>
{
[
pack_u16(self.status),
self.token.to_vec(),
].concat()
}
}

View File

@ -1,4 +1,4 @@
const DEBUG_PRINT :bool = false;
const DEBUG_PRINT :bool = true;
pub struct Log {

View File

@ -7,3 +7,5 @@ button.warn {
background-color:#471414;
color:#e0e0e0;
}
span.monospace {font-family:monospace;}

View File

@ -56,6 +56,9 @@ const OpCode = {
UserList :0x0100,
UserInfo :0x0101,
InviteList :0x0180,
InviteAcquire :0x0181,
AccountManage :0x1000,
AccountCommit :0x1001,

View File

@ -43,13 +43,13 @@ const SCENES = {
let container = document.createElement("section");
let form = document.createElement("form");
let tb_handle = UI.textbox("handle", "");
let tb_handle = UI.textbox("handle", "user");
tb_handle.setAttribute("maxlength", 24);
form.appendChild(UI.table(null, [
[ UI.label(LANG("handle"), "handle"), tb_handle ],
[ UI.label(LANG("secret"), "secret"), UI.password("secret") ],
[ UI.label(LANG("invitation"), "code"), UI.password("code") ],
[ UI.label(LANG("invitation"), "code"), UI.textbox("code", "####-####-####") ],
]));
let button = UI.submit(LANG("register"));
@ -68,13 +68,16 @@ const SCENES = {
code.removeAttribute("class");
event.target.removeAttribute("class");
if(handle.value.length > 0 && handle.value.length < 24 && secret.value.length > 0 && code.value.length > 0) {
let invitation = code.value;
invitation = invitation.replace(/\-/g, '');
if(handle.value.length > 0 && handle.value.length < 24 && secret.value.length > 0 && invitation.length > 0) {
event.target.setAttribute("disabled", "");
let enc = new TextEncoder();
let enc_handle = enc.encode(handle.value);
let enc_secret = enc.encode(secret.value);
let enc_code = enc.encode(code.value);
let enc_code = enc.encode(invitation);
MESSAGE_COMPOSE([
PACK.u16(OpCode.Register),
@ -147,7 +150,7 @@ const SCENES = {
let container = document.createElement("section");
let form = document.createElement("form");
let tb_handle = UI.textbox("handle", "");
let tb_handle = UI.textbox("handle", "user");
tb_handle.setAttribute("maxlength", 24);
form.appendChild(UI.table(null, [
@ -677,7 +680,13 @@ const SCENES = {
UI.mainmenu_account(null, "invitations");
// Left Buttons
let buttons_left = [ ];
let buttons_left = [
UI.button("Acquire", () => {
MESSAGE_COMPOSE([
PACK.u16(OpCode.InviteAcquire),
])
}),
];
// Right Buttons
let buttons_right = [ ];
@ -685,10 +694,36 @@ const SCENES = {
UI.mainnav(buttons_left, buttons_right);
// Main Content
let table = document.createElement("table");
table.setAttribute("id", "content");
table.setAttribute("class", "list session");
UI.maincontent(table);
MESSAGE_COMPOSE([
PACK.u16(OpCode.InviteList),
]);
history.pushState(null, "Dzura - About", "/u/" + CONTEXT.Auth.handle);
return true;
}
message(code, data) {
switch(code) {
case OpCode.InviteAcquire: {
MESSAGE_COMPOSE([
PACK.u16(OpCode.InviteList),
]);
} break;
case OpCode.InviteList: {
let table = document.getElementById("content");
UI.clear(table);
if(data !== null) {
table.appendChild(UI.invite_table(data.records));
}
} break;
}
}
},
GameLoad:class{

View File

@ -490,6 +490,62 @@ function MESSAGE(event) {
}
} break;
case OpCode.InviteList: {
console.log("RECV InviteList");
data = {
status:0,
records:[ ],
};
// Status
result = UNPACK.u16(bytes, index);
index = result.index;
data.status = result.data;
// Records
result = UNPACK.u16(bytes, index);
index = result.index;
let length = result.data;
for(let i = 0; i < length; ++i) {
let token = "";
for(let c = 0; c < 4; ++c) {
token += String.fromCharCode(bytes[index++]);
} token += "-";
for(let c = 0; c < 4; ++c) {
token += String.fromCharCode(bytes[index++]);
} token += "-";
for(let c = 0; c < 4; ++c) {
token += String.fromCharCode(bytes[index++]);
}
data.records.push(token);
}
} break;
case OpCode.InviteAcquire: {
console.log("RECV InviteAcquire");
data = {
status:0,
token:"",
};
// Status
result = UNPACK.u16(bytes, index);
index = result.index;
data.status = result.data;
// Token
for(let c = 0; c < 12; ++c) {
data.token += String.fromCharCode(bytes[index++]);
}
} break;
case OpCode.TestResult: {
result = UNPACK.u8(bytes, index);
index = result.index;

View File

@ -55,6 +55,7 @@ const UI = {
password(id) {
let input = document.createElement("input");
input.setAttribute("type", "password");
input.setAttribute("placeholder", "••••••••••••");
if(id !== null) { input.setAttribute("id", id); }
return input;
},
@ -411,6 +412,26 @@ const UI = {
return tbody;
},
invite_table(records) {
let rows = [ ];
for(let r = 0; r < records.length; ++r) {
let record = records[r];
rows.push([
UI.span([UI.text(record)], "monospace"),
UI.text(""),
]);
}
let tbody = UI.table_content(
[ LANG("invitation"), "" ],
rows,
);
return tbody;
},
page_indicator(first, last, total) {
let ind = document.getElementById("indicator-page");
UI.clear(ind);