Skip to content

Commit

Permalink
feature(extension): Allow login directly with an API key
Browse files Browse the repository at this point in the history
* [Feature request] NextAuth Providers for OAuth/SSO #92
Added API key based authentication to the extension to make the extension usable when OAuth is in use

* Minor UI tweak

---------

Co-authored-by: MohamedBassem <me@mbassem.com>
  • Loading branch information
kamtschatka and MohamedBassem authored Sep 21, 2024
1 parent 9dd6f21 commit 26521b7
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 9 deletions.
94 changes: 86 additions & 8 deletions apps/browser-extension/src/SignInPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,41 @@ 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 }));
navigate("/options");
},
});

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

Expand All @@ -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
Expand Down Expand Up @@ -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>
);
}
11 changes: 10 additions & 1 deletion packages/trpc/routers/apiKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
};
}),
});

0 comments on commit 26521b7

Please sign in to comment.