This commit is contained in:
+1
-1
Submodule graphql-contracts updated: 36474ab670...95486d75fc
+40
@@ -32,6 +32,24 @@ function scopesFromPayload(payload: JWTPayload): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function claimList(payload: JWTPayload, key: string): string[] {
|
||||
const value = (payload as Record<string, unknown>)[key];
|
||||
if (typeof value === "string") return value.split(" ");
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((item): item is string => typeof item === "string");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function hasManagerClaim(payload: JWTPayload): boolean {
|
||||
const scopes = scopesFromPayload(payload);
|
||||
return (
|
||||
scopes.includes("manager") ||
|
||||
claimList(payload, "roles").includes("manager") ||
|
||||
claimList(payload, "permissions").includes("manager")
|
||||
);
|
||||
}
|
||||
|
||||
export async function publicContext(req: FastifyRequest): Promise<AuthContext> {
|
||||
const token = getBearerToken(req);
|
||||
if (!token) return { scopes: [] };
|
||||
@@ -68,6 +86,28 @@ export async function userContext(req: FastifyRequest): Promise<AuthContext> {
|
||||
return { userId: payload.sub, scopes };
|
||||
}
|
||||
|
||||
export async function managerContext(req: FastifyRequest): Promise<AuthContext> {
|
||||
const token = getBearerToken(req);
|
||||
if (!token) {
|
||||
throw new GraphQLError("Unauthorized", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: LOGTO_ISSUER,
|
||||
audience: LOGTO_KYC_AUDIENCE,
|
||||
});
|
||||
if (!payload.sub || !hasManagerClaim(payload)) {
|
||||
throw new GraphQLError("Manager access required", {
|
||||
extensions: { code: "FORBIDDEN" },
|
||||
});
|
||||
}
|
||||
return {
|
||||
userId: payload.sub,
|
||||
scopes: [...new Set([...scopesFromPayload(payload), "manager"])],
|
||||
};
|
||||
}
|
||||
|
||||
export async function m2mContext(): Promise<AuthContext> {
|
||||
return { scopes: [], isM2M: true };
|
||||
}
|
||||
|
||||
+10
-1
@@ -5,8 +5,9 @@ import mercurius from "mercurius";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { publicTypeDefs, publicResolvers } from "./schemas/public.js";
|
||||
import { userTypeDefs, userResolvers } from "./schemas/user.js";
|
||||
import { managerTypeDefs, managerResolvers } from "./schemas/manager.js";
|
||||
import { m2mTypeDefs, m2mResolvers } from "./schemas/m2m.js";
|
||||
import { publicContext, userContext, m2mContext } from "./auth.js";
|
||||
import { publicContext, userContext, managerContext, m2mContext } from "./auth.js";
|
||||
|
||||
const PORT = Number.parseInt(process.env.PORT || "8000", 10);
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || "";
|
||||
@@ -74,6 +75,13 @@ await registerGraphqlEndpoint(
|
||||
userResolvers,
|
||||
userContext,
|
||||
);
|
||||
await registerGraphqlEndpoint(
|
||||
app,
|
||||
"/graphql/manager",
|
||||
managerTypeDefs,
|
||||
managerResolvers,
|
||||
managerContext,
|
||||
);
|
||||
await registerGraphqlEndpoint(
|
||||
app,
|
||||
"/graphql/m2m",
|
||||
@@ -88,4 +96,5 @@ await app.listen({ port: PORT, host: "0.0.0.0" });
|
||||
console.log(`KYC server ready on port ${PORT}`);
|
||||
console.log(` /graphql/public - public (optional auth)`);
|
||||
console.log(` /graphql/user - id token auth`);
|
||||
console.log(` /graphql/manager - manager auth`);
|
||||
console.log(` /graphql/m2m - internal services (no auth)`);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { prisma } from '../db.js'
|
||||
import type { AuthContext } from '../auth.js'
|
||||
|
||||
export const managerTypeDefs = `#graphql
|
||||
type KYCApplication {
|
||||
id: Int!
|
||||
uuid: String!
|
||||
userId: String!
|
||||
teamName: String
|
||||
countryCode: String
|
||||
workflowStatus: String!
|
||||
score: Int!
|
||||
contactPerson: String
|
||||
contactEmail: String
|
||||
contactPhone: String
|
||||
countryData: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type UpdateKYCApplicationStatusResult {
|
||||
kycApplication: KYCApplication
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Query {
|
||||
managerKycApplications: [KYCApplication!]!
|
||||
managerKycRequests: [KYCApplication!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
updateKycApplicationStatus(uuid: String!, status: String!): UpdateKYCApplicationStatusResult!
|
||||
updateKycRequestStatus(uuid: String!, status: String!): UpdateKYCApplicationStatusResult!
|
||||
}
|
||||
`
|
||||
|
||||
function assertManager(ctx: AuthContext) {
|
||||
if (!ctx.userId || !ctx.scopes.includes('manager')) {
|
||||
throw new GraphQLError('Manager access required', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function getCountryData(app: { objectId: number | null }) {
|
||||
if (!app.objectId) return null
|
||||
const details = await prisma.kYCDetailsRussia.findUnique({ where: { id: app.objectId } })
|
||||
if (!details) return null
|
||||
return JSON.stringify({
|
||||
company_name: details.companyName,
|
||||
company_full_name: details.companyFullName,
|
||||
inn: details.inn,
|
||||
kpp: details.kpp,
|
||||
ogrn: details.ogrn,
|
||||
address: details.address,
|
||||
bank_name: details.bankName,
|
||||
bik: details.bik,
|
||||
correspondent_account: details.correspondentAccount,
|
||||
})
|
||||
}
|
||||
|
||||
async function updateStatus(
|
||||
args: { uuid: string; status: string },
|
||||
ctx: AuthContext,
|
||||
) {
|
||||
assertManager(ctx)
|
||||
const status = args.status.trim()
|
||||
if (status.length === 0) {
|
||||
throw new GraphQLError('Status is required')
|
||||
}
|
||||
const approved = status === 'approved'
|
||||
const kycApplication = await prisma.kYCApplication.update({
|
||||
where: { uuid: args.uuid },
|
||||
data: {
|
||||
workflowStatus: status,
|
||||
approvedBy: approved ? ctx.userId : null,
|
||||
approvedAt: approved ? new Date() : null,
|
||||
},
|
||||
})
|
||||
return { kycApplication, success: true }
|
||||
}
|
||||
|
||||
export const managerResolvers = {
|
||||
Query: {
|
||||
managerKycApplications: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
||||
assertManager(ctx)
|
||||
return prisma.kYCApplication.findMany({ orderBy: { updatedAt: 'desc' } })
|
||||
},
|
||||
managerKycRequests: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
||||
assertManager(ctx)
|
||||
return prisma.kYCApplication.findMany({ orderBy: { updatedAt: 'desc' } })
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
updateKycApplicationStatus: (_: unknown, args: { uuid: string; status: string }, ctx: AuthContext) =>
|
||||
updateStatus(args, ctx),
|
||||
updateKycRequestStatus: (_: unknown, args: { uuid: string; status: string }, ctx: AuthContext) =>
|
||||
updateStatus(args, ctx),
|
||||
},
|
||||
KYCApplication: {
|
||||
countryData: async (parent: { objectId: number | null }) => getCountryData(parent),
|
||||
createdAt: (parent: { createdAt: Date }) => parent.createdAt.toISOString(),
|
||||
updatedAt: (parent: { updatedAt: Date }) => parent.updatedAt.toISOString(),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user