add: simple api-key authentication

This commit is contained in:
minoplhy 2024-11-01 13:45:13 +07:00
parent 33d59ed633
commit 7ff86b3e02
Signed by: minoplhy
GPG Key ID: 41D406044E2434BF
7 changed files with 119 additions and 10 deletions

View File

@ -2,8 +2,12 @@
// SPDX-License-Identifier: MIT
use actix_session::Session;
use actix_web::{web, HttpRequest};
use std::{env, time::SystemTime};
use crate::database::get_api_key;
use crate::AppState;
// Validate a given password
pub fn validate(session: Session) -> bool {
// If there's no password provided, just return true
@ -22,6 +26,15 @@ pub fn validate(session: Session) -> bool {
}
}
// Validate x-api-header to match the key in database
pub fn apikey_validate(httprequest: HttpRequest, data: web::Data<AppState>) -> bool {
httprequest.headers()
.get("x-api-key")
.and_then(|h| h.to_str().ok())
.map(|key| key == get_api_key(&data.db))
.unwrap_or(false)
}
// Check a token cryptographically
fn check(token: Option<String>) -> bool {
if let Some(token_body) = token {

View File

@ -86,6 +86,23 @@ pub fn edit_link(shortlink: String, longlink: String, db: &Connection) -> bool {
.is_ok()
}
pub fn add_api_key(api_key: String, db: &Connection) -> bool {
db.execute(
"INSERT OR REPLACE INTO api (id, api_key) VALUES (0, ?1);",
[api_key]
)
.is_ok()
}
pub fn get_api_key(db: &Connection) -> String {
let query = db.query_row(
"SELECT api_key FROM api WHERE id = 0",
[],
|row| row.get::<_, String>(0),
);
query.expect("Failed to fetch API key")
}
// Open the DB, and create schema if missing
pub fn open_db(path: String) -> Connection {
let db = Connection::open(path).expect("Unable to open database!");
@ -100,5 +117,13 @@ pub fn open_db(path: String) -> Connection {
[],
)
.expect("Unable to initialize empty database.");
// create table if doesn't exist. For API key!
db.execute("CREATE TABLE IF NOT EXISTS api (
id INTEGER PRIMARY KEY CHECK (id = 0),
api_key TEXT NOT NULL
)",
[]
)
.expect("Unable to initialize empty database.");
db
}

View File

@ -66,6 +66,7 @@ async fn main() -> Result<()> {
.service(services::edit_link)
.service(services::delete_link)
.service(services::login)
.service(services::gen_api_key)
.service(services::logout)
.service(Files::new("/", "./resources/").index_file("index.html"))
.default_service(actix_web::web::get().to(services::error404))

View File

@ -4,11 +4,11 @@
use actix_files::NamedFile;
use actix_session::Session;
use actix_web::{
delete, get, http::StatusCode, post, put, web::{self, Redirect}, Either, HttpResponse, Responder
delete, get, http::StatusCode, post, put, web::{self, Redirect}, Either, HttpRequest, HttpResponse, Responder
};
use std::env;
use crate::auth;
use crate::auth::{self, apikey_validate};
use crate::database;
use crate::utils;
use crate::AppState;
@ -20,8 +20,13 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
// Add new links
#[post("/api/new")]
pub async fn add_link(req: String, data: web::Data<AppState>, session: Session) -> HttpResponse {
if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) {
pub async fn add_link(
req: String,
data: web::Data<AppState>,
session: Session,
httprequest: HttpRequest)
-> HttpResponse {
if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) || apikey_validate(httprequest, data.clone()) {
let out = utils::add_link(req, &data.db);
if out.0 {
HttpResponse::Created().body(out.1)
@ -35,8 +40,12 @@ pub async fn add_link(req: String, data: web::Data<AppState>, session: Session)
// Return all active links
#[get("/api/all")]
pub async fn getall(data: web::Data<AppState>, session: Session) -> HttpResponse {
if auth::validate(session) {
pub async fn getall(
data: web::Data<AppState>,
session: Session,
httprequest: HttpRequest
) -> HttpResponse {
if auth::validate(session) || apikey_validate(httprequest, data.clone()) {
HttpResponse::Ok().body(utils::getall(&data.db))
} else {
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
@ -114,6 +123,25 @@ pub async fn login(req: String, session: Session) -> HttpResponse {
HttpResponse::Ok().body("Correct password!")
}
// Create API Key, Will be disabled on public mode
#[post("/api/key")]
pub async fn gen_api_key(session: Session, httprequest: HttpRequest, data: web::Data<AppState>) -> HttpResponse {
if env::var("public_mode") == Ok(String::from("Enable")) {
return HttpResponse::Forbidden().body("Public mode is enabled!");
}
if auth::validate(session) || apikey_validate(httprequest, data.clone()) {
let key = utils::gen_api_key(&data.db);
if key.0 {
HttpResponse::Ok().body(key.1)
} else {
HttpResponse::Conflict().body("Generate Api Key Error!")
}
} else {
HttpResponse::Unauthorized().body("Not logged in!")
}
}
// Handle logout
#[delete("/api/logout")]
pub async fn logout(session: Session) -> HttpResponse {
@ -131,8 +159,9 @@ pub async fn edit_link(
shortlink: web::Path<String>,
data: web::Data<AppState>,
session: Session,
httprequest: HttpRequest,
) -> HttpResponse {
if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) {
if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) || apikey_validate(httprequest, data.clone()) {
let out = utils::edit_link(req, shortlink.to_string(), &data.db);
if out.0 {
HttpResponse::Created().body(out.1)
@ -150,8 +179,9 @@ pub async fn delete_link(
shortlink: web::Path<String>,
data: web::Data<AppState>,
session: Session,
httprequest: HttpRequest,
) -> HttpResponse {
if auth::validate(session) {
if auth::validate(session) || apikey_validate(httprequest, data.clone()) {
if utils::delete_link(shortlink.to_string(), &data.db) {
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
} else {

View File

@ -6,8 +6,9 @@ use rand::seq::SliceRandom;
use regex::Regex;
use rusqlite::Connection;
use serde::Deserialize;
use std::env;
use std::{env, iter};
use once_cell::sync::Lazy;
use rand::Rng;
use crate::database;
@ -144,6 +145,22 @@ pub fn delete_link(shortlink: String, db: &Connection) -> bool {
}
}
// Generate a simple API Key(totally not secured!)
pub fn gen_api_key(db: &Connection) -> (bool, String) {
let generated_key: String = generate_string(32);
(database::add_api_key(generated_key.clone(), db),
generated_key)
}
// Generate Random String
// From: https://stackoverflow.com/a/74953997
fn generate_string(len: usize) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::thread_rng();
let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
iter::repeat_with(one_char).take(len).collect()
}
// Generate a random link using either adjective-name pair (default) of a slug or a-z, 0-9
fn gen_link(style: String, len: usize) -> String {
#[rustfmt::skip]

View File

@ -69,11 +69,14 @@
<div name="links-div">
<a id="admin-button" href="javascript:getLogin()" hidden>login</a>
&nbsp;
<a id="api-key-button" href="javascript:fetchApiKey()" hidden>API Key</a>
&nbsp;
<a id="version-number" href="https://github.com/SinTan1729/chhoto-url" target="_blank" rel="noopener noreferrer"
hidden>Source Code</a>
<!-- The version number would be inserted here -->
</div>
<div id="api-key-container"></div>
<dialog id="login-dialog">
<form class="pure-form" name="login-form">
<p>Please enter password to access this website</p>

View File

@ -48,6 +48,7 @@ const refreshData = async () => {
console.log(errorMsg);
if (errorMsg == "Using public mode.") {
document.getElementById("admin-button").hidden = false;
document.getElementById("api-key-button").hidden = true; // Hide initially
loading_text = document.getElementById("loading-text");
loading_text.hidden = true;
showVersion();
@ -57,6 +58,7 @@ const refreshData = async () => {
} else {
let data = await res.json();
displayData(data);
document.getElementById("api-key-button").hidden = false; // Show API Key button when logged in
}
}
@ -68,6 +70,10 @@ const displayData = async (data) => {
admin_button.href = "javascript:logOut()";
admin_button.hidden = false;
apikey_button = document.getElementById("api-key-button");
apikey_button.innerText = "API Key"
apikey_button.hidden = false;
table_box = document.getElementById("table-box");
loading_text = document.getElementById("loading-text");
const table = document.getElementById("url-table");
@ -289,6 +295,20 @@ const submitLogin = () => {
})
}
const fetchApiKey = async () => {
const response = await fetch(prepSubdir("/api/key"), {
method: "POST"
});
if (response.ok) {
const apiKey = await response.text();
prompt("API Key:", apiKey);
} else {
const error_text = await response.text();
alert("Failed to fetch API Key: " + error_text);
}
}
const logOut = async () => {
let reply = await fetch(prepSubdir("/api/logout"), {method: "DELETE"}).then(res => res.text());
console.log(reply);