mirror of
https://github.com/minoplhy/chhoto-url.git
synced 2024-11-24 01:46:45 +00:00
add: simple api-key authentication
This commit is contained in:
parent
33d59ed633
commit
7ff86b3e02
@ -2,8 +2,12 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
|
use actix_web::{web, HttpRequest};
|
||||||
use std::{env, time::SystemTime};
|
use std::{env, time::SystemTime};
|
||||||
|
|
||||||
|
use crate::database::get_api_key;
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
// Validate a given password
|
// Validate a given password
|
||||||
pub fn validate(session: Session) -> bool {
|
pub fn validate(session: Session) -> bool {
|
||||||
// If there's no password provided, just return true
|
// 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
|
// Check a token cryptographically
|
||||||
fn check(token: Option<String>) -> bool {
|
fn check(token: Option<String>) -> bool {
|
||||||
if let Some(token_body) = token {
|
if let Some(token_body) = token {
|
||||||
|
@ -86,6 +86,23 @@ pub fn edit_link(shortlink: String, longlink: String, db: &Connection) -> bool {
|
|||||||
.is_ok()
|
.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
|
// Open the DB, and create schema if missing
|
||||||
pub fn open_db(path: String) -> Connection {
|
pub fn open_db(path: String) -> Connection {
|
||||||
let db = Connection::open(path).expect("Unable to open database!");
|
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.");
|
.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
|
db
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ async fn main() -> Result<()> {
|
|||||||
.service(services::edit_link)
|
.service(services::edit_link)
|
||||||
.service(services::delete_link)
|
.service(services::delete_link)
|
||||||
.service(services::login)
|
.service(services::login)
|
||||||
|
.service(services::gen_api_key)
|
||||||
.service(services::logout)
|
.service(services::logout)
|
||||||
.service(Files::new("/", "./resources/").index_file("index.html"))
|
.service(Files::new("/", "./resources/").index_file("index.html"))
|
||||||
.default_service(actix_web::web::get().to(services::error404))
|
.default_service(actix_web::web::get().to(services::error404))
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
use actix_web::{
|
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 std::env;
|
||||||
|
|
||||||
use crate::auth;
|
use crate::auth::{self, apikey_validate};
|
||||||
use crate::database;
|
use crate::database;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
@ -20,8 +20,13 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||||||
|
|
||||||
// Add new links
|
// Add new links
|
||||||
#[post("/api/new")]
|
#[post("/api/new")]
|
||||||
pub async fn add_link(req: String, data: web::Data<AppState>, session: Session) -> HttpResponse {
|
pub async fn add_link(
|
||||||
if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) {
|
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);
|
let out = utils::add_link(req, &data.db);
|
||||||
if out.0 {
|
if out.0 {
|
||||||
HttpResponse::Created().body(out.1)
|
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
|
// Return all active links
|
||||||
#[get("/api/all")]
|
#[get("/api/all")]
|
||||||
pub async fn getall(data: web::Data<AppState>, session: Session) -> HttpResponse {
|
pub async fn getall(
|
||||||
if auth::validate(session) {
|
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))
|
HttpResponse::Ok().body(utils::getall(&data.db))
|
||||||
} else {
|
} else {
|
||||||
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
|
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!")
|
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
|
// Handle logout
|
||||||
#[delete("/api/logout")]
|
#[delete("/api/logout")]
|
||||||
pub async fn logout(session: Session) -> HttpResponse {
|
pub async fn logout(session: Session) -> HttpResponse {
|
||||||
@ -131,8 +159,9 @@ pub async fn edit_link(
|
|||||||
shortlink: web::Path<String>,
|
shortlink: web::Path<String>,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
httprequest: HttpRequest,
|
||||||
) -> HttpResponse {
|
) -> 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);
|
let out = utils::edit_link(req, shortlink.to_string(), &data.db);
|
||||||
if out.0 {
|
if out.0 {
|
||||||
HttpResponse::Created().body(out.1)
|
HttpResponse::Created().body(out.1)
|
||||||
@ -150,8 +179,9 @@ pub async fn delete_link(
|
|||||||
shortlink: web::Path<String>,
|
shortlink: web::Path<String>,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
httprequest: HttpRequest,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
if auth::validate(session) {
|
if auth::validate(session) || apikey_validate(httprequest, data.clone()) {
|
||||||
if utils::delete_link(shortlink.to_string(), &data.db) {
|
if utils::delete_link(shortlink.to_string(), &data.db) {
|
||||||
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,8 +6,9 @@ use rand::seq::SliceRandom;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::env;
|
use std::{env, iter};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
use crate::database;
|
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
|
// 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 {
|
fn gen_link(style: String, len: usize) -> String {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
@ -69,11 +69,14 @@
|
|||||||
<div name="links-div">
|
<div name="links-div">
|
||||||
<a id="admin-button" href="javascript:getLogin()" hidden>login</a>
|
<a id="admin-button" href="javascript:getLogin()" hidden>login</a>
|
||||||
|
|
||||||
|
<a id="api-key-button" href="javascript:fetchApiKey()" hidden>API Key</a>
|
||||||
|
|
||||||
<a id="version-number" href="https://github.com/SinTan1729/chhoto-url" target="_blank" rel="noopener noreferrer"
|
<a id="version-number" href="https://github.com/SinTan1729/chhoto-url" target="_blank" rel="noopener noreferrer"
|
||||||
hidden>Source Code</a>
|
hidden>Source Code</a>
|
||||||
<!-- The version number would be inserted here -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="api-key-container"></div>
|
||||||
|
|
||||||
<dialog id="login-dialog">
|
<dialog id="login-dialog">
|
||||||
<form class="pure-form" name="login-form">
|
<form class="pure-form" name="login-form">
|
||||||
<p>Please enter password to access this website</p>
|
<p>Please enter password to access this website</p>
|
||||||
|
@ -48,6 +48,7 @@ const refreshData = async () => {
|
|||||||
console.log(errorMsg);
|
console.log(errorMsg);
|
||||||
if (errorMsg == "Using public mode.") {
|
if (errorMsg == "Using public mode.") {
|
||||||
document.getElementById("admin-button").hidden = false;
|
document.getElementById("admin-button").hidden = false;
|
||||||
|
document.getElementById("api-key-button").hidden = true; // Hide initially
|
||||||
loading_text = document.getElementById("loading-text");
|
loading_text = document.getElementById("loading-text");
|
||||||
loading_text.hidden = true;
|
loading_text.hidden = true;
|
||||||
showVersion();
|
showVersion();
|
||||||
@ -57,6 +58,7 @@ const refreshData = async () => {
|
|||||||
} else {
|
} else {
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
displayData(data);
|
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.href = "javascript:logOut()";
|
||||||
admin_button.hidden = false;
|
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");
|
table_box = document.getElementById("table-box");
|
||||||
loading_text = document.getElementById("loading-text");
|
loading_text = document.getElementById("loading-text");
|
||||||
const table = document.getElementById("url-table");
|
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 () => {
|
const logOut = async () => {
|
||||||
let reply = await fetch(prepSubdir("/api/logout"), {method: "DELETE"}).then(res => res.text());
|
let reply = await fetch(prepSubdir("/api/logout"), {method: "DELETE"}).then(res => res.text());
|
||||||
console.log(reply);
|
console.log(reply);
|
||||||
|
Loading…
Reference in New Issue
Block a user