From ac13e77dc4cab4608d9d3021bd35536f2bb7abe7 Mon Sep 17 00:00:00 2001 From: WaveringAna Date: Mon, 27 Jan 2025 22:32:49 -0500 Subject: [PATCH] add admin setup token i love admin setup token --- ...0ad5183b647eaaff90decbce15e10d83c7538.json | 20 +++++++++ Cargo.lock | 1 + Cargo.toml | 1 + admin-setup-token.txt | 1 + docker-compose.yml | 6 +-- frontend/src/api/client.ts | 3 +- frontend/src/components/AuthForms.tsx | 24 +++++++++-- frontend/src/components/LinkList.tsx | 6 ++- frontend/src/context/AuthContext.tsx | 6 +-- frontend/src/types/api.ts | 6 +++ frontend/vite.config.ts | 12 ++---- src/handlers.rs | 21 ++++++++++ src/lib.rs | 41 +++++++++++++++++++ src/main.rs | 8 +++- src/models.rs | 1 + 15 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 .sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json create mode 100644 admin-setup-token.txt diff --git a/.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json b/.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json new file mode 100644 index 0000000..f193747 --- /dev/null +++ b/.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) as count FROM users", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538" +} diff --git a/Cargo.lock b/Cargo.lock index de84c46..b3743bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2068,6 +2068,7 @@ dependencies = [ "dotenv", "jsonwebtoken", "lazy_static", + "rand", "regex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e2c2be6..09e3ac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ chrono = { version = "0.4", features = ["serde"] } regex = "1.10" lazy_static = "1.4" argon2 = "0.5.3" +rand = { version = "0.8", features = ["std"] } \ No newline at end of file diff --git a/admin-setup-token.txt b/admin-setup-token.txt new file mode 100644 index 0000000..0c800c0 --- /dev/null +++ b/admin-setup-token.txt @@ -0,0 +1 @@ +fqfO6awRz3mkc2Kxunkp1uTQcXaSfGD9 diff --git a/docker-compose.yml b/docker-compose.yml index e159faf..e5f57b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,14 +24,14 @@ services: context: . dockerfile: Dockerfile args: - - API_URL=${API_URL:-http://localhost:8080} + - API_URL=${API_URL:-http://localhost:3000} container_name: shortener-app ports: - - "8080:8080" + - "3000:3000" environment: - DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener - SERVER_HOST=0.0.0.0 - - SERVER_PORT=8080 + - SERVER_PORT=3000 depends_on: db: condition: service_healthy diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 32a8ca9..6f2d224 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -24,10 +24,11 @@ export const login = async (email: string, password: string) => { return response.data; }; -export const register = async (email: string, password: string) => { +export const register = async (email: string, password: string, adminToken: string) => { const response = await api.post('/auth/register', { email, password, + admin_token: adminToken, }); return response.data; }; diff --git a/frontend/src/components/AuthForms.tsx b/frontend/src/components/AuthForms.tsx index 7b9032a..d655dfd 100644 --- a/frontend/src/components/AuthForms.tsx +++ b/frontend/src/components/AuthForms.tsx @@ -20,6 +20,7 @@ import { useToast } from '@/hooks/use-toast' const formSchema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(6, 'Password must be at least 6 characters long'), + adminToken: z.string(), }) type FormValues = z.infer @@ -34,6 +35,7 @@ export function AuthForms() { defaultValues: { email: '', password: '', + adminToken: '', }, }) @@ -42,14 +44,14 @@ export function AuthForms() { if (activeTab === 'login') { await login(values.email, values.password) } else { - await register(values.email, values.password) + await register(values.email, values.password, values.adminToken) } form.reset() } catch (err: any) { toast({ variant: 'destructive', title: 'Error', - description: err.response?.data?.error || 'An error occurred', + description: err.response?.data || 'An error occurred', }) } } @@ -93,6 +95,22 @@ export function AuthForms() { )} /> + {activeTab === 'register' && ( + ( + + Admin Setup Token + + + + + + )} + /> + )} + @@ -102,4 +120,4 @@ export function AuthForms() { ) -} \ No newline at end of file +} diff --git a/frontend/src/components/LinkList.tsx b/frontend/src/components/LinkList.tsx index d953a1c..40db5c1 100644 --- a/frontend/src/components/LinkList.tsx +++ b/frontend/src/components/LinkList.tsx @@ -81,9 +81,11 @@ export function LinkList({ refresh = 0 }: LinkListProps) { } const handleCopy = (shortCode: string) => { - navigator.clipboard.writeText(`http://localhost:8080/${shortCode}`) + // Use import.meta.env.VITE_BASE_URL or fall back to window.location.origin + const baseUrl = import.meta.env.VITE_API_URL || window.location.origin + navigator.clipboard.writeText(`${baseUrl}/${shortCode}`) toast({ - description: "Link copied to clipboard", + description: "Link copied to clipboard", }) } diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 19cabdc..a4f25db 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -5,7 +5,7 @@ import * as api from '../api/client'; interface AuthContextType { user: User | null; login: (email: string, password: string) => Promise; - register: (email: string, password: string) => Promise; + register: (email: string, password: string, adminToken: string) => Promise; logout: () => void; isLoading: boolean; } @@ -33,8 +33,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setUser(user); }; - const register = async (email: string, password: string) => { - const response = await api.register(email, password); + const register = async (email: string, password: string, adminToken: string) => { + const response = await api.register(email, password, adminToken); const { token, user } = response; localStorage.setItem('token', token); localStorage.setItem('user', JSON.stringify(user)); diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index f3c1c87..2d30756 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -35,3 +35,9 @@ export interface SourceStats { source: string; count: number; } + +export interface RegisterRequest { + email: string; + password: string; + admin_token: string; +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a37f6f6..4b1c051 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,15 +3,12 @@ import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import path from "path" -export default defineConfig({ - plugins: [ - react(), - tailwindcss(), - ], +export default defineConfig(() => ({ + plugins: [react(), tailwindcss()], server: { proxy: { '/api': { - target: 'http://localhost:8080', + target: process.env.VITE_API_URL || 'http://localhost:8080', changeOrigin: true, }, }, @@ -21,5 +18,4 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, -}) - +})) diff --git a/src/handlers.rs b/src/handlers.rs index c03fa0e..db6edff 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -189,6 +189,27 @@ pub async fn register( state: web::Data, payload: web::Json, ) -> Result { + // Check if any users exist + let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users") + .fetch_one(&state.db) + .await? + .count + .unwrap_or(0); + + // If users exist, registration is closed - no exceptions + if user_count > 0 { + return Err(AppError::Auth("Registration is closed".to_string())); + } + + // Verify admin token for first user + match (&state.admin_token, &payload.admin_token) { + (Some(stored_token), Some(provided_token)) if stored_token == provided_token => { + // Token matches, proceed with registration + } + _ => return Err(AppError::Auth("Invalid admin setup token".to_string())), + } + + // Check if email already exists let exists = sqlx::query!("SELECT id FROM users WHERE email = $1", payload.email) .fetch_optional(&state.db) .await?; diff --git a/src/lib.rs b/src/lib.rs index 38bc5b3..a169cb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,8 @@ +use rand::Rng; use sqlx::PgPool; +use std::fs::File; +use std::io::Write; +use tracing::info; pub mod auth; pub mod error; @@ -8,4 +12,41 @@ pub mod models; #[derive(Clone)] pub struct AppState { pub db: PgPool, + pub admin_token: Option, +} + +pub async fn check_and_generate_admin_token(pool: &sqlx::PgPool) -> anyhow::Result> { + // Check if any users exist + let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users") + .fetch_one(pool) + .await? + .count + .unwrap_or(0); + + if user_count == 0 { + // Generate a random token using simple characters + let token: String = (0..32) + .map(|_| { + let idx = rand::thread_rng().gen_range(0..62); + match idx { + 0..=9 => (b'0' + idx as u8) as char, + 10..=35 => (b'a' + (idx - 10) as u8) as char, + _ => (b'A' + (idx - 36) as u8) as char, + } + }) + .collect(); + + // Save token to file + let mut file = File::create("admin-setup-token.txt")?; + writeln!(file, "{}", token)?; + + info!("No users found - generated admin setup token"); + info!("Token has been saved to admin-setup-token.txt"); + info!("Use this token to create the admin user"); + info!("Admin setup token: {}", token); + + Ok(Some(token)) + } else { + Ok(None) + } } diff --git a/src/main.rs b/src/main.rs index f02382a..6b613d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use actix_cors::Cors; use actix_files as fs; use actix_web::{web, App, HttpServer}; use anyhow::Result; +use simplelink::check_and_generate_admin_token; use simplelink::{handlers, AppState}; use sqlx::postgres::PgPoolOptions; use tracing::info; @@ -27,7 +28,12 @@ async fn main() -> Result<()> { // Run database migrations sqlx::migrate!("./migrations").run(&pool).await?; - let state = AppState { db: pool }; + let admin_token = check_and_generate_admin_token(&pool).await?; + + let state = AppState { + db: pool, + admin_token, + }; let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string()); diff --git a/src/models.rs b/src/models.rs index a10c136..05f60b6 100644 --- a/src/models.rs +++ b/src/models.rs @@ -49,6 +49,7 @@ pub struct LoginRequest { pub struct RegisterRequest { pub email: String, pub password: String, + pub admin_token: Option, } #[derive(Serialize)]