diff --git a/src/app/renderer/src/components/CreateListModal/index.tsx b/src/app/renderer/src/components/CreateListModal/index.tsx
index 10a924d..30c8f15 100644
--- a/src/app/renderer/src/components/CreateListModal/index.tsx
+++ b/src/app/renderer/src/components/CreateListModal/index.tsx
@@ -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);
}}
diff --git a/src/app/renderer/src/components/EditListModal/index.tsx b/src/app/renderer/src/components/EditListModal/index.tsx
index ed6b01c..45c1769 100644
--- a/src/app/renderer/src/components/EditListModal/index.tsx
+++ b/src/app/renderer/src/components/EditListModal/index.tsx
@@ -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);
diff --git a/src/app/renderer/src/components/FormModal/index.tsx b/src/app/renderer/src/components/FormModal/index.tsx
index b4b700a..54c3907 100644
--- a/src/app/renderer/src/components/FormModal/index.tsx
+++ b/src/app/renderer/src/components/FormModal/index.tsx
@@ -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"
/>
);
diff --git a/src/app/renderer/src/components/LibraryEntryModal/index.tsx b/src/app/renderer/src/components/LibraryEntryModal/index.tsx
index 0d08d32..d42ffa7 100644
--- a/src/app/renderer/src/components/LibraryEntryModal/index.tsx
+++ b/src/app/renderer/src/components/LibraryEntryModal/index.tsx
@@ -565,7 +565,7 @@ function NumberInput({ ...props }: { [key: string]: any }) {
{
setValue(e.target.value);
@@ -626,7 +626,7 @@ function DateInput({ ...props }: { [key: string]: any }) {
@@ -639,7 +639,7 @@ function TextArea({ ...props }: { [key: string]: any }) {
return (
diff --git a/src/app/renderer/src/components/ReviewModal/index.tsx b/src/app/renderer/src/components/ReviewModal/index.tsx
index f4c3949..7b0033c 100644
--- a/src/app/renderer/src/components/ReviewModal/index.tsx
+++ b/src/app/renderer/src/components/ReviewModal/index.tsx
@@ -129,7 +129,7 @@ function FormModal(props: Options) {
onSubmit={(e) => {
e.preventDefault();
}}
- autoComplete="none"
+ autoComplete="off"
>
Write a Review
diff --git a/src/app/renderer/src/components/SettingsModal/tabs/connections.tsx b/src/app/renderer/src/components/SettingsModal/tabs/connections.tsx
index b7c2cc7..dbe7099 100644
--- a/src/app/renderer/src/components/SettingsModal/tabs/connections.tsx
+++ b/src/app/renderer/src/components/SettingsModal/tabs/connections.tsx
@@ -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();
@@ -173,7 +175,7 @@ function Account({ account }: { account: ExternalAccount }) {
setIsConnectAccountModalOpen(true);
}}
>
- {account.auth?.username
+ {account.isAuthed
? m(
"settings_connections_account_reconnect"
)
@@ -183,7 +185,7 @@ function Account({ account }: { account: ExternalAccount }) {
-
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" ? (
+ setIsConnectAccountModalOpen(false)}
+ account={account}
+ providerAPI={api}
+ />
+ ) : (
+ setIsConnectAccountModalOpen(false)}
+ account={account}
+ providerAPI={api}
+ />
+ )}
setIsSelectListTypeImportModalOpen(false)}
@@ -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 (
+ {
+ 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 (
+ {
+ account.authorize(props);
+ }}
+ />
+ );
+}
diff --git a/src/app/renderer/src/lib/crypto.ts b/src/app/renderer/src/lib/crypto.ts
new file mode 100644
index 0000000..35d8565
--- /dev/null
+++ b/src/app/renderer/src/lib/crypto.ts
@@ -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;
+}
diff --git a/src/app/renderer/src/lib/db/types.ts b/src/app/renderer/src/lib/db/types.ts
index f8434b6..12c6541 100644
--- a/src/app/renderer/src/lib/db/types.ts
+++ b/src/app/renderer/src/lib/db/types.ts
@@ -124,6 +124,7 @@ export interface LibraryEntry {
startDate: Date | null;
finishDate: Date | null;
notes: string;
+ isPrivate: boolean;
mapping: Mapping;
updatedAt: Date;
}
@@ -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(
diff --git a/src/app/renderer/src/lib/messages/translations/en-US.ts b/src/app/renderer/src/lib/messages/translations/en-US.ts
index 69cfedf..0648708 100644
--- a/src/app/renderer/src/lib/messages/translations/en-US.ts
+++ b/src/app/renderer/src/lib/messages/translations/en-US.ts
@@ -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",
diff --git a/src/app/renderer/src/lib/providers/anilist/index.tsx b/src/app/renderer/src/lib/providers/anilist/index.tsx
index 3f9f0b8..178eb5b 100644
--- a/src/app/renderer/src/lib/providers/anilist/index.tsx
+++ b/src/app/renderer/src/lib/providers/anilist/index.tsx
@@ -1,7 +1,8 @@
-import userQuery from "./queries/user";
-import mediaQuery from "./queries/media";
-import searchQuery from "./queries/search";
-import libraryQuery from "./queries/library";
+import userQuery from "./queries/User";
+import mediaQuery from "./queries/Media";
+import searchQuery from "./queries/Search";
+import viewerQuery from "./queries/Viewer";
+import libraryQuery from "./queries/Library";
import config from "./config";
import { BaseProvider } from "../base";
import {
@@ -16,6 +17,8 @@ import {
MediaType,
UserData,
} from "@/lib/db/types";
+import { decrypt } from "@/lib/crypto";
+import { db } from "@/lib/db";
function normalizeGenre(genre: string): MediaGenre | undefined {
return {
@@ -202,9 +205,11 @@ export class AniList extends BaseProvider {
countryOfOrigin: options.countryOfOrigin,
}
: {}),
- ...(!options.adult ? {
- isAdult: false
- } : {})
+ ...(!options.adult
+ ? {
+ isAdult: false,
+ }
+ : {}),
},
}),
}).then((res) => res.json());
@@ -375,6 +380,7 @@ export class AniList extends BaseProvider {
chapterProgress: entry.progress,
volumeProgress: entry.progressVolumes,
notes: entry.notes,
+ isPrivate: entry.private,
mapping: media.mapping,
updatedAt: new Date(entry.updatedAt),
});
@@ -387,4 +393,43 @@ export class AniList extends BaseProvider {
entries: newLibraryEntries,
};
}
+
+ async authorize(account: ExternalAccount, { code }: { code: string }) {
+ const accessToken = decrypt(
+ code,
+ Buffer.from(
+ sessionStorage.getItem(`Naoka:Provider:${this.name}:OAuthKey`)!,
+ "hex"
+ ),
+ Buffer.from(
+ sessionStorage.getItem(`Naoka:Provider:${this.name}:OAuthIV`)!,
+ "hex"
+ )
+ );
+
+ account.auth = {
+ accessToken: accessToken,
+ };
+
+ const res = await fetch("https://graphql.anilist.co", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ body: JSON.stringify({
+ query: viewerQuery,
+ variables: {
+ accessToken: accessToken,
+ },
+ }),
+ });
+
+ if (!res.ok) throw Error("Could not login");
+
+ account.user = await this.getUser(account);
+
+ await db.externalAccounts.update(account.id!, account);
+ }
}
diff --git a/src/app/renderer/src/lib/providers/anilist/queries/update-review.js b/src/app/renderer/src/lib/providers/anilist/queries/SaveReview.js
similarity index 100%
rename from src/app/renderer/src/lib/providers/anilist/queries/update-review.js
rename to src/app/renderer/src/lib/providers/anilist/queries/SaveReview.js
diff --git a/src/app/renderer/src/lib/providers/anilist/queries/Viewer.js b/src/app/renderer/src/lib/providers/anilist/queries/Viewer.js
new file mode 100644
index 0000000..fdab9a0
--- /dev/null
+++ b/src/app/renderer/src/lib/providers/anilist/queries/Viewer.js
@@ -0,0 +1,10 @@
+export default `
+query {
+ Viewer {
+ id
+ name
+ avatar {
+ large
+ }
+ }
+}`;
diff --git a/src/app/renderer/src/lib/providers/anilist/queries/library.js b/src/app/renderer/src/lib/providers/anilist/queries/library.js
index 270998d..33a0a84 100644
--- a/src/app/renderer/src/lib/providers/anilist/queries/library.js
+++ b/src/app/renderer/src/lib/providers/anilist/queries/library.js
@@ -27,6 +27,7 @@ query Library($username: String, $type: MediaType, $page: Int) {
day
}
notes
+ private
updatedAt
media {
id
diff --git a/src/app/renderer/src/lib/providers/base.ts b/src/app/renderer/src/lib/providers/base.ts
index ffbb3c5..97e674e 100644
--- a/src/app/renderer/src/lib/providers/base.ts
+++ b/src/app/renderer/src/lib/providers/base.ts
@@ -24,7 +24,7 @@ export interface Config {
mediaTypes: MediaType[];
syncing?: {
auth: {
- type: "oauth" | "basic" | "client" | "none";
+ type: "oauth" | "basic" | "username" | "none";
};
import?: {
// Importable list types.
@@ -108,4 +108,11 @@ export class BaseProvider {
*/
throw Error("Not implemented");
}
+
+ async authorize(account: ExternalAccount, props: { [key: string]: any }) {
+ /*
+ Authorizes and updates the external account.
+ */
+ throw Error("Not implemented");
+ }
}
diff --git a/src/app/renderer/src/lib/providers/index.ts b/src/app/renderer/src/lib/providers/index.ts
index 708df82..2a08113 100644
--- a/src/app/renderer/src/lib/providers/index.ts
+++ b/src/app/renderer/src/lib/providers/index.ts
@@ -103,4 +103,8 @@ export class ProviderAPI {
async getUser(account: ExternalAccount): Promise {
return this.api.getUser(account);
}
+
+ async authorize(account: ExternalAccount, props: { [key: string]: any }) {
+ return this.api.authorize(account, props);
+ }
}
diff --git a/src/app/renderer/src/lib/providers/myanimelist/config.ts b/src/app/renderer/src/lib/providers/myanimelist/config.ts
index 4e653a3..9af6c9e 100644
--- a/src/app/renderer/src/lib/providers/myanimelist/config.ts
+++ b/src/app/renderer/src/lib/providers/myanimelist/config.ts
@@ -5,7 +5,7 @@ const config: Config = {
mediaTypes: ["anime", "manga"],
syncing: {
auth: {
- type: "oauth",
+ type: "username",
},
import: {
mediaTypes: ["anime", "manga"],
diff --git a/src/app/renderer/src/lib/providers/myanimelist/index.tsx b/src/app/renderer/src/lib/providers/myanimelist/index.tsx
index 7877eaf..74e690e 100644
--- a/src/app/renderer/src/lib/providers/myanimelist/index.tsx
+++ b/src/app/renderer/src/lib/providers/myanimelist/index.tsx
@@ -322,6 +322,7 @@ export class MyAnimeList extends BaseProvider {
startDate: entry.start_date ? new Date(entry.start_date) : null,
finishDate: entry.finish_date ? new Date(entry.finish_date) : null,
notes: entry.comments,
+ isPrivate: false,
updatedAt: new Date(entry.updated_at),
mapping,
};