Allow manager orders access without team claim
Build and deploy Docker image / build (push) Successful in 2m4s
Build and deploy Docker image / build (push) Successful in 2m4s
This commit is contained in:
+6
-2
@@ -116,12 +116,16 @@ export async function managerContext(
|
||||
const teamUuid = (payload as Record<string, unknown>).team_uuid as
|
||||
| string
|
||||
| undefined;
|
||||
if (!payload.sub || !hasManagerClaim(payload) || !teamUuid) {
|
||||
if (!payload.sub || !hasManagerClaim(payload)) {
|
||||
throw new GraphQLError("Unauthorized", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
}
|
||||
return { userId: payload.sub, teamUuid, scopes: ["teams:member", "manager"] };
|
||||
return {
|
||||
userId: payload.sub,
|
||||
teamUuid,
|
||||
scopes: [...new Set([...scopes, "manager"])],
|
||||
};
|
||||
}
|
||||
|
||||
export function requireScopes(ctx: AuthContext, ...required: string[]): void {
|
||||
|
||||
+69
-35
@@ -401,7 +401,18 @@ function isoString(value: Date | null | undefined): string | null {
|
||||
return value ? value.toISOString() : null
|
||||
}
|
||||
|
||||
function assertTeamAccess(ctx: AuthContext): { teamUuid: string; userId: string } {
|
||||
function assertTeamAccess(ctx: AuthContext): { teamUuid?: string; userId: string; isManager: boolean } {
|
||||
if (ctx.scopes.includes('manager')) {
|
||||
if (!ctx.userId) {
|
||||
throw new GraphQLError('User not authenticated')
|
||||
}
|
||||
return {
|
||||
teamUuid: ctx.teamUuid,
|
||||
userId: ctx.userId,
|
||||
isManager: true,
|
||||
}
|
||||
}
|
||||
|
||||
requireScopes(ctx, 'teams:member')
|
||||
if (!ctx.teamUuid || !ctx.userId) {
|
||||
throw new GraphQLError('User not authenticated')
|
||||
@@ -410,9 +421,25 @@ function assertTeamAccess(ctx: AuthContext): { teamUuid: string; userId: string
|
||||
return {
|
||||
teamUuid: ctx.teamUuid,
|
||||
userId: ctx.userId,
|
||||
isManager: false,
|
||||
}
|
||||
}
|
||||
|
||||
function assertScopedTeamAccess(ctx: AuthContext): { teamUuid: string; userId: string } {
|
||||
const access = assertTeamAccess(ctx)
|
||||
if (!access.teamUuid) {
|
||||
throw new GraphQLError('Team context is required')
|
||||
}
|
||||
return {
|
||||
teamUuid: access.teamUuid,
|
||||
userId: access.userId,
|
||||
}
|
||||
}
|
||||
|
||||
function teamAccessWhere(access: { teamUuid?: string }): { teamUuid?: string } {
|
||||
return access.teamUuid ? { teamUuid: access.teamUuid } : {}
|
||||
}
|
||||
|
||||
function mapTariffReference(item: PrismaTariffReference) {
|
||||
return {
|
||||
uuid: item.uuid,
|
||||
@@ -734,11 +761,11 @@ function computeTariffMatches(
|
||||
})
|
||||
}
|
||||
|
||||
async function loadQuotationOrThrow(quotationUuid: string, teamUuid: string) {
|
||||
async function loadQuotationOrThrow(quotationUuid: string, teamUuid?: string) {
|
||||
const item = await prisma.quotation.findFirst({
|
||||
where: {
|
||||
uuid: quotationUuid,
|
||||
teamUuid,
|
||||
...teamAccessWhere({ teamUuid }),
|
||||
},
|
||||
include: quotationInclude,
|
||||
})
|
||||
@@ -750,11 +777,11 @@ async function loadQuotationOrThrow(quotationUuid: string, teamUuid: string) {
|
||||
return item
|
||||
}
|
||||
|
||||
async function loadTariffOrThrow(tariffReferenceUuid: string, teamUuid: string) {
|
||||
async function loadTariffOrThrow(tariffReferenceUuid: string, teamUuid?: string) {
|
||||
const item = await prisma.tariffReference.findFirst({
|
||||
where: {
|
||||
uuid: tariffReferenceUuid,
|
||||
teamUuid,
|
||||
...teamAccessWhere({ teamUuid }),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -882,11 +909,11 @@ async function refreshTeamQuotations(teamUuid: string, actor: { userId: string;
|
||||
export const teamResolvers = {
|
||||
Query: {
|
||||
getTeamOrders: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
||||
const { teamUuid } = assertTeamAccess(ctx)
|
||||
const access = assertTeamAccess(ctx)
|
||||
|
||||
const localOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
teamUuid,
|
||||
...teamAccessWhere(access),
|
||||
},
|
||||
include: orderInclude,
|
||||
orderBy: {
|
||||
@@ -898,12 +925,12 @@ export const teamResolvers = {
|
||||
},
|
||||
|
||||
getOrder: async (_: unknown, args: { orderUuid: string }, ctx: AuthContext) => {
|
||||
const { teamUuid } = assertTeamAccess(ctx)
|
||||
const access = assertTeamAccess(ctx)
|
||||
|
||||
const order = await prisma.order.findFirst({
|
||||
where: {
|
||||
uuid: args.orderUuid,
|
||||
teamUuid,
|
||||
...teamAccessWhere(access),
|
||||
},
|
||||
include: orderInclude,
|
||||
})
|
||||
@@ -912,11 +939,11 @@ export const teamResolvers = {
|
||||
},
|
||||
|
||||
quotations: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
||||
const { teamUuid } = assertTeamAccess(ctx)
|
||||
const access = assertTeamAccess(ctx)
|
||||
|
||||
const items = await prisma.quotation.findMany({
|
||||
where: {
|
||||
teamUuid,
|
||||
...teamAccessWhere(access),
|
||||
status: normalizeString(args.status) ?? undefined,
|
||||
},
|
||||
include: quotationInclude,
|
||||
@@ -929,11 +956,11 @@ export const teamResolvers = {
|
||||
},
|
||||
|
||||
quotation: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||
const { teamUuid } = assertTeamAccess(ctx)
|
||||
const access = assertTeamAccess(ctx)
|
||||
const item = await prisma.quotation.findFirst({
|
||||
where: {
|
||||
uuid: args.quotationUuid,
|
||||
teamUuid,
|
||||
...teamAccessWhere(access),
|
||||
},
|
||||
include: quotationInclude,
|
||||
})
|
||||
@@ -941,11 +968,11 @@ export const teamResolvers = {
|
||||
},
|
||||
|
||||
tariffReferences: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
||||
const { teamUuid } = assertTeamAccess(ctx)
|
||||
const access = assertTeamAccess(ctx)
|
||||
|
||||
const items = await prisma.tariffReference.findMany({
|
||||
where: {
|
||||
teamUuid,
|
||||
...teamAccessWhere(access),
|
||||
status: normalizeString(args.status) ?? undefined,
|
||||
},
|
||||
orderBy: [
|
||||
@@ -958,22 +985,22 @@ export const teamResolvers = {
|
||||
},
|
||||
|
||||
tariffReference: async (_: unknown, args: { tariffReferenceUuid: string }, ctx: AuthContext) => {
|
||||
const { teamUuid } = assertTeamAccess(ctx)
|
||||
const access = assertTeamAccess(ctx)
|
||||
const item = await prisma.tariffReference.findFirst({
|
||||
where: {
|
||||
uuid: args.tariffReferenceUuid,
|
||||
teamUuid,
|
||||
...teamAccessWhere(access),
|
||||
},
|
||||
})
|
||||
return item ? mapTariffReference(item) : null
|
||||
},
|
||||
|
||||
quotationTariffMatches: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||
const { teamUuid } = assertTeamAccess(ctx)
|
||||
const quotation = await loadQuotationOrThrow(args.quotationUuid, teamUuid)
|
||||
const access = assertTeamAccess(ctx)
|
||||
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||
const tariffs = await prisma.tariffReference.findMany({
|
||||
where: {
|
||||
teamUuid,
|
||||
teamUuid: quotation.teamUuid,
|
||||
status: 'active',
|
||||
},
|
||||
})
|
||||
@@ -992,7 +1019,7 @@ export const teamResolvers = {
|
||||
|
||||
Mutation: {
|
||||
createTariffReference: async (_: unknown, args: { input: CreateTariffReferenceInput }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const access = assertScopedTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const item = await prisma.tariffReference.create({
|
||||
data: buildCreateTariffData(args.input, access),
|
||||
@@ -1003,33 +1030,35 @@ export const teamResolvers = {
|
||||
|
||||
updateTariffReference: async (_: unknown, args: { tariffReferenceUuid: string; input: UpdateTariffReferenceInput }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const item = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
||||
const teamAccess = { teamUuid: item.teamUuid, userId: access.userId }
|
||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||
const updated = await prisma.tariffReference.update({
|
||||
where: {
|
||||
id: item.id,
|
||||
},
|
||||
data: buildUpdateTariffData(args.input),
|
||||
})
|
||||
await refreshTeamQuotations(access.teamUuid, actor)
|
||||
await refreshTeamQuotations(item.teamUuid, actor)
|
||||
return mapTariffReference(updated)
|
||||
},
|
||||
|
||||
deleteTariffReference: async (_: unknown, args: { tariffReferenceUuid: string }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const item = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
||||
const teamAccess = { teamUuid: item.teamUuid, userId: access.userId }
|
||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||
await prisma.tariffReference.delete({
|
||||
where: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
await refreshTeamQuotations(access.teamUuid, actor)
|
||||
await refreshTeamQuotations(item.teamUuid, actor)
|
||||
return true
|
||||
},
|
||||
|
||||
createQuotation: async (_: unknown, args: { input: CreateQuotationInput }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const access = assertScopedTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const created = await prisma.quotation.create({
|
||||
data: buildCreateQuotationData(args.input, access),
|
||||
@@ -1045,8 +1074,9 @@ export const teamResolvers = {
|
||||
|
||||
updateQuotation: async (_: unknown, args: { quotationUuid: string; input: UpdateQuotationInput }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const current = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||
const teamAccess = { teamUuid: current.teamUuid, userId: access.userId }
|
||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||
await prisma.quotation.update({
|
||||
where: {
|
||||
id: current.id,
|
||||
@@ -1059,22 +1089,25 @@ export const teamResolvers = {
|
||||
updatedFields: Object.keys(args.input),
|
||||
})
|
||||
|
||||
const refreshed = await refreshQuotationSelection(current.uuid, access.teamUuid, actor)
|
||||
const refreshed = await refreshQuotationSelection(current.uuid, current.teamUuid, actor)
|
||||
return mapQuotation(refreshed)
|
||||
},
|
||||
|
||||
refreshQuotationTariff: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const refreshed = await refreshQuotationSelection(args.quotationUuid, access.teamUuid, actor)
|
||||
const current = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||
const teamAccess = { teamUuid: current.teamUuid, userId: access.userId }
|
||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||
const refreshed = await refreshQuotationSelection(current.uuid, current.teamUuid, actor)
|
||||
return mapQuotation(refreshed)
|
||||
},
|
||||
|
||||
selectQuotationTariff: async (_: unknown, args: { quotationUuid: string; tariffReferenceUuid: string }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||
const tariff = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
||||
const teamAccess = { teamUuid: quotation.teamUuid, userId: access.userId }
|
||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||
const tariff = await loadTariffOrThrow(args.tariffReferenceUuid, quotation.teamUuid)
|
||||
|
||||
const projectedAmount = Math.max(Number(tariff.amountUsd), numberFromDecimal(tariff.minPriceUsd) ?? 0)
|
||||
await prisma.quotation.update({
|
||||
@@ -1107,13 +1140,14 @@ export const teamResolvers = {
|
||||
|
||||
createOrderFromQuotation: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||
const access = assertTeamAccess(ctx)
|
||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||
const teamAccess = { teamUuid: quotation.teamUuid, userId: access.userId }
|
||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||
|
||||
const existingOrder = await prisma.order.findFirst({
|
||||
where: {
|
||||
quotationId: quotation.id,
|
||||
teamUuid: access.teamUuid,
|
||||
teamUuid: quotation.teamUuid,
|
||||
},
|
||||
include: orderInclude,
|
||||
})
|
||||
@@ -1128,7 +1162,7 @@ export const teamResolvers = {
|
||||
|
||||
const order = await prisma.order.create({
|
||||
data: {
|
||||
teamUuid: access.teamUuid,
|
||||
teamUuid: quotation.teamUuid,
|
||||
quotationId: quotation.id,
|
||||
createdByUserId: access.userId,
|
||||
name: quotation.title.startsWith('Order') ? quotation.title : `Order for ${quotation.title}`,
|
||||
|
||||
Reference in New Issue
Block a user