karrio-server-core 2025.5rc12__py3-none-any.whl → 2026.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- karrio/server/core/authentication.py +59 -25
- karrio/server/core/config.py +31 -0
- karrio/server/core/datatypes.py +30 -4
- karrio/server/core/dataunits.py +53 -22
- karrio/server/core/exceptions.py +287 -17
- karrio/server/core/filters.py +14 -0
- karrio/server/core/gateway.py +285 -10
- karrio/server/core/logging.py +403 -0
- karrio/server/core/management/commands/runserver.py +5 -0
- karrio/server/core/middleware.py +104 -2
- karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
- karrio/server/core/models/base.py +34 -1
- karrio/server/core/oauth_validators.py +2 -3
- karrio/server/core/permissions.py +1 -2
- karrio/server/core/serializers.py +183 -10
- karrio/server/core/signals.py +22 -28
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests/__init__.py +27 -0
- karrio/server/core/{tests.py → tests/base.py} +6 -7
- karrio/server/core/tests/test_exception_level.py +159 -0
- karrio/server/core/tests/test_resource_token.py +593 -0
- karrio/server/core/utils.py +688 -38
- karrio/server/core/validators.py +144 -222
- karrio/server/core/views/oauth.py +13 -12
- karrio/server/core/views/references.py +2 -2
- karrio/server/iam/apps.py +1 -4
- karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
- karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
- karrio/server/iam/permissions.py +7 -134
- karrio/server/iam/serializers.py +17 -2
- karrio/server/iam/signals.py +2 -4
- karrio/server/providers/admin.py +1 -1
- karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
- karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
- karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
- karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
- karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
- karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
- karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
- karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
- karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
- karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
- karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
- karrio/server/providers/models/__init__.py +1 -2
- karrio/server/providers/models/carrier.py +103 -18
- karrio/server/providers/models/service.py +188 -1
- karrio/server/providers/models/sheet.py +371 -0
- karrio/server/providers/serializers/base.py +263 -2
- karrio/server/providers/signals.py +2 -4
- karrio/server/providers/templates/providers/oauth_callback.html +105 -0
- karrio/server/providers/tests/__init__.py +5 -0
- karrio/server/providers/tests/test_connections.py +895 -0
- karrio/server/providers/views/carriers.py +1 -3
- karrio/server/providers/views/connections.py +322 -2
- karrio/server/samples.py +1 -1
- karrio/server/serializers/abstract.py +116 -21
- karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
- karrio/server/tracing/models.py +2 -0
- karrio/server/tracing/utils.py +5 -8
- karrio/server/user/migrations/0007_user_metadata.py +25 -0
- karrio/server/user/models.py +38 -23
- karrio/server/user/serializers.py +1 -0
- karrio/server/user/templates/registration/registration_confirm_email.html +1 -1
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +67 -86
- karrio/server/providers/extension/__init__.py +0 -1
- karrio/server/providers/extension/models/__init__.py +0 -1
- karrio/server/providers/extension/models/allied_express.py +0 -22
- karrio/server/providers/extension/models/allied_express_local.py +0 -22
- karrio/server/providers/extension/models/amazon_shipping.py +0 -27
- karrio/server/providers/extension/models/aramex.py +0 -25
- karrio/server/providers/extension/models/asendia_us.py +0 -21
- karrio/server/providers/extension/models/australiapost.py +0 -20
- karrio/server/providers/extension/models/boxknight.py +0 -19
- karrio/server/providers/extension/models/bpost.py +0 -21
- karrio/server/providers/extension/models/canadapost.py +0 -21
- karrio/server/providers/extension/models/canpar.py +0 -19
- karrio/server/providers/extension/models/chronopost.py +0 -22
- karrio/server/providers/extension/models/colissimo.py +0 -22
- karrio/server/providers/extension/models/dhl_express.py +0 -23
- karrio/server/providers/extension/models/dhl_parcel_de.py +0 -25
- karrio/server/providers/extension/models/dhl_poland.py +0 -22
- karrio/server/providers/extension/models/dhl_universal.py +0 -19
- karrio/server/providers/extension/models/dicom.py +0 -20
- karrio/server/providers/extension/models/dpd.py +0 -37
- karrio/server/providers/extension/models/dpdhl.py +0 -26
- karrio/server/providers/extension/models/easypost.py +0 -20
- karrio/server/providers/extension/models/eshipper.py +0 -21
- karrio/server/providers/extension/models/fedex.py +0 -25
- karrio/server/providers/extension/models/fedex_ws.py +0 -24
- karrio/server/providers/extension/models/freightcom.py +0 -21
- karrio/server/providers/extension/models/generic.py +0 -35
- karrio/server/providers/extension/models/geodis.py +0 -22
- karrio/server/providers/extension/models/hay_post.py +0 -22
- karrio/server/providers/extension/models/laposte.py +0 -19
- karrio/server/providers/extension/models/locate2u.py +0 -22
- karrio/server/providers/extension/models/nationex.py +0 -22
- karrio/server/providers/extension/models/purolator.py +0 -21
- karrio/server/providers/extension/models/roadie.py +0 -18
- karrio/server/providers/extension/models/royalmail.py +0 -19
- karrio/server/providers/extension/models/sendle.py +0 -22
- karrio/server/providers/extension/models/tge.py +0 -63
- karrio/server/providers/extension/models/tnt.py +0 -23
- karrio/server/providers/extension/models/ups.py +0 -23
- karrio/server/providers/extension/models/usps.py +0 -23
- karrio/server/providers/extension/models/usps_international.py +0 -23
- karrio/server/providers/extension/models/usps_wt.py +0 -24
- karrio/server/providers/extension/models/usps_wt_international.py +0 -24
- karrio/server/providers/extension/models/zoom2u.py +0 -23
- karrio/server/providers/tests.py +0 -3
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
import django.db.transaction as transaction
|
|
3
|
+
from rest_framework import status as http_status
|
|
3
4
|
|
|
4
5
|
import karrio.lib as lib
|
|
5
6
|
import karrio.references as references
|
|
@@ -7,8 +8,9 @@ import karrio.server.openapi as openapi
|
|
|
7
8
|
import karrio.server.core.utils as utils
|
|
8
9
|
import karrio.server.serializers as serializers
|
|
9
10
|
import karrio.server.core.dataunits as dataunits
|
|
11
|
+
import karrio.server.core.exceptions as exceptions
|
|
10
12
|
import karrio.server.providers.models as providers
|
|
11
|
-
from karrio.server.core.serializers import CARRIERS
|
|
13
|
+
from karrio.server.core.serializers import CARRIERS, Message
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def generate_carrier_serializers() -> typing.Dict[str, serializers.Serializer]:
|
|
@@ -201,7 +203,7 @@ class CarrierConnectionModelSerializer(serializers.ModelSerializer):
|
|
|
201
203
|
context: serializers.Context,
|
|
202
204
|
**kwargs,
|
|
203
205
|
) -> providers.Carrier:
|
|
204
|
-
config = validated_data.pop("config")
|
|
206
|
+
config = validated_data.pop("config", None)
|
|
205
207
|
carrier_name = validated_data.pop("carrier_name")
|
|
206
208
|
default_capabilities = references.get_carrier_capabilities(carrier_name)
|
|
207
209
|
capabilities = lib.identity(
|
|
@@ -275,3 +277,262 @@ class CarrierConnectionModelSerializer(serializers.ModelSerializer):
|
|
|
275
277
|
)
|
|
276
278
|
|
|
277
279
|
return super().update(instance, validated_data, **kwargs)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# =============================================================================
|
|
283
|
+
# Webhook Management Serializers
|
|
284
|
+
# =============================================================================
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class WebhookOperationResponse(serializers.Serializer):
|
|
288
|
+
"""Response serializer for webhook operations."""
|
|
289
|
+
|
|
290
|
+
operation = serializers.CharField(help_text="The operation performed")
|
|
291
|
+
success = serializers.BooleanField(help_text="Whether the operation was successful")
|
|
292
|
+
carrier_name = serializers.CharField(help_text="The carrier name")
|
|
293
|
+
carrier_id = serializers.CharField(help_text="The carrier connection ID")
|
|
294
|
+
messages = Message(
|
|
295
|
+
required=False,
|
|
296
|
+
many=True,
|
|
297
|
+
help_text="Operation messages or errors",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class WebhookRegisterData(serializers.Serializer):
|
|
302
|
+
"""Request serializer for webhook registration."""
|
|
303
|
+
|
|
304
|
+
enabled_events = serializers.StringListField(
|
|
305
|
+
required=False,
|
|
306
|
+
default=["*"],
|
|
307
|
+
help_text="Events to subscribe to. Defaults to all events.",
|
|
308
|
+
)
|
|
309
|
+
description = serializers.CharField(
|
|
310
|
+
required=False,
|
|
311
|
+
allow_blank=True,
|
|
312
|
+
help_text="Description for the webhook registration.",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class WebhookRegisterSerializer(serializers.Serializer):
|
|
317
|
+
"""Handles webhook registration with carriers. Returns webhook details on success."""
|
|
318
|
+
|
|
319
|
+
webhook_url = serializers.URLField(
|
|
320
|
+
required=True,
|
|
321
|
+
help_text="The URL to receive webhook events.",
|
|
322
|
+
)
|
|
323
|
+
description = serializers.CharField(
|
|
324
|
+
required=False,
|
|
325
|
+
allow_blank=True,
|
|
326
|
+
help_text="Description for the webhook registration.",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
@utils.error_wrapper
|
|
330
|
+
def update(self, connection: providers.Carrier, validated_data: dict, **kwargs):
|
|
331
|
+
import karrio.server.core.gateway as gateway
|
|
332
|
+
|
|
333
|
+
webhook_url = validated_data["webhook_url"]
|
|
334
|
+
description = validated_data.get(
|
|
335
|
+
"description", f"Karrio webhook for {connection.carrier_id}"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
webhook_details, messages = gateway.Webhooks.register(
|
|
339
|
+
dict(url=webhook_url, description=description),
|
|
340
|
+
carrier=connection,
|
|
341
|
+
**kwargs,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if webhook_details is None:
|
|
345
|
+
raise exceptions.APIException(
|
|
346
|
+
detail=messages,
|
|
347
|
+
status_code=http_status.HTTP_424_FAILED_DEPENDENCY,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return webhook_details
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class WebhookDeregisterSerializer(serializers.Serializer):
|
|
354
|
+
"""Handles webhook deregistration from carriers. Returns confirmation on success."""
|
|
355
|
+
|
|
356
|
+
webhook_id = serializers.CharField(
|
|
357
|
+
required=True,
|
|
358
|
+
help_text="The webhook ID to deregister.",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@utils.error_wrapper
|
|
362
|
+
def update(self, connection: providers.Carrier, validated_data: dict, **kwargs):
|
|
363
|
+
import karrio.server.core.gateway as gateway
|
|
364
|
+
|
|
365
|
+
confirmation, messages = gateway.Webhooks.unregister(
|
|
366
|
+
payload=dict(webhook_id=validated_data["webhook_id"]),
|
|
367
|
+
carrier=connection,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if not (confirmation and confirmation.success):
|
|
371
|
+
raise exceptions.APIException(
|
|
372
|
+
detail=messages,
|
|
373
|
+
status_code=http_status.HTTP_424_FAILED_DEPENDENCY,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return confirmation
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# =============================================================================
|
|
380
|
+
# OAuth Callback Serializers
|
|
381
|
+
# =============================================================================
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class OAuthAuthorizeData(serializers.Serializer):
|
|
385
|
+
"""Request serializer for OAuth authorization."""
|
|
386
|
+
|
|
387
|
+
frontend_url = serializers.CharField(
|
|
388
|
+
required=False,
|
|
389
|
+
allow_blank=True,
|
|
390
|
+
help_text="Frontend URL to redirect to after OAuth callback.",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class OAuthCallbackData(serializers.Serializer):
|
|
395
|
+
"""Request serializer for OAuth callback data."""
|
|
396
|
+
|
|
397
|
+
query = serializers.PlainDictField(
|
|
398
|
+
required=False,
|
|
399
|
+
default={},
|
|
400
|
+
help_text="Query parameters from the OAuth callback.",
|
|
401
|
+
)
|
|
402
|
+
body = serializers.PlainDictField(
|
|
403
|
+
required=False,
|
|
404
|
+
default={},
|
|
405
|
+
help_text="Body data from the OAuth callback.",
|
|
406
|
+
)
|
|
407
|
+
headers = serializers.PlainDictField(
|
|
408
|
+
required=False,
|
|
409
|
+
default={},
|
|
410
|
+
help_text="Headers from the OAuth callback.",
|
|
411
|
+
)
|
|
412
|
+
url = serializers.CharField(
|
|
413
|
+
required=False,
|
|
414
|
+
help_text="The full callback URL.",
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class OAuthCallbackSerializer(serializers.Serializer):
|
|
419
|
+
"""Handles OAuth callback processing logic."""
|
|
420
|
+
|
|
421
|
+
@staticmethod
|
|
422
|
+
def process_callback(
|
|
423
|
+
request,
|
|
424
|
+
carrier_name: str,
|
|
425
|
+
) -> dict:
|
|
426
|
+
"""Process OAuth callback and return result dict."""
|
|
427
|
+
import json
|
|
428
|
+
import base64
|
|
429
|
+
import karrio.lib as lib
|
|
430
|
+
import karrio.server.core.gateway as gateway
|
|
431
|
+
|
|
432
|
+
payload = OAuthCallbackData.map(
|
|
433
|
+
data=dict(
|
|
434
|
+
query=request.query_params.dict(),
|
|
435
|
+
body=(
|
|
436
|
+
request.data.dict()
|
|
437
|
+
if hasattr(request.data, "dict")
|
|
438
|
+
else dict(request.data or {})
|
|
439
|
+
),
|
|
440
|
+
headers=dict(request.headers),
|
|
441
|
+
url=request.build_absolute_uri(),
|
|
442
|
+
)
|
|
443
|
+
).data
|
|
444
|
+
|
|
445
|
+
[output, messages] = gateway.Hooks.on_oauth_callback(
|
|
446
|
+
payload=payload,
|
|
447
|
+
carrier_name=carrier_name,
|
|
448
|
+
test_mode=request.test_mode,
|
|
449
|
+
context=request,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
result = dict(
|
|
453
|
+
type="oauth_callback",
|
|
454
|
+
success=output is not None,
|
|
455
|
+
carrier_name=carrier_name,
|
|
456
|
+
credentials=lib.to_dict(output) if output else None,
|
|
457
|
+
messages=lib.to_dict(messages),
|
|
458
|
+
state=request.query_params.get("state"),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
frontend_url = None
|
|
462
|
+
state = request.query_params.get("state")
|
|
463
|
+
if state:
|
|
464
|
+
try:
|
|
465
|
+
state_data = json.loads(base64.b64decode(state).decode("utf-8"))
|
|
466
|
+
frontend_url = state_data.get("frontend_url")
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
return result, frontend_url
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# =============================================================================
|
|
474
|
+
# Webhook Event Serializers
|
|
475
|
+
# =============================================================================
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class WebhookEventSerializer(serializers.Serializer):
|
|
479
|
+
"""Handles webhook event processing logic."""
|
|
480
|
+
|
|
481
|
+
@staticmethod
|
|
482
|
+
def process_event(request, pk: str) -> tuple:
|
|
483
|
+
"""
|
|
484
|
+
Process webhook event and return response data and status code.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
tuple: (response_data, http_status_code)
|
|
488
|
+
"""
|
|
489
|
+
import django.db.models as django
|
|
490
|
+
import karrio.lib as lib
|
|
491
|
+
import karrio.server.core.gateway as gateway
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
connection = providers.Carrier.objects.get(pk=pk)
|
|
495
|
+
except providers.Carrier.DoesNotExist:
|
|
496
|
+
return (
|
|
497
|
+
dict(
|
|
498
|
+
operation="Webhook event",
|
|
499
|
+
success=False,
|
|
500
|
+
messages=[{"message": f"Connection not found: {pk}"}],
|
|
501
|
+
),
|
|
502
|
+
http_status.HTTP_404_NOT_FOUND,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
event, messages = gateway.Hooks.on_webhook_event(
|
|
506
|
+
payload=dict(
|
|
507
|
+
url=request.build_absolute_uri(),
|
|
508
|
+
body=request.data,
|
|
509
|
+
query=dict(request.query_params),
|
|
510
|
+
headers=dict(request.headers),
|
|
511
|
+
),
|
|
512
|
+
carrier=connection,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if event and event.tracking:
|
|
516
|
+
import karrio.server.manager.models as manager_models
|
|
517
|
+
import karrio.server.manager.serializers.tracking as tracking_serializers
|
|
518
|
+
|
|
519
|
+
tracker = manager_models.Tracking.objects.filter(
|
|
520
|
+
django.Q(tracking_number=event.tracking.tracking_number)
|
|
521
|
+
| django.Q(tracking_carrier=connection)
|
|
522
|
+
).first()
|
|
523
|
+
|
|
524
|
+
if tracker:
|
|
525
|
+
tracking_serializers.update_tracker(
|
|
526
|
+
tracker, lib.to_dict(event.tracking)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
dict(
|
|
531
|
+
operation="Webhook event",
|
|
532
|
+
success=len(messages) == 0,
|
|
533
|
+
carrier_name=connection.carrier_name,
|
|
534
|
+
carrier_id=connection.carrier_id,
|
|
535
|
+
messages=lib.to_dict(messages),
|
|
536
|
+
),
|
|
537
|
+
http_status.HTTP_200_OK,
|
|
538
|
+
)
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from django.db.models import signals
|
|
3
2
|
|
|
4
3
|
import karrio.references as ref
|
|
5
4
|
import karrio.server.core.utils as utils
|
|
5
|
+
from karrio.server.core.logging import logger
|
|
6
6
|
import karrio.server.providers.models as models
|
|
7
7
|
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
8
|
|
|
11
9
|
def register_signals():
|
|
12
10
|
signals.post_save.connect(carrier_changed, sender=models.Carrier)
|
|
13
11
|
|
|
14
|
-
logger.info("
|
|
12
|
+
logger.info("Karrio providers signals registered")
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
@utils.disable_for_loaddata
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>OAuth Callback</title>
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
align-items: center;
|
|
11
|
+
height: 100vh;
|
|
12
|
+
margin: 0;
|
|
13
|
+
background: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
.container {
|
|
16
|
+
text-align: center;
|
|
17
|
+
padding: 40px;
|
|
18
|
+
background: white;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
21
|
+
max-width: 400px;
|
|
22
|
+
}
|
|
23
|
+
.success { color: #16a34a; }
|
|
24
|
+
.error { color: #dc2626; }
|
|
25
|
+
.icon {
|
|
26
|
+
font-size: 48px;
|
|
27
|
+
margin-bottom: 16px;
|
|
28
|
+
}
|
|
29
|
+
h2 { margin-bottom: 10px; }
|
|
30
|
+
p { color: #666; margin-bottom: 20px; }
|
|
31
|
+
.close-hint {
|
|
32
|
+
font-size: 12px;
|
|
33
|
+
color: #999;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="container">
|
|
39
|
+
<div class="icon">{% if success %}✓{% else %}✗{% endif %}</div>
|
|
40
|
+
<h2 class="{% if success %}success{% else %}error{% endif %}">
|
|
41
|
+
{% if success %}Authorization Successful{% else %}Authorization Failed{% endif %}
|
|
42
|
+
</h2>
|
|
43
|
+
<p>{% if success %}You can close this window and return to the application.{% else %}{{ error_message }}{% endif %}</p>
|
|
44
|
+
<p class="close-hint">This window will close automatically...</p>
|
|
45
|
+
</div>
|
|
46
|
+
<script>
|
|
47
|
+
(async function() {
|
|
48
|
+
var result = {{ result_json|safe }};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Encrypts data using AES-GCM with Web Crypto API.
|
|
52
|
+
* Returns an object containing the encrypted data, IV, and key (all base64 encoded).
|
|
53
|
+
*/
|
|
54
|
+
async function encryptData(data) {
|
|
55
|
+
// Generate a random 256-bit key
|
|
56
|
+
var key = await crypto.subtle.generateKey(
|
|
57
|
+
{ name: "AES-GCM", length: 256 },
|
|
58
|
+
true, // extractable - needed to export the key
|
|
59
|
+
["encrypt", "decrypt"]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Generate a random 96-bit IV (recommended for AES-GCM)
|
|
63
|
+
var iv = crypto.getRandomValues(new Uint8Array(12));
|
|
64
|
+
|
|
65
|
+
// Encode the data as UTF-8
|
|
66
|
+
var encoder = new TextEncoder();
|
|
67
|
+
var encodedData = encoder.encode(data);
|
|
68
|
+
|
|
69
|
+
// Encrypt the data
|
|
70
|
+
var encryptedBuffer = await crypto.subtle.encrypt(
|
|
71
|
+
{ name: "AES-GCM", iv: iv },
|
|
72
|
+
key,
|
|
73
|
+
encodedData
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Export the key for storage
|
|
77
|
+
var exportedKey = await crypto.subtle.exportKey("raw", key);
|
|
78
|
+
|
|
79
|
+
// Convert to base64 for storage
|
|
80
|
+
var ciphertext = btoa(String.fromCharCode.apply(null, new Uint8Array(encryptedBuffer)));
|
|
81
|
+
var ivBase64 = btoa(String.fromCharCode.apply(null, iv));
|
|
82
|
+
var keyBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(exportedKey)));
|
|
83
|
+
|
|
84
|
+
return { ciphertext: ciphertext, iv: ivBase64, key: keyBase64 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Store encrypted result in localStorage for the opener to read
|
|
88
|
+
// Using localStorage (not sessionStorage) because popup windows have
|
|
89
|
+
// separate sessionStorage contexts from their opener window.
|
|
90
|
+
// Data is encrypted with AES-GCM for security.
|
|
91
|
+
try {
|
|
92
|
+
var encrypted = await encryptData(JSON.stringify(result));
|
|
93
|
+
localStorage.setItem('karrio_oauth_result', JSON.stringify(encrypted));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error('Failed to encrypt/store OAuth result:', e);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Try to close the window after a short delay
|
|
99
|
+
setTimeout(function() {
|
|
100
|
+
window.close();
|
|
101
|
+
}, 2000);
|
|
102
|
+
})();
|
|
103
|
+
</script>
|
|
104
|
+
</body>
|
|
105
|
+
</html>
|