Skip to content

Commit

Permalink
feature(mobile): Allow users to login with API keys in the mobile app
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Sep 22, 2024
1 parent 26521b7 commit 5fe330c
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 75 deletions.
10 changes: 10 additions & 0 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app/dashboard/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" />;
}
}
87 changes: 87 additions & 0 deletions apps/mobile/app/server-address.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
188 changes: 115 additions & 73 deletions apps/mobile/app/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,87 @@ 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;
}>({
email: "",
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 (
Expand All @@ -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>
Expand Down

0 comments on commit 5fe330c

Please sign in to comment.