diff options
| author | MohamedBassem <me@mbassem.com> | 2024-09-22 02:17:43 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-09-22 02:17:43 +0100 |
| commit | 5fe330cd79be034e41f9c5181455e50e8c2fc5c1 (patch) | |
| tree | 6bedc00f3c6bb38f2132cd325be4df95eb819cf8 | |
| parent | 26521b70a79c42442f44c8053590bbb8c5e5f1b1 (diff) | |
| download | karakeep-5fe330cd79be034e41f9c5181455e50e8c2fc5c1.tar.zst | |
feature(mobile): Allow users to login with API keys in the mobile app
| -rw-r--r-- | apps/mobile/app/_layout.tsx | 10 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/_layout.tsx | 2 | ||||
| -rw-r--r-- | apps/mobile/app/index.tsx | 2 | ||||
| -rw-r--r-- | apps/mobile/app/server-address.tsx | 87 | ||||
| -rw-r--r-- | apps/mobile/app/signin.tsx | 188 |
5 files changed, 214 insertions, 75 deletions
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 677a0638..5eadad80 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -52,6 +52,16 @@ export default function RootLayout() { }} > <Stack.Screen name="index" /> + <Stack.Screen + name="signin" + options={{ + headerShown: true, + headerBackVisible: true, + headerBackTitle: "Back", + title: "", + }} + /> + <Stack.Screen name="server-address" /> <Stack.Screen name="sharing" /> <Stack.Screen name="test-connection" diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx index 22d1ed07..a1a25398 100644 --- a/apps/mobile/app/dashboard/_layout.tsx +++ b/apps/mobile/app/dashboard/_layout.tsx @@ -19,7 +19,7 @@ export default function Dashboard() { const isLoggedIn = useIsLoggedIn(); useEffect(() => { if (isLoggedIn !== undefined && !isLoggedIn) { - return router.replace("signin"); + return router.replace("server-address"); } }, [isLoggedIn]); diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx index dbbea97e..702269a5 100644 --- a/apps/mobile/app/index.tsx +++ b/apps/mobile/app/index.tsx @@ -11,6 +11,6 @@ export default function App() { } else if (isLoggedIn) { return <Redirect href="dashboard" />; } else { - return <Redirect href="signin" />; + return <Redirect href="server-address" />; } } diff --git a/apps/mobile/app/server-address.tsx b/apps/mobile/app/server-address.tsx new file mode 100644 index 00000000..c34806b3 --- /dev/null +++ b/apps/mobile/app/server-address.tsx @@ -0,0 +1,87 @@ +import { useState } from "react"; +import { + Keyboard, + KeyboardAvoidingView, + Platform, + Pressable, + Text, + TouchableWithoutFeedback, + View, +} from "react-native"; +import { Redirect, useRouter } from "expo-router"; +import Logo from "@/components/Logo"; +import { TailwindResolver } from "@/components/TailwindResolver"; +import { Button, buttonVariants } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; +import useAppSettings from "@/lib/settings"; +import { cn } from "@/lib/utils"; +import { Bug } from "lucide-react-native"; + +export default function ServerAddress() { + const router = useRouter(); + const { settings, setSettings } = useAppSettings(); + const [serverAddress, setServerAddress] = useState(settings.address); + + if (settings.apiKey) { + return <Redirect href="dashboard" />; + } + + return ( + <KeyboardAvoidingView + behavior={Platform.OS === "ios" ? "padding" : "height"} + > + <TouchableWithoutFeedback onPress={Keyboard.dismiss}> + <View className="flex h-full flex-col justify-center gap-2 px-4"> + <View className="items-center"> + <TailwindResolver + className="color-foreground" + comp={(styles) => ( + <Logo + height={150} + width={200} + fill={styles?.color?.toString()} + /> + )} + /> + </View> + <View className="gap-2"> + <Text className="font-bold">Server Address</Text> + <Input + className="w-full" + placeholder="Server Address" + value={serverAddress} + autoCapitalize="none" + keyboardType="url" + onChangeText={(e) => { + setServerAddress(e); + setSettings({ ...settings, address: e.replace(/\/$/, "") }); + }} + /> + </View> + <View className="flex flex-row items-center justify-between gap-2"> + <Button + className="flex-1" + label="Next" + onPress={() => router.push("/signin")} + /> + <Pressable + className={cn( + buttonVariants({ variant: "default" }), + !settings.address && "bg-gray-500", + )} + onPress={() => router.push("/test-connection")} + disabled={!settings.address} + > + <TailwindResolver + comp={(styles) => ( + <Bug size={20} color={styles?.color?.toString()} /> + )} + className="text-background" + /> + </Pressable> + </View> + </View> + </TouchableWithoutFeedback> + </KeyboardAvoidingView> + ); +} diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index 6190c258..df0c2e5f 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -8,37 +8,63 @@ import { TouchableWithoutFeedback, View, } from "react-native"; -import { Redirect, useRouter } from "expo-router"; +import { Redirect } from "expo-router"; import Logo from "@/components/Logo"; import { TailwindResolver } from "@/components/TailwindResolver"; -import { Button, buttonVariants } from "@/components/ui/Button"; +import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; -import { cn } from "@/lib/utils"; -import { Bug } from "lucide-react-native"; + +enum LoginType { + Password, + ApiKey, +} export default function Signin() { - const router = useRouter(); const { settings, setSettings } = useAppSettings(); - const [serverAddress, setServerAddress] = useState(settings.address); const [error, setError] = useState<string | undefined>(); - - const { mutate: login, isPending } = api.apiKeys.exchange.useMutation({ - onSuccess: (resp) => { - setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id }); - }, - onError: (e) => { - if (e.data?.code === "UNAUTHORIZED") { - setError("Wrong username or password"); + const [loginType, setLoginType] = useState<LoginType>(LoginType.Password); + const toggleLoginType = () => { + setLoginType((prev) => { + if (prev === LoginType.Password) { + return LoginType.ApiKey; } else { - setError(`${e.message}`); + return LoginType.Password; } - }, - }); + }); + }; + + const { mutate: login, isPending: userNamePasswordRequestIsPending } = + api.apiKeys.exchange.useMutation({ + onSuccess: (resp) => { + setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id }); + }, + onError: (e) => { + if (e.data?.code === "UNAUTHORIZED") { + setError("Wrong username or password"); + } else { + setError(`${e.message}`); + } + }, + }); + + const { mutate: validateApiKey, isPending: apiKeyValueRequestIsPending } = + api.apiKeys.validate.useMutation({ + onSuccess: () => { + setSettings({ ...settings, apiKey: apiFormData.apiKey }); + }, + onError: (e) => { + if (e.data?.code === "UNAUTHORIZED") { + setError("Invalid API key"); + } else { + setError(`${e.message}`); + } + }, + }); - const [formData, setFormData] = useState<{ + const [usernameFormData, setUserNameFormData] = useState<{ email: string; password: string; }>({ @@ -46,13 +72,23 @@ export default function Signin() { password: "", }); + const [apiFormData, setApiFormData] = useState<{ + apiKey: string; + }>({ + apiKey: "", + }); + if (settings.apiKey) { return <Redirect href="dashboard" />; } const onSignin = () => { - const randStr = (Math.random() + 1).toString(36).substring(5); - login({ ...formData, keyName: `Mobile App: (${randStr})` }); + if (loginType === LoginType.Password) { + const randStr = (Math.random() + 1).toString(36).substring(5); + login({ ...usernameFormData, keyName: `Mobile App: (${randStr})` }); + } else if (loginType === LoginType.ApiKey) { + validateApiKey({ apiKey: apiFormData.apiKey }); + } }; return ( @@ -76,66 +112,72 @@ export default function Signin() { {error && ( <Text className="w-full text-center text-red-500">{error}</Text> )} - <View className="gap-2"> - <Text className="font-bold">Server Address</Text> - <Input - className="w-full" - placeholder="Server Address" - value={serverAddress} - autoCapitalize="none" - keyboardType="url" - onChangeText={(e) => { - setServerAddress(e); - setSettings({ ...settings, address: e.replace(/\/$/, "") }); - }} - /> - </View> - <View className="gap-2"> - <Text className="font-bold">Email</Text> - <Input - className="w-full" - placeholder="Email" - keyboardType="email-address" - autoCapitalize="none" - value={formData.email} - onChangeText={(e) => setFormData((s) => ({ ...s, email: e }))} - /> - </View> - <View className="gap-2"> - <Text className="font-bold">Password</Text> - <Input - className="w-full" - placeholder="Password" - secureTextEntry - value={formData.password} - autoCapitalize="none" - textContentType="password" - onChangeText={(e) => setFormData((s) => ({ ...s, password: e }))} - /> - </View> + {loginType === LoginType.Password && ( + <> + <View className="gap-2"> + <Text className="font-bold">Email</Text> + <Input + className="w-full" + placeholder="Email" + keyboardType="email-address" + autoCapitalize="none" + value={usernameFormData.email} + onChangeText={(e) => + setUserNameFormData((s) => ({ ...s, email: e })) + } + /> + </View> + <View className="gap-2"> + <Text className="font-bold">Password</Text> + <Input + className="w-full" + placeholder="Password" + secureTextEntry + value={usernameFormData.password} + autoCapitalize="none" + textContentType="password" + onChangeText={(e) => + setUserNameFormData((s) => ({ ...s, password: e })) + } + /> + </View> + </> + )} + + {loginType === LoginType.ApiKey && ( + <View className="gap-2"> + <Text className="font-bold">API Key</Text> + <Input + className="w-full" + placeholder="API Key" + secureTextEntry + value={apiFormData.apiKey} + autoCapitalize="none" + textContentType="password" + onChangeText={(e) => + setApiFormData((s) => ({ ...s, apiKey: e })) + } + /> + </View> + )} + <View className="flex flex-row items-center justify-between gap-2"> <Button className="flex-1" label="Sign In" onPress={onSignin} - disabled={isPending} + disabled={ + userNamePasswordRequestIsPending || apiKeyValueRequestIsPending + } /> - <Pressable - className={cn( - buttonVariants({ variant: "default" }), - !settings.address && "bg-gray-500", - )} - onPress={() => router.push("/test-connection")} - disabled={!settings.address} - > - <TailwindResolver - comp={(styles) => ( - <Bug size={20} color={styles?.color?.toString()} /> - )} - className="text-background" - /> - </Pressable> </View> + <Pressable onPress={toggleLoginType}> + <Text className="mt-2 text-center text-gray-500"> + {loginType === LoginType.Password + ? "Use API key instead?" + : "Use password instead?"} + </Text> + </Pressable> </View> </TouchableWithoutFeedback> </KeyboardAvoidingView> |
