feat: add legacy device migration flow
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}设备授权 | 虾皮圈{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="container py-5">
|
||||
<h1 class="h3 mb-4">设备授权</h1>
|
||||
<div class="table-responsive"><table class="table align-middle">
|
||||
<thead><tr><th>产品</th><th>设备平台</th><th>客户端版本</th><th>到期时间</th><th>状态</th><th></th></tr></thead>
|
||||
<tbody>{% for credential in credentials %}<tr>
|
||||
<td>{{ credential.get_product_code_display }}</td><td>{{ credential.device.get_platform_display }}</td><td>{{ credential.device.client_version }}</td><td>{{ credential.expires_at }}</td>
|
||||
<td>{% if credential.revoked_at %}已吊销{% else %}有效{% endif %}</td>
|
||||
<td>{% if not credential.revoked_at %}<form method="post" action="{% url 'portal-device-credential-revoke' credential.pk %}">{% csrf_token %}<button class="btn btn-outline-danger btn-sm">解绑</button></form>{% endif %}</td>
|
||||
</tr>{% empty %}<tr><td colspan="6" class="text-muted">暂无已确认设备。</td></tr>{% endfor %}</tbody>
|
||||
</table></div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "portal/base.html" %}
|
||||
|
||||
{% block title %}确认设备迁移 | 虾皮圈{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="container py-5" style="max-width: 720px;">
|
||||
<h1 class="h3 mb-3">确认设备迁移</h1>
|
||||
<p class="text-muted">确认后,当前设备会占用一个软件授权席位。</p>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">产品</dt><dd class="col-sm-8">{{ migration_request.device.get_product_code_display }}</dd>
|
||||
<dt class="col-sm-4">客户端版本</dt><dd class="col-sm-8">{{ migration_request.device.client_version }}</dd>
|
||||
<dt class="col-sm-4">确认截止</dt><dd class="col-sm-8">{{ migration_request.expires_at }}</dd>
|
||||
<dt class="col-sm-4">状态</dt><dd class="col-sm-8">{{ migration_request.get_status_display }}</dd>
|
||||
</dl>
|
||||
{% if migration_request.status == "pending" %}
|
||||
<form method="post">{% csrf_token %}<button type="submit" class="btn btn-primary">确认当前设备</button></form>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -5,8 +5,11 @@ from .views import (
|
||||
ApiKeyDeleteView,
|
||||
ApiKeyListCreateView,
|
||||
DashboardView,
|
||||
DeviceCredentialListView,
|
||||
DeviceCredentialRevokeView,
|
||||
HomeView,
|
||||
ModelCatalogView,
|
||||
MigrationConfirmView,
|
||||
RechargePageView,
|
||||
RechargeRecordListView,
|
||||
UsageRecordListView,
|
||||
@@ -22,6 +25,17 @@ urlpatterns = [
|
||||
path("apikeys/<int:pk>/delete", ApiKeyDeleteView.as_view(), name="portal-apikey-delete"),
|
||||
path("models", ModelCatalogView.as_view(), name="portal-models"),
|
||||
path("recharge", RechargePageView.as_view(), name="portal-recharge"),
|
||||
path(
|
||||
"migration/confirm/<uuid:request_id>",
|
||||
MigrationConfirmView.as_view(),
|
||||
name="portal-migration-confirm",
|
||||
),
|
||||
path("migration/devices", DeviceCredentialListView.as_view(), name="portal-device-credentials"),
|
||||
path(
|
||||
"migration/credentials/<int:pk>/revoke",
|
||||
DeviceCredentialRevokeView.as_view(),
|
||||
name="portal-device-credential-revoke",
|
||||
),
|
||||
path("records/recharge", RechargeRecordListView.as_view(), name="portal-recharge-records"),
|
||||
path("records/usage", UsageRecordListView.as_view(), name="portal-usage-records"),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Sum
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
@@ -17,6 +18,12 @@ from apps.billing.services import (
|
||||
get_balance_snapshot,
|
||||
)
|
||||
from apps.users.models import ApiKey
|
||||
from apps.licensing.models import DeviceCredential, MigrationRequest
|
||||
from apps.licensing.services import (
|
||||
LicensingError,
|
||||
confirm_migration_request,
|
||||
revoke_device_credential,
|
||||
)
|
||||
|
||||
from .forms import ApiKeyCreateForm, RechargeCreateForm
|
||||
from .models import DownloadRelease, ImportTemplate
|
||||
@@ -243,6 +250,69 @@ class RechargePageView(LoginRequiredMixin, FormView):
|
||||
return redirect(f"{recharge_url}?order_no={order.order_no}")
|
||||
|
||||
|
||||
class MigrationConfirmView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "portal/migration_confirm.html"
|
||||
|
||||
def get_migration_request(self):
|
||||
migration_request = get_object_or_404(
|
||||
MigrationRequest.objects.select_related("device", "migration_grant__entitlement"),
|
||||
request_id=self.kwargs["request_id"],
|
||||
)
|
||||
if migration_request.user_id != self.request.user.id:
|
||||
raise PermissionDenied("迁移请求不属于当前账号")
|
||||
return migration_request
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["migration_request"] = self.get_migration_request()
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
migration_request = self.get_migration_request()
|
||||
try:
|
||||
_request, _credential, created = confirm_migration_request(
|
||||
request_id=migration_request.request_id,
|
||||
user=request.user,
|
||||
)
|
||||
except LicensingError as exc:
|
||||
messages.error(request, exc.message)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
"设备迁移已确认。" if created else "该设备迁移已确认,无需重复操作。",
|
||||
)
|
||||
return redirect("portal-migration-confirm", request_id=migration_request.request_id)
|
||||
|
||||
|
||||
class DeviceCredentialListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "portal/device_credentials.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["credentials"] = (
|
||||
DeviceCredential.objects.filter(user=self.request.user)
|
||||
.select_related("device", "entitlement", "seat")
|
||||
.order_by("-created_at", "-id")
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class DeviceCredentialRevokeView(LoginRequiredMixin, View):
|
||||
def post(self, request, pk):
|
||||
credential = get_object_or_404(DeviceCredential, pk=pk, user=request.user)
|
||||
try:
|
||||
revoke_device_credential(
|
||||
credential=credential,
|
||||
reason="用户自助解绑设备",
|
||||
actor=request.user,
|
||||
)
|
||||
except LicensingError as exc:
|
||||
messages.error(request, exc.message)
|
||||
else:
|
||||
messages.success(request, "设备已解绑,原设备凭证已吊销。")
|
||||
return redirect("portal-device-credentials")
|
||||
|
||||
|
||||
class RechargeRecordListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "portal/recharge_records.html"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user