tangled
alpha
login
or
join now
nekomimi.pet
/
simplelink
0
fork
atom
A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
0
fork
atom
overview
issues
pulls
pipelines
add admin setup token i love admin setup token
nekomimi.pet
1 year ago
ac13e77d
660da706
+136
-21
15 changed files
expand all
collapse all
unified
split
.sqlx
query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
Cargo.lock
Cargo.toml
admin-setup-token.txt
docker-compose.yml
frontend
src
api
client.ts
components
AuthForms.tsx
LinkList.tsx
context
AuthContext.tsx
types
api.ts
vite.config.ts
src
handlers.rs
lib.rs
main.rs
models.rs
+20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "SELECT COUNT(*) as count FROM users",
4
4
+
"describe": {
5
5
+
"columns": [
6
6
+
{
7
7
+
"ordinal": 0,
8
8
+
"name": "count",
9
9
+
"type_info": "Int8"
10
10
+
}
11
11
+
],
12
12
+
"parameters": {
13
13
+
"Left": []
14
14
+
},
15
15
+
"nullable": [
16
16
+
null
17
17
+
]
18
18
+
},
19
19
+
"hash": "fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538"
20
20
+
}
+1
Cargo.lock
···
2068
2068
"dotenv",
2069
2069
"jsonwebtoken",
2070
2070
"lazy_static",
2071
2071
+
"rand",
2071
2072
"regex",
2072
2073
"serde",
2073
2074
"serde_json",
+1
Cargo.toml
···
28
28
regex = "1.10"
29
29
lazy_static = "1.4"
30
30
argon2 = "0.5.3"
31
31
+
rand = { version = "0.8", features = ["std"] }
+1
admin-setup-token.txt
···
1
1
+
fqfO6awRz3mkc2Kxunkp1uTQcXaSfGD9
+3
-3
docker-compose.yml
···
24
24
context: .
25
25
dockerfile: Dockerfile
26
26
args:
27
27
-
- API_URL=${API_URL:-http://localhost:8080}
27
27
+
- API_URL=${API_URL:-http://localhost:3000}
28
28
container_name: shortener-app
29
29
ports:
30
30
-
- "8080:8080"
30
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
34
-
- SERVER_PORT=8080
34
34
+
- SERVER_PORT=3000
35
35
depends_on:
36
36
db:
37
37
condition: service_healthy
+2
-1
frontend/src/api/client.ts
···
24
24
return response.data;
25
25
};
26
26
27
27
-
export const register = async (email: string, password: string) => {
27
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
31
+
admin_token: adminToken,
31
32
});
32
33
return response.data;
33
34
};
+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
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
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
45
-
await register(values.email, values.password)
47
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
52
-
description: err.response?.data?.error || 'An error occurred',
54
54
+
description: err.response?.data || 'An error occurred',
53
55
})
54
56
}
55
57
}
···
93
95
)}
94
96
/>
95
97
98
98
+
{activeTab === 'register' && (
99
99
+
<FormField
100
100
+
control={form.control}
101
101
+
name="adminToken"
102
102
+
render={({ field }) => (
103
103
+
<FormItem>
104
104
+
<FormLabel>Admin Setup Token</FormLabel>
105
105
+
<FormControl>
106
106
+
<Input type="text" {...field} />
107
107
+
</FormControl>
108
108
+
<FormMessage />
109
109
+
</FormItem>
110
110
+
)}
111
111
+
/>
112
112
+
)}
113
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
105
-
}
123
123
+
}
+4
-2
frontend/src/components/LinkList.tsx
···
81
81
}
82
82
83
83
const handleCopy = (shortCode: string) => {
84
84
-
navigator.clipboard.writeText(`http://localhost:8080/${shortCode}`)
84
84
+
// Use import.meta.env.VITE_BASE_URL or fall back to window.location.origin
85
85
+
const baseUrl = import.meta.env.VITE_API_URL || window.location.origin
86
86
+
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
85
87
toast({
86
86
-
description: "Link copied to clipboard",
88
88
+
description: "Link copied to clipboard",
87
89
})
88
90
}
89
91
+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
8
-
register: (email: string, password: string) => Promise<void>;
8
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
36
-
const register = async (email: string, password: string) => {
37
37
-
const response = await api.register(email, password);
36
36
+
const register = async (email: string, password: string, adminToken: string) => {
37
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
···
35
35
source: string;
36
36
count: number;
37
37
}
38
38
+
39
39
+
export interface RegisterRequest {
40
40
+
email: string;
41
41
+
password: string;
42
42
+
admin_token: string;
43
43
+
}
+4
-8
frontend/vite.config.ts
···
3
3
import tailwindcss from '@tailwindcss/vite'
4
4
import path from "path"
5
5
6
6
-
export default defineConfig({
7
7
-
plugins: [
8
8
-
react(),
9
9
-
tailwindcss(),
10
10
-
],
6
6
+
export default defineConfig(() => ({
7
7
+
plugins: [react(), tailwindcss()],
11
8
server: {
12
9
proxy: {
13
10
'/api': {
14
14
-
target: 'http://localhost:8080',
11
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
24
-
})
25
25
-
21
21
+
}))
+21
src/handlers.rs
···
189
189
state: web::Data<AppState>,
190
190
payload: web::Json<RegisterRequest>,
191
191
) -> Result<impl Responder, AppError> {
192
192
+
// Check if any users exist
193
193
+
let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users")
194
194
+
.fetch_one(&state.db)
195
195
+
.await?
196
196
+
.count
197
197
+
.unwrap_or(0);
198
198
+
199
199
+
// If users exist, registration is closed - no exceptions
200
200
+
if user_count > 0 {
201
201
+
return Err(AppError::Auth("Registration is closed".to_string()));
202
202
+
}
203
203
+
204
204
+
// Verify admin token for first user
205
205
+
match (&state.admin_token, &payload.admin_token) {
206
206
+
(Some(stored_token), Some(provided_token)) if stored_token == provided_token => {
207
207
+
// Token matches, proceed with registration
208
208
+
}
209
209
+
_ => return Err(AppError::Auth("Invalid admin setup token".to_string())),
210
210
+
}
211
211
+
212
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
···
1
1
+
use rand::Rng;
1
2
use sqlx::PgPool;
3
3
+
use std::fs::File;
4
4
+
use std::io::Write;
5
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
15
+
pub admin_token: Option<String>,
16
16
+
}
17
17
+
18
18
+
pub async fn check_and_generate_admin_token(pool: &sqlx::PgPool) -> anyhow::Result<Option<String>> {
19
19
+
// Check if any users exist
20
20
+
let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users")
21
21
+
.fetch_one(pool)
22
22
+
.await?
23
23
+
.count
24
24
+
.unwrap_or(0);
25
25
+
26
26
+
if user_count == 0 {
27
27
+
// Generate a random token using simple characters
28
28
+
let token: String = (0..32)
29
29
+
.map(|_| {
30
30
+
let idx = rand::thread_rng().gen_range(0..62);
31
31
+
match idx {
32
32
+
0..=9 => (b'0' + idx as u8) as char,
33
33
+
10..=35 => (b'a' + (idx - 10) as u8) as char,
34
34
+
_ => (b'A' + (idx - 36) as u8) as char,
35
35
+
}
36
36
+
})
37
37
+
.collect();
38
38
+
39
39
+
// Save token to file
40
40
+
let mut file = File::create("admin-setup-token.txt")?;
41
41
+
writeln!(file, "{}", token)?;
42
42
+
43
43
+
info!("No users found - generated admin setup token");
44
44
+
info!("Token has been saved to admin-setup-token.txt");
45
45
+
info!("Use this token to create the admin user");
46
46
+
info!("Admin setup token: {}", token);
47
47
+
48
48
+
Ok(Some(token))
49
49
+
} else {
50
50
+
Ok(None)
51
51
+
}
11
52
}
+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
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
30
-
let state = AppState { db: pool };
31
31
+
let admin_token = check_and_generate_admin_token(&pool).await?;
32
32
+
33
33
+
let state = AppState {
34
34
+
db: pool,
35
35
+
admin_token,
36
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());
+1
src/models.rs
···
49
49
pub struct RegisterRequest {
50
50
pub email: String,
51
51
pub password: String,
52
52
+
pub admin_token: Option<String>,
52
53
}
53
54
54
55
#[derive(Serialize)]