Skip to content

Commit

Permalink
Merge pull request #166 from Nekidev/13-add-support-for-authorizing-p…
Browse files Browse the repository at this point in the history
…roviders

update(app): Add support for OAuth2 login
  • Loading branch information
Nekidev authored Jan 26, 2024
2 parents 631b2e2 + 429bece commit 4de0a72
Show file tree
Hide file tree
Showing 17 changed files with 255 additions and 70 deletions.
2 changes: 1 addition & 1 deletion src/app/renderer/src/components/CreateListModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default function CreateListModal({
className="w-full border border-zinc-900 rounded bg-zinc-900 p-2 leading-none outline-none placeholder:text-zinc-400 focus:border-zinc-100 transition"
placeholder="Name"
value={name}
autoComplete="none"
autoComplete="off"
onChange={(e) => {
setName(e.target.value);
}}
Expand Down
2 changes: 1 addition & 1 deletion src/app/renderer/src/components/EditListModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default function EditListModal({
type="text"
className="w-full border border-zinc-900 rounded bg-zinc-900 p-2 leading-none outline-none placeholder:text-zinc-400 focus:border-zinc-100 transition"
placeholder="Name"
autoComplete="none"
autoComplete="off"
value={name}
onChange={(e) => {
setName(e.target.value);
Expand Down
2 changes: 1 addition & 1 deletion src/app/renderer/src/components/FormModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function FormModalContent(props: FormModalProps) {
);
}}
className="leading-none p-2 rounded bg-zinc-900 w-full border border-zinc-900 focus:border-zinc-100 transition placeholder:text-zinc-400"
autoComplete="none"
autoComplete="off"
/>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions src/app/renderer/src/components/LibraryEntryModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ function NumberInput({ ...props }: { [key: string]: any }) {
<div className="w-full relative">
<input
type="number"
autoComplete="none"
autoComplete="off"
className="rounded p-2 leading-none bg-zinc-900 w-full text-sm outline-none peer h-8"
onChange={(e) => {
setValue(e.target.value);
Expand Down Expand Up @@ -626,7 +626,7 @@ function DateInput({ ...props }: { [key: string]: any }) {
<div className="w-full relative">
<input
type="date"
autoComplete="none"
autoComplete="off"
className="rounded p-2 leading-none bg-zinc-900 w-full text-sm outline-none peer h-8 placeholder:text-zinc-400"
{...props}
/>
Expand All @@ -639,7 +639,7 @@ function TextArea({ ...props }: { [key: string]: any }) {
return (
<div className="w-full relative">
<textarea
autoComplete="none"
autoComplete="off"
className="rounded px-2 py-1.5 bg-zinc-900 w-full text-sm outline-none peer placeholder:text-zinc-400 resize-y"
{...props}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/app/renderer/src/components/ReviewModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function FormModal(props: Options) {
onSubmit={(e) => {
e.preventDefault();
}}
autoComplete="none"
autoComplete="off"
>
<div className="flex flex-col gap-2 p-4">
<h2 className="text-xl leading-none">Write a Review</h2>
Expand Down
176 changes: 125 additions & 51 deletions src/app/renderer/src/components/SettingsModal/tabs/connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { ProviderAPI, providers } from "@/lib/providers";
import { ExternalAccount, ImportMethod, MediaType } from "@/lib/db/types";
import { useMessages } from "@/lib/messages";
import { InputType } from "@/lib/forms";
import { open } from "@tauri-apps/api/shell";
import * as crypto from "crypto";

export default function Connections() {
const m = useMessages();
Expand Down Expand Up @@ -173,7 +175,7 @@ function Account({ account }: { account: ExternalAccount }) {
setIsConnectAccountModalOpen(true);
}}
>
{account.auth?.username
{account.isAuthed
? m(
"settings_connections_account_reconnect"
)
Expand All @@ -183,7 +185,7 @@ function Account({ account }: { account: ExternalAccount }) {
<div className="flex flex-row items-center gap-2">
<Button
disabled={
!account.auth?.username ||
!account.isAuthed ||
api.config.syncing?.import?.mediaTypes
.length === 0
}
Expand All @@ -201,55 +203,21 @@ function Account({ account }: { account: ExternalAccount }) {
</div>
</div>
</div>
<FormModal
isOpen={isConnectAccountModalOpen}
closeModal={() => setIsConnectAccountModalOpen(false)}
steps={[
{
title: m("settings_connections_connect_title", {
provider: api.name,
}),
subtitle: m("settings_connections_connect_subtitle"),
fields: [
{
type: InputType.Text,
name: "username",
label: m(
"settings_connections_connect_username"
),
defaultValue: account.auth?.username || "",
},
],
},
]}
onSubmit={({ username }) => {
const oldAuth = account.auth;
const newAuth = {
...(oldAuth || {}),
username,
};

db.externalAccounts
.update(account.id!, {
auth: newAuth,
})
.then(() => {
account.auth = newAuth;
api.getUser(account)
.then((user) => {
db.externalAccounts.update(account.id!, {
user,
});
})
.catch((e) => {
// Revert changes
db.externalAccounts.update(account.id!, {
auth: oldAuth,
});
});
});
}}
/>
{api.config.syncing?.auth.type === "username" ? (
<ConnectAccountUsernameModal
isOpen={isConnectAccountModalOpen}
closeModal={() => setIsConnectAccountModalOpen(false)}
account={account}
providerAPI={api}
/>
) : (
<ConnectAccountOAuthModal
isOpen={isConnectAccountModalOpen}
closeModal={() => setIsConnectAccountModalOpen(false)}
account={account}
providerAPI={api}
/>
)}
<FormModal
isOpen={isSelectListTypeImportModalOpen}
closeModal={() => setIsSelectListTypeImportModalOpen(false)}
Expand Down Expand Up @@ -405,3 +373,109 @@ function Account({ account }: { account: ExternalAccount }) {
</>
);
}

function ConnectAccountUsernameModal({
isOpen,
closeModal,
account,
providerAPI,
}: {
isOpen: boolean;
closeModal: () => void;
account: ExternalAccount;
providerAPI: ProviderAPI;
}) {
const m = useMessages();

return (
<FormModal
isOpen={isOpen}
closeModal={closeModal}
steps={[
{
title: m("settings_connections_connect_username_title", {
provider: providerAPI.name,
}),
subtitle: m(
"settings_connections_connect_username_subtitle"
),
fields: [
{
type: InputType.Text,
name: "username",
label: m(
"settings_connections_connect_username_username"
),
defaultValue: account.auth?.username || "",
},
],
},
]}
onSubmit={(props) => {
account.authorize(props);
}}
/>
);
}

function ConnectAccountOAuthModal({
isOpen,
closeModal,
account,
providerAPI,
}: {
isOpen: boolean;
closeModal: () => void;
account: ExternalAccount;
providerAPI: ProviderAPI;
}) {
const m = useMessages();

React.useEffect(() => {
if (isOpen) {
const key = crypto.randomBytes(32).toString("hex");
const iv = crypto.randomBytes(16).toString("hex");

sessionStorage.setItem(
`Naoka:Provider:${providerAPI.name}:OAuthKey`,
key
);
sessionStorage.setItem(
`Naoka:Provider:${providerAPI.name}:OAuthIV`,
iv
);

open(
`https://naoka.nyeki.dev/api/auth/oauth2/anilist?key=${encodeURIComponent(
key
)}&iv=${encodeURIComponent(iv)}`
);
}
}, [isOpen]);

return (
<FormModal
isOpen={isOpen}
closeModal={closeModal}
steps={[
{
title: m("settings_connections_connect_oauth_title", {
provider: providerAPI.name,
}),
subtitle: m("settings_connections_connect_oauth_subtitle"),
fields: [
{
type: InputType.Text,
name: "code",
label: m("settings_connections_connect_oauth_code"),
defaultValue: account.auth?.username || "",
},
],
},
]}
onSubmit={(props) => {
account.authorize(props);
}}
/>
);
}
17 changes: 17 additions & 0 deletions src/app/renderer/src/lib/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as crypto from "crypto";


export function encrypt(text: string, key: Buffer, iv: Buffer): string {
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(text, 'utf-8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}

// Decryption
export function decrypt(encryptedText: string, key: Buffer, iv: Buffer): string {
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}
23 changes: 23 additions & 0 deletions src/app/renderer/src/lib/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export interface LibraryEntry {
startDate: Date | null;
finishDate: Date | null;
notes: string;
isPrivate: boolean;
mapping: Mapping;
updatedAt: Date;
}
Expand Down Expand Up @@ -203,6 +204,28 @@ export class ExternalAccount extends Data {
return api.getUser(this);
}

async authorize(props: { [key: string]: any }) {
const api = new ProviderAPI(this.provider);
return api.authorize(this, props);
}

get isAuthed(): boolean {
const api = new ProviderAPI(this.provider);

switch (api.config.syncing?.auth.type) {
case "username":
return !!this.auth?.username;

case "oauth":
return !!this.auth?.accessToken;

case "basic":
return !!this.auth?.username && !!this.auth?.password;
}

return false;
}

// I burnt out my brain working on this function, so I wouldn't be suprised
// if you cannot understand how tf it works.
async importLibrary(
Expand Down
9 changes: 6 additions & 3 deletions src/app/renderer/src/lib/messages/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,12 @@ const messages = {
settings_connections_account_reconnect: "Reconnect",
settings_connections_account_import: "Import",
settings_connections_account_export: "Export",
settings_connections_connect_title: "Connect to {provider}",
settings_connections_connect_subtitle: "Link your account to import your lists",
settings_connections_connect_username: "Username",
settings_connections_connect_username_title: "Connect to {provider}",
settings_connections_connect_username_subtitle: "Link your account to import your lists",
settings_connections_connect_username_username: "Username",
settings_connections_connect_oauth_title: "Connect to {provider}",
settings_connections_connect_oauth_subtitle: "Link your account to import your lists",
settings_connections_connect_oauth_code: "Code",
settings_connections_connect_import_title: "Select the list to import",
settings_connections_connect_import_subtitle: "Select your list from {provider}",
settings_connections_connect_import_anime_title: "Anime list",
Expand Down
Loading

0 comments on commit 4de0a72

Please sign in to comment.