-
-
Notifications
You must be signed in to change notification settings - Fork 4
Release v1.0.6 #380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Release v1.0.6 #380
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
2c77e2a
(SP: 3) [Backend] add Nova Poshta shipping foundation + checkout pers…
liudmylasovetovs 4dd23b2
(SP: 2) [Frontend] Reduce Vercel variable costs via caching and analy…
ViktorSvertoka cf43f36
fix(build): align Netlify Node version and remove SpeedInsights import
ViktorSvertoka e07941e
chore(release): bump version to 1.0.4
ViktorSvertoka 006b39b
Merge remote-tracking branch 'origin/main' into develop
ViktorSvertoka 6e3526f
(SP: 2) [Frontend] Remove [locale] layout force-dynamic and move auth…
ViktorSvertoka 9130df1
(SP: 2) [Frontend] Reduce auth overhead and sync auth state across t…
ViktorSvertoka 3a59b4e
(SP: 2) [Frontend] Quizzes page ISR + client-side progress + GitHub s…
LesiaUKR d6697f4
(SP: 1) [Frontend] Fix quiz timer flash and card layout shift on quiz…
LesiaUKR fe667af
chore(release): v1.0.5
ViktorSvertoka b94a599
Merge branch 'main' into develop
ViktorSvertoka ffca8ca
(SP: 3)[Shop][DB] Reduce Neon compute: throttle janitor + relax check…
liudmylasovetovs bf6d333
(SP: 3) [SHOP] audit-driven e2e purchase readiness hardening (events,…
liudmylasovetovs fee0978
feat(ui): add devops/cloud category icons and styles (#379)
ViktorSvertoka aa5d692
chore(release): prepare v1.0.6 changelog
ViktorSvertoka 5409e13
chore: bump version to 1.0.6
ViktorSvertoka e216a39
Merge branch 'main' into develop
ViktorSvertoka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,8 @@ | |
| # testing | ||
| /coverage | ||
| /coverage-quiz | ||
| /playwright-report | ||
| /test-results | ||
|
|
||
| # next.js | ||
| /.next/ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
frontend/app/api/shop/admin/orders/[id]/quote/offer/route.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| import crypto from 'node:crypto'; | ||
|
|
||
| import { NextRequest, NextResponse } from 'next/server'; | ||
|
|
||
| import { | ||
| AdminApiDisabledError, | ||
| AdminForbiddenError, | ||
| AdminUnauthorizedError, | ||
| requireAdminApi, | ||
| } from '@/lib/auth/admin'; | ||
| import { logError, logWarn } from '@/lib/logging'; | ||
| import { requireAdminCsrf } from '@/lib/security/admin-csrf'; | ||
| import { guardBrowserSameOrigin } from '@/lib/security/origin'; | ||
| import { | ||
| InvalidPayloadError, | ||
| OrderNotFoundError, | ||
| } from '@/lib/services/errors'; | ||
| import { offerIntlQuote } from '@/lib/services/shop/quotes'; | ||
| import { | ||
| intlQuoteOfferPayloadSchema, | ||
| orderIdParamSchema, | ||
| } from '@/lib/validation/shop'; | ||
|
|
||
| function noStoreJson(body: unknown, init?: { status?: number }) { | ||
| const res = NextResponse.json(body, { status: init?.status ?? 200 }); | ||
| res.headers.set('Cache-Control', 'no-store'); | ||
| return res; | ||
| } | ||
|
|
||
| function mapQuoteErrorStatus(code: string): number { | ||
| if ( | ||
| code === 'QUOTE_VERSION_CONFLICT' || | ||
| code === 'QUOTE_NOT_APPLICABLE' || | ||
| code === 'QUOTE_ALREADY_ACCEPTED' | ||
| ) { | ||
| return 409; | ||
| } | ||
| if (code === 'QUOTE_INVALID_EXPIRY') return 422; | ||
| return 400; | ||
| } | ||
|
|
||
| export const runtime = 'nodejs'; | ||
|
|
||
| export async function POST( | ||
| request: NextRequest, | ||
| context: { params: Promise<{ id: string }> } | ||
| ) { | ||
| const requestId = | ||
| request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); | ||
| const baseMeta = { | ||
| requestId, | ||
| route: request.nextUrl.pathname, | ||
| method: request.method, | ||
| }; | ||
| let orderIdForLog: string | null = null; | ||
|
|
||
| const blocked = guardBrowserSameOrigin(request); | ||
| if (blocked) { | ||
| blocked.headers.set('Cache-Control', 'no-store'); | ||
| return blocked; | ||
| } | ||
|
|
||
| try { | ||
| const admin = await requireAdminApi(request); | ||
| const csrfRes = requireAdminCsrf(request, 'admin:orders:quote:offer'); | ||
| if (csrfRes) { | ||
| csrfRes.headers.set('Cache-Control', 'no-store'); | ||
| return csrfRes; | ||
| } | ||
|
|
||
| const parsedParams = orderIdParamSchema.safeParse(await context.params); | ||
| if (!parsedParams.success) { | ||
| return noStoreJson( | ||
| { code: 'INVALID_ORDER_ID', message: 'Invalid order id.' }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
| orderIdForLog = parsedParams.data.id; | ||
|
|
||
| let rawBody: unknown; | ||
| try { | ||
| rawBody = await request.json(); | ||
| } catch { | ||
| return noStoreJson( | ||
| { code: 'INVALID_PAYLOAD', message: 'Invalid JSON body.' }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| const parsedBody = intlQuoteOfferPayloadSchema.safeParse(rawBody); | ||
| if (!parsedBody.success) { | ||
| return noStoreJson( | ||
| { code: 'INVALID_PAYLOAD', message: 'Invalid payload.' }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| const payload = parsedBody.data; | ||
| const result = await offerIntlQuote({ | ||
| orderId: orderIdForLog, | ||
| requestId, | ||
| actorUserId: typeof admin.id === 'string' ? admin.id : null, | ||
| version: payload.version, | ||
| currency: payload.currency, | ||
| shippingQuoteMinor: payload.shippingQuoteMinor, | ||
| expiresAt: payload.expiresAt ?? null, | ||
| payload: payload.payload, | ||
| }); | ||
|
|
||
| return noStoreJson( | ||
| { | ||
| success: true, | ||
| orderId: result.orderId, | ||
| version: result.version, | ||
| quoteStatus: result.quoteStatus, | ||
| shippingQuoteMinor: result.shippingQuoteMinor, | ||
| currency: result.currency, | ||
| expiresAt: result.expiresAt.toISOString(), | ||
| }, | ||
| { status: 200 } | ||
| ); | ||
| } catch (error) { | ||
| if (error instanceof AdminApiDisabledError) { | ||
| return noStoreJson( | ||
| { code: 'ADMIN_API_DISABLED', message: 'Admin API is disabled.' }, | ||
| { status: 403 } | ||
| ); | ||
| } | ||
| if (error instanceof AdminUnauthorizedError) { | ||
| return noStoreJson( | ||
| { code: error.code, message: 'Unauthorized.' }, | ||
| { status: 401 } | ||
| ); | ||
| } | ||
| if (error instanceof AdminForbiddenError) { | ||
| return noStoreJson({ code: error.code, message: 'Forbidden.' }, { status: 403 }); | ||
| } | ||
| if (error instanceof OrderNotFoundError) { | ||
| return noStoreJson({ code: error.code }, { status: 404 }); | ||
| } | ||
| if (error instanceof InvalidPayloadError) { | ||
| logWarn('admin_quote_offer_rejected', { | ||
| ...baseMeta, | ||
| orderId: orderIdForLog, | ||
| code: error.code, | ||
| }); | ||
| return noStoreJson( | ||
| { | ||
| code: error.code, | ||
| message: error.message, | ||
| ...(error.details ? { details: error.details } : {}), | ||
| }, | ||
| { status: mapQuoteErrorStatus(error.code) } | ||
| ); | ||
| } | ||
|
|
||
| logError('admin_quote_offer_failed', error, { | ||
| ...baseMeta, | ||
| orderId: orderIdForLog, | ||
| code: 'ADMIN_QUOTE_OFFER_FAILED', | ||
| }); | ||
| return noStoreJson( | ||
| { code: 'INTERNAL_ERROR', message: 'Unable to offer quote.' }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Audit failure causes 500 error even though product update succeeded.
When
writeAdminAuditfails (line 491-505), the error is rethrown and eventually returns a 500 response. However,updateProductat line 455 already succeeded. This creates a confusing user experience:This is inconsistent with the DELETE handler (lines 774-788) which logs audit failures but still returns success.
🔧 Recommended fix
Follow the same pattern as DELETE - log the failure but return success:
try { await writeAdminAudit({ actorUserId, action: 'product_admin_action.update', // ... rest of args }); } catch (auditError) { logWarn('admin_product_update_audit_failed', { ...baseMeta, code: 'AUDIT_WRITE_FAILED', // ... rest of meta }); - throw auditError; + // Update is complete; keep success response to avoid misleading retries. + // Audit failure is logged and should be monitored/alerted separately. } return noStoreJson({ success: true, product: updated }, { status: 200 });🤖 Prompt for AI Agents