Merge pull request #8 from WaveringAna/v0.2

only show register if no users, otherwise show only login
This commit is contained in:
Wavering Ana 2025-01-29 21:21:12 -05:00 committed by GitHub
commit 01ffe80b99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 116 additions and 185 deletions

120
Cargo.lock generated
View file

@ -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]]

View file

@ -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"

View file

@ -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 };

View file

@ -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<typeof formSchema>
export function AuthForms() {
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
const [isFirstUser, setIsFirstUser] = useState<boolean | null>(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,15 +70,23 @@ export function AuthForms() {
}
}
if (isFirstUser === null) {
return <div>Loading...</div>
}
return (
<Card className="w-full max-w-md mx-auto p-6">
<Tabs value={activeTab} onValueChange={(value: string) => setActiveTab(value as 'login' | 'register')}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
</TabsList>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold">
{isFirstUser ? 'Create Admin Account' : 'Login'}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{isFirstUser
? 'Set up your admin account to get started'
: 'Welcome back! Please login to your account'}
</p>
</div>
<TabsContent value={activeTab}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
@ -95,7 +117,7 @@ export function AuthForms() {
)}
/>
{activeTab === 'register' && (
{isFirstUser && (
<FormField
control={form.control}
name="adminToken"
@ -112,12 +134,10 @@ export function AuthForms() {
)}
<Button type="submit" className="w-full">
{activeTab === 'login' ? 'Sign in' : 'Create account'}
{isFirstUser ? 'Create Account' : 'Sign in'}
</Button>
</form>
</Form>
</TabsContent>
</Tabs>
</Card>
)
}

View file

@ -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<AppState>) -> Result<impl Responder, AppError> {
let user_count = match &state.db {
DatabasePool::Postgres(pool) => {
sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users")
.fetch_one(pool)
.await?
.0
}
DatabasePool::Sqlite(pool) => {
sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users")
.fetch_one(pool)
.await?
.0
}
};
Ok(HttpResponse::Ok().json(json!({
"isFirstUser": user_count == 0
})))
}

View file

@ -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)))