Add user-generated invitation codes.
This commit is contained in:
parent
ddff618361
commit
b98c5055c5
7
server/src/app/invitation.rs
Normal file
7
server/src/app/invitation.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub type InviteToken = [u8; 12];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Invitation {
|
||||
pub token:InviteToken,
|
||||
pub parent:Option<u32>,
|
||||
}
|
@ -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())
|
||||
|
@ -1,2 +1,2 @@
|
||||
pub const VERSION :&str = env!("GIT_HASH");
|
||||
pub const REGISTER_CODE :&str = "mountain";
|
||||
//pub const REGISTER_CODE :&str = "mountain";
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -55,6 +55,12 @@ pub enum QRPacketData {
|
||||
QChallengeList,
|
||||
RChallengeList(PacketChallengeListResponse),
|
||||
|
||||
QInviteAcquire,
|
||||
RInviteAcquire(PacketInviteAcquireResponse),
|
||||
|
||||
QInviteList,
|
||||
RInviteList(PacketInviteListResponse),
|
||||
|
||||
TestResult(PacketTestResult),
|
||||
}
|
||||
|
||||
|
32
server/src/protocol/packet/invite_acquire.rs
Normal file
32
server/src/protocol/packet/invite_acquire.rs
Normal 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()
|
||||
}
|
||||
}
|
40
server/src/protocol/packet/invite_list.rs
Normal file
40
server/src/protocol/packet/invite_list.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
const DEBUG_PRINT :bool = false;
|
||||
const DEBUG_PRINT :bool = true;
|
||||
|
||||
pub struct Log {
|
||||
|
||||
|
@ -7,3 +7,5 @@ button.warn {
|
||||
background-color:#471414;
|
||||
color:#e0e0e0;
|
||||
}
|
||||
|
||||
span.monospace {font-family:monospace;}
|
||||
|
@ -56,6 +56,9 @@ const OpCode = {
|
||||
UserList :0x0100,
|
||||
UserInfo :0x0101,
|
||||
|
||||
InviteList :0x0180,
|
||||
InviteAcquire :0x0181,
|
||||
|
||||
AccountManage :0x1000,
|
||||
AccountCommit :0x1001,
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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;
|
||||
|
21
www/js/ui.js
21
www/js/ui.js
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user