diff --git a/Cargo.lock b/Cargo.lock index a2a1704..c2e6367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -606,16 +606,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -843,21 +833,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1463,23 +1438,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1568,50 +1526,6 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "openssl" -version = "0.10.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "overload" version = "0.1.1" @@ -1953,44 +1867,12 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.25" @@ -2220,7 +2102,6 @@ dependencies = [ "indexmap", "log", "memchr", - "native-tls", "once_cell", "percent-encoding", "serde", @@ -2741,7 +2622,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "getrandom", - "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0341831..f468ed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,15 @@ jsonwebtoken = "9" actix-web = "4.4" actix-files = "0.6" actix-cors = "0.6" -tokio = { version = "1.36", features = ["full"] } -sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "sqlite", "chrono"] } +tokio = { version = "1.36", features = ["rt-multi-thread", "macros"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "sqlite", "chrono"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" thiserror = "1.0" tracing = "0.1" tracing-subscriber = "0.3" -uuid = { version = "1.7", features = ["v4", "serde"] } +uuid = { version = "1.7", features = ["v4"] } # Remove serde if not using UUID serialization base62 = "2.0" clap = { version = "4.5", features = ["derive"] } dotenv = "0.15" diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 1cf80b3..b5b39f7 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -72,4 +72,9 @@ export const getLinkSourceStats = async (id: number) => { return response.data; }; +export const checkFirstUser = async () => { + const response = await api.get<{ isFirstUser: boolean }>('/auth/check-first-user'); + return response.data.isFirstUser; +}; + export { api }; \ No newline at end of file diff --git a/frontend/src/components/AuthForms.tsx b/frontend/src/components/AuthForms.tsx index d655dfd..938e6d2 100644 --- a/frontend/src/components/AuthForms.tsx +++ b/frontend/src/components/AuthForms.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import { zodResolver } from '@hookform/resolvers/zod' @@ -6,7 +6,6 @@ import { useAuth } from '../context/AuthContext' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card } from '@/components/ui/card' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Form, FormControl, @@ -16,17 +15,18 @@ import { FormMessage, } from '@/components/ui/form' import { useToast } from '@/hooks/use-toast' +import { checkFirstUser } from '../api/client' 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(), + adminToken: z.string().optional(), }) type FormValues = z.infer export function AuthForms() { - const [activeTab, setActiveTab] = useState<'login' | 'register'>('login') + const [isFirstUser, setIsFirstUser] = useState(null) const { login, register } = useAuth() const { toast } = useToast() @@ -39,12 +39,26 @@ export function AuthForms() { }, }) + useEffect(() => { + const init = async () => { + try { + const isFirst = await checkFirstUser() + setIsFirstUser(isFirst) + } catch (err) { + console.error('Error checking first user:', err) + setIsFirstUser(false) + } + } + + init() + }, []) + const onSubmit = async (values: FormValues) => { try { - if (activeTab === 'login') { - await login(values.email, values.password) + if (isFirstUser) { + await register(values.email, values.password, values.adminToken || '') } else { - await register(values.email, values.password, values.adminToken) + await login(values.email, values.password) } form.reset() } catch (err: any) { @@ -56,68 +70,74 @@ export function AuthForms() { } } + if (isFirstUser === null) { + return
Loading...
+ } + return ( - setActiveTab(value as 'login' | 'register')}> - - Login - Register - +
+

+ {isFirstUser ? 'Create Admin Account' : 'Login'} +

+

+ {isFirstUser + ? 'Set up your admin account to get started' + : 'Welcome back! Please login to your account'} +

+
- -
- - ( - - Email - - - - - - )} - /> + + + ( + + Email + + + + + + )} + /> - ( - - Password - - - - - - )} - /> + ( + + Password + + + + + + )} + /> - {activeTab === 'register' && ( - ( - - Admin Setup Token - - - - - - )} - /> + {isFirstUser && ( + ( + + Admin Setup Token + + + + + )} + /> + )} - - - -
-
+ + +
) } diff --git a/src/handlers.rs b/src/handlers.rs index 2e7ba74..fdecc7d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -16,6 +16,7 @@ use argon2::{Argon2, PasswordHash, PasswordHasher}; use jsonwebtoken::{encode, EncodingKey, Header}; use lazy_static::lazy_static; use regex::Regex; +use serde_json::json; use sqlx::{Postgres, Sqlite}; lazy_static! { @@ -690,3 +691,24 @@ pub async fn get_link_sources( Ok(HttpResponse::Ok().json(sources)) } + +pub async fn check_first_user(state: web::Data) -> Result { + let user_count = match &state.db { + DatabasePool::Postgres(pool) => { + sqlx::query_as::("SELECT COUNT(*)::bigint FROM users") + .fetch_one(pool) + .await? + .0 + } + DatabasePool::Sqlite(pool) => { + sqlx::query_as::("SELECT COUNT(*) FROM users") + .fetch_one(pool) + .await? + .0 + } + }; + + Ok(HttpResponse::Ok().json(json!({ + "isFirstUser": user_count == 0 + }))) +} diff --git a/src/main.rs b/src/main.rs index fc60fc4..50c52bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,10 @@ async fn main() -> Result<()> { ) .route("/auth/register", web::post().to(handlers::register)) .route("/auth/login", web::post().to(handlers::login)) + .route( + "/auth/check-first-user", + web::get().to(handlers::check_first_user), + ) .route("/health", web::get().to(handlers::health_check)), ) .service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url)))