+20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
+20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT COUNT(*) as count FROM users",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "count",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": []
14
+
},
15
+
"nullable": [
16
+
null
17
+
]
18
+
},
19
+
"hash": "fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538"
20
+
}
+1
Cargo.lock
+1
Cargo.lock
+1
Cargo.toml
+1
Cargo.toml
+1
admin-setup-token.txt
+1
admin-setup-token.txt
···
1
+
fqfO6awRz3mkc2Kxunkp1uTQcXaSfGD9
+3
-3
docker-compose.yml
+3
-3
docker-compose.yml
···
24
24
context: .
25
25
dockerfile: Dockerfile
26
26
args:
27
-
- API_URL=${API_URL:-http://localhost:8080}
27
+
- API_URL=${API_URL:-http://localhost:3000}
28
28
container_name: shortener-app
29
29
ports:
30
-
- "8080:8080"
30
+
- "3000:3000"
31
31
environment:
32
32
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
33
33
- SERVER_HOST=0.0.0.0
34
-
- SERVER_PORT=8080
34
+
- SERVER_PORT=3000
35
35
depends_on:
36
36
db:
37
37
condition: service_healthy
+2
-1
frontend/src/api/client.ts
+2
-1
frontend/src/api/client.ts
···
24
24
return response.data;
25
25
};
26
26
27
-
export const register = async (email: string, password: string) => {
27
+
export const register = async (email: string, password: string, adminToken: string) => {
28
28
const response = await api.post<AuthResponse>('/auth/register', {
29
29
email,
30
30
password,
31
+
admin_token: adminToken,
31
32
});
32
33
return response.data;
33
34
};
+21
-3
frontend/src/components/AuthForms.tsx
+21
-3
frontend/src/components/AuthForms.tsx
···
20
20
const formSchema = z.object({
21
21
email: z.string().email('Invalid email address'),
22
22
password: z.string().min(6, 'Password must be at least 6 characters long'),
23
+
adminToken: z.string(),
23
24
})
24
25
25
26
type FormValues = z.infer<typeof formSchema>
···
34
35
defaultValues: {
35
36
email: '',
36
37
password: '',
38
+
adminToken: '',
37
39
},
38
40
})
39
41
···
42
44
if (activeTab === 'login') {
43
45
await login(values.email, values.password)
44
46
} else {
45
-
await register(values.email, values.password)
47
+
await register(values.email, values.password, values.adminToken)
46
48
}
47
49
form.reset()
48
50
} catch (err: any) {
49
51
toast({
50
52
variant: 'destructive',
51
53
title: 'Error',
52
-
description: err.response?.data?.error || 'An error occurred',
54
+
description: err.response?.data || 'An error occurred',
53
55
})
54
56
}
55
57
}
···
93
95
)}
94
96
/>
95
97
98
+
{activeTab === 'register' && (
99
+
<FormField
100
+
control={form.control}
101
+
name="adminToken"
102
+
render={({ field }) => (
103
+
<FormItem>
104
+
<FormLabel>Admin Setup Token</FormLabel>
105
+
<FormControl>
106
+
<Input type="text" {...field} />
107
+
</FormControl>
108
+
<FormMessage />
109
+
</FormItem>
110
+
)}
111
+
/>
112
+
)}
113
+
96
114
<Button type="submit" className="w-full">
97
115
{activeTab === 'login' ? 'Sign in' : 'Create account'}
98
116
</Button>
···
102
120
</Tabs>
103
121
</Card>
104
122
)
105
-
}
123
+
}
+4
-2
frontend/src/components/LinkList.tsx
+4
-2
frontend/src/components/LinkList.tsx
···
81
81
}
82
82
83
83
const handleCopy = (shortCode: string) => {
84
-
navigator.clipboard.writeText(`http://localhost:8080/${shortCode}`)
84
+
// Use import.meta.env.VITE_BASE_URL or fall back to window.location.origin
85
+
const baseUrl = import.meta.env.VITE_API_URL || window.location.origin
86
+
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
85
87
toast({
86
-
description: "Link copied to clipboard",
88
+
description: "Link copied to clipboard",
87
89
})
88
90
}
89
91
+3
-3
frontend/src/context/AuthContext.tsx
+3
-3
frontend/src/context/AuthContext.tsx
···
5
5
interface AuthContextType {
6
6
user: User | null;
7
7
login: (email: string, password: string) => Promise<void>;
8
-
register: (email: string, password: string) => Promise<void>;
8
+
register: (email: string, password: string, adminToken: string) => Promise<void>;
9
9
logout: () => void;
10
10
isLoading: boolean;
11
11
}
···
33
33
setUser(user);
34
34
};
35
35
36
-
const register = async (email: string, password: string) => {
37
-
const response = await api.register(email, password);
36
+
const register = async (email: string, password: string, adminToken: string) => {
37
+
const response = await api.register(email, password, adminToken);
38
38
const { token, user } = response;
39
39
localStorage.setItem('token', token);
40
40
localStorage.setItem('user', JSON.stringify(user));
+6
frontend/src/types/api.ts
+6
frontend/src/types/api.ts
+4
-8
frontend/vite.config.ts
+4
-8
frontend/vite.config.ts
···
3
3
import tailwindcss from '@tailwindcss/vite'
4
4
import path from "path"
5
5
6
-
export default defineConfig({
7
-
plugins: [
8
-
react(),
9
-
tailwindcss(),
10
-
],
6
+
export default defineConfig(() => ({
7
+
plugins: [react(), tailwindcss()],
11
8
server: {
12
9
proxy: {
13
10
'/api': {
14
-
target: 'http://localhost:8080',
11
+
target: process.env.VITE_API_URL || 'http://localhost:8080',
15
12
changeOrigin: true,
16
13
},
17
14
},
···
21
18
"@": path.resolve(__dirname, "./src"),
22
19
},
23
20
},
24
-
})
25
-
21
+
}))
+21
src/handlers.rs
+21
src/handlers.rs
···
189
189
state: web::Data<AppState>,
190
190
payload: web::Json<RegisterRequest>,
191
191
) -> Result<impl Responder, AppError> {
192
+
// Check if any users exist
193
+
let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users")
194
+
.fetch_one(&state.db)
195
+
.await?
196
+
.count
197
+
.unwrap_or(0);
198
+
199
+
// If users exist, registration is closed - no exceptions
200
+
if user_count > 0 {
201
+
return Err(AppError::Auth("Registration is closed".to_string()));
202
+
}
203
+
204
+
// Verify admin token for first user
205
+
match (&state.admin_token, &payload.admin_token) {
206
+
(Some(stored_token), Some(provided_token)) if stored_token == provided_token => {
207
+
// Token matches, proceed with registration
208
+
}
209
+
_ => return Err(AppError::Auth("Invalid admin setup token".to_string())),
210
+
}
211
+
212
+
// Check if email already exists
192
213
let exists = sqlx::query!("SELECT id FROM users WHERE email = $1", payload.email)
193
214
.fetch_optional(&state.db)
194
215
.await?;
+41
src/lib.rs
+41
src/lib.rs
···
1
+
use rand::Rng;
1
2
use sqlx::PgPool;
3
+
use std::fs::File;
4
+
use std::io::Write;
5
+
use tracing::info;
2
6
3
7
pub mod auth;
4
8
pub mod error;
···
8
12
#[derive(Clone)]
9
13
pub struct AppState {
10
14
pub db: PgPool,
15
+
pub admin_token: Option<String>,
16
+
}
17
+
18
+
pub async fn check_and_generate_admin_token(pool: &sqlx::PgPool) -> anyhow::Result<Option<String>> {
19
+
// Check if any users exist
20
+
let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users")
21
+
.fetch_one(pool)
22
+
.await?
23
+
.count
24
+
.unwrap_or(0);
25
+
26
+
if user_count == 0 {
27
+
// Generate a random token using simple characters
28
+
let token: String = (0..32)
29
+
.map(|_| {
30
+
let idx = rand::thread_rng().gen_range(0..62);
31
+
match idx {
32
+
0..=9 => (b'0' + idx as u8) as char,
33
+
10..=35 => (b'a' + (idx - 10) as u8) as char,
34
+
_ => (b'A' + (idx - 36) as u8) as char,
35
+
}
36
+
})
37
+
.collect();
38
+
39
+
// Save token to file
40
+
let mut file = File::create("admin-setup-token.txt")?;
41
+
writeln!(file, "{}", token)?;
42
+
43
+
info!("No users found - generated admin setup token");
44
+
info!("Token has been saved to admin-setup-token.txt");
45
+
info!("Use this token to create the admin user");
46
+
info!("Admin setup token: {}", token);
47
+
48
+
Ok(Some(token))
49
+
} else {
50
+
Ok(None)
51
+
}
11
52
}
+7
-1
src/main.rs
+7
-1
src/main.rs
···
2
2
use actix_files as fs;
3
3
use actix_web::{web, App, HttpServer};
4
4
use anyhow::Result;
5
+
use simplelink::check_and_generate_admin_token;
5
6
use simplelink::{handlers, AppState};
6
7
use sqlx::postgres::PgPoolOptions;
7
8
use tracing::info;
···
27
28
// Run database migrations
28
29
sqlx::migrate!("./migrations").run(&pool).await?;
29
30
30
-
let state = AppState { db: pool };
31
+
let admin_token = check_and_generate_admin_token(&pool).await?;
32
+
33
+
let state = AppState {
34
+
db: pool,
35
+
admin_token,
36
+
};
31
37
32
38
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
33
39
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());