mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-13 17:58:42 +03:00
feat: kai
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { MakeAction } from "#lib/actions/actions.js";
|
||||
import DB from "#services/pg/db.js";
|
||||
import { Random20Hex } from "#utils/misc.js";
|
||||
|
||||
export const ACTION_CreateOAuth2AuthCode = MakeAction(
|
||||
"CREATE_OAUTH2_AUTH_CODE",
|
||||
async (taker) => {
|
||||
const code = Random20Hex();
|
||||
const createdOn = Date.now();
|
||||
|
||||
await DB.insertInto("priv_oauth2_auth_token")
|
||||
.values({
|
||||
token: code,
|
||||
user_id: taker.acct.id,
|
||||
created_on: new Date(createdOn).toISOString(),
|
||||
})
|
||||
.execute();
|
||||
|
||||
return {
|
||||
code,
|
||||
userID: taker.acct.id,
|
||||
createdOn,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MakeAction } from "#lib/actions/actions.js";
|
||||
import DB from "#services/pg/db.js";
|
||||
|
||||
export const ACTION_RevokeKaiAuthToken = MakeAction(
|
||||
"REVOKE_KAI_AUTH_TOKEN",
|
||||
async (taker, { service }) => {
|
||||
await DB.deleteFrom("priv_svc_kai_auth_token")
|
||||
.where("user_id", "=", taker.acct.id)
|
||||
.where("service", "=", service)
|
||||
.execute();
|
||||
|
||||
return {};
|
||||
},
|
||||
);
|
||||
@@ -6,15 +6,17 @@ interface SeedApiClientOpts {
|
||||
clientId: string;
|
||||
authorId: number;
|
||||
name?: string;
|
||||
clientSecret?: string;
|
||||
submitScore?: boolean;
|
||||
customiseProfile?: boolean;
|
||||
redirectUri?: string | null;
|
||||
}
|
||||
|
||||
export async function seedApiClient(opts: SeedApiClientOpts) {
|
||||
await DB.insertInto("priv_api_client")
|
||||
.values({
|
||||
client_id: opts.clientId,
|
||||
client_secret: "CS_test_secret",
|
||||
client_secret: opts.clientSecret ?? "CS_test_secret",
|
||||
name: opts.name ?? "Test Client",
|
||||
author: opts.authorId,
|
||||
pm_submit_score: opts.submitScore ?? null,
|
||||
@@ -28,7 +30,7 @@ export async function seedApiClient(opts: SeedApiClientOpts) {
|
||||
api_key_template: null,
|
||||
api_key_filename: null,
|
||||
webhook_uri: null,
|
||||
redirect_uri: null,
|
||||
redirect_uri: opts.redirectUri ?? null,
|
||||
is_builtin: false,
|
||||
})
|
||||
.execute();
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MakeAction } from "#lib/actions/actions.js";
|
||||
import { upsertKaiAuthTokensInDb } from "#lib/kai-auth-token/persist.js";
|
||||
import DB from "#services/pg/db.js";
|
||||
|
||||
export const ACTION_UpsertKaiAuthToken = MakeAction(
|
||||
"UPSERT_KAI_AUTH_TOKEN",
|
||||
async (taker, { service, token, refreshToken }) => {
|
||||
await upsertKaiAuthTokensInDb(DB, taker.acct.id, service, token, refreshToken);
|
||||
|
||||
return {};
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,93 @@
|
||||
import { MakeAnonAction } from "#lib/actions/actions";
|
||||
import { SELECT_API_TOKEN, ToAPITokenDocument } from "#lib/db-formats/api-token";
|
||||
import DB from "#services/pg/db";
|
||||
import { Random20Hex } from "#utils/misc";
|
||||
import { ExpectedErr } from "bliss";
|
||||
|
||||
const SELECT_OAUTH_CLIENT = [
|
||||
"priv_api_client.client_id",
|
||||
"priv_api_client.client_secret",
|
||||
"priv_api_client.name",
|
||||
"priv_api_client.redirect_uri",
|
||||
"priv_api_client.pm_customise_profile",
|
||||
"priv_api_client.pm_customise_score",
|
||||
"priv_api_client.pm_customise_session",
|
||||
"priv_api_client.pm_delete_score",
|
||||
"priv_api_client.pm_manage_rivals",
|
||||
"priv_api_client.pm_manage_targets",
|
||||
"priv_api_client.pm_submit_score",
|
||||
"priv_api_client.pm_manage_challenges",
|
||||
] as const;
|
||||
|
||||
export const ANON_ACTION_OAuthTokenExchange = MakeAnonAction(
|
||||
"OAUTH_TOKEN_EXCHANGE",
|
||||
async (_taker, input) => {
|
||||
const client = await DB.selectFrom("priv_api_client")
|
||||
.select(SELECT_OAUTH_CLIENT)
|
||||
.where("client_id", "=", input.client_id)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!client) {
|
||||
throw new ExpectedErr(404, `This client does not exist.`);
|
||||
}
|
||||
|
||||
if (client.client_secret !== input.client_secret) {
|
||||
throw new ExpectedErr(403, `Invalid secret.`);
|
||||
}
|
||||
|
||||
if (client.redirect_uri !== input.redirect_uri) {
|
||||
throw new ExpectedErr(
|
||||
400,
|
||||
`This redirect_uri does not match with your registered client redirect_uri ${client.redirect_uri}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const apiToken = Random20Hex();
|
||||
const identifier = `${client.name} Token`;
|
||||
|
||||
await DB.transaction().execute(async (txn) => {
|
||||
const deleted = await txn
|
||||
.deleteFrom("priv_oauth2_auth_token")
|
||||
.where("token", "=", input.code)
|
||||
.returning("user_id")
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!deleted) {
|
||||
throw new ExpectedErr(404, `This code does not exist.`);
|
||||
}
|
||||
|
||||
await txn
|
||||
.insertInto("priv_api_token")
|
||||
.values({
|
||||
token: apiToken,
|
||||
user_id: deleted.user_id,
|
||||
identifier,
|
||||
from_oauth2_client: client.client_id,
|
||||
pm_customise_profile: client.pm_customise_profile,
|
||||
pm_customise_score: client.pm_customise_score,
|
||||
pm_customise_session: client.pm_customise_session,
|
||||
pm_delete_score: client.pm_delete_score,
|
||||
pm_manage_rivals: client.pm_manage_rivals,
|
||||
pm_manage_targets: client.pm_manage_targets,
|
||||
pm_submit_score: client.pm_submit_score,
|
||||
pm_manage_challenges: client.pm_manage_challenges,
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
|
||||
const row = await DB.selectFrom("priv_api_token")
|
||||
.select(SELECT_API_TOKEN)
|
||||
.where("token", "=", apiToken)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const doc = ToAPITokenDocument(row);
|
||||
|
||||
return {
|
||||
userID: Number(doc.userID),
|
||||
token: doc.token!,
|
||||
identifier: doc.identifier,
|
||||
permissions: doc.permissions as Record<string, boolean>,
|
||||
fromAPIClient: doc.fromAPIClient,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -258,6 +258,28 @@ export const ActionSignatures = {
|
||||
consumedBy: z.null(),
|
||||
}),
|
||||
},
|
||||
UPSERT_KAI_AUTH_TOKEN: {
|
||||
input: z.object({
|
||||
service: z.enum(["EAG", "FLO", "MIN"]),
|
||||
token: z.string(),
|
||||
refreshToken: z.string(),
|
||||
}),
|
||||
output: z.object({}),
|
||||
},
|
||||
REVOKE_KAI_AUTH_TOKEN: {
|
||||
input: z.object({
|
||||
service: z.enum(["EAG", "FLO", "MIN"]),
|
||||
}),
|
||||
output: z.object({}),
|
||||
},
|
||||
CREATE_OAUTH2_AUTH_CODE: {
|
||||
input: z.object({}),
|
||||
output: z.object({
|
||||
code: z.string(),
|
||||
userID: z.number().int(),
|
||||
createdOn: z.number().int(),
|
||||
}),
|
||||
},
|
||||
} satisfies Record<string, ActionSignature>;
|
||||
|
||||
export const AnonActionSignatures = {
|
||||
@@ -296,6 +318,22 @@ export const AnonActionSignatures = {
|
||||
userID: z.number().int(),
|
||||
}),
|
||||
},
|
||||
OAUTH_TOKEN_EXCHANGE: {
|
||||
input: z.object({
|
||||
client_id: z.string(),
|
||||
client_secret: z.string(),
|
||||
grant_type: z.literal("authorization_code"),
|
||||
redirect_uri: z.string(),
|
||||
code: z.string(),
|
||||
}),
|
||||
output: z.object({
|
||||
userID: z.number().int(),
|
||||
token: z.string(),
|
||||
identifier: z.string(),
|
||||
permissions: z.record(z.string(), z.boolean()),
|
||||
fromAPIClient: z.string().nullable(),
|
||||
}),
|
||||
},
|
||||
} satisfies Record<string, ActionSignature>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type Selection } from "kysely";
|
||||
import { type KaiAuthDocument } from "tachi-common";
|
||||
import { type Database } from "tachi-db";
|
||||
|
||||
export const SELECT_KAI_AUTH_TOKEN = [
|
||||
"priv_svc_kai_auth_token.user_id",
|
||||
"priv_svc_kai_auth_token.service",
|
||||
"priv_svc_kai_auth_token.token",
|
||||
"priv_svc_kai_auth_token.refresh_token",
|
||||
] as const;
|
||||
|
||||
export function ToKaiAuthDocument(
|
||||
row: Selection<Database, "priv_svc_kai_auth_token", (typeof SELECT_KAI_AUTH_TOKEN)[number]>,
|
||||
): KaiAuthDocument {
|
||||
return {
|
||||
userID: row.user_id,
|
||||
service: row.service as KaiAuthDocument["service"],
|
||||
token: row.token,
|
||||
refreshToken: row.refresh_token,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { type Kysely, type Transaction } from "kysely";
|
||||
import { type Database } from "tachi-db";
|
||||
|
||||
export type KaiService = "EAG" | "FLO" | "MIN";
|
||||
|
||||
export async function upsertKaiAuthTokensInDb(
|
||||
db: Kysely<Database> | Transaction<Database>,
|
||||
userId: number,
|
||||
service: KaiService,
|
||||
token: string,
|
||||
refreshToken: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.insertInto("priv_svc_kai_auth_token")
|
||||
.values({
|
||||
user_id: userId,
|
||||
service,
|
||||
token,
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc.columns(["user_id", "service"]).doUpdateSet({
|
||||
token,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function updateKaiAuthTokensInDb(
|
||||
db: Kysely<Database> | Transaction<Database>,
|
||||
userId: number,
|
||||
service: KaiService,
|
||||
token: string,
|
||||
refreshToken: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.updateTable("priv_svc_kai_auth_token")
|
||||
.set({
|
||||
token,
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
.where("user_id", "=", userId)
|
||||
.where("service", "=", service)
|
||||
.execute();
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import type { KaiAuthDocument } from "tachi-common";
|
||||
|
||||
import { log } from "#lib/log/log.js";
|
||||
import { ServerConfig } from "#lib/setup/config";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import { MockJSONFetch } from "#test-utils/mock-fetch";
|
||||
import ResetDBState from "#test-utils/resets";
|
||||
import deepmerge from "deepmerge";
|
||||
import t from "tap";
|
||||
|
||||
import { CreateKaiReauthFunction } from "./reauth";
|
||||
|
||||
const authDoc: KaiAuthDocument = {
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
service: "FLO",
|
||||
token: "foobar",
|
||||
userID: 1,
|
||||
};
|
||||
|
||||
t.test("#CreateKaiReauthFunction", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
// eslint-disable-next-line no-return-await
|
||||
t.beforeEach(async () => await MONGODB_KILL["kai-auth-tokens"].remove({}));
|
||||
|
||||
if (!ServerConfig.FLO_OAUTH2_INFO) {
|
||||
throw new Error(
|
||||
`Panic in test - No dummy FLO_OAUTH2_INFO configured, and the test depends on some dummy data here.`,
|
||||
);
|
||||
}
|
||||
|
||||
t.test("Should create a working reauthentication for the service.", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
[`${ServerConfig.FLO_API_URL}/oauth/token`]: {
|
||||
access_token: "NEW_ACCESS_TOKEN",
|
||||
refresh_token: "NEW_REFRESH_TOKEN",
|
||||
},
|
||||
});
|
||||
|
||||
await MONGODB_KILL["kai-auth-tokens"].insert(deepmerge(authDoc, {}));
|
||||
|
||||
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
|
||||
|
||||
t.equal(reauthFn.length, 0, "Should return a function with arity 0.");
|
||||
|
||||
const data = await reauthFn();
|
||||
|
||||
t.equal(data, "NEW_ACCESS_TOKEN");
|
||||
|
||||
const dbChange = await MONGODB_KILL["kai-auth-tokens"].findOne({
|
||||
userID: 1,
|
||||
service: "FLO",
|
||||
});
|
||||
|
||||
// should also update the db
|
||||
t.equal(dbChange?.token, "NEW_ACCESS_TOKEN");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on fetch error.", async (t) => {
|
||||
// will fail
|
||||
const mockFetch = MockJSONFetch({});
|
||||
|
||||
await MONGODB_KILL["kai-auth-tokens"].insert(deepmerge(authDoc, {}));
|
||||
|
||||
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
|
||||
|
||||
t.rejects(() => reauthFn(), {
|
||||
message: "An error has occured while attempting reauthentication.",
|
||||
});
|
||||
|
||||
const dbChange = await MONGODB_KILL["kai-auth-tokens"].findOne({
|
||||
userID: 1,
|
||||
service: "FLO",
|
||||
});
|
||||
|
||||
t.equal(
|
||||
dbChange?.refreshToken,
|
||||
authDoc.refreshToken,
|
||||
"DB should not be changed for refreshToken.",
|
||||
);
|
||||
t.equal(dbChange?.token, authDoc.token, "DB should not be changed for token.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw on invalid JSON response", async (t) => {
|
||||
const mockFetch = MockJSONFetch({
|
||||
[`${ServerConfig.FLO_API_URL}/oauth/token?refresh_token=${
|
||||
authDoc.refreshToken
|
||||
}&grant_type=refresh_token&client_secret=${
|
||||
ServerConfig.FLO_OAUTH2_INFO!.CLIENT_SECRET
|
||||
}&client_id=${ServerConfig.FLO_OAUTH2_INFO!.CLIENT_ID}`]: {
|
||||
// missing access_token
|
||||
},
|
||||
});
|
||||
|
||||
await MONGODB_KILL["kai-auth-tokens"].insert(deepmerge(authDoc, {}));
|
||||
|
||||
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
|
||||
|
||||
t.rejects(() => reauthFn(), {
|
||||
message: "An error has occured while attempting reauthentication.",
|
||||
});
|
||||
|
||||
const dbChange = await MONGODB_KILL["kai-auth-tokens"].findOne({
|
||||
userID: 1,
|
||||
service: "FLO",
|
||||
});
|
||||
|
||||
t.equal(
|
||||
dbChange?.refreshToken,
|
||||
authDoc.refreshToken,
|
||||
"DB should not be changed for refreshToken.",
|
||||
);
|
||||
t.equal(dbChange?.token, authDoc.token, "DB should not be changed for token.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { log } from "#lib/log/log";
|
||||
import { ServerConfig } from "#lib/setup/config";
|
||||
import DB from "#services/pg/db";
|
||||
import { MockJSONFetch } from "#test-utils/mock-fetch";
|
||||
import { seedUser } from "#test-utils/pg-fixtures";
|
||||
import type { KaiAuthDocument } from "tachi-common";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { CreateKaiReauthFunction } from "./reauth";
|
||||
|
||||
describe("#CreateKaiReauthFunction", () => {
|
||||
let userId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ id: userId } = await seedUser({
|
||||
username: "reauth_user",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
}));
|
||||
|
||||
await DB.insertInto("priv_svc_kai_auth_token")
|
||||
.values({
|
||||
user_id: userId,
|
||||
service: "FLO",
|
||||
token: "foobar",
|
||||
refresh_token: "REFRESH_TOKEN",
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
|
||||
it("creates a working reauthentication for the service", async () => {
|
||||
if (!ServerConfig.FLO_OAUTH2_INFO) {
|
||||
throw new Error(
|
||||
`Panic in test - No dummy FLO_OAUTH2_INFO configured, and the test depends on some dummy data here.`,
|
||||
);
|
||||
}
|
||||
|
||||
const authDoc: KaiAuthDocument = {
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
service: "FLO",
|
||||
token: "foobar",
|
||||
userID: userId,
|
||||
};
|
||||
|
||||
const mockFetch = MockJSONFetch({
|
||||
[`${ServerConfig.FLO_API_URL}/oauth/token`]: {
|
||||
access_token: "NEW_ACCESS_TOKEN",
|
||||
refresh_token: "NEW_REFRESH_TOKEN",
|
||||
},
|
||||
});
|
||||
|
||||
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
|
||||
|
||||
expect(reauthFn.length).toBe(0);
|
||||
|
||||
const data = await reauthFn();
|
||||
|
||||
expect(data).toBe("NEW_ACCESS_TOKEN");
|
||||
|
||||
const dbChange = await DB.selectFrom("priv_svc_kai_auth_token")
|
||||
.selectAll()
|
||||
.where("user_id", "=", userId)
|
||||
.where("service", "=", "FLO")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(dbChange.token).toBe("NEW_ACCESS_TOKEN");
|
||||
expect(dbChange.refresh_token).toBe("NEW_REFRESH_TOKEN");
|
||||
});
|
||||
|
||||
it("throws on fetch error without changing tokens", async () => {
|
||||
if (!ServerConfig.FLO_OAUTH2_INFO) {
|
||||
throw new Error(
|
||||
`Panic in test - No dummy FLO_OAUTH2_INFO configured, and the test depends on some dummy data here.`,
|
||||
);
|
||||
}
|
||||
|
||||
const authDoc: KaiAuthDocument = {
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
service: "FLO",
|
||||
token: "foobar",
|
||||
userID: userId,
|
||||
};
|
||||
|
||||
const mockFetch = MockJSONFetch({});
|
||||
|
||||
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
|
||||
|
||||
await expect(reauthFn()).rejects.toMatchObject({
|
||||
message: "An error has occured while attempting reauthentication.",
|
||||
});
|
||||
|
||||
const dbChange = await DB.selectFrom("priv_svc_kai_auth_token")
|
||||
.selectAll()
|
||||
.where("user_id", "=", userId)
|
||||
.where("service", "=", "FLO")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(dbChange.refresh_token).toBe(authDoc.refreshToken);
|
||||
expect(dbChange.token).toBe(authDoc.token);
|
||||
});
|
||||
|
||||
it("throws on invalid JSON response without changing tokens", async () => {
|
||||
if (!ServerConfig.FLO_OAUTH2_INFO) {
|
||||
throw new Error(
|
||||
`Panic in test - No dummy FLO_OAUTH2_INFO configured, and the test depends on some dummy data here.`,
|
||||
);
|
||||
}
|
||||
|
||||
const authDoc: KaiAuthDocument = {
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
service: "FLO",
|
||||
token: "foobar",
|
||||
userID: userId,
|
||||
};
|
||||
|
||||
const mockFetch = MockJSONFetch({
|
||||
[`${ServerConfig.FLO_API_URL}/oauth/token`]: {
|
||||
/* missing access_token */
|
||||
},
|
||||
});
|
||||
|
||||
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
|
||||
|
||||
await expect(reauthFn()).rejects.toMatchObject({
|
||||
message: "An error has occured while attempting reauthentication.",
|
||||
});
|
||||
|
||||
const dbChange = await DB.selectFrom("priv_svc_kai_auth_token")
|
||||
.selectAll()
|
||||
.where("user_id", "=", userId)
|
||||
.where("service", "=", "FLO")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(dbChange.refresh_token).toBe(authDoc.refreshToken);
|
||||
expect(dbChange.token).toBe(authDoc.token);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { KtLogger } from "#lib/log/log";
|
||||
import type { KaiAuthDocument } from "tachi-common";
|
||||
|
||||
import { updateKaiAuthTokensInDb } from "#lib/kai-auth-token/persist";
|
||||
import ScoreImportFatalError from "#lib/score-import/framework/score-importing/score-import-error";
|
||||
import { ServerConfig } from "#lib/setup/config";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import DB from "#services/pg/db";
|
||||
import nodeFetch from "#utils/fetch";
|
||||
import { p } from "prudence";
|
||||
|
||||
@@ -109,17 +110,12 @@ export function CreateKaiReauthFunction(
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
await MONGODB_KILL["kai-auth-tokens"].update(
|
||||
{
|
||||
userID: authDoc.userID,
|
||||
service: authDoc.service,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
token: validatedContent.access_token,
|
||||
refreshToken: validatedContent.refresh_token,
|
||||
},
|
||||
},
|
||||
await updateKaiAuthTokensInDb(
|
||||
DB,
|
||||
authDoc.userID,
|
||||
authDoc.service,
|
||||
validatedContent.access_token,
|
||||
validatedContent.refresh_token,
|
||||
);
|
||||
|
||||
return validatedContent.access_token;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { log } from "#lib/log/log";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import { WrapScriptPromise } from "#utils/misc";
|
||||
import { FormatUserDoc, ResolveUser } from "#utils/user";
|
||||
import { UserAuthLevels } from "tachi-common";
|
||||
|
||||
const userID = process.argv[2];
|
||||
|
||||
async function MakeUserAdmin(userID: string) {
|
||||
const user = await ResolveUser(userID);
|
||||
|
||||
if (!user) {
|
||||
log.error(`No such user '${userID}' exists.`);
|
||||
throw new Error(`No such user '${userID}' exists.`);
|
||||
}
|
||||
|
||||
await MONGODB_KILL.users.update(
|
||||
{
|
||||
id: user.id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
authLevel: UserAuthLevels.ADMIN,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
log.info(`Made ${FormatUserDoc(user)} an administrator.`);
|
||||
}
|
||||
|
||||
if (!userID) {
|
||||
log.error(`Usage: pnpm make-user-admin <userID>.`);
|
||||
throw new Error(`No userID provided.`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
WrapScriptPromise(MakeUserAdmin(userID), log);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import mockApi from "#test-utils/mock-api";
|
||||
import ResetDBState from "#test-utils/resets";
|
||||
import t from "tap";
|
||||
|
||||
t.test("POST /api/v1/oauth/token", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should grant and create an api token.", async (t) => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: "OAUTH2_CLIENT_ID",
|
||||
client_secret: "OAUTH2_CLIENT_SECRET",
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: "https://example.com/callback",
|
||||
code: "AUTH_CODE",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
|
||||
const tokenDoc = await MONGODB_KILL["api-tokens"].findOne({
|
||||
token: res.body.body.token,
|
||||
});
|
||||
|
||||
t.not(tokenDoc, null);
|
||||
t.equal(tokenDoc?.userID, 1);
|
||||
t.equal(tokenDoc?.fromAPIClient, "OAUTH2_CLIENT_ID");
|
||||
t.strictSame(
|
||||
tokenDoc?.permissions,
|
||||
{
|
||||
customise_profile: true,
|
||||
},
|
||||
"Should assign the permissions this client has configured.",
|
||||
);
|
||||
|
||||
const exists = await MONGODB_KILL["oauth2-auth-codes"].findOne({ code: "AUTH_CODE" });
|
||||
|
||||
t.equal(exists, null, "Should remove the code from the database after being used.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Requires a valid code.", async (t) => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: "OAUTH2_CLIENT_ID",
|
||||
client_secret: "OAUTH2_CLIENT_SECRET",
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: "https://example.com/callback",
|
||||
code: "invalidcode",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 404);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Requires a valid clientID.", async (t) => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: "INVALID_CLIENT_ID",
|
||||
client_secret: "OAUTH2_CLIENT_SECRET",
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: "https://example.com/callback",
|
||||
code: "AUTH_CODE",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 404);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Requires a valid client secret.", async (t) => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: "OAUTH2_CLIENT_ID",
|
||||
client_secret: "INVALID_SECRET",
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: "https://example.com/callback",
|
||||
code: "AUTH_CODE",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 403);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Requires an identical redirect_uri.", async (t) => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: "OAUTH2_CLIENT_ID",
|
||||
client_secret: "OAUTH2_CLIENT_SECRET",
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: "https://invalid.example.com/callback",
|
||||
code: "AUTH_CODE",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { seedApiClient } from "#actions/test-utils/api-tokens";
|
||||
import DB from "#services/pg/db";
|
||||
import mockApi, { CloseServerConnection } from "#test-utils/mock-api";
|
||||
import { seedUser } from "#test-utils/pg-fixtures";
|
||||
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
async function loginAs(username: string, password = "password123") {
|
||||
const res = await mockApi.post("/api/v1/auth/login").send({
|
||||
username,
|
||||
"!password": password,
|
||||
captcha: "test",
|
||||
});
|
||||
|
||||
return res.headers["set-cookie"] as unknown as string[];
|
||||
}
|
||||
|
||||
afterAll(() => CloseServerConnection());
|
||||
|
||||
describe("POST /api/v1/oauth/token", () => {
|
||||
let userId: number;
|
||||
const clientId = "OAUTH2_CLIENT_ID";
|
||||
const clientSecret = "OAUTH2_CLIENT_SECRET";
|
||||
const redirectUri = "https://example.com/callback";
|
||||
const authCode = "AUTH_CODE";
|
||||
|
||||
beforeEach(async () => {
|
||||
({ id: userId } = await seedUser({
|
||||
username: "oauth_user",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
}));
|
||||
|
||||
await seedApiClient({
|
||||
clientId,
|
||||
authorId: userId,
|
||||
name: "Test_Service",
|
||||
clientSecret,
|
||||
customiseProfile: true,
|
||||
redirectUri,
|
||||
});
|
||||
|
||||
await DB.insertInto("priv_oauth2_auth_token")
|
||||
.values({
|
||||
token: authCode,
|
||||
user_id: userId,
|
||||
created_on: new Date().toISOString(),
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
|
||||
it("grants an api token and consumes the auth code", async () => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
code: authCode,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.body.userID).toBe(userId);
|
||||
expect(res.body.body.fromAPIClient).toBe(clientId);
|
||||
expect(res.body.body.permissions).toEqual({
|
||||
customise_profile: true,
|
||||
});
|
||||
|
||||
const tokenRow = await DB.selectFrom("priv_api_token")
|
||||
.selectAll()
|
||||
.where("token", "=", res.body.body.token)
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(tokenRow).toBeDefined();
|
||||
expect(tokenRow?.user_id).toBe(userId);
|
||||
expect(tokenRow?.from_oauth2_client).toBe(clientId);
|
||||
|
||||
const codeRow = await DB.selectFrom("priv_oauth2_auth_token")
|
||||
.selectAll()
|
||||
.where("token", "=", authCode)
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(codeRow).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns 404 for invalid code", async () => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
code: "invalidcode",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns 404 for invalid client id", async () => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: "INVALID_CLIENT_ID",
|
||||
client_secret: clientSecret,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
code: authCode,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns 403 for invalid client secret", async () => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: clientId,
|
||||
client_secret: "INVALID_SECRET",
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
code: authCode,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("returns 400 for redirect_uri mismatch", async () => {
|
||||
const res = await mockApi.post(`/api/v1/oauth/token`).send({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: "https://invalid.example.com/callback",
|
||||
code: authCode,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/v1/oauth/create-code", () => {
|
||||
it("returns 401 when not authenticated", async () => {
|
||||
const res = await mockApi.post("/api/v1/oauth/create-code");
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("creates a row in priv_oauth2_auth_token", async () => {
|
||||
const { id: uid } = await seedUser({
|
||||
username: "oauth_code_user",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
});
|
||||
const cookie = await loginAs("oauth_code_user");
|
||||
|
||||
const res = await mockApi.post("/api/v1/oauth/create-code").set("Cookie", cookie);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.body.code).toBeTruthy();
|
||||
expect(typeof res.body.body.createdOn).toBe("number");
|
||||
expect(res.body.body.userID).toBe(uid);
|
||||
|
||||
const row = await DB.selectFrom("priv_oauth2_auth_token")
|
||||
.selectAll()
|
||||
.where("token", "=", res.body.body.code)
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(row).toBeDefined();
|
||||
expect(Number(row?.user_id)).toBe(uid);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ACTION_CreateOAuth2AuthCode } from "#actions/create-oauth2-auth-code";
|
||||
import { ANON_ACTION_OAuthTokenExchange } from "#anon-actions/oauth-token-exchange";
|
||||
import prValidate from "#server/middleware/prudence-validate";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import { Random20Hex } from "#utils/misc";
|
||||
import { Router } from "express";
|
||||
import { p } from "prudence";
|
||||
|
||||
@@ -39,57 +39,16 @@ router.post(
|
||||
redirect_uri: string;
|
||||
};
|
||||
|
||||
const client = await MONGODB_KILL["api-clients"].findOne({
|
||||
clientID: body.client_id,
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `This client does not exist.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (client.clientSecret !== body.client_secret) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
description: `Invalid secret.`,
|
||||
});
|
||||
}
|
||||
|
||||
// I honest to god have no idea what the point of this check is
|
||||
// but it's part of the oauth spec.
|
||||
if (client.redirectUri !== body.redirect_uri) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `This redirect_uri does not match with your registered client redirect_uri ${client.redirectUri}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const codeDoc = await MONGODB_KILL["oauth2-auth-codes"].findOne({ code: body.code });
|
||||
|
||||
if (!codeDoc) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `This code does not exist.`,
|
||||
});
|
||||
}
|
||||
|
||||
// don't let people auth with the same code multiple times.
|
||||
await MONGODB_KILL["oauth2-auth-codes"].remove({ code: body.code });
|
||||
|
||||
const apiDoc = {
|
||||
userID: codeDoc.userID,
|
||||
token: Random20Hex(),
|
||||
identifier: `${client.name} Token`,
|
||||
|
||||
// converts ["a","b"] to {a: true, b: true}.
|
||||
permissions: Object.fromEntries(client.requestedPermissions.map((e) => [e, true])),
|
||||
fromAPIClient: client.clientID,
|
||||
};
|
||||
|
||||
// Now we can actually register the api key (lol)
|
||||
await MONGODB_KILL["api-tokens"].insert(apiDoc);
|
||||
const apiDoc = await ANON_ACTION_OAuthTokenExchange(
|
||||
{ ip: req.ip },
|
||||
{
|
||||
client_id: body.client_id,
|
||||
client_secret: body.client_secret,
|
||||
grant_type: body.grant_type,
|
||||
redirect_uri: body.redirect_uri,
|
||||
code: body.code,
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -112,11 +71,10 @@ router.post("/create-code", async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const code = Random20Hex();
|
||||
const user = req.session.tachi.user;
|
||||
const taker = { ip: req.ip, acct: { id: user.id, username: user.username } };
|
||||
|
||||
const doc = { code, userID: req.session.tachi.user.id, createdOn: Date.now() };
|
||||
|
||||
await MONGODB_KILL["oauth2-auth-codes"].insert(doc);
|
||||
const doc = await ACTION_CreateOAuth2AuthCode(taker, {});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import { CreateFakeAuthCookie } from "#test-utils/fake-auth";
|
||||
import mockApi from "#test-utils/mock-api";
|
||||
import ResetDBState from "#test-utils/resets";
|
||||
import t from "tap";
|
||||
|
||||
t.test("GET /api/v1/users/:userID/integrations/kai/:kaiType", async (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const cookie = await CreateFakeAuthCookie(mockApi);
|
||||
|
||||
t.test("Should return false if unauthed with this kaiType", async (t) => {
|
||||
const res = await mockApi.get("/api/v1/users/1/integrations/kai/flo").set("Cookie", cookie);
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
t.equal(res.body.body.authStatus, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return true if authed with this kaiType", async (t) => {
|
||||
await MONGODB_KILL["kai-auth-tokens"].insert({
|
||||
refreshToken: "refresh",
|
||||
service: "FLO",
|
||||
token: "bar",
|
||||
userID: 1,
|
||||
});
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/1/integrations/kai/flo").set("Cookie", cookie);
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
t.equal(res.body.body.authStatus, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return the auth status of this user specifically.", async (t) => {
|
||||
await MONGODB_KILL["kai-auth-tokens"].insert([
|
||||
{
|
||||
refreshToken: "refresh",
|
||||
service: "EAG",
|
||||
token: "bar",
|
||||
userID: 1,
|
||||
},
|
||||
{
|
||||
refreshToken: "refresh",
|
||||
service: "FLO",
|
||||
token: "baz",
|
||||
userID: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/1/integrations/kai/flo").set("Cookie", cookie);
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
t.equal(res.body.body.authStatus, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// test is currently undoable due to issues with mocking out real Fetch calls.
|
||||
t.todo("PATCH /api/v1/users/:userID/integrations/kai/:kaiType/oauth2callback");
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import DB from "#services/pg/db";
|
||||
import mockApi, { CloseServerConnection } from "#test-utils/mock-api";
|
||||
import { seedUser } from "#test-utils/pg-fixtures";
|
||||
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
async function loginAs(username: string, password = "password123") {
|
||||
const res = await mockApi.post("/api/v1/auth/login").send({
|
||||
username,
|
||||
"!password": password,
|
||||
captcha: "test",
|
||||
});
|
||||
|
||||
return res.headers["set-cookie"] as unknown as string[];
|
||||
}
|
||||
|
||||
afterAll(() => CloseServerConnection());
|
||||
|
||||
describe("GET /api/v1/users/:userID/integrations/kai/:kaiType", () => {
|
||||
let cookie: string[];
|
||||
let userId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ id: userId } = await seedUser({
|
||||
username: "kai_int_user",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
}));
|
||||
cookie = await loginAs("kai_int_user");
|
||||
});
|
||||
|
||||
it("returns authStatus false when no token row exists", async () => {
|
||||
const res = await mockApi
|
||||
.get(`/api/v1/users/${userId}/integrations/kai/flo`)
|
||||
.set("Cookie", cookie);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.body.authStatus).toBe(false);
|
||||
});
|
||||
|
||||
it("returns authStatus true when a token row exists for this user and service", async () => {
|
||||
await DB.insertInto("priv_svc_kai_auth_token")
|
||||
.values({
|
||||
user_id: userId,
|
||||
service: "FLO",
|
||||
token: "access",
|
||||
refresh_token: "refresh",
|
||||
})
|
||||
.execute();
|
||||
|
||||
const res = await mockApi
|
||||
.get(`/api/v1/users/${userId}/integrations/kai/flo`)
|
||||
.set("Cookie", cookie);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.body.authStatus).toBe(true);
|
||||
});
|
||||
|
||||
it("returns authStatus false when another user has FLO auth", async () => {
|
||||
const { id: otherId } = await seedUser({
|
||||
username: "kai_other",
|
||||
email: "kai_other@example.com",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
});
|
||||
|
||||
await DB.insertInto("priv_svc_kai_auth_token")
|
||||
.values({
|
||||
user_id: otherId,
|
||||
service: "FLO",
|
||||
token: "t",
|
||||
refresh_token: "r",
|
||||
})
|
||||
.execute();
|
||||
|
||||
const res = await mockApi
|
||||
.get(`/api/v1/users/${userId}/integrations/kai/flo`)
|
||||
.set("Cookie", cookie);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.body.authStatus).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/v1/users/:userID/integrations/kai/:kaiType", () => {
|
||||
let cookie: string[];
|
||||
let userId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ id: userId } = await seedUser({
|
||||
username: "kai_del_user",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
}));
|
||||
cookie = await loginAs("kai_del_user");
|
||||
});
|
||||
|
||||
it("returns 409 when not linked", async () => {
|
||||
const res = await mockApi
|
||||
.delete(`/api/v1/users/${userId}/integrations/kai/eag`)
|
||||
.set("Cookie", cookie);
|
||||
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
|
||||
it("removes the priv_svc_kai_auth_token row", async () => {
|
||||
await DB.insertInto("priv_svc_kai_auth_token")
|
||||
.values({
|
||||
user_id: userId,
|
||||
service: "EAG",
|
||||
token: "a",
|
||||
refresh_token: "b",
|
||||
})
|
||||
.execute();
|
||||
|
||||
const res = await mockApi
|
||||
.delete(`/api/v1/users/${userId}/integrations/kai/eag`)
|
||||
.set("Cookie", cookie);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const row = await DB.selectFrom("priv_svc_kai_auth_token")
|
||||
.selectAll()
|
||||
.where("user_id", "=", userId)
|
||||
.where("service", "=", "EAG")
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(row).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+13
-20
@@ -1,3 +1,5 @@
|
||||
import { ACTION_RevokeKaiAuthToken } from "#actions/revoke-kai-auth-token";
|
||||
import { ACTION_UpsertKaiAuthToken } from "#actions/upsert-kai-auth-token";
|
||||
import { log } from "#lib/log/log";
|
||||
import {
|
||||
GetKaiTypeClientCredentials,
|
||||
@@ -5,10 +7,9 @@ import {
|
||||
} from "#lib/score-import/import-types/common/api-kai/utils";
|
||||
import prValidate from "#server/middleware/prudence-validate";
|
||||
import { RequireKamaitachi } from "#server/middleware/type-require";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import fetch from "#utils/fetch";
|
||||
import { NotNullish } from "#utils/misc";
|
||||
import { GetKaiAuth, RevokeKaiAuth } from "#utils/queries/auth";
|
||||
import { GetKaiAuth } from "#utils/queries/auth";
|
||||
import { GetTachiData } from "#utils/req-tachi-data";
|
||||
import { FormatUserDoc } from "#utils/user";
|
||||
import { Router } from "express";
|
||||
@@ -65,7 +66,9 @@ router.delete<any>("/", async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
await RevokeKaiAuth(user.id, kaiType);
|
||||
const taker = { ip: req.ip, acct: { id: user.id, username: user.username } };
|
||||
|
||||
await ACTION_RevokeKaiAuthToken(taker, { service: kaiType });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -189,23 +192,13 @@ router.post(
|
||||
|
||||
const j = json as { access_token: string; refresh_token: string };
|
||||
|
||||
await MONGODB_KILL["kai-auth-tokens"].update(
|
||||
{
|
||||
userID: user.id,
|
||||
service: kaiType,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
userID: user.id,
|
||||
service: kaiType,
|
||||
refreshToken: j.refresh_token,
|
||||
token: j.access_token,
|
||||
},
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
},
|
||||
);
|
||||
const taker = { ip: req.ip, acct: { id: user.id, username: user.username } };
|
||||
|
||||
await ACTION_UpsertKaiAuthToken(taker, {
|
||||
service: kaiType,
|
||||
token: j.access_token,
|
||||
refreshToken: j.refresh_token,
|
||||
});
|
||||
|
||||
log.info(`Updated Auth for ${kaiType} for user ${FormatUserDoc(user)}.`);
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { KtLogger } from "#lib/log/log";
|
||||
import type { integer } from "tachi-common";
|
||||
import type { integer, KaiAuthDocument } from "tachi-common";
|
||||
|
||||
import { SELECT_KAI_AUTH_TOKEN, ToKaiAuthDocument } from "#lib/db-formats/kai-auth-token";
|
||||
import ScoreImportFatalError from "#lib/score-import/framework/score-importing/score-import-error";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import DB from "#services/pg/db";
|
||||
|
||||
export function GetKaiAuth(userID: integer, service: "EAG" | "FLO" | "MIN") {
|
||||
return MONGODB_KILL["kai-auth-tokens"].findOne({
|
||||
userID,
|
||||
service,
|
||||
});
|
||||
}
|
||||
export async function GetKaiAuth(
|
||||
userID: integer,
|
||||
service: "EAG" | "FLO" | "MIN",
|
||||
): Promise<KaiAuthDocument | null> {
|
||||
const row = await DB.selectFrom("priv_svc_kai_auth_token")
|
||||
.select(SELECT_KAI_AUTH_TOKEN)
|
||||
.where("user_id", "=", userID)
|
||||
.where("service", "=", service)
|
||||
.executeTakeFirst();
|
||||
|
||||
export function RevokeKaiAuth(userID: integer, service: "EAG" | "FLO" | "MIN") {
|
||||
return MONGODB_KILL["kai-auth-tokens"].remove({
|
||||
userID,
|
||||
service,
|
||||
});
|
||||
return row ? ToKaiAuthDocument(row) : null;
|
||||
}
|
||||
|
||||
export async function GetKaiAuthGuaranteed(
|
||||
|
||||
Reference in New Issue
Block a user