add admin setup token i love admin setup token

This commit is contained in:
WaveringAna 2025-01-27 22:32:49 -05:00
parent 660da70666
commit ac13e77dc4
15 changed files with 136 additions and 21 deletions

View file

@ -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"
}

1
Cargo.lock generated
View file

@ -2068,6 +2068,7 @@ dependencies = [
"dotenv",
"jsonwebtoken",
"lazy_static",
"rand",
"regex",
"serde",
"serde_json",

View file

@ -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"] }

1
admin-setup-token.txt Normal file
View file

@ -0,0 +1 @@
fqfO6awRz3mkc2Kxunkp1uTQcXaSfGD9

View file

@ -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

View file

@ -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<AuthResponse>('/auth/register', {
email,
password,
admin_token: adminToken,
});
return response.data;
};

View file

@ -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<typeof formSchema>
@ -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' && (
<FormField
control={form.control}
name="adminToken"
render={({ field }) => (
<FormItem>
<FormLabel>Admin Setup Token</FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button type="submit" className="w-full">
{activeTab === 'login' ? 'Sign in' : 'Create account'}
</Button>

View file

@ -81,7 +81,9 @@ 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",
})

View file

@ -5,7 +5,7 @@ import * as api from '../api/client';
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, adminToken: string) => Promise<void>;
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));

View file

@ -35,3 +35,9 @@ export interface SourceStats {
source: string;
count: number;
}
export interface RegisterRequest {
email: string;
password: string;
admin_token: string;
}

View file

@ -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"),
},
},
})
}))

View file

@ -189,6 +189,27 @@ pub async fn register(
state: web::Data<AppState>,
payload: web::Json<RegisterRequest>,
) -> Result<impl Responder, AppError> {
// 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?;

View file

@ -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<String>,
}
pub async fn check_and_generate_admin_token(pool: &sqlx::PgPool) -> anyhow::Result<Option<String>> {
// 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)
}
}

View file

@ -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());

View file

@ -49,6 +49,7 @@ pub struct LoginRequest {
pub struct RegisterRequest {
pub email: String,
pub password: String,
pub admin_token: Option<String>,
}
#[derive(Serialize)]