This commit is contained in:
+1
-1
Submodule graphql-contracts updated: 36474ab670...95486d75fc
+40
@@ -32,6 +32,24 @@ function scopesFromPayload(payload: JWTPayload): string[] {
|
|||||||
return [];
|
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> {
|
export async function publicContext(req: FastifyRequest): Promise<AuthContext> {
|
||||||
const token = getBearerToken(req);
|
const token = getBearerToken(req);
|
||||||
if (!token) return { scopes: [] };
|
if (!token) return { scopes: [] };
|
||||||
@@ -68,6 +86,28 @@ export async function userContext(req: FastifyRequest): Promise<AuthContext> {
|
|||||||
return { userId: payload.sub, scopes };
|
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> {
|
export async function m2mContext(): Promise<AuthContext> {
|
||||||
return { scopes: [], isM2M: true };
|
return { scopes: [], isM2M: true };
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -5,8 +5,9 @@ import mercurius from "mercurius";
|
|||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { publicTypeDefs, publicResolvers } from "./schemas/public.js";
|
import { publicTypeDefs, publicResolvers } from "./schemas/public.js";
|
||||||
import { userTypeDefs, userResolvers } from "./schemas/user.js";
|
import { userTypeDefs, userResolvers } from "./schemas/user.js";
|
||||||
|
import { managerTypeDefs, managerResolvers } from "./schemas/manager.js";
|
||||||
import { m2mTypeDefs, m2mResolvers } from "./schemas/m2m.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 PORT = Number.parseInt(process.env.PORT || "8000", 10);
|
||||||
const SENTRY_DSN = process.env.SENTRY_DSN || "";
|
const SENTRY_DSN = process.env.SENTRY_DSN || "";
|
||||||
@@ -74,6 +75,13 @@ await registerGraphqlEndpoint(
|
|||||||
userResolvers,
|
userResolvers,
|
||||||
userContext,
|
userContext,
|
||||||
);
|
);
|
||||||
|
await registerGraphqlEndpoint(
|
||||||
|
app,
|
||||||
|
"/graphql/manager",
|
||||||
|
managerTypeDefs,
|
||||||
|
managerResolvers,
|
||||||
|
managerContext,
|
||||||
|
);
|
||||||
await registerGraphqlEndpoint(
|
await registerGraphqlEndpoint(
|
||||||
app,
|
app,
|
||||||
"/graphql/m2m",
|
"/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(`KYC server ready on port ${PORT}`);
|
||||||
console.log(` /graphql/public - public (optional auth)`);
|
console.log(` /graphql/public - public (optional auth)`);
|
||||||
console.log(` /graphql/user - id token auth`);
|
console.log(` /graphql/user - id token auth`);
|
||||||
|
console.log(` /graphql/manager - manager auth`);
|
||||||
console.log(` /graphql/m2m - internal services (no 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