add admin setup token i love admin setup token
This commit is contained in:
parent
660da70666
commit
ac13e77dc4
15 changed files with 136 additions and 21 deletions
20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
generated
Normal file
20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
generated
Normal 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
1
Cargo.lock
generated
|
@ -2068,6 +2068,7 @@ dependencies = [
|
|||
"dotenv",
|
||||
"jsonwebtoken",
|
||||
"lazy_static",
|
||||
"rand",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -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
1
admin-setup-token.txt
Normal file
|
@ -0,0 +1 @@
|
|||
fqfO6awRz3mkc2Kxunkp1uTQcXaSfGD9
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -35,3 +35,9 @@ export interface SourceStats {
|
|||
source: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
admin_token: string;
|
||||
}
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}))
|
||||
|
|
|
@ -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?;
|
||||
|
|
41
src/lib.rs
41
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<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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -49,6 +49,7 @@ pub struct LoginRequest {
|
|||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub admin_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue