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",
|
"dotenv",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -28,3 +28,4 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
argon2 = "0.5.3"
|
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: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
- API_URL=${API_URL:-http://localhost:8080}
|
- API_URL=${API_URL:-http://localhost:3000}
|
||||||
container_name: shortener-app
|
container_name: shortener-app
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
|
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
|
||||||
- SERVER_HOST=0.0.0.0
|
- SERVER_HOST=0.0.0.0
|
||||||
- SERVER_PORT=8080
|
- SERVER_PORT=3000
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
@ -24,10 +24,11 @@ export const login = async (email: string, password: string) => {
|
||||||
return response.data;
|
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', {
|
const response = await api.post<AuthResponse>('/auth/register', {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
admin_token: adminToken,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { useToast } from '@/hooks/use-toast'
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email('Invalid email address'),
|
email: z.string().email('Invalid email address'),
|
||||||
password: z.string().min(6, 'Password must be at least 6 characters long'),
|
password: z.string().min(6, 'Password must be at least 6 characters long'),
|
||||||
|
adminToken: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>
|
type FormValues = z.infer<typeof formSchema>
|
||||||
|
@ -34,6 +35,7 @@ export function AuthForms() {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
adminToken: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -42,14 +44,14 @@ export function AuthForms() {
|
||||||
if (activeTab === 'login') {
|
if (activeTab === 'login') {
|
||||||
await login(values.email, values.password)
|
await login(values.email, values.password)
|
||||||
} else {
|
} else {
|
||||||
await register(values.email, values.password)
|
await register(values.email, values.password, values.adminToken)
|
||||||
}
|
}
|
||||||
form.reset()
|
form.reset()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: 'Error',
|
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">
|
<Button type="submit" className="w-full">
|
||||||
{activeTab === 'login' ? 'Sign in' : 'Create account'}
|
{activeTab === 'login' ? 'Sign in' : 'Create account'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -81,7 +81,9 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopy = (shortCode: string) => {
|
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({
|
toast({
|
||||||
description: "Link copied to clipboard",
|
description: "Link copied to clipboard",
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as api from '../api/client';
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
login: (email: string, password: string) => Promise<void>;
|
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;
|
logout: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
setUser(user);
|
setUser(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = async (email: string, password: string) => {
|
const register = async (email: string, password: string, adminToken: string) => {
|
||||||
const response = await api.register(email, password);
|
const response = await api.register(email, password, adminToken);
|
||||||
const { token, user } = response;
|
const { token, user } = response;
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
|
|
@ -35,3 +35,9 @@ export interface SourceStats {
|
||||||
source: string;
|
source: string;
|
||||||
count: number;
|
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 tailwindcss from '@tailwindcss/vite'
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(() => ({
|
||||||
plugins: [
|
plugins: [react(), tailwindcss()],
|
||||||
react(),
|
|
||||||
tailwindcss(),
|
|
||||||
],
|
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8080',
|
target: process.env.VITE_API_URL || 'http://localhost:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -21,5 +18,4 @@ export default defineConfig({
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
|
|
||||||
|
|
|
@ -189,6 +189,27 @@ pub async fn register(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
payload: web::Json<RegisterRequest>,
|
payload: web::Json<RegisterRequest>,
|
||||||
) -> Result<impl Responder, AppError> {
|
) -> 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)
|
let exists = sqlx::query!("SELECT id FROM users WHERE email = $1", payload.email)
|
||||||
.fetch_optional(&state.db)
|
.fetch_optional(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
41
src/lib.rs
41
src/lib.rs
|
@ -1,4 +1,8 @@
|
||||||
|
use rand::Rng;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
@ -8,4 +12,41 @@ pub mod models;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: PgPool,
|
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_files as fs;
|
||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use simplelink::check_and_generate_admin_token;
|
||||||
use simplelink::{handlers, AppState};
|
use simplelink::{handlers, AppState};
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
@ -27,7 +28,12 @@ async fn main() -> Result<()> {
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
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 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());
|
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
|
||||||
|
|
|
@ -49,6 +49,7 @@ pub struct LoginRequest {
|
||||||
pub struct RegisterRequest {
|
pub struct RegisterRequest {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
pub admin_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue