8 changed files with 228 additions and 6 deletions
@ -0,0 +1,90 @@ |
|||||
|
use rocket_contrib::json::Json; |
||||
|
use serde_json::Value; |
||||
|
|
||||
|
use crate::db::models::*; |
||||
|
use crate::db::DbConn; |
||||
|
|
||||
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase}; |
||||
|
|
||||
|
use rocket::{Route, Outcome}; |
||||
|
use rocket::request::{self, Request, FromRequest}; |
||||
|
|
||||
|
pub fn routes() -> Vec<Route> { |
||||
|
routes![ |
||||
|
get_users, |
||||
|
invite_user, |
||||
|
delete_user, |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
#[derive(Deserialize, Debug)] |
||||
|
#[allow(non_snake_case)] |
||||
|
struct InviteData { |
||||
|
Email: String, |
||||
|
} |
||||
|
|
||||
|
#[get("/users")] |
||||
|
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult { |
||||
|
let users = User::get_all(&conn); |
||||
|
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect(); |
||||
|
|
||||
|
Ok(Json(Value::Array(users_json))) |
||||
|
} |
||||
|
|
||||
|
#[post("/users", data="<data>")] |
||||
|
fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) -> EmptyResult { |
||||
|
let data: InviteData = data.into_inner().data; |
||||
|
|
||||
|
if User::find_by_mail(&data.Email, &conn).is_some() { |
||||
|
err!("User already exists") |
||||
|
} |
||||
|
|
||||
|
err!("Unimplemented") |
||||
|
} |
||||
|
|
||||
|
#[delete("/users/<uuid>")] |
||||
|
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { |
||||
|
let _user = match User::find_by_uuid(&uuid, &conn) { |
||||
|
Some(user) => user, |
||||
|
None => err!("User doesn't exist") |
||||
|
}; |
||||
|
|
||||
|
// TODO: Enable this once we have a more secure auth method
|
||||
|
err!("Unimplemented") |
||||
|
/* |
||||
|
match user.delete(&conn) { |
||||
|
Ok(_) => Ok(()), |
||||
|
Err(e) => err!("Error deleting user", e) |
||||
|
} |
||||
|
*/ |
||||
|
} |
||||
|
|
||||
|
|
||||
|
pub struct AdminToken {} |
||||
|
|
||||
|
impl<'a, 'r> FromRequest<'a, 'r> for AdminToken { |
||||
|
type Error = &'static str; |
||||
|
|
||||
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { |
||||
|
// Get access_token
|
||||
|
let access_token: &str = match request.headers().get_one("Authorization") { |
||||
|
Some(a) => match a.rsplit("Bearer ").next() { |
||||
|
Some(split) => split, |
||||
|
None => err_handler!("No access token provided"), |
||||
|
}, |
||||
|
None => err_handler!("No access token provided"), |
||||
|
}; |
||||
|
|
||||
|
// TODO: What authentication to use?
|
||||
|
// Option 1: Make it a config option
|
||||
|
// Option 2: Generate random token, and
|
||||
|
// Option 2a: Send it to admin email, like upstream
|
||||
|
// Option 2b: Print in console or save to data dir, so admin can check
|
||||
|
|
||||
|
if access_token != "token123" { |
||||
|
err_handler!("Invalid admin token") |
||||
|
} |
||||
|
|
||||
|
Outcome::Success(AdminToken {}) |
||||
|
} |
||||
|
} |
@ -0,0 +1,127 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
|
||||
|
<head> |
||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
|
<meta name="description" content=""> |
||||
|
<meta name="author" content=""> |
||||
|
<title>Bitwarden_rs Admin Panel</title> |
||||
|
|
||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css" |
||||
|
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous" /> |
||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" |
||||
|
crossorigin="anonymous"></script> |
||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.10.0/js/md5.js" integrity="sha256-tCQ/BldMlN2vWe5gAiNoNb5svoOgVUhlUgv7UjONKKQ=" |
||||
|
crossorigin="anonymous"></script> |
||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/identicon.js/2.3.3/identicon.min.js" integrity="sha256-nYoL3nK/HA1e1pJvLwNPnpKuKG9q89VFX862r5aohmA=" |
||||
|
crossorigin="anonymous"></script> |
||||
|
|
||||
|
<style> |
||||
|
body { padding-top: 70px; } |
||||
|
img { width: 48px; height: 48px; } |
||||
|
#logo { width: 48px; height: 48px; } |
||||
|
</style> |
||||
|
|
||||
|
<script> |
||||
|
let key = null; |
||||
|
|
||||
|
function getIdenticon(email) { |
||||
|
const data = new Identicon(md5(email), { |
||||
|
size: 48, |
||||
|
format: 'svg' |
||||
|
}).toString(); |
||||
|
|
||||
|
return "data:image/svg+xml;base64," + data; |
||||
|
} |
||||
|
|
||||
|
function loadUsers() { |
||||
|
$("#users-list").empty(); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: "GET", |
||||
|
url: "/admin/users", |
||||
|
headers: { "Authorization": "Bearer " + key } |
||||
|
}).done(function (data) { |
||||
|
for (i in data) { |
||||
|
let user = data[i]; |
||||
|
let row = $("#tmp-user-row").clone(); |
||||
|
|
||||
|
row.attr("id", "user-row:" + user.Id); |
||||
|
row.find(".tmp-user-name").text(user.Name); |
||||
|
row.find(".tmp-user-mail").text(user.Email); |
||||
|
row.find(".tmp-user-icon").attr("src", getIdenticon(user.Email)) |
||||
|
|
||||
|
row.find(".tmp-user-del").on("click", function (e) { |
||||
|
alert("Not Implemented: Deleting UUID " + user.Id); |
||||
|
}); |
||||
|
|
||||
|
row.appendTo("#users-list"); |
||||
|
row.removeClass('d-none'); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
$(window).on('load', function () { |
||||
|
key = new URLSearchParams(window.location.search).get('key'); |
||||
|
if (key) { |
||||
|
$("#no-key-form").addClass('d-none'); |
||||
|
loadUsers(); |
||||
|
} else { |
||||
|
$("#users-block").addClass('d-none'); |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
</head> |
||||
|
|
||||
|
<body class="bg-light"> |
||||
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top shadow"> |
||||
|
<a class="navbar-brand" href="#">Bitwarden_rs Admin</a> |
||||
|
<div class="navbar-collapse"> |
||||
|
<ul class="navbar-nav"> |
||||
|
<li class="nav-item active"> |
||||
|
<a class="nav-link" href="#">Dashboard</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" href="#">Other</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</nav> |
||||
|
<main class="container"> |
||||
|
<div id="no-key-form" class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow"> |
||||
|
<div> |
||||
|
<h6 class="mb-0 text-white">Authentication key needed to continue</h6> |
||||
|
<small>Please provide it below:</small> |
||||
|
|
||||
|
<form class="form-inline" method="get"> |
||||
|
<input type="text" class="form-control mr-2" id="key" name="key" placeholder="Enter admin key"> |
||||
|
<button type="submit" class="btn btn-primary">Submit</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div id="users-block" class="my-3 p-3 bg-white rounded shadow"> |
||||
|
<h6 class="border-bottom pb-2 mb-0">Registered Users</h6> |
||||
|
|
||||
|
<div id="users-list"></div> |
||||
|
|
||||
|
<small class="d-block text-right mt-3"> |
||||
|
<a href="#" onclick="loadUsers();">Reload users</a> |
||||
|
</small> |
||||
|
</div> |
||||
|
|
||||
|
<div id="tmp-user-row" class="d-none media pt-3"> |
||||
|
<img src="#" alt="identicon" class="mr-2 rounded tmp-user-icon"> |
||||
|
<div class="media-body pb-3 mb-0 small border-bottom"> |
||||
|
<div class="d-flex justify-content-between"> |
||||
|
<strong class="tmp-user-name">Full Name</strong> |
||||
|
<a class="tmp-user-del mr-3" href="#">Delete User</a> |
||||
|
</div> |
||||
|
<span class="d-block tmp-user-mail">Email</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
Loading…
Reference in new issue