-120
Cargo.lock
-120
Cargo.lock
···
607
607
]
608
608
609
609
[[package]]
610
-
name = "core-foundation"
611
-
version = "0.9.4"
612
-
source = "registry+https://github.com/rust-lang/crates.io-index"
613
-
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
614
-
dependencies = [
615
-
"core-foundation-sys",
616
-
"libc",
617
-
]
618
-
619
-
[[package]]
620
610
name = "core-foundation-sys"
621
611
version = "0.8.7"
622
612
source = "registry+https://github.com/rust-lang/crates.io-index"
···
842
832
version = "0.1.4"
843
833
source = "registry+https://github.com/rust-lang/crates.io-index"
844
834
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
845
-
846
-
[[package]]
847
-
name = "foreign-types"
848
-
version = "0.3.2"
849
-
source = "registry+https://github.com/rust-lang/crates.io-index"
850
-
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
851
-
dependencies = [
852
-
"foreign-types-shared",
853
-
]
854
-
855
-
[[package]]
856
-
name = "foreign-types-shared"
857
-
version = "0.1.1"
858
-
source = "registry+https://github.com/rust-lang/crates.io-index"
859
-
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
860
835
861
836
[[package]]
862
837
name = "form_urlencoded"
···
1464
1439
]
1465
1440
1466
1441
[[package]]
1467
-
name = "native-tls"
1468
-
version = "0.2.12"
1469
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1470
-
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
1471
-
dependencies = [
1472
-
"libc",
1473
-
"log",
1474
-
"openssl",
1475
-
"openssl-probe",
1476
-
"openssl-sys",
1477
-
"schannel",
1478
-
"security-framework",
1479
-
"security-framework-sys",
1480
-
"tempfile",
1481
-
]
1482
-
1483
-
[[package]]
1484
1442
name = "nu-ansi-term"
1485
1443
version = "0.46.0"
1486
1444
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1569
1527
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
1570
1528
1571
1529
[[package]]
1572
-
name = "openssl"
1573
-
version = "0.10.68"
1574
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1575
-
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
1576
-
dependencies = [
1577
-
"bitflags",
1578
-
"cfg-if",
1579
-
"foreign-types",
1580
-
"libc",
1581
-
"once_cell",
1582
-
"openssl-macros",
1583
-
"openssl-sys",
1584
-
]
1585
-
1586
-
[[package]]
1587
-
name = "openssl-macros"
1588
-
version = "0.1.1"
1589
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1590
-
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
1591
-
dependencies = [
1592
-
"proc-macro2",
1593
-
"quote",
1594
-
"syn",
1595
-
]
1596
-
1597
-
[[package]]
1598
-
name = "openssl-probe"
1599
-
version = "0.1.6"
1600
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1601
-
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
1602
-
1603
-
[[package]]
1604
-
name = "openssl-sys"
1605
-
version = "0.9.104"
1606
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1607
-
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
1608
-
dependencies = [
1609
-
"cc",
1610
-
"libc",
1611
-
"pkg-config",
1612
-
"vcpkg",
1613
-
]
1614
-
1615
-
[[package]]
1616
1530
name = "overload"
1617
1531
version = "0.1.1"
1618
1532
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1954
1868
]
1955
1869
1956
1870
[[package]]
1957
-
name = "schannel"
1958
-
version = "0.1.27"
1959
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1960
-
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
1961
-
dependencies = [
1962
-
"windows-sys 0.59.0",
1963
-
]
1964
-
1965
-
[[package]]
1966
1871
name = "scopeguard"
1967
1872
version = "1.2.0"
1968
1873
source = "registry+https://github.com/rust-lang/crates.io-index"
1969
1874
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1970
-
1971
-
[[package]]
1972
-
name = "security-framework"
1973
-
version = "2.11.1"
1974
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1975
-
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
1976
-
dependencies = [
1977
-
"bitflags",
1978
-
"core-foundation",
1979
-
"core-foundation-sys",
1980
-
"libc",
1981
-
"security-framework-sys",
1982
-
]
1983
-
1984
-
[[package]]
1985
-
name = "security-framework-sys"
1986
-
version = "2.14.0"
1987
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1988
-
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
1989
-
dependencies = [
1990
-
"core-foundation-sys",
1991
-
"libc",
1992
-
]
1993
1875
1994
1876
[[package]]
1995
1877
name = "semver"
···
2220
2102
"indexmap",
2221
2103
"log",
2222
2104
"memchr",
2223
-
"native-tls",
2224
2105
"once_cell",
2225
2106
"percent-encoding",
2226
2107
"serde",
···
2741
2622
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
2742
2623
dependencies = [
2743
2624
"getrandom",
2744
-
"serde",
2745
2625
]
2746
2626
2747
2627
[[package]]
+3
-3
Cargo.toml
+3
-3
Cargo.toml
···
13
13
actix-web = "4.4"
14
14
actix-files = "0.6"
15
15
actix-cors = "0.6"
16
-
tokio = { version = "1.36", features = ["full"] }
17
-
sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "sqlite", "chrono"] }
16
+
tokio = { version = "1.36", features = ["rt-multi-thread", "macros"] }
17
+
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "sqlite", "chrono"] }
18
18
serde = { version = "1.0", features = ["derive"] }
19
19
serde_json = "1.0"
20
20
anyhow = "1.0"
21
21
thiserror = "1.0"
22
22
tracing = "0.1"
23
23
tracing-subscriber = "0.3"
24
-
uuid = { version = "1.7", features = ["v4", "serde"] }
24
+
uuid = { version = "1.7", features = ["v4"] } # Remove serde if not using UUID serialization
25
25
base62 = "2.0"
26
26
clap = { version = "4.5", features = ["derive"] }
27
27
dotenv = "0.15"
+5
frontend/src/api/client.ts
+5
frontend/src/api/client.ts
+82
-62
frontend/src/components/AuthForms.tsx
+82
-62
frontend/src/components/AuthForms.tsx
···
1
-
import { useState } from 'react'
1
+
import { useState, useEffect } from 'react'
2
2
import { useForm } from 'react-hook-form'
3
3
import { z } from 'zod'
4
4
import { zodResolver } from '@hookform/resolvers/zod'
···
6
6
import { Button } from '@/components/ui/button'
7
7
import { Input } from '@/components/ui/input'
8
8
import { Card } from '@/components/ui/card'
9
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
10
9
import {
11
10
Form,
12
11
FormControl,
···
16
15
FormMessage,
17
16
} from '@/components/ui/form'
18
17
import { useToast } from '@/hooks/use-toast'
18
+
import { checkFirstUser } from '../api/client'
19
19
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
+
adminToken: z.string().optional(),
24
24
})
25
25
26
26
type FormValues = z.infer<typeof formSchema>
27
27
28
28
export function AuthForms() {
29
-
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
29
+
const [isFirstUser, setIsFirstUser] = useState<boolean | null>(null)
30
30
const { login, register } = useAuth()
31
31
const { toast } = useToast()
32
32
···
39
39
},
40
40
})
41
41
42
+
useEffect(() => {
43
+
const init = async () => {
44
+
try {
45
+
const isFirst = await checkFirstUser()
46
+
setIsFirstUser(isFirst)
47
+
} catch (err) {
48
+
console.error('Error checking first user:', err)
49
+
setIsFirstUser(false)
50
+
}
51
+
}
52
+
53
+
init()
54
+
}, [])
55
+
42
56
const onSubmit = async (values: FormValues) => {
43
57
try {
44
-
if (activeTab === 'login') {
45
-
await login(values.email, values.password)
58
+
if (isFirstUser) {
59
+
await register(values.email, values.password, values.adminToken || '')
46
60
} else {
47
-
await register(values.email, values.password, values.adminToken)
61
+
await login(values.email, values.password)
48
62
}
49
63
form.reset()
50
64
} catch (err: any) {
···
56
70
}
57
71
}
58
72
73
+
if (isFirstUser === null) {
74
+
return <div>Loading...</div>
75
+
}
76
+
59
77
return (
60
78
<Card className="w-full max-w-md mx-auto p-6">
61
-
<Tabs value={activeTab} onValueChange={(value: string) => setActiveTab(value as 'login' | 'register')}>
62
-
<TabsList className="grid w-full grid-cols-2">
63
-
<TabsTrigger value="login">Login</TabsTrigger>
64
-
<TabsTrigger value="register">Register</TabsTrigger>
65
-
</TabsList>
79
+
<div className="mb-6 text-center">
80
+
<h2 className="text-2xl font-bold">
81
+
{isFirstUser ? 'Create Admin Account' : 'Login'}
82
+
</h2>
83
+
<p className="text-sm text-muted-foreground mt-1">
84
+
{isFirstUser
85
+
? 'Set up your admin account to get started'
86
+
: 'Welcome back! Please login to your account'}
87
+
</p>
88
+
</div>
66
89
67
-
<TabsContent value={activeTab}>
68
-
<Form {...form}>
69
-
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
70
-
<FormField
71
-
control={form.control}
72
-
name="email"
73
-
render={({ field }) => (
74
-
<FormItem>
75
-
<FormLabel>Email</FormLabel>
76
-
<FormControl>
77
-
<Input type="email" {...field} />
78
-
</FormControl>
79
-
<FormMessage />
80
-
</FormItem>
81
-
)}
82
-
/>
90
+
<Form {...form}>
91
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
92
+
<FormField
93
+
control={form.control}
94
+
name="email"
95
+
render={({ field }) => (
96
+
<FormItem>
97
+
<FormLabel>Email</FormLabel>
98
+
<FormControl>
99
+
<Input type="email" {...field} />
100
+
</FormControl>
101
+
<FormMessage />
102
+
</FormItem>
103
+
)}
104
+
/>
83
105
84
-
<FormField
85
-
control={form.control}
86
-
name="password"
87
-
render={({ field }) => (
88
-
<FormItem>
89
-
<FormLabel>Password</FormLabel>
90
-
<FormControl>
91
-
<Input type="password" {...field} />
92
-
</FormControl>
93
-
<FormMessage />
94
-
</FormItem>
95
-
)}
96
-
/>
106
+
<FormField
107
+
control={form.control}
108
+
name="password"
109
+
render={({ field }) => (
110
+
<FormItem>
111
+
<FormLabel>Password</FormLabel>
112
+
<FormControl>
113
+
<Input type="password" {...field} />
114
+
</FormControl>
115
+
<FormMessage />
116
+
</FormItem>
117
+
)}
118
+
/>
97
119
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
-
/>
120
+
{isFirstUser && (
121
+
<FormField
122
+
control={form.control}
123
+
name="adminToken"
124
+
render={({ field }) => (
125
+
<FormItem>
126
+
<FormLabel>Admin Setup Token</FormLabel>
127
+
<FormControl>
128
+
<Input type="text" {...field} />
129
+
</FormControl>
130
+
<FormMessage />
131
+
</FormItem>
112
132
)}
133
+
/>
134
+
)}
113
135
114
-
<Button type="submit" className="w-full">
115
-
{activeTab === 'login' ? 'Sign in' : 'Create account'}
116
-
</Button>
117
-
</form>
118
-
</Form>
119
-
</TabsContent>
120
-
</Tabs>
136
+
<Button type="submit" className="w-full">
137
+
{isFirstUser ? 'Create Account' : 'Sign in'}
138
+
</Button>
139
+
</form>
140
+
</Form>
121
141
</Card>
122
142
)
123
143
}
+22
src/handlers.rs
+22
src/handlers.rs
···
16
16
use jsonwebtoken::{encode, EncodingKey, Header};
17
17
use lazy_static::lazy_static;
18
18
use regex::Regex;
19
+
use serde_json::json;
19
20
use sqlx::{Postgres, Sqlite};
20
21
21
22
lazy_static! {
···
690
691
691
692
Ok(HttpResponse::Ok().json(sources))
692
693
}
694
+
695
+
pub async fn check_first_user(state: web::Data<AppState>) -> Result<impl Responder, AppError> {
696
+
let user_count = match &state.db {
697
+
DatabasePool::Postgres(pool) => {
698
+
sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users")
699
+
.fetch_one(pool)
700
+
.await?
701
+
.0
702
+
}
703
+
DatabasePool::Sqlite(pool) => {
704
+
sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users")
705
+
.fetch_one(pool)
706
+
.await?
707
+
.0
708
+
}
709
+
};
710
+
711
+
Ok(HttpResponse::Ok().json(json!({
712
+
"isFirstUser": user_count == 0
713
+
})))
714
+
}
+4
src/main.rs
+4
src/main.rs
···
72
72
)
73
73
.route("/auth/register", web::post().to(handlers::register))
74
74
.route("/auth/login", web::post().to(handlers::login))
75
+
.route(
76
+
"/auth/check-first-user",
77
+
web::get().to(handlers::check_first_user),
78
+
)
75
79
.route("/health", web::get().to(handlers::health_check)),
76
80
)
77
81
.service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url)))