Skip to content

Commit 2faecb8

Browse files
committed
feat: API key
1 parent 1a299e2 commit 2faecb8

File tree

12 files changed

+191
-15
lines changed

12 files changed

+191
-15
lines changed

anify-backend/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ MANGAProxies.json
5252
METAProxies.json
5353
proxy-config.json
5454
configs
55-
.wrangler
55+
.wrangler
56+
test.ts
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ApiKeyRepository } from "../../../../database/impl/wrapper/impl/apiKey";
2+
import { IApiKey } from "../../../../types/impl/database/impl/schema/apiKey";
3+
import { redis } from "../../..";
4+
import createResponse from "./response";
5+
import { db } from "../../../../database";
6+
7+
export default async (req: Request): Promise<IApiKey | Response> => {
8+
const apiKey = req.headers.get("X-API-Key") ?? new URL(req.url).searchParams.get("apikey");
9+
10+
if (!apiKey) {
11+
return createResponse(JSON.stringify({ error: "No API key provided." }), 401);
12+
}
13+
14+
const cachedKey = await redis.get(`api-key:${apiKey}`);
15+
if (cachedKey) {
16+
try {
17+
const parsedKey = JSON.parse(cachedKey);
18+
if (typeof parsedKey.valid === "boolean" && parsedKey.valid === false) {
19+
return createResponse(JSON.stringify({ error: "Invalid API key." }), 401);
20+
}
21+
return parsedKey as IApiKey;
22+
} catch (e) {
23+
console.error("Failed to parse cached API key:", e);
24+
}
25+
}
26+
27+
const dbApiKey = await ApiKeyRepository.getByKey(db, apiKey);
28+
29+
if (!dbApiKey) {
30+
await redis.set(`api-key:${apiKey}`, JSON.stringify({ valid: false }), "EX", 60);
31+
return createResponse(JSON.stringify({ error: "Invalid API key." }), 401);
32+
}
33+
34+
await redis.set(`api-key:${apiKey}`, JSON.stringify(dbApiKey), "EX", 3600);
35+
36+
return dbApiKey;
37+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { IColumnDefinition } from "../../../../types/impl/database";
2+
3+
const apiKeySchema = [
4+
{ name: "id", type: "TEXT", primaryKey: true, defaultValue: "gen_random_uuid()" },
5+
{ name: "key", type: "TEXT", unique: true },
6+
{ name: "autoCharge", type: "JSONB", defaultValue: `'{"enabled": false, "autoChargeThreshold": 250, "autoChargeAmount": 500}'::JSONB` },
7+
{ name: "ignoreAutoLimits", type: "BOOLEAN", defaultValue: "false" },
8+
{ name: "limits", type: "JSONB", defaultValue: `'{"premiumLimit": 0}'::JSONB` },
9+
{ name: "stats", type: "JSONB", defaultValue: `'{"generatedAt": "2025-02-20T01:57:29.355Z", "lastReset": "2025-05-15T00:35:05.426Z", "premiumUsed": 0, "additionalPurchasedUsed": 1}'::JSONB` },
10+
{ name: "subId", type: "TEXT" },
11+
{ name: "webhooks", type: "TEXT[]", defaultValue: "'{}'::TEXT[]" },
12+
{ name: "createdAt", type: "TIMESTAMP", defaultValue: "NOW()" },
13+
{ name: "updatedAt", type: "TIMESTAMP", defaultValue: "NOW()" },
14+
] as IColumnDefinition[];
15+
16+
export default apiKeySchema;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { IApiKey } from "../../../../types/impl/database/impl/schema/apiKey";
2+
import DatabaseHandler from "../../handler";
3+
4+
export class ApiKeyRepository {
5+
/**
6+
* @description The table name for API keys.
7+
*/
8+
public static readonly tableName = "anify.api_key";
9+
10+
/**
11+
* @method getById Retrieves a single API key record by ID.
12+
* @param db The DatabaseHandler instance.
13+
* @param id The ID of the API key to retrieve.
14+
* @returns IApiKey or null if not found.
15+
*/
16+
public static async getById(db: DatabaseHandler, id: string): Promise<IApiKey | null> {
17+
const rows = await db.select<IApiKey>(ApiKeyRepository.tableName, { id });
18+
return rows.length > 0 ? rows[0] : null;
19+
}
20+
21+
/**
22+
* @method getByKey Retrieves a single API key record by key.
23+
* @param db The DatabaseHandler instance.
24+
* @param key The key of the API key to retrieve.
25+
* @returns IApiKey or null if not found.
26+
*/
27+
public static async getByKey(db: DatabaseHandler, key: string): Promise<IApiKey | null> {
28+
const rows = await db.select<IApiKey>(ApiKeyRepository.tableName, { key });
29+
return rows.length > 0 ? rows[0] : null;
30+
}
31+
32+
/**
33+
* @method insert Inserts a new API key record and returns the newly created row.
34+
* @param db The DatabaseHandler instance.
35+
* @param apiKey The API key object to insert.
36+
* @returns The inserted API key row (with defaults applied).
37+
*/
38+
public static async insert(db: DatabaseHandler, apiKey: IApiKey): Promise<IApiKey> {
39+
const inserted = await db.insert<IApiKey>(ApiKeyRepository.tableName, apiKey);
40+
return inserted;
41+
}
42+
43+
/**
44+
* @method updatePartially Updates a subset of fields in the API key record.
45+
* @param db The DatabaseHandler instance.
46+
* @param id The API key ID to update.
47+
* @param newData The fields to update.
48+
*/
49+
public static async updatePartially(db: DatabaseHandler, id: string, newData: Partial<IApiKey>): Promise<void> {
50+
await db.update<IApiKey>(ApiKeyRepository.tableName, newData, { id });
51+
}
52+
53+
/**
54+
* @method deleteById
55+
* Deletes an API key record by its ID.
56+
*
57+
* @param db The DatabaseHandler instance.
58+
* @param id The ID of the record to delete.
59+
*/
60+
public static async deleteById(db: DatabaseHandler, id: string): Promise<void> {
61+
const sql = `
62+
DELETE FROM "${ApiKeyRepository.tableName}"
63+
WHERE "id" = $1
64+
`;
65+
await db.query(sql, [id]);
66+
}
67+
68+
/**
69+
* @method countAll
70+
* Returns the total number of API keys in the API key table.
71+
*
72+
* @param db The DatabaseHandler instance.
73+
*/
74+
public static async countAll(db: DatabaseHandler): Promise<number> {
75+
const sql = `
76+
SELECT COUNT(*) AS "total"
77+
FROM "${ApiKeyRepository.tableName}"
78+
`;
79+
const result = await db.query(sql);
80+
// Convert the result's string to a number
81+
return parseInt(result.rows[0].total, 10);
82+
}
83+
84+
/**
85+
* @method fetchAll Retrieves all API key records.
86+
* @param db The DatabaseHandler instance.
87+
* @returns Array of IApiKey objects.
88+
*/
89+
public static async fetchAll(db: DatabaseHandler): Promise<IApiKey[]> {
90+
return db.select<IApiKey>(ApiKeyRepository.tableName);
91+
}
92+
}

anify-backend/src/database/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { env } from "../env";
22
import DatabaseHandler from "./impl/handler";
33
import SchemaBuilder from "./impl/schema";
44
import animeSchema from "./impl/schema/impl/anime";
5+
import apiKeySchema from "./impl/schema/impl/apiKey";
56
import mangaSchema from "./impl/schema/impl/manga";
67
import skipTimesSchema from "./impl/schema/impl/skipTimes";
78
import { AnimeRepository } from "./impl/wrapper/impl/anime";
9+
import { ApiKeyRepository } from "./impl/wrapper/impl/apiKey";
810
import { MangaRepository } from "./impl/wrapper/impl/manga";
911
import { SkipTimesRepository } from "./impl/wrapper/impl/skipTimes";
1012

11-
export const schemaBuilder = new SchemaBuilder().defineTable(AnimeRepository.tableName, animeSchema).defineTable(MangaRepository.tableName, mangaSchema).defineTable(SkipTimesRepository.tableName, skipTimesSchema);
13+
export const schemaBuilder = new SchemaBuilder().defineTable(AnimeRepository.tableName, animeSchema).defineTable(ApiKeyRepository.tableName, apiKeySchema).defineTable(MangaRepository.tableName, mangaSchema).defineTable(SkipTimesRepository.tableName, skipTimesSchema);
1214

1315
export const db = new DatabaseHandler(
1416
{

anify-backend/src/mappings/impl/anime/impl/hianime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ export default class HiAnime extends AnimeProvider {
88
override rateLimit = 0;
99
override maxConcurrentRequests: number = -1;
1010
override id = "hianime";
11-
override url = "https://hianime.to";
11+
override url = "https://hianimez.to";
1212

13-
public needsProxy: boolean = false;
13+
public needsProxy: boolean = true;
1414
public useGoogleTranslate: boolean = false;
1515

1616
override formats: MediaFormat[] = [MediaFormat.MOVIE, MediaFormat.ONA, MediaFormat.OVA, MediaFormat.SPECIAL, MediaFormat.TV, MediaFormat.TV_SHORT];

anify-backend/src/mappings/impl/base/impl/anilist.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,7 @@ export default class AniListBase extends BaseProvider {
546546
});
547547

548548
const data: IMedia = ((await req.json()) as { data: { Media: IMedia } }).data?.Media;
549+
if (data.isAdult === undefined) console.log(data);
549550

550551
if (data.isAdult) return undefined;
551552

anify-backend/src/mappings/impl/meta/impl/anilist.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export default class AniListMeta extends MetaProvider {
7676
},
7777
});
7878
const json = (await req?.json()) as { data: { Page: { media: Media[] } } };
79+
if (json?.data?.Page?.media === undefined) console.log(json);
7980
const media = json?.data?.Page?.media;
8081

8182
media.map((data: Media) => {

anify-backend/src/mappings/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ export const META_PROVIDERS = [
103103
const { default: AniListMeta } = await import("./impl/meta/impl/anilist");
104104
return new AniListMeta();
105105
},
106-
async () => {
107-
const { default: AniDBMeta } = await import("./impl/meta/impl/anidb");
108-
return new AniDBMeta();
109-
},
106+
// async () => {
107+
// const { default: AniDBMeta } = await import("./impl/meta/impl/anidb");
108+
// return new AniDBMeta();
109+
// },
110110
async () => {
111111
const { default: KitsuMeta } = await import("./impl/meta/impl/kitsu");
112112
return new KitsuMeta();

anify-backend/src/proxies/impl/manager/impl/file/saveProviderProxies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export async function saveProviderProxies(providerType: ProviderType): Promise<v
6767
// Check if any provider for this proxy has valid metrics
6868
for (const providerId in proxyCache.validProxies[providerType]) {
6969
const metrics = proxy.providerMetrics[providerId];
70-
if (metrics && metrics.healthScore > 0 && metrics.consecutiveFailures < 3) {
70+
if (metrics && metrics.healthScore >= 0 && metrics.consecutiveFailures < 3) {
7171
return true;
7272
}
7373
}

0 commit comments

Comments
 (0)