aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/browser-extension/src/SignInPage.tsx94
-rw-r--r--packages/trpc/routers/apiKeys.ts11
2 files changed, 96 insertions, 9 deletions
diff --git a/apps/browser-extension/src/SignInPage.tsx b/apps/browser-extension/src/SignInPage.tsx
index 4e846070..f1899d5a 100644
--- a/apps/browser-extension/src/SignInPage.tsx
+++ b/apps/browser-extension/src/SignInPage.tsx
@@ -7,14 +7,20 @@ import Logo from "./Logo";
import usePluginSettings from "./utils/settings";
import { api } from "./utils/trpc";
+const enum LoginState {
+ NONE = "NONE",
+ USERNAME_PASSWORD = "USERNAME/PASSWORD",
+ API_KEY = "API_KEY",
+}
+
export default function SignInPage() {
const navigate = useNavigate();
const { setSettings } = usePluginSettings();
const {
mutate: login,
- error,
- isPending,
+ error: usernamePasswordError,
+ isPending: userNamePasswordRequestIsPending,
} = api.apiKeys.exchange.useMutation({
onSuccess: (resp) => {
setSettings((s) => ({ ...s, apiKey: resp.key, apiKeyId: resp.id }));
@@ -22,6 +28,20 @@ export default function SignInPage() {
},
});
+ const {
+ mutate: validateApiKey,
+ error: apiKeyValidationError,
+ isPending: apiKeyValueRequestIsPending,
+ } = api.apiKeys.validate.useMutation({
+ onSuccess: () => {
+ setSettings((s) => ({ ...s, apiKey: apiKeyFormData.apiKey }));
+ navigate("/options");
+ },
+ });
+
+ const [lastLoginAttemptSource, setLastLoginAttemptSource] =
+ useState<LoginState>(LoginState.NONE);
+
const [formData, setFormData] = useState<{
email: string;
password: string;
@@ -30,18 +50,40 @@ export default function SignInPage() {
password: "",
});
- const onSubmit = (e: React.FormEvent) => {
+ const [apiKeyFormData, setApiKeyFormData] = useState<{
+ apiKey: string;
+ }>({
+ apiKey: "",
+ });
+
+ const onUserNamePasswordSubmit = (e: React.FormEvent) => {
e.preventDefault();
+ setLastLoginAttemptSource(LoginState.USERNAME_PASSWORD);
const randStr = (Math.random() + 1).toString(36).substring(5);
login({ ...formData, keyName: `Browser extension: (${randStr})` });
};
+ const onApiKeySubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setLastLoginAttemptSource(LoginState.API_KEY);
+ validateApiKey({ ...apiKeyFormData });
+ };
+
let errorMessage = "";
- if (error) {
- if (error.data?.code == "UNAUTHORIZED") {
+ let loginError;
+ switch (lastLoginAttemptSource) {
+ case LoginState.USERNAME_PASSWORD:
+ loginError = usernamePasswordError;
+ break;
+ case LoginState.API_KEY:
+ loginError = apiKeyValidationError;
+ break;
+ }
+ if (loginError) {
+ if (loginError.data?.code == "UNAUTHORIZED") {
errorMessage = "Wrong username or password";
} else {
- errorMessage = error.message;
+ errorMessage = loginError.message;
}
}
@@ -50,7 +92,10 @@ export default function SignInPage() {
<Logo />
<p className="text-lg">Login</p>
<p className="text-red-500">{errorMessage}</p>
- <form className="flex flex-col gap-y-2" onSubmit={onSubmit}>
+ <form
+ className="flex flex-col gap-y-2"
+ onSubmit={onUserNamePasswordSubmit}
+ >
<div className="flex flex-col gap-y-1">
<label className="my-auto font-bold">Email</label>
<Input
@@ -78,10 +123,43 @@ export default function SignInPage() {
className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
/>
</div>
- <Button type="submit" disabled={isPending}>
+ <Button
+ type="submit"
+ disabled={
+ userNamePasswordRequestIsPending || apiKeyValueRequestIsPending
+ }
+ >
Login
</Button>
</form>
+ <div className="flex w-full flex-row items-center gap-3">
+ <hr className="flex-1" />
+ Or
+ <hr className="flex-1" />
+ </div>
+
+ <form className="flex flex-col gap-y-2" onSubmit={onApiKeySubmit}>
+ <div className="flex flex-col gap-y-1">
+ <label className="my-auto font-bold">API Key</label>
+ <Input
+ value={apiKeyFormData.apiKey}
+ onChange={(e) =>
+ setApiKeyFormData((f) => ({ ...f, apiKey: e.target.value }))
+ }
+ type="text"
+ name="apiKey"
+ className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
+ />
+ </div>
+ <Button
+ type="submit"
+ disabled={
+ userNamePasswordRequestIsPending || apiKeyValueRequestIsPending
+ }
+ >
+ Login with API key
+ </Button>
+ </form>
</div>
);
}
diff --git a/packages/trpc/routers/apiKeys.ts b/packages/trpc/routers/apiKeys.ts
index deeb108f..81e3bb2b 100644
--- a/packages/trpc/routers/apiKeys.ts
+++ b/packages/trpc/routers/apiKeys.ts
@@ -4,7 +4,7 @@ import { z } from "zod";
import { apiKeys } from "@hoarder/db/schema";
-import { generateApiKey, validatePassword } from "../auth";
+import { authenticateApiKey, generateApiKey, validatePassword } from "../auth";
import { authedProcedure, publicProcedure, router } from "../index";
const zApiKeySchema = z.object({
@@ -81,4 +81,13 @@ export const apiKeysAppRouter = router({
}
return await generateApiKey(input.keyName, user.id);
}),
+ validate: publicProcedure
+ .input(z.object({ apiKey: z.string() }))
+ .output(z.object({ success: z.boolean() }))
+ .mutation(async ({ input }) => {
+ await authenticateApiKey(input.apiKey); // Throws if the key is invalid
+ return {
+ success: true,
+ };
+ }),
});