Setup page
This commit is contained in:
parent
239d7dd94a
commit
89c19e08d9
2
migrations/20240320000004_add_admin_field.sql
Normal file
2
migrations/20240320000004_add_admin_field.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Add admin field to users table
|
||||
ALTER TABLE users ADD COLUMN admin BOOLEAN NOT NULL DEFAULT FALSE;
|
24
src/main.rs
24
src/main.rs
@ -10,7 +10,7 @@ mod user;
|
||||
|
||||
use rocket::fs::FileServer;
|
||||
use rocket::response::Redirect;
|
||||
use rocket_db_pools::{sqlx, Database};
|
||||
use rocket_db_pools::{sqlx, Database, Connection};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use user::AuthenticatedUser;
|
||||
|
||||
@ -33,8 +33,20 @@ fn index(_user: AuthenticatedUser) -> Template {
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
fn index_redirect() -> Redirect {
|
||||
async fn index_redirect(mut db: Connection<Db>) -> Redirect {
|
||||
// Check if any users exist
|
||||
let count = sqlx::query!("SELECT COUNT(*) as count FROM users")
|
||||
.fetch_one(&mut **db)
|
||||
.await
|
||||
.map_err(|_| Redirect::to(uri!(login)))
|
||||
.unwrap()
|
||||
.count;
|
||||
|
||||
if count == 0 {
|
||||
Redirect::to(uri!(user::setup_page))
|
||||
} else {
|
||||
Redirect::to(uri!(login))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/login")]
|
||||
@ -48,6 +60,12 @@ fn rocket() -> _ {
|
||||
|
||||
let db_url = format!("sqlite:{}", args.database);
|
||||
|
||||
// Check if database file exists, create it if it doesn't
|
||||
if !std::path::Path::new(&args.database).exists() {
|
||||
use std::fs::File;
|
||||
File::create(&args.database).expect("Failed to create database file");
|
||||
}
|
||||
|
||||
// Run migrations before starting the server
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
@ -75,6 +93,8 @@ fn rocket() -> _ {
|
||||
user::delete_user,
|
||||
user::login,
|
||||
user::logout,
|
||||
user::setup_page,
|
||||
user::setup,
|
||||
feeds::create_feed,
|
||||
feeds::get_feed,
|
||||
feeds::list_feeds,
|
||||
|
96
src/user.rs
96
src/user.rs
@ -1,6 +1,7 @@
|
||||
use rocket::http::{Cookie, CookieJar, Status};
|
||||
use rocket::serde::{json::Json, Deserialize, Serialize};
|
||||
use rocket_db_pools::Connection;
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::Db;
|
||||
@ -14,6 +15,7 @@ pub struct User {
|
||||
pub email: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
@ -30,6 +32,7 @@ impl User {
|
||||
email,
|
||||
display_name,
|
||||
created_at: chrono::Utc::now(),
|
||||
admin: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,6 +60,12 @@ pub struct LoginResponse {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct SetupError {
|
||||
error: String,
|
||||
}
|
||||
|
||||
#[post("/users", data = "<new_user>")]
|
||||
pub async fn create_user(
|
||||
mut db: Connection<Db>,
|
||||
@ -76,7 +85,7 @@ pub async fn create_user(
|
||||
);
|
||||
|
||||
let query = sqlx::query(
|
||||
"INSERT INTO users (id, username, password_hash, email, display_name, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
|
||||
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
|
||||
)
|
||||
.bind(user.id.to_string())
|
||||
.bind(user.username.as_str())
|
||||
@ -84,6 +93,7 @@ pub async fn create_user(
|
||||
.bind(user.email.as_ref())
|
||||
.bind(user.display_name.as_ref())
|
||||
.bind(user.created_at.to_rfc3339())
|
||||
.bind(false)
|
||||
.execute(&mut **db).await;
|
||||
|
||||
match query {
|
||||
@ -110,7 +120,8 @@ pub async fn get_users(mut db: Connection<Db>) -> Result<Json<Vec<User>>, Status
|
||||
password_hash,
|
||||
email,
|
||||
display_name,
|
||||
created_at as "created_at: chrono::DateTime<chrono::Utc>"
|
||||
created_at as "created_at: chrono::DateTime<chrono::Utc>",
|
||||
admin as "admin: bool"
|
||||
FROM users
|
||||
"#
|
||||
)
|
||||
@ -131,6 +142,7 @@ pub async fn get_users(mut db: Connection<Db>) -> Result<Json<Vec<User>>, Status
|
||||
email: row.email,
|
||||
display_name: row.display_name,
|
||||
created_at: row.created_at,
|
||||
admin: row.admin,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@ -247,3 +259,83 @@ impl<'r> rocket::request::FromRequest<'r> for AuthenticatedUser {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/setup")]
|
||||
pub async fn setup_page(mut db: Connection<Db>) -> Result<Template, Status> {
|
||||
// Check if any users exist
|
||||
let count = sqlx::query!("SELECT COUNT(*) as count FROM users")
|
||||
.fetch_one(&mut **db)
|
||||
.await
|
||||
.map_err(|_| Status::InternalServerError)?
|
||||
.count;
|
||||
|
||||
if count > 0 {
|
||||
// If users exist, redirect to login
|
||||
Err(Status::SeeOther)
|
||||
} else {
|
||||
// Show setup page
|
||||
Ok(Template::render("setup", context! {}))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/setup", data = "<new_user>")]
|
||||
pub async fn setup(mut db: Connection<Db>, new_user: Json<NewUser>) -> Result<Status, Json<SetupError>> {
|
||||
// Check if any users exist
|
||||
let count = sqlx::query!("SELECT COUNT(*) as count FROM users")
|
||||
.fetch_one(&mut **db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Database error: {}", e);
|
||||
Json(SetupError {
|
||||
error: "Internal server error".to_string(),
|
||||
})
|
||||
})?
|
||||
.count;
|
||||
|
||||
if count > 0 {
|
||||
return Err(Json(SetupError {
|
||||
error: "Setup has already been completed".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
let password_hash = bcrypt::hash(new_user.password.as_bytes(), bcrypt::DEFAULT_COST).map_err(|e| {
|
||||
eprintln!("Password hashing error: {}", e);
|
||||
Json(SetupError {
|
||||
error: "Failed to process password".to_string(),
|
||||
})
|
||||
})?;
|
||||
|
||||
// Create admin user
|
||||
let user_id = Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
)
|
||||
.bind(&user_id)
|
||||
.bind(&new_user.username)
|
||||
.bind(&password_hash)
|
||||
.bind(&new_user.email)
|
||||
.bind(&new_user.display_name)
|
||||
.bind(&now)
|
||||
.bind(true) // This is an admin user
|
||||
.execute(&mut **db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(Status::Created),
|
||||
Err(e) => {
|
||||
eprintln!("Database error: {}", e);
|
||||
match e {
|
||||
sqlx::Error::Database(db_err) if db_err.is_unique_violation() => Err(Json(SetupError {
|
||||
error: "Username already exists".to_string(),
|
||||
})),
|
||||
_ => Err(Json(SetupError {
|
||||
error: "Failed to create user".to_string(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -385,3 +385,59 @@ button:disabled {
|
||||
background-color: rgba(244, 63, 63, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.setup-container {
|
||||
max-width: 500px;
|
||||
margin: 50px auto;
|
||||
padding: 2rem;
|
||||
background-color: var(--sidebar-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.setup-container h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.setup-container p {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.setup-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.setup-form .form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.setup-form label {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setup-form input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.setup-form input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2);
|
||||
}
|
||||
|
||||
.setup-form button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
72
templates/setup.html.tera
Normal file
72
templates/setup.html.tera
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Initial Setup - RSS Reader</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="setup-container">
|
||||
<h1>Welcome to RSS Reader</h1>
|
||||
<p>Please create your initial admin user account.</p>
|
||||
|
||||
<form id="setupForm" class="setup-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email (optional)</label>
|
||||
<input type="email" id="email" name="email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="displayName">Display Name (optional)</label>
|
||||
<input type="text" id="displayName" name="displayName">
|
||||
</div>
|
||||
<div class="error-message" id="setupError" style="display: none;"></div>
|
||||
<button type="submit" class="btn-primary">Create Admin Account</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('setupForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const errorElement = document.getElementById('setupError');
|
||||
errorElement.style.display = 'none';
|
||||
|
||||
const formData = {
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value,
|
||||
email: document.getElementById('email').value || null,
|
||||
display_name: document.getElementById('displayName').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/setup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
const data = await response.json().catch(() => null);
|
||||
errorElement.textContent = data?.error || 'Failed to create admin account';
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
errorElement.textContent = 'An error occurred. Please try again.';
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user