Compare commits

...

4 commits

Author SHA1 Message Date
0b25e3fb87 feat: add user lookup to api
lets you use an access token and a uuid to look up users on the instance
2025-05-01 07:06:58 +02:00
83872ed7a6 feat: add a function to check access token
lets me reuse something that will happen often instead of having to write it manually in every file
2025-05-01 07:06:14 +02:00
3c976d666d fix: add NOT NULL to table keys 2025-05-01 07:05:31 +02:00
0b516a269d fix: remove unused import 2025-05-01 07:04:56 +02:00
5 changed files with 103 additions and 6 deletions

View file

@ -1,7 +1,7 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{error, post, web, Error, HttpResponse}; use actix_web::{error, post, web, Error, HttpResponse};
use argon2::{Argon2, PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use futures::StreamExt; use futures::StreamExt;

View file

@ -1,4 +1,8 @@
use actix_web::{Scope, web}; use std::{str::FromStr, time::{SystemTime, UNIX_EPOCH}};
use actix_web::{web, HttpResponse, Scope};
use sqlx::Postgres;
use uuid::Uuid;
mod register; mod register;
mod login; mod login;
@ -10,3 +14,28 @@ pub fn web() -> Scope {
.service(login::response) .service(login::response)
.service(refresh::res) .service(refresh::res)
} }
pub async fn check_access_token(access_token: String, pool: sqlx::Pool<Postgres>) -> Result<Uuid, HttpResponse> {
match sqlx::query_as("SELECT CAST(uuid as VARCHAR), created FROM access_tokens WHERE token = $1")
.bind(&access_token)
.fetch_one(&pool)
.await {
Ok(row) => {
let (uuid, created): (String, i64) = row;
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
let lifetime = current_time - created;
if lifetime > 3600 {
return Err(HttpResponse::Unauthorized().finish())
}
Ok(Uuid::from_str(&uuid).unwrap())
},
Err(error) => {
eprintln!("{}", error);
Err(HttpResponse::InternalServerError().finish())
}
}
}

View file

@ -2,9 +2,11 @@ use actix_web::{Scope, web};
mod stats; mod stats;
mod auth; mod auth;
mod user;
pub fn web() -> Scope { pub fn web() -> Scope {
web::scope("/v1") web::scope("/v1")
.service(stats::res) .service(stats::res)
.service(auth::web()) .service(auth::web())
.service(user::res)
} }

66
src/api/v1/user.rs Normal file
View file

@ -0,0 +1,66 @@
use actix_web::{error, post, web, Error, HttpResponse};
use serde::{Deserialize, Serialize};
use futures::StreamExt;
use crate::{api::v1::auth::check_access_token, Data};
#[derive(Deserialize)]
struct AuthenticationRequest {
access_token: String,
}
#[derive(Serialize)]
struct Response {
uuid: String,
username: String,
display_name: String,
}
const MAX_SIZE: usize = 262_144;
#[post("/user/{uuid}")]
pub async fn res(mut payload: web::Payload, path: web::Path<(String,)>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
}
let request = path.into_inner().0;
let authentication_request = serde_json::from_slice::<AuthenticationRequest>(&body)?;
let authorized = check_access_token(authentication_request.access_token, data.pool.clone()).await;
if authorized.is_err() {
return Ok(authorized.unwrap_err())
}
let uuid = authorized.unwrap();
if request == "me" {
let row = sqlx::query_as(&format!("SELECT username, display_name FROM users WHERE uuid = '{}'", uuid))
.fetch_one(&data.pool)
.await
.unwrap();
let (username, display_name): (String, Option<String>) = row;
return Ok(HttpResponse::Ok().json(Response { uuid: uuid.to_string(), username, display_name: display_name.unwrap_or_default() }))
} else {
println!("{}", request);
if let Ok(row) = sqlx::query_as(&format!("SELECT CAST(uuid as VARCHAR), username, display_name FROM users WHERE uuid = '{}'", request))
.fetch_one(&data.pool)
.await {
let (uuid, username, display_name): (String, String, Option<String>) = row;
return Ok(HttpResponse::Ok().json(Response { uuid, username, display_name: display_name.unwrap_or_default() }))
}
Ok(HttpResponse::NotFound().finish())
}
}

View file

@ -50,18 +50,18 @@ async fn main() -> Result<(), Error> {
email_verified boolean NOT NULL DEFAULT FALSE email_verified boolean NOT NULL DEFAULT FALSE
); );
CREATE TABLE IF NOT EXISTS instance_permissions ( CREATE TABLE IF NOT EXISTS instance_permissions (
uuid uuid REFERENCES users(uuid), uuid uuid NOT NULL REFERENCES users(uuid),
administrator boolean NOT NULL DEFAULT FALSE administrator boolean NOT NULL DEFAULT FALSE
); );
CREATE TABLE IF NOT EXISTS refresh_tokens ( CREATE TABLE IF NOT EXISTS refresh_tokens (
token varchar(64) PRIMARY KEY UNIQUE NOT NULL, token varchar(64) PRIMARY KEY UNIQUE NOT NULL,
uuid uuid REFERENCES users(uuid), uuid uuid NOT NULL REFERENCES users(uuid),
created int8 NOT NULL created int8 NOT NULL
); );
CREATE TABLE IF NOT EXISTS access_tokens ( CREATE TABLE IF NOT EXISTS access_tokens (
token varchar(32) PRIMARY KEY UNIQUE NOT NULL, token varchar(32) PRIMARY KEY UNIQUE NOT NULL,
refresh_token varchar(64) UNIQUE REFERENCES refresh_tokens(token), refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token),
uuid uuid REFERENCES users(uuid), uuid uuid NOT NULL REFERENCES users(uuid),
created int8 NOT NULL created int8 NOT NULL
) )
"#) "#)