feat(ui,backend): implemented login - UNTESTED!
This commit is contained in:
parent
6c855b24f7
commit
ba25f98d58
File diff suppressed because it is too large
Load Diff
|
@ -11,3 +11,8 @@ tokio = { version = "1.32", features = ["macros", "rt-multi-thread"] }
|
|||
tui = {version = "0.19"}
|
||||
tui-textarea = { version = "0.2", features = ["crossterm"] }
|
||||
crossterm = { version = "0.25" }
|
||||
reqwest = { version = "0.11.20", features = ["json"] }
|
||||
clap = { version = "4.4.2", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.183", features = ["derive"] }
|
||||
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
use anyhow::{Error, Ok, Result};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UserIdResponse {
|
||||
name: String,
|
||||
id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UserInfoResponse {
|
||||
id: i64,
|
||||
name: String,
|
||||
joined: i64,
|
||||
is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Api {
|
||||
token: String,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub async fn from_creds(
|
||||
base_url: &str,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<Result<Self>> {
|
||||
let token = {
|
||||
let body = json!({
|
||||
"username": username,
|
||||
"password": password,
|
||||
});
|
||||
let request = format!("{base_url}/account/authenticate");
|
||||
|
||||
let response = Client::new().post(request).json(&body).send().await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => (),
|
||||
StatusCode::BAD_REQUEST => return Ok(Err(Error::msg("400 - Bad Request"))),
|
||||
StatusCode::UNAUTHORIZED => return Ok(Err(Error::msg("Wrong password!"))),
|
||||
StatusCode::FORBIDDEN => {
|
||||
return Ok(Err(Error::msg("Blocked for security reasons!")))
|
||||
}
|
||||
StatusCode::NOT_FOUND => return Ok(Err(Error::msg("Unknown username!"))),
|
||||
StatusCode::FAILED_DEPENDENCY => {
|
||||
return Ok(Err(Error::msg("Account not verified!")))
|
||||
}
|
||||
_ => return Ok(Err(Error::msg("Unknown error"))),
|
||||
}
|
||||
|
||||
let response_body: AuthResponse = response.json().await?;
|
||||
response_body.token
|
||||
};
|
||||
|
||||
Api::from_token(base_url, token).await
|
||||
}
|
||||
|
||||
pub async fn from_token(base_url: &str, token: String) -> Result<Result<Self>> {
|
||||
let id = {
|
||||
let request = format!("{base_url}/account/id");
|
||||
|
||||
let response = Client::new()
|
||||
.post(request)
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => (),
|
||||
StatusCode::BAD_REQUEST => return Ok(Err(Error::msg("400 - Bad Request"))),
|
||||
StatusCode::UNAUTHORIZED => return Ok(Err(Error::msg("Invalid token!"))),
|
||||
StatusCode::FORBIDDEN => {
|
||||
return Ok(Err(Error::msg("Blocked for security reasons!")))
|
||||
}
|
||||
_ => return Ok(Err(Error::msg("Unknown error"))),
|
||||
}
|
||||
|
||||
let response_body: UserIdResponse = response.json().await?;
|
||||
response_body.id
|
||||
};
|
||||
|
||||
{
|
||||
let request = format!("{base_url}/user/info");
|
||||
|
||||
let response = Client::new()
|
||||
.get(request)
|
||||
.query(&[("id", id)])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => (),
|
||||
StatusCode::BAD_REQUEST => return Ok(Err(Error::msg("400 - Bad Request"))),
|
||||
StatusCode::FORBIDDEN => {
|
||||
return Ok(Err(Error::msg("Blocked for security reasons!")))
|
||||
}
|
||||
StatusCode::NOT_FOUND => return Ok(Err(Error::msg("User not found"))),
|
||||
_ => return Ok(Err(Error::msg("Unknown error"))),
|
||||
}
|
||||
|
||||
let response_body: UserInfoResponse = response.json().await?;
|
||||
|
||||
if !response_body.is_admin {
|
||||
return Ok(Err(Error::msg("You're not an admin!")));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Ok(Self {
|
||||
token,
|
||||
base_url: base_url.to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
23
src/main.rs
23
src/main.rs
|
@ -1,15 +1,32 @@
|
|||
mod ui;
|
||||
mod api;
|
||||
mod state;
|
||||
mod ui;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
|
||||
/// A tui for administrating nerdcult.net
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// The base URL of the API
|
||||
#[arg(short, long, default_value = "https://api.nerdcult.net")]
|
||||
base_url: Option<String>,
|
||||
|
||||
/// An access token for the API
|
||||
#[arg(short, long)]
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let stdout = ui::prepare()?;
|
||||
let base_url = args.base_url.unwrap();
|
||||
|
||||
let mut login = ui::login::UI::new(stdout)?;
|
||||
login.run().await?;
|
||||
let mut login = ui::login::UI::new(stdout, base_url)?;
|
||||
let api = login.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
use std::io::Stdout;
|
||||
use crate::api::Api;
|
||||
use anyhow::Result;
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
layout::{Constraint, Direction, Layout, Alignment},
|
||||
Terminal,
|
||||
};
|
||||
use tui_textarea::TextArea;
|
||||
use crossterm::{
|
||||
event::{Event, KeyEvent, KeyCode, DisableMouseCapture},
|
||||
event::{DisableMouseCapture, Event, KeyCode, KeyEvent},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||
};
|
||||
use std::io::Stdout;
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Terminal,
|
||||
};
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
trait TextAreaExt {
|
||||
fn disable(&mut self);
|
||||
|
@ -52,6 +53,8 @@ pub struct UI<'a> {
|
|||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
|
||||
position: InputPosition,
|
||||
base_url: String,
|
||||
status: String,
|
||||
|
||||
username: TextArea<'a>,
|
||||
password: TextArea<'a>,
|
||||
|
@ -73,7 +76,7 @@ impl Drop for UI<'_> {
|
|||
}
|
||||
|
||||
impl UI<'_> {
|
||||
pub fn new(stdout: Stdout) -> Result<Self> {
|
||||
pub fn new(stdout: Stdout, base_url: String) -> Result<Self> {
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let terminal = Terminal::new(backend)?;
|
||||
|
||||
|
@ -87,17 +90,20 @@ impl UI<'_> {
|
|||
username.enable();
|
||||
password.disable();
|
||||
|
||||
Ok(Self{
|
||||
Ok(Self {
|
||||
terminal,
|
||||
position: InputPosition::Username,
|
||||
base_url,
|
||||
status: "".to_string(),
|
||||
username,
|
||||
password,
|
||||
password_data
|
||||
password_data,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<String> {
|
||||
pub async fn run(&mut self) -> Result<Api> {
|
||||
loop {
|
||||
self.draw().await?;
|
||||
let event = crossterm::event::read()?;
|
||||
|
||||
match event {
|
||||
|
@ -114,15 +120,18 @@ impl UI<'_> {
|
|||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
}) => {
|
||||
match self.position {
|
||||
InputPosition::Ok => {
|
||||
let username = self.username.lines()[0].clone();
|
||||
let password = self.password_data.lines()[0].clone();
|
||||
}) => match self.position {
|
||||
InputPosition::Ok => {
|
||||
let username = self.username.lines()[0].clone();
|
||||
let password = self.password_data.lines()[0].clone();
|
||||
let api = Api::from_creds(&self.base_url, username, password).await?;
|
||||
match api {
|
||||
Ok(a) => return Ok(a),
|
||||
Err(e) => self.status = e.to_string(),
|
||||
}
|
||||
_ => self.cycle(),
|
||||
}
|
||||
}
|
||||
_ => self.cycle(),
|
||||
},
|
||||
input => match self.position {
|
||||
InputPosition::Username => {
|
||||
self.username.input(input.to_owned());
|
||||
|
@ -143,11 +152,9 @@ impl UI<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
}
|
||||
|
||||
self.draw().await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +181,7 @@ impl UI<'_> {
|
|||
async fn draw(&mut self) -> Result<()> {
|
||||
// define a 32 * 6 chunk in the middle of the screen
|
||||
let mut chunk = self.terminal.size()?;
|
||||
chunk.x = (chunk.width / 2) - 24;
|
||||
chunk.x = (chunk.width / 2) - 14;
|
||||
chunk.y = (chunk.height / 2) - 5;
|
||||
chunk.height = 9;
|
||||
chunk.width = 28;
|
||||
|
@ -185,6 +192,10 @@ impl UI<'_> {
|
|||
inner_chunk.height -= 1;
|
||||
inner_chunk.width -= 2;
|
||||
|
||||
let mut status_chunk = self.terminal.size()?;
|
||||
status_chunk.y = chunk.y - 1;
|
||||
status_chunk.height = 1;
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
|
@ -193,7 +204,7 @@ impl UI<'_> {
|
|||
Constraint::Length(3), // 2. Password:
|
||||
Constraint::Length(1), // 3. OK
|
||||
]
|
||||
.as_ref(),
|
||||
.as_ref(),
|
||||
)
|
||||
.split(inner_chunk);
|
||||
|
||||
|
@ -204,11 +215,15 @@ impl UI<'_> {
|
|||
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
|
||||
};
|
||||
|
||||
let content_status = Span::styled(self.status.clone(), Style::default().fg(Color::Red));
|
||||
|
||||
let block = Block::default().title("Login").borders(Borders::ALL);
|
||||
|
||||
let ok = Paragraph::new(content_ok).alignment(Alignment::Center);
|
||||
let status = Paragraph::new(content_status).alignment(Alignment::Center);
|
||||
|
||||
self.terminal.draw(|f| {
|
||||
f.render_widget(status, status_chunk);
|
||||
f.render_widget(block, chunk);
|
||||
f.render_widget(self.username.widget(), chunks[0]);
|
||||
f.render_widget(self.password.widget(), chunks[1]);
|
||||
|
@ -217,4 +232,4 @@ impl UI<'_> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue