From a1aecb196caa047196e639a6aa4ca8809aa51f30 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 7 May 2025 18:02:19 +0800
Subject: [PATCH 01/30] chore(charts): use condition

---
 charts/passport/templates/_helpers.tpl        |  4 +--
 .../templates/passport-configmap.yaml         |  2 --
 .../templates/passport-deployment.yaml        |  2 --
 .../templates/passport-job-upgrade.yaml       |  2 --
 .../templates/passport-secret-creds.yaml      |  2 --
 .../templates/passport-service-account.yaml   |  2 --
 .../passport/templates/passport-service.yaml  |  2 --
 charts/passport/values.yaml                   | 25 +++++--------------
 8 files changed, 8 insertions(+), 33 deletions(-)

diff --git a/charts/passport/templates/_helpers.tpl b/charts/passport/templates/_helpers.tpl
index eb50418..95f84ac 100644
--- a/charts/passport/templates/_helpers.tpl
+++ b/charts/passport/templates/_helpers.tpl
@@ -34,7 +34,7 @@ env:
     secretKeyRef:
       name: passport-creds
       key: valkey-url
-{{- else if eq .Values.global.valkeyLocation "on-cluster"  }}
+{{- else if .Values.valkey.enabled  }}
 - name: VALKEY_PASSWORD
   valueFrom:
     secretKeyRef:
@@ -56,7 +56,7 @@ env:
       name: passport-creds
       key: database-replica-url
 {{- end }}
-{{- else if eq .Values.global.databaseLocation "on-cluster" }}
+{{- else if .Values.database.enabled }}
 - name: DRYCC_DATABASE_USER
   valueFrom:
     secretKeyRef:
diff --git a/charts/passport/templates/passport-configmap.yaml b/charts/passport/templates/passport-configmap.yaml
index 858b6cb..71d582e 100644
--- a/charts/passport/templates/passport-configmap.yaml
+++ b/charts/passport/templates/passport-configmap.yaml
@@ -1,4 +1,3 @@
-{{- if eq .Values.global.passportLocation "on-cluster" }}
 apiVersion: v1
 kind: ConfigMap
 metadata:
@@ -9,4 +8,3 @@ data:
 data:
   init-applications.json: |-
 {{ toPrettyJson .Values.initApplications | indent 4 }}
-{{- end }}
diff --git a/charts/passport/templates/passport-deployment.yaml b/charts/passport/templates/passport-deployment.yaml
index 8017c06..6e5c583 100644
--- a/charts/passport/templates/passport-deployment.yaml
+++ b/charts/passport/templates/passport-deployment.yaml
@@ -1,4 +1,3 @@
-{{- if eq .Values.global.passportLocation "on-cluster" }}
 apiVersion: apps/v1
 kind: Deployment
 metadata:
@@ -67,4 +66,3 @@ spec:
         {{- include "passport.envs" . | indent 8 }}
         {{- include "passport.volumeMounts" . | indent 8 }}
       {{- include "passport.volumes" . | indent 6 }}
-{{- end }}
diff --git a/charts/passport/templates/passport-job-upgrade.yaml b/charts/passport/templates/passport-job-upgrade.yaml
index 710edfb..433215c 100644
--- a/charts/passport/templates/passport-job-upgrade.yaml
+++ b/charts/passport/templates/passport-job-upgrade.yaml
@@ -1,4 +1,3 @@
-{{- if eq .Values.global.passportLocation "on-cluster" }}
 apiVersion: batch/v1
 kind: Job
 metadata:
@@ -45,4 +44,3 @@ spec:
         {{- include "passport.volumeMounts" . | indent 8 }}
       {{- include "passport.volumes" . | indent 6 }}
       restartPolicy: Never
-{{- end }}
diff --git a/charts/passport/templates/passport-secret-creds.yaml b/charts/passport/templates/passport-secret-creds.yaml
index d43e344..b5d0f90 100644
--- a/charts/passport/templates/passport-secret-creds.yaml
+++ b/charts/passport/templates/passport-secret-creds.yaml
@@ -1,4 +1,3 @@
-{{- if eq .Values.global.passportLocation "on-cluster" }}
 apiVersion: v1
 kind: Secret
 metadata:
@@ -24,4 +23,3 @@ data:
   drycc-passport-{{$name}}-secret: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" "drycc-passport-($name)-secret" "defaultValue" ($item.secret | default (randAlphaNum 64)) "context" $)) }}
   {{- end }}
   {{- end }}
-{{- end }}
diff --git a/charts/passport/templates/passport-service-account.yaml b/charts/passport/templates/passport-service-account.yaml
index 9952519..c5714ad 100644
--- a/charts/passport/templates/passport-service-account.yaml
+++ b/charts/passport/templates/passport-service-account.yaml
@@ -1,8 +1,6 @@
-{{- if eq .Values.global.passportLocation "on-cluster" }}
 apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: drycc-passport
   labels:
     heritage: drycc
-{{- end }}
diff --git a/charts/passport/templates/passport-service.yaml b/charts/passport/templates/passport-service.yaml
index 1549301..1b47a57 100644
--- a/charts/passport/templates/passport-service.yaml
+++ b/charts/passport/templates/passport-service.yaml
@@ -1,4 +1,3 @@
-{{- if eq .Values.global.passportLocation "on-cluster" }}
 apiVersion: v1
 kind: Service
 metadata:
@@ -16,4 +15,3 @@ spec:
       targetPort: 8000
   selector:
     app: drycc-passport
-{{- end }}
\ No newline at end of file
diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index 8e845c7..03fbea2 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -106,27 +106,15 @@ service:
   # Provide any additional service annotations
   annotations: {}
 
-global:
-  # Set the location of Workflow's grafana instance
-  #
-  # Valid values are:
-  # - on-cluster: Run Grafana within the Kubernetes cluster
-  # - off-cluster: Grafana is running outside of the cluster
-  grafanaLocation: "on-cluster"
+valkey:
+  enabled: true
+
+database:
+  enabled: true
 
+global:
   # Admin email, used for each component to send email to administrator
   email: "drycc@drycc.cc"
-  # Set the location of Workflow's PostgreSQL database
-  #
-  # Valid values are:
-  # - on-cluster: Run PostgreSQL within the Kubernetes cluster (credentials are generated
-  #   automatically; backups are sent to object storage
-  #   configured above)
-  # - off-cluster: Run PostgreSQL outside the Kubernetes cluster (configure in database section)
-  databaseLocation: "on-cluster"
-
-  # Please check `kubernetes.io/ingress.class`
-  ingressClass: ""
   # A domain name consists of one or more parts.
   # Periods (.) are used to separate these parts.
   # Each part must be 1 to 63 characters in length and can contain lowercase letters, digits, and hyphens (-).
@@ -138,4 +126,3 @@ global:
   platformDomain: ""
   # Whether cert_passport is enabled to automatically generate passport certificates
   certManagerEnabled: true
-  passportLocation: "on-cluster"

From 63974ecc62c41dedf1e595b4e7efa78664cd8488 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 12 May 2025 09:49:12 +0800
Subject: [PATCH 02/30] chore(charts): change resources format

---
 charts/passport/templates/_helpers.tpl            | 15 ---------------
 .../passport/templates/passport-deployment.yaml   |  5 ++++-
 .../passport/templates/passport-job-upgrade.yaml  |  5 ++++-
 charts/passport/values.yaml                       | 11 ++++++++---
 4 files changed, 16 insertions(+), 20 deletions(-)

diff --git a/charts/passport/templates/_helpers.tpl b/charts/passport/templates/_helpers.tpl
index 95f84ac..8157a5d 100644
--- a/charts/passport/templates/_helpers.tpl
+++ b/charts/passport/templates/_helpers.tpl
@@ -79,21 +79,6 @@ env:
 {{- end }}
 
 
-{{/* Generate passport deployment limits */}}
-{{- define "passport.limits" -}}
-{{- if or (.Values.limitsCpu) (.Values.limitsMemory) }}
-resources:
-  limits:
-{{- if (.Values.limitsCpu) }}
-    cpu: {{.Values.limitsCpu}}
-{{- end }}
-{{- if (.Values.limitsMemory) }}
-    memory: {{.Values.limitsMemory}}
-{{- end }}
-{{- end }}
-{{- end }}
-
-
 {{/* Generate passport deployment volumeMounts */}}
 {{- define "passport.volumeMounts" }}
 volumeMounts:
diff --git a/charts/passport/templates/passport-deployment.yaml b/charts/passport/templates/passport-deployment.yaml
index 6e5c583..9d38074 100644
--- a/charts/passport/templates/passport-deployment.yaml
+++ b/charts/passport/templates/passport-deployment.yaml
@@ -62,7 +62,10 @@ spec:
         ports:
           - containerPort: 8000
             name: http
-        {{- include "passport.limits" . | indent 8 }}
+        {{- with index .Values "resources" }}
+        resources:
+          {{- toYaml . | nindent 10 }}
+        {{- end }}
         {{- include "passport.envs" . | indent 8 }}
         {{- include "passport.volumeMounts" . | indent 8 }}
       {{- include "passport.volumes" . | indent 6 }}
diff --git a/charts/passport/templates/passport-job-upgrade.yaml b/charts/passport/templates/passport-job-upgrade.yaml
index 433215c..ec06d3b 100644
--- a/charts/passport/templates/passport-job-upgrade.yaml
+++ b/charts/passport/templates/passport-job-upgrade.yaml
@@ -39,7 +39,10 @@ spec:
           fi
           python /workspace/manage.py create_oauth2_application --path /etc/drycc/passport/init-applications.json
         {{- end }}
-        {{- include "passport.limits" . | indent 8 }}
+        {{- with index .Values "resources" }}
+        resources:
+          {{- toYaml . | nindent 10 }}
+        {{- end }}
         {{- include "passport.envs" . | indent 8 }}
         {{- include "passport.volumeMounts" . | indent 8 }}
       {{- include "passport.volumes" . | indent 6 }}
diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index 03fbea2..59f8fa3 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -18,6 +18,14 @@ diagnosticMode:
   args:
     - infinity
 
+resources: {}
+  # limits:
+  #   cpu: 200m
+  #   memory: 50Mi
+  # requests:
+  #   cpu: 100m
+  #   memory: 30Mi
+
 nodeAffinityPreset:
   key: "drycc.cc/node"
   type: "soft"
@@ -36,9 +44,6 @@ podAntiAffinityPreset:
 
 # Set passport deployment replicas
 replicas: 1
-# limitsCpu: "100m"
-# limitsMemory: "50Mi"
-
 ## valkeyUrl is will no longer use the built-in valkey component
 valkeyUrl: ""
 ## databaseUrl and databaseReplicaUrl are will no longer use the built-in database component

From ff8f9db5f9b7c3673507303397803f8ea0f2f51d Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Tue, 27 May 2025 13:22:39 +0800
Subject: [PATCH 03/30] chore(passport): add user organizations

---
 rootfs/api/models.py            | 11 +++++++++++
 rootfs/api/oauth2_validators.py |  1 +
 rootfs/api/serializers.py       |  4 ++--
 3 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/rootfs/api/models.py b/rootfs/api/models.py
index cd4a53c..327e8d8 100644
--- a/rootfs/api/models.py
+++ b/rootfs/api/models.py
@@ -8,6 +8,17 @@
 class User(AbstractUser):
     email = models.EmailField(_('email address'), unique=True)
 
+    @property
+    def organizations(self) -> list[str]:
+        results = []
+        if self.is_superuser:
+            results.append("admin")
+        if self.is_staff:
+            results.append("staff")
+        if self.is_active:
+            results.append(self.username)
+        return results
+
 
 class Application(AbstractApplication):
 
diff --git a/rootfs/api/oauth2_validators.py b/rootfs/api/oauth2_validators.py
index ee6f654..9fc78ca 100644
--- a/rootfs/api/oauth2_validators.py
+++ b/rootfs/api/oauth2_validators.py
@@ -16,4 +16,5 @@ def get_additional_claims(self, request):
         claims["is_staff"] = request.user.is_staff
         claims["is_active"] = request.user.is_active
         claims["is_superuser"] = request.user.is_superuser
+        claims["organizations"] = request.user.organizations
         return claims
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index 9deb711..c1d8ac2 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -18,9 +18,9 @@ class UserSerializer(serializers.ModelSerializer):
     class Meta:
         model = User
         fields = ('id', 'username', 'email', 'first_name', 'last_name',
-                  'is_staff', 'is_active', 'is_superuser')
+                  'is_staff', 'is_active', 'is_superuser', "organizations")
         read_only_fields = ('id', 'username', 'is_staff', 'is_active',
-                            'is_superuser')
+                            'is_superuser', "organizations")
 
 
 class UserEmailSerializer(serializers.ModelSerializer):

From 0ca83b54f2f5214dfac96e189283d8ed86088b67 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 29 May 2025 13:23:39 +0800
Subject: [PATCH 04/30] chore(passport): use django redis cache

---
 rootfs/api/settings/production.py | 17 +----------------
 rootfs/requirements.txt           |  3 ++-
 2 files changed, 3 insertions(+), 17 deletions(-)

diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index 9d73673..c7c790c 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -328,26 +328,11 @@
 DRYCC_VALKEY_ADDRS = os.environ.get('DRYCC_VALKEY_ADDRS', '127.0.0.1:6379').split(",")
 DRYCC_VALKEY_PASSWORD = os.environ.get('DRYCC_VALKEY_PASSWORD', '')
 
-# Cache Configuration
-CACHES = {
-    "default": {
-        "BACKEND": "django_redis.cache.RedisCache",
-        "LOCATION": ['redis://:{}@{}'.format(DRYCC_VALKEY_PASSWORD, DRYCC_VALKEY_ADDR) \
-                     for DRYCC_VALKEY_ADDR in DRYCC_VALKEY_ADDRS],  # noqa
-        "OPTIONS": {
-            "CLIENT_CLASS": "django_redis.client.ShardClient",
-        }
-    }
-}
-
 # Cache Valkey Configuration
 CACHES = {
     "default": {
-        "BACKEND": "django_redis.cache.RedisCache",
+        "BACKEND": "django.core.cache.backends.redis.RedisCache",
         "LOCATION": os.environ.get('DRYCC_VALKEY_URL', 'redis://:@127.0.0.1:6379'),
-        "OPTIONS": {
-            "CLIENT_CLASS": "django_redis.client.DefaultClient",
-        }
     }
 }
 
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index c70171d..ea92393 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -10,7 +10,8 @@ asgiref==3.8.1
 psycopg[binary]==3.2.6
 requests==2.32.3
 requests-toolbelt==1.0.0
-django_redis==5.4.0
+redis==6.2.0
+hiredis==3.2.1
 dj-database-url==2.3.0
 django-oauth-toolkit==3.0.1
 whitenoise==6.9.0

From 75ea4d7f4e2c4596b37c4679f09596fd36f557a7 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 9 Jun 2025 10:04:46 +0800
Subject: [PATCH 05/30] chore(passport): change user organizations to roles

---
 rootfs/api/models.py              |  4 ++--
 rootfs/api/oauth2_validators.py   | 18 ++++++++++++++++--
 rootfs/api/serializers.py         |  8 ++++----
 rootfs/api/settings/production.py |  3 ++-
 4 files changed, 24 insertions(+), 9 deletions(-)

diff --git a/rootfs/api/models.py b/rootfs/api/models.py
index 327e8d8..9b97bc0 100644
--- a/rootfs/api/models.py
+++ b/rootfs/api/models.py
@@ -9,14 +9,14 @@ class User(AbstractUser):
     email = models.EmailField(_('email address'), unique=True)
 
     @property
-    def organizations(self) -> list[str]:
+    def roles(self) -> list[str]:
         results = []
         if self.is_superuser:
             results.append("admin")
         if self.is_staff:
             results.append("staff")
         if self.is_active:
-            results.append(self.username)
+            results.append("users")
         return results
 
 
diff --git a/rootfs/api/oauth2_validators.py b/rootfs/api/oauth2_validators.py
index 9fc78ca..c265583 100644
--- a/rootfs/api/oauth2_validators.py
+++ b/rootfs/api/oauth2_validators.py
@@ -3,7 +3,20 @@
 
 class CustomOAuth2Validator(OAuth2Validator):
 
-    oidc_claim_scope = None
+    oidc_claim_scope = OAuth2Validator.oidc_claim_scope
+    oidc_claim_scope.update({
+        "id": "profile",
+        "name": "profile",
+        "username": "profile",
+        "email": "email",
+        "roles": "profile",
+        "first_name": "profile",
+        "last_name": "profile",
+        "is_staff": "profile",
+        "is_active": "profile",
+        "is_superuser": "profile",
+        "preferred_username": "profile",
+    })
 
     def get_additional_claims(self, request):
         claims = super().get_additional_claims(request)
@@ -11,10 +24,11 @@ def get_additional_claims(self, request):
         claims["name"] = request.user.username
         claims["username"] = request.user.username
         claims["email"] = request.user.email
+        claims["roles"] = request.user.roles
         claims["first_name"] = request.user.first_name
         claims["last_name"] = request.user.last_name
         claims["is_staff"] = request.user.is_staff
         claims["is_active"] = request.user.is_active
         claims["is_superuser"] = request.user.is_superuser
-        claims["organizations"] = request.user.organizations
+        claims["preferred_username"] = request.user.username
         return claims
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index c1d8ac2..76d43cb 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -17,10 +17,10 @@
 class UserSerializer(serializers.ModelSerializer):
     class Meta:
         model = User
-        fields = ('id', 'username', 'email', 'first_name', 'last_name',
-                  'is_staff', 'is_active', 'is_superuser', "organizations")
-        read_only_fields = ('id', 'username', 'is_staff', 'is_active',
-                            'is_superuser', "organizations")
+        fields = ('id', 'username', 'email', 'roles', 'first_name', 'last_name',
+                  'is_staff', 'is_active', 'is_superuser')
+        read_only_fields = ('id', 'username', 'roles', 'is_staff', 'is_active',
+                            'is_superuser')
 
 
 class UserEmailSerializer(serializers.ModelSerializer):
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index c7c790c..1c642ec 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -317,10 +317,11 @@
     "REFRESH_TOKEN_EXPIRE_SECONDS": int(os.environ.get('REFRESH_TOKEN_EXPIRE_SECONDS', 60 * 86400)),  # noqa
     "ROTATE_REFRESH_TOKEN": True,
     "SCOPES": {
+        "email": "Email",
         "profile": "Profile",
         "openid": "OpenID Connect scope",
     },
-    "DEFAULT_SCOPES": ['openid', ],
+    "DEFAULT_SCOPES": ['openid', 'email', 'profile'],
     "DEFAULT_CODE_CHALLENGE_METHOD": 'S256',
 }
 OAUTH2_PROVIDER_APPLICATION_MODEL = 'api.Application'

From 519f74839b0ece82a6bfaf441aa3983ad81c5688 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 16 Jun 2025 14:09:57 +0800
Subject: [PATCH 06/30] chore(auth): add username regex

---
 rootfs/api/models.py              | 8 ++++++++
 rootfs/api/settings/production.py | 3 +++
 2 files changed, 11 insertions(+)

diff --git a/rootfs/api/models.py b/rootfs/api/models.py
index 9b97bc0..787e7e2 100644
--- a/rootfs/api/models.py
+++ b/rootfs/api/models.py
@@ -1,4 +1,6 @@
 from django.db import models
+from django.conf import settings
+from django.contrib.auth import validators
 from django.contrib.auth.models import AbstractUser
 from django.utils.translation import gettext_lazy as _
 
@@ -6,6 +8,12 @@
 
 
 class User(AbstractUser):
+    username_validator = validators.UnicodeUsernameValidator(
+        regex=settings.USERNAME_REGEX,
+        message=_("Enter a valid username. This value may match the regex {}.".format(
+            settings.USERNAME_REGEX
+        ))
+    )
     email = models.EmailField(_('email address'), unique=True)
 
     @property
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index 1c642ec..7091cc3 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -399,5 +399,8 @@
 EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'false').lower() == "true"
 EMAIL_USE_SSL = os.environ.get('EMAIL_USE_SSL', 'false').lower() == "true"
 
+# username regex
+USERNAME_REGEX = os.environ.get('USERNAME_REGEX', '^[a-z][a-z0-9]{4,}$')
+# hcaptcha config
 H_CAPTCHA_KEY = os.environ.get("H_CAPTCHA_KEY")
 H_CAPTCHA_SECRET = os.environ.get("H_CAPTCHA_SECRET")

From e8580188d89fd9c52edc43177c64796d43f9dfae Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 19 Jun 2025 10:51:53 +0800
Subject: [PATCH 07/30] chore(passport): add username validators

---
 charts/passport/templates/_helpers.tpl        | 75 ++++++++++++++++++-
 .../templates/passport-configmap.yaml         |  7 +-
 charts/passport/values.yaml                   |  3 +-
 rootfs/api/models.py                          | 22 +-----
 rootfs/api/oauth2_validators.py               |  2 -
 rootfs/api/serializers.py                     |  4 +-
 rootfs/api/settings/production.py             | 10 +++
 rootfs/api/validators.py                      | 20 +++++
 8 files changed, 116 insertions(+), 27 deletions(-)
 create mode 100644 rootfs/api/validators.py

diff --git a/charts/passport/templates/_helpers.tpl b/charts/passport/templates/_helpers.tpl
index 8157a5d..c19b5c2 100644
--- a/charts/passport/templates/_helpers.tpl
+++ b/charts/passport/templates/_helpers.tpl
@@ -90,7 +90,6 @@ volumeMounts:
     readOnly: true
 {{- end }}
 
-
 {{/* Generate passport deployment volumes */}}
 {{- define "passport.volumes" }}
 volumes:
@@ -101,3 +100,77 @@ volumes:
     configMap:
       name: passport-config
 {{- end }}
+
+{{/* Generate passport default reserved usernames */}}
+{{- define "passport.defaultReservedUsernames" }}
+admin
+administrator
+anonymous
+asshole
+bastard
+billing
+callback
+cancer
+cocaine
+contact
+coronavirus
+cracker
+database
+developer
+doopai
+drycc
+email
+explore
+faggot
+feedback
+hacker
+helpdesk
+hentai
+heroin
+hitler
+homophobic
+horny
+idiot
+killer
+login
+logout
+moderator
+murder
+nigger
+nigga
+official
+payment
+pedophile
+pornhub
+profile
+racist
+rapist
+recovery
+register
+retard
+scammer
+security
+service
+settings
+sexist
+signup
+signin
+slave
+spammer
+staff
+suicide
+support
+system
+terrorism
+trending
+undefined
+update
+username
+verification
+verify
+webmaster
+webhook
+wetback
+whore
+xvideos
+{{- end }}
diff --git a/charts/passport/templates/passport-configmap.yaml b/charts/passport/templates/passport-configmap.yaml
index 71d582e..62dab9b 100644
--- a/charts/passport/templates/passport-configmap.yaml
+++ b/charts/passport/templates/passport-configmap.yaml
@@ -5,6 +5,11 @@ metadata:
   labels:
     heritage: drycc
 data:
-data:
+  reserved-usernames.txt: |-
+{{- if .Values.reservedUsernames }}
+{{- (tpl .Values.reservedUsernames $) | nindent 4 }}
+{{- else}}
+{{- include "passport.defaultReservedUsernames" . | nindent 4 }}
+{{- end }}
   init-applications.json: |-
 {{ toPrettyJson .Values.initApplications | indent 4 }}
diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index 59f8fa3..511b70c 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -86,7 +86,8 @@ environment:
 adminUsername: "admin"
 adminPassword: "admin"
 adminEmail: "admin@email.com"
-
+# Reserved usernames
+reservedUsernames: ""
 # The following configurations to initialize oauth2 application
 # Names are all lowercase letters
 # The key and secret are generated automatically if they are empty
diff --git a/rootfs/api/models.py b/rootfs/api/models.py
index 787e7e2..397af2f 100644
--- a/rootfs/api/models.py
+++ b/rootfs/api/models.py
@@ -1,32 +1,14 @@
 from django.db import models
-from django.conf import settings
-from django.contrib.auth import validators
 from django.contrib.auth.models import AbstractUser
 from django.utils.translation import gettext_lazy as _
-
 from oauth2_provider.models import AbstractApplication
+from .validators import UsernameValidator
 
 
 class User(AbstractUser):
-    username_validator = validators.UnicodeUsernameValidator(
-        regex=settings.USERNAME_REGEX,
-        message=_("Enter a valid username. This value may match the regex {}.".format(
-            settings.USERNAME_REGEX
-        ))
-    )
+    username_validator = UsernameValidator()
     email = models.EmailField(_('email address'), unique=True)
 
-    @property
-    def roles(self) -> list[str]:
-        results = []
-        if self.is_superuser:
-            results.append("admin")
-        if self.is_staff:
-            results.append("staff")
-        if self.is_active:
-            results.append("users")
-        return results
-
 
 class Application(AbstractApplication):
 
diff --git a/rootfs/api/oauth2_validators.py b/rootfs/api/oauth2_validators.py
index c265583..b91e976 100644
--- a/rootfs/api/oauth2_validators.py
+++ b/rootfs/api/oauth2_validators.py
@@ -9,7 +9,6 @@ class CustomOAuth2Validator(OAuth2Validator):
         "name": "profile",
         "username": "profile",
         "email": "email",
-        "roles": "profile",
         "first_name": "profile",
         "last_name": "profile",
         "is_staff": "profile",
@@ -24,7 +23,6 @@ def get_additional_claims(self, request):
         claims["name"] = request.user.username
         claims["username"] = request.user.username
         claims["email"] = request.user.email
-        claims["roles"] = request.user.roles
         claims["first_name"] = request.user.first_name
         claims["last_name"] = request.user.last_name
         claims["is_staff"] = request.user.is_staff
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index 76d43cb..9deb711 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -17,9 +17,9 @@
 class UserSerializer(serializers.ModelSerializer):
     class Meta:
         model = User
-        fields = ('id', 'username', 'email', 'roles', 'first_name', 'last_name',
+        fields = ('id', 'username', 'email', 'first_name', 'last_name',
                   'is_staff', 'is_active', 'is_superuser')
-        read_only_fields = ('id', 'username', 'roles', 'is_staff', 'is_active',
+        read_only_fields = ('id', 'username', 'is_staff', 'is_active',
                             'is_superuser')
 
 
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index 7091cc3..e2960c4 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -401,6 +401,16 @@
 
 # username regex
 USERNAME_REGEX = os.environ.get('USERNAME_REGEX', '^[a-z][a-z0-9]{4,}$')
+
+# reserved username
+RESERVED_USERNAMES_PATH = os.environ.get(
+    'RESERVED_USERNAMES_PATH', '/etc/drycc/passport/reserved-usernames.txt')
+if os.path.exists(RESERVED_USERNAMES_PATH):
+    with open(RESERVED_USERNAMES_PATH) as f:
+        RESERVED_USERNAMES = [line.strip() for line in f if line]
+else:
+    RESERVED_USERNAMES = ["drycc", "admin", "doopai"]
+
 # hcaptcha config
 H_CAPTCHA_KEY = os.environ.get("H_CAPTCHA_KEY")
 H_CAPTCHA_SECRET = os.environ.get("H_CAPTCHA_SECRET")
diff --git a/rootfs/api/validators.py b/rootfs/api/validators.py
new file mode 100644
index 0000000..e442287
--- /dev/null
+++ b/rootfs/api/validators.py
@@ -0,0 +1,20 @@
+from django.contrib.auth import validators
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from django.core.exceptions import ValidationError
+
+
+class UsernameValidator(validators.UnicodeUsernameValidator):
+    regex = settings.USERNAME_REGEX
+    message = _(
+        f"Enter a valid username. This value may match the regex {regex}."
+    )
+
+    def __call__(self, value):
+        if value in settings.RESERVED_USERNAMES:
+            raise ValidationError(
+                _("The current username is on the blocklist."),
+                code=self.code,
+                params={"value": value}
+            )
+        super().__call__(value)

From 19bb429874605b46b2982c6addc63404014ee4aa Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Fri, 20 Jun 2025 08:38:52 +0800
Subject: [PATCH 08/30] chore(grafana): change grafana oauth2 url

---
 charts/passport/values.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index 511b70c..a8f5a6d 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -105,7 +105,7 @@ initApplications:
   secret: ""
   prefix: "drycc-grafana"
   grant_type: "authorization-code"
-  redirect_uri: "/login/generic_oauth"
+  redirect_uri: "/oauth2/callback"
 
 # Service
 service:

From a8e2e8993a9a27f6b885572c7592d5a9813dd84b Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Tue, 15 Jul 2025 23:26:22 +0800
Subject: [PATCH 09/30] chore(charts): remove cluster domain

---
 charts/passport/templates/_helpers.tpl | 6 +++---
 charts/passport/values.yaml            | 7 -------
 2 files changed, 3 insertions(+), 10 deletions(-)

diff --git a/charts/passport/templates/_helpers.tpl b/charts/passport/templates/_helpers.tpl
index c19b5c2..4d429bb 100644
--- a/charts/passport/templates/_helpers.tpl
+++ b/charts/passport/templates/_helpers.tpl
@@ -41,7 +41,7 @@ env:
       name: valkey-creds
       key: password
 - name: DRYCC_VALKEY_URL
-  value: "redis://:$(VALKEY_PASSWORD)@drycc-valkey.{{.Release.Namespace}}.svc.{{.Values.global.clusterDomain}}:16379/1"
+  value: "redis://:$(VALKEY_PASSWORD)@drycc-valkey:16379/1"
 {{- end }}
 {{- if (.Values.databaseUrl) }}
 - name: DRYCC_DATABASE_URL
@@ -68,9 +68,9 @@ env:
       name: database-creds
       key: password
 - name: DRYCC_DATABASE_URL
-  value: "postgres://$(DRYCC_DATABASE_USER):$(DRYCC_DATABASE_PASSWORD)@drycc-database.{{.Release.Namespace}}.svc.{{.Values.global.clusterDomain}}:5432/passport"
+  value: "postgres://$(DRYCC_DATABASE_USER):$(DRYCC_DATABASE_PASSWORD)@drycc-database:5432/passport"
 - name: DRYCC_DATABASE_REPLICA_URL
-  value: "postgres://$(DRYCC_DATABASE_USER):$(DRYCC_DATABASE_PASSWORD)@drycc-database-replica.{{.Release.Namespace}}.svc.{{.Values.global.clusterDomain}}:5432/passport"
+  value: "postgres://$(DRYCC_DATABASE_USER):$(DRYCC_DATABASE_PASSWORD)@drycc-database-replica:5432/passport"
 {{- end }}
 {{- range $key, $value := .Values.environment }}
 - name: {{ $key }}
diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index a8f5a6d..8fd8778 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -119,13 +119,6 @@ database:
   enabled: true
 
 global:
-  # Admin email, used for each component to send email to administrator
-  email: "drycc@drycc.cc"
-  # A domain name consists of one or more parts.
-  # Periods (.) are used to separate these parts.
-  # Each part must be 1 to 63 characters in length and can contain lowercase letters, digits, and hyphens (-).
-  # It must start and end with a lowercase letter or digit.
-  clusterDomain: "cluster.local"
   # The public resolvable hostname to build your cluster with.
   #
   # This will be the hostname that is used to build endpoints such as "drycc.$HOSTNAME"

From d8387ad0d2eae12f4eab45a18163712d8fecdee9 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 3 Sep 2025 09:57:44 +0800
Subject: [PATCH 10/30] fix(passport): token_checksum field is required

---
 rootfs/api/serializers.py         |  1 +
 rootfs/api/urls.py                |  5 ++---
 rootfs/api/views.py               | 11 +----------
 rootfs/requirements.txt           | 18 +++++++++---------
 rootfs/web/src/services/tokens.js |  2 +-
 5 files changed, 14 insertions(+), 23 deletions(-)

diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index 9deb711..9c73ff3 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -56,6 +56,7 @@ class Meta:
 class UserTokensSerializer(serializers.ModelSerializer):
     """Serialize user status for a AccessToken model."""
     application = serializers.ReadOnlyField(source='application.name')
+    token_checksum = serializers.ReadOnlyField(required=False)
 
     class Meta:
         model = AccessToken
diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py
index c1c1aeb..eabd62d 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -38,14 +38,13 @@
     re_path(r'logout/?$', views.UserLogoutView.as_view(), name='user_logout'),
 
     re_path(r'tokens/?$',
-            views.UserTokensTemplateView.as_view({'get': 'retrieve'}),
+            views.UserTokensView.as_view({'get': 'list'}),
             name='user_tokens'),
     re_path(r'tokens/(?P<pk>.+)/?$',
-            views.UserTokenDeleteView.as_view({'delete': 'destroy'}),
+            views.UserTokensView.as_view({'delete': 'destroy'}),
             name='user_grants'),
     re_path(r'email/?$', views.UserEmailView.as_view({'get': 'retrieve'})),
     re_path(r'password/?$',
             views.UserAccountPasswordView.as_view({'put': 'update'}),
             name='user_account_update_password'),
-
 ]
diff --git a/rootfs/api/views.py b/rootfs/api/views.py
index 1f54de2..ac5ce61 100644
--- a/rootfs/api/views.py
+++ b/rootfs/api/views.py
@@ -261,20 +261,11 @@ def get_queryset(self, *args, **kwargs):
             q, **serializer.validated_data).order_by(self.order_by)[0:100]
 
 
-class UserTokensTemplateView(ListViewSet):
+class UserTokensView(ListViewSet):
     model = AccessToken
     serializer_class = serializers.UserTokensSerializer
     order_by = '-created'
 
-    def retrieve(self, request, *args, **kwargs):
-        tokens = self.get_queryset(*args, **kwargs)
-        serializer = self.get_serializer(tokens, many=True)
-        return Response(serializer.data)
-
-
-class UserTokenDeleteView(ListViewSet):
-    model = AccessToken
-
     def destroy(self, request, *args, **kwargs):
         token = get_object_or_404(self.model,
                                   id=self.kwargs['pk'],
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index ea92393..a387fa9 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,17 +1,17 @@
 # Drycc passport requirements
-django==4.2.20
+django==4.2.23
 pytz==2025.2
-django-auth-ldap==5.1.0
+django-auth-ldap==5.2.0
 django-cors-headers==4.7.0
-djangorestframework==3.16.0
+djangorestframework==3.16.1
 gunicorn==23.0.0
-uvicorn==0.34.0
-asgiref==3.8.1
-psycopg[binary]==3.2.6
-requests==2.32.3
+uvicorn==0.35.0
+asgiref==3.9.1
+psycopg[binary]==3.2.9
+requests==2.32.5
 requests-toolbelt==1.0.0
-redis==6.2.0
+redis==6.4.0
 hiredis==3.2.1
-dj-database-url==2.3.0
+dj-database-url==3.0.1
 django-oauth-toolkit==3.0.1
 whitenoise==6.9.0
diff --git a/rootfs/web/src/services/tokens.js b/rootfs/web/src/services/tokens.js
index 42a702a..e628add 100644
--- a/rootfs/web/src/services/tokens.js
+++ b/rootfs/web/src/services/tokens.js
@@ -5,7 +5,7 @@ export function getAccessTokenList() {
 }
 
 export function dealAccessTokenList(obj) {
-    return obj.data.map(item => {
+    return obj.data.results.map(item => {
         return {
             id: item.id,
             application: item.application,

From 06ba34255ceadb05eb0181f870236cb7fbccc8db Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 4 Sep 2025 09:18:13 +0800
Subject: [PATCH 11/30] chore(charts): use randAlphaNum replace randAscii

---
 charts/passport/templates/passport-secret-creds.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/charts/passport/templates/passport-secret-creds.yaml b/charts/passport/templates/passport-secret-creds.yaml
index b5d0f90..c05e224 100644
--- a/charts/passport/templates/passport-secret-creds.yaml
+++ b/charts/passport/templates/passport-secret-creds.yaml
@@ -14,7 +14,7 @@ data:
   {{- if (.Values.databaseReplicaUrl) }}
   database-replica-url: {{ .Values.databaseReplicaUrl | b64enc }}
   {{- end }}
-  django-secret-key: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" "django-secret-key" "defaultValue" (randAscii 64) "context" $)) }}
+  django-secret-key: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" "django-secret-key" "defaultValue" (randAlphaNum 64) "context" $)) }}
   oidc-rsa-private-key: {{genPrivateKey "rsa" | b64enc}}
   {{- range $item := .Values.initApplications }}
   {{- if ($item.prefix) }}

From 91ffae13b52253d6060783cbbd4a005e09cf566d Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 15 Sep 2025 23:11:26 +0800
Subject: [PATCH 12/30] chore(django): bump version to 5.2.6

---
 rootfs/Dockerfile.test      | 4 ++--
 rootfs/dev_requirements.txt | 4 ++--
 rootfs/requirements.txt     | 8 ++++----
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/rootfs/Dockerfile.test b/rootfs/Dockerfile.test
index 5cb59f6..e7e2134 100644
--- a/rootfs/Dockerfile.test
+++ b/rootfs/Dockerfile.test
@@ -4,8 +4,8 @@ FROM registry.drycc.cc/drycc/base:${CODENAME}
 ENV DRYCC_HOME_DIR=/workspace \
   PGDATA="/var/lib/postgresql/data" \
   PYTHON_VERSION="3.13" \
-  POSTGRES_VERSION="15.12" \
-  GOSU_VERSION="1.17"
+  POSTGRES_VERSION="17.6" \
+  GOSU_VERSION="1.18"
 
 COPY requirements.txt ${DRYCC_HOME_DIR}/requirements.txt
 COPY dev_requirements.txt ${DRYCC_HOME_DIR}/dev_requirements.txt
diff --git a/rootfs/dev_requirements.txt b/rootfs/dev_requirements.txt
index 9f542ed..ef0abcb 100644
--- a/rootfs/dev_requirements.txt
+++ b/rootfs/dev_requirements.txt
@@ -1,10 +1,10 @@
 # test module
 # test
 # Run "make test-unit" for the % of code exercised during tests
-coverage==7.8.0
+coverage==7.10.6
 
 # Run "make test-style" to check python syntax and style
-flake8==7.2.0
+flake8==7.3.0
 
 # mock out python-requests, mostly k8s
 requests-mock==1.12.1
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index a387fa9..e007b6a 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,17 +1,17 @@
 # Drycc passport requirements
-django==4.2.23
+django==5.2.6
 pytz==2025.2
 django-auth-ldap==5.2.0
-django-cors-headers==4.7.0
+django-cors-headers==4.8.0
 djangorestframework==3.16.1
 gunicorn==23.0.0
 uvicorn==0.35.0
 asgiref==3.9.1
-psycopg[binary]==3.2.9
+psycopg[binary]==3.2.10
 requests==2.32.5
 requests-toolbelt==1.0.0
 redis==6.4.0
 hiredis==3.2.1
 dj-database-url==3.0.1
 django-oauth-toolkit==3.0.1
-whitenoise==6.9.0
+whitenoise==6.10.0

From c72ecfceacb02235fdfc90d260ef7dc8fe5b1b2d Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Tue, 16 Sep 2025 16:37:25 +0800
Subject: [PATCH 13/30] fix(charts): name variable reference error

---
 charts/passport/templates/passport-secret-creds.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/charts/passport/templates/passport-secret-creds.yaml b/charts/passport/templates/passport-secret-creds.yaml
index c05e224..1304712 100644
--- a/charts/passport/templates/passport-secret-creds.yaml
+++ b/charts/passport/templates/passport-secret-creds.yaml
@@ -19,7 +19,7 @@ data:
   {{- range $item := .Values.initApplications }}
   {{- if ($item.prefix) }}
   {{- $name := ($item.name | replace " " "-" | lower) }}
-  drycc-passport-{{$name}}-key: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" "drycc-passport-($name)-key" "defaultValue" ($item.key | default (randAlphaNum 40)) "context" $)) }}
-  drycc-passport-{{$name}}-secret: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" "drycc-passport-($name)-secret" "defaultValue" ($item.secret | default (randAlphaNum 64)) "context" $)) }}
+  drycc-passport-{{$name}}-key: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" (printf "drycc-passport-%s-key" $name) "defaultValue" ($item.key | default (randAlphaNum 40)) "context" $)) }}
+  drycc-passport-{{$name}}-secret: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" (printf "drycc-passport-%s-secret" $name) "defaultValue" ($item.secret | default (randAlphaNum 64)) "context" $)) }}
   {{- end }}
   {{- end }}

From 53a91c4a6e56f00b71df4a57ff10b7f5d68c63f8 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Wed, 24 Sep 2025 16:05:26 +0800
Subject: [PATCH 14/30] chore(passport): logout request method not allow (#10)

---
 rootfs/web/src/components/UserMenu.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/rootfs/web/src/components/UserMenu.vue b/rootfs/web/src/components/UserMenu.vue
index b740305..cec87a6 100644
--- a/rootfs/web/src/components/UserMenu.vue
+++ b/rootfs/web/src/components/UserMenu.vue
@@ -28,7 +28,7 @@
         </li>
 
         <li class="glostick__menu__listitem">
-          <a class="glostick__menu__item glostick__menu__item--docs" href="/user/logout">Sign out</a>
+            <a class="glostick__menu__item glostick__menu__item--docs" @click="logout">Sign out</a>
         </li>
     </ul>
 </template>

From f6ecde8f48ca8e3628c6cc8c749ced7becd12f3b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Oct 2025 16:06:03 +0800
Subject: [PATCH 15/30] chore(deps): bump axios from 0.26.1 to 0.30.2 in
 /rootfs/web (#12)

Bumps [axios](https://github.com/axios/axios) from 0.26.1 to 0.30.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.26.1...v0.30.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 0.30.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 rootfs/web/package-lock.json | 424 +++++++++++++++++++++--
 rootfs/web/package.json      |   2 +-
 rootfs/web/yarn.lock         | 653 ++++++++++++++++++++++-------------
 3 files changed, 807 insertions(+), 272 deletions(-)

diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index 798ffc0..c8ac336 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -7,9 +7,10 @@
     "": {
       "name": "web",
       "version": "0.0.0",
+      "license": "Apache-2.0",
       "dependencies": {
         "@icon-park/vue-next": "^1.3.6",
-        "axios": "^0.26.1",
+        "axios": "^0.30.2",
         "vant": "^3.4.5",
         "vue": "^3.2.31",
         "vue-i18n": "^9.1.9",
@@ -274,12 +275,42 @@
       "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
       "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
     "node_modules/axios": {
-      "version": "0.26.1",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
-      "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+      "version": "0.30.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.2.tgz",
+      "integrity": "sha512-0pE4RQ4UQi1jKY6p7u6i1Tkzqmu+d+/tHS7Q7rKunWLB9WyilBTpHHpXzPNMDj5hTbK0B0PTLSz07yqMBiF6xg==",
       "dependencies": {
-        "follow-redirects": "^1.14.8"
+        "follow-redirects": "^1.15.4",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
       }
     },
     "node_modules/csstype": {
@@ -287,6 +318,68 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
       "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/esbuild": {
       "version": "0.14.25",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
@@ -648,9 +741,9 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
     },
     "node_modules/follow-redirects": {
-      "version": "1.14.9",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
-      "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
       "funding": [
         {
           "type": "individual",
@@ -666,6 +759,21 @@
         }
       }
     },
+    "node_modules/form-data": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fsevents": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -681,10 +789,58 @@
       }
     },
     "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/has": {
       "version": "1.0.3",
@@ -698,6 +854,42 @@
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/is-core-module": {
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
@@ -718,6 +910,33 @@
         "sourcemap-codec": "^1.4.8"
       }
     },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/nanoid": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
@@ -757,6 +976,11 @@
         "url": "https://opencollective.com/postcss/"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "node_modules/resolve": {
       "version": "1.22.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
@@ -1136,12 +1360,36 @@
       "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
       "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
     },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
     "axios": {
-      "version": "0.26.1",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
-      "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+      "version": "0.30.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.2.tgz",
+      "integrity": "sha512-0pE4RQ4UQi1jKY6p7u6i1Tkzqmu+d+/tHS7Q7rKunWLB9WyilBTpHHpXzPNMDj5hTbK0B0PTLSz07yqMBiF6xg==",
       "requires": {
-        "follow-redirects": "^1.14.8"
+        "follow-redirects": "^1.15.4",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "requires": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
       }
     },
     "csstype": {
@@ -1149,6 +1397,50 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
       "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
     },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+    },
+    "dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "requires": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      }
+    },
+    "es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
+    },
+    "es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
+    },
+    "es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "requires": {
+        "es-errors": "^1.3.0"
+      }
+    },
+    "es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "requires": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      }
+    },
     "esbuild": {
       "version": "0.14.25",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
@@ -1323,9 +1615,21 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
     },
     "follow-redirects": {
-      "version": "1.14.9",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
-      "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
+    },
+    "form-data": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      }
     },
     "fsevents": {
       "version": "2.3.2",
@@ -1335,10 +1639,40 @@
       "optional": true
     },
     "function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+    },
+    "get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "requires": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      }
+    },
+    "get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "requires": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      }
+    },
+    "gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
     },
     "has": {
       "version": "1.0.3",
@@ -1349,6 +1683,27 @@
         "function-bind": "^1.1.1"
       }
     },
+    "has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
+    },
+    "has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "requires": {
+        "has-symbols": "^1.0.3"
+      }
+    },
+    "hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "requires": {
+        "function-bind": "^1.1.2"
+      }
+    },
     "is-core-module": {
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
@@ -1366,6 +1721,24 @@
         "sourcemap-codec": "^1.4.8"
       }
     },
+    "math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
+    },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+    },
+    "mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
     "nanoid": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
@@ -1392,6 +1765,11 @@
         "source-map-js": "^1.0.2"
       }
     },
+    "proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "resolve": {
       "version": "1.22.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
diff --git a/rootfs/web/package.json b/rootfs/web/package.json
index bd7d73b..178c079 100644
--- a/rootfs/web/package.json
+++ b/rootfs/web/package.json
@@ -9,7 +9,7 @@
   },
   "dependencies": {
     "@icon-park/vue-next": "^1.3.6",
-    "axios": "^0.26.1",
+    "axios": "^0.30.2",
     "vant": "^3.4.5",
     "vue": "^3.2.31",
     "vue-i18n": "^9.1.9",
diff --git a/rootfs/web/yarn.lock b/rootfs/web/yarn.lock
index 726bfe6..dfd0be5 100644
--- a/rootfs/web/yarn.lock
+++ b/rootfs/web/yarn.lock
@@ -3,19 +3,16 @@
 
 
 "@babel/parser@^7.16.4":
-  "integrity" "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA=="
-  "resolved" "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz"
-  "version" "7.16.7"
+  version "7.16.7"
+  resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz"
 
 "@icon-park/vue-next@^1.3.6":
-  "integrity" "sha512-AZaCcjRPU9vTNVfcrG03CYJM+uv0SXx5lC4njanlRorFo+TV/x0ZTYGYGpTR/l6ek6QmFu9THmrgqKl7i/8yHg=="
-  "resolved" "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.3.6.tgz"
-  "version" "1.3.6"
+  version "1.3.6"
+  resolved "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.3.6.tgz"
 
 "@intlify/core-base@9.1.9":
-  "integrity" "sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw=="
-  "resolved" "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz"
-  "version" "9.1.9"
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz"
   dependencies:
     "@intlify/devtools-if" "9.1.9"
     "@intlify/message-compiler" "9.1.9"
@@ -25,98 +22,84 @@
     "@intlify/vue-devtools" "9.1.9"
 
 "@intlify/devtools-if@9.1.9":
-  "integrity" "sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ=="
-  "resolved" "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz"
-  "version" "9.1.9"
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz"
   dependencies:
     "@intlify/shared" "9.1.9"
 
 "@intlify/message-compiler@9.1.9":
-  "integrity" "sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ=="
-  "resolved" "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz"
-  "version" "9.1.9"
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz"
   dependencies:
     "@intlify/message-resolver" "9.1.9"
     "@intlify/shared" "9.1.9"
-    "source-map" "0.6.1"
+    source-map "0.6.1"
 
 "@intlify/message-resolver@9.1.9":
-  "integrity" "sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA=="
-  "resolved" "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz"
-  "version" "9.1.9"
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz"
 
 "@intlify/runtime@9.1.9":
-  "integrity" "sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg=="
-  "resolved" "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz"
-  "version" "9.1.9"
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz"
   dependencies:
     "@intlify/message-compiler" "9.1.9"
     "@intlify/message-resolver" "9.1.9"
     "@intlify/shared" "9.1.9"
 
 "@intlify/shared@9.1.9":
-  "integrity" "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw=="
-  "resolved" "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz"
-  "version" "9.1.9"
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz"
 
 "@intlify/vue-devtools@9.1.9":
-  "integrity" "sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og=="
-  "resolved" "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz"
-  "version" "9.1.9"
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz"
   dependencies:
     "@intlify/message-resolver" "9.1.9"
     "@intlify/runtime" "9.1.9"
     "@intlify/shared" "9.1.9"
 
 "@popperjs/core@^2.9.2":
-  "integrity" "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
-  "resolved" "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz"
-  "version" "2.11.2"
+  version "2.11.2"
+  resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz"
 
 "@vant/icons@^1.7.1":
-  "integrity" "sha512-tW4EqzxN4kXw1rnlnQJQHofEifPbt/gECOWiibomht8QLyvoGuE4iUmDFS288dJ07ZjuTy0bhdABj0SENo2fmQ=="
-  "resolved" "https://registry.npmjs.org/@vant/icons/-/icons-1.7.3.tgz"
-  "version" "1.7.3"
+  version "1.7.3"
+  resolved "https://registry.npmjs.org/@vant/icons/-/icons-1.7.3.tgz"
 
 "@vant/popperjs@^1.1.0":
-  "integrity" "sha512-8MD1gz146awV/uPxYjz4pet22f7a9YVKqk7T+gFkWFwT9mEcrIUEg/xPrdOnWKLP9puXyYtm7oVfSDSefZ/p/w=="
-  "resolved" "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.1.0.tgz"
-  "version" "1.1.0"
+  version "1.1.0"
+  resolved "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.1.0.tgz"
   dependencies:
     "@popperjs/core" "^2.9.2"
 
 "@vant/use@^1.3.5":
-  "integrity" "sha512-3z+nywPaV2F5BdJO7RQxWlgfzJeEOmViD2yHMb7Tg+R4NR/7iQskqW8v2Cnv9FWSJgTOSHlcr7UzeLpiTAP4HA=="
-  "resolved" "https://registry.npmjs.org/@vant/use/-/use-1.3.6.tgz"
-  "version" "1.3.6"
+  version "1.3.6"
+  resolved "https://registry.npmjs.org/@vant/use/-/use-1.3.6.tgz"
 
 "@vitejs/plugin-vue@^2.2.4":
-  "integrity" "sha512-ev9AOlp0ljCaDkFZF3JwC/pD2N4Hh+r5srl5JHM6BKg5+99jiiK0rE/XaRs3pVm1wzyKkjUy/StBSoXX5fFzcw=="
-  "resolved" "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz"
-  "version" "2.2.4"
+  version "2.2.4"
+  resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz"
 
 "@vue/compiler-core@3.2.31":
-  "integrity" "sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ=="
-  "resolved" "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz"
   dependencies:
     "@babel/parser" "^7.16.4"
     "@vue/shared" "3.2.31"
-    "estree-walker" "^2.0.2"
-    "source-map" "^0.6.1"
+    estree-walker "^2.0.2"
+    source-map "^0.6.1"
 
 "@vue/compiler-dom@3.2.31":
-  "integrity" "sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg=="
-  "resolved" "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz"
   dependencies:
     "@vue/compiler-core" "3.2.31"
     "@vue/shared" "3.2.31"
 
-"@vue/compiler-sfc@^3.2.31", "@vue/compiler-sfc@3.2.31":
-  "integrity" "sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ=="
-  "resolved" "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz"
-  "version" "3.2.31"
+"@vue/compiler-sfc@3.2.31", "@vue/compiler-sfc@^3.2.31":
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz"
   dependencies:
     "@babel/parser" "^7.16.4"
     "@vue/compiler-core" "3.2.31"
@@ -124,253 +107,428 @@
     "@vue/compiler-ssr" "3.2.31"
     "@vue/reactivity-transform" "3.2.31"
     "@vue/shared" "3.2.31"
-    "estree-walker" "^2.0.2"
-    "magic-string" "^0.25.7"
-    "postcss" "^8.1.10"
-    "source-map" "^0.6.1"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+    postcss "^8.1.10"
+    source-map "^0.6.1"
 
 "@vue/compiler-ssr@3.2.31":
-  "integrity" "sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw=="
-  "resolved" "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz"
   dependencies:
     "@vue/compiler-dom" "3.2.31"
     "@vue/shared" "3.2.31"
 
 "@vue/devtools-api@^6.0.0", "@vue/devtools-api@^6.0.0-beta.7":
-  "integrity" "sha512-T34EjcArVqzANedEZe2kXQ+AZsld2z1ptJlkOGm87+blk+s6udnP4ze/NYqV8lz1o9AIivimN0xxteLlWiWQdg=="
-  "resolved" "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.13.tgz"
-  "version" "6.0.13"
+  version "6.0.13"
+  resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.13.tgz"
 
 "@vue/reactivity-transform@3.2.31":
-  "integrity" "sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA=="
-  "resolved" "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz"
   dependencies:
     "@babel/parser" "^7.16.4"
     "@vue/compiler-core" "3.2.31"
     "@vue/shared" "3.2.31"
-    "estree-walker" "^2.0.2"
-    "magic-string" "^0.25.7"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
 
 "@vue/reactivity@3.2.31":
-  "integrity" "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw=="
-  "resolved" "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz"
   dependencies:
     "@vue/shared" "3.2.31"
 
 "@vue/runtime-core@3.2.31":
-  "integrity" "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA=="
-  "resolved" "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz"
   dependencies:
     "@vue/reactivity" "3.2.31"
     "@vue/shared" "3.2.31"
 
 "@vue/runtime-dom@3.2.31":
-  "integrity" "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g=="
-  "resolved" "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz"
   dependencies:
     "@vue/runtime-core" "3.2.31"
     "@vue/shared" "3.2.31"
-    "csstype" "^2.6.8"
+    csstype "^2.6.8"
 
 "@vue/server-renderer@3.2.31":
-  "integrity" "sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg=="
-  "resolved" "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz"
-  "version" "3.2.31"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz"
   dependencies:
     "@vue/compiler-ssr" "3.2.31"
     "@vue/shared" "3.2.31"
 
 "@vue/shared@3.2.31":
-  "integrity" "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
-  "resolved" "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz"
-  "version" "3.2.31"
-
-"axios@^0.26.1":
-  "integrity" "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA=="
-  "resolved" "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz"
-  "version" "0.26.1"
-  dependencies:
-    "follow-redirects" "^1.14.8"
-
-"csstype@^2.6.8":
-  "integrity" "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
-  "resolved" "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz"
-  "version" "2.6.20"
-
-"esbuild-linux-64@0.14.25":
-  "integrity" "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug=="
-  "resolved" "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz"
-  "version" "0.14.25"
-
-"esbuild@^0.14.14":
-  "integrity" "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q=="
-  "resolved" "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz"
-  "version" "0.14.25"
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz"
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+axios@^0.30.2:
+  version "0.30.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.30.2.tgz#256d3a8ee765cc27188d08b8b545a5f5a0c77dad"
+  dependencies:
+    follow-redirects "^1.15.4"
+    form-data "^4.0.4"
+    proxy-from-env "^1.1.0"
+
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+  dependencies:
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  dependencies:
+    delayed-stream "~1.0.0"
+
+csstype@^2.6.8:
+  version "2.6.20"
+  resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+dunder-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+  dependencies:
+    call-bind-apply-helpers "^1.0.1"
+    es-errors "^1.3.0"
+    gopd "^1.2.0"
+
+es-define-property@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+
+es-errors@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+  dependencies:
+    es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+  dependencies:
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.6"
+    has-tostringtag "^1.0.2"
+    hasown "^2.0.2"
+
+esbuild-android-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz#d532d38cb5fe0ae45167ce35f4bbc784c636be40"
+
+esbuild-android-arm64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz#9c5bb3366aabfd14a1c726d36978b79441dfcb6e"
+
+esbuild-darwin-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz#05dcdb6d884f427039ffee5e92ff97527e56c26d"
+
+esbuild-darwin-arm64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz#28e080da4ea0cfe9498071e7f8060498caee1a95"
+
+esbuild-freebsd-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz#200d3664a3b945bc9fdcba73614b49a11ebd1cfa"
+
+esbuild-freebsd-arm64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz#624b08c5da6013bdc312aaa23c4ff409580f5c3c"
+
+esbuild-linux-32@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz#0238e597eb0b60aa06c7e98fccbbfd6bb9a0d6c5"
+
+esbuild-linux-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz"
+
+esbuild-linux-arm64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz#7ac94371418a2640ba413bc1700aaedeb2794e52"
+
+esbuild-linux-arm@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz#034bd18e9310b9f010c89f90ef7f05706689600b"
+
+esbuild-linux-mips64le@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz#05f98a8cf6b578eab6b4e6b0ab094f37530934f4"
+
+esbuild-linux-ppc64le@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz#46fd0add8d8535678439d7a9c2876ad20042d952"
+
+esbuild-linux-riscv64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz#ea2e986f0f3e5df73c635135dd778051734fc605"
+
+esbuild-linux-s390x@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz#efe89486e9a1b1508925048076e3f3a6698aa6a3"
+
+esbuild-netbsd-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz#439fe27d8ee3b5887501ee63988e85f920107db6"
+
+esbuild-openbsd-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz#31ebf616aadf6e60674469f2b92cec92280d9930"
+
+esbuild-sunos-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz#815e4f936d74970292a63ccfd5791fe5e3569f5f"
+
+esbuild-windows-32@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz#189e14df2478f2c193c86968ab1fb54e1ceaafd2"
+
+esbuild-windows-64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz#3d5fbfdc3856850bb47439299e3b60dd18be111f"
+
+esbuild-windows-arm64@0.14.25:
+  version "0.14.25"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz#8b243cbbad8a86cf98697da9ccb88c05df2ef458"
+
+esbuild@^0.14.14:
+  version "0.14.25"
+  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz"
   optionalDependencies:
-    "esbuild-android-64" "0.14.25"
-    "esbuild-android-arm64" "0.14.25"
-    "esbuild-darwin-64" "0.14.25"
-    "esbuild-darwin-arm64" "0.14.25"
-    "esbuild-freebsd-64" "0.14.25"
-    "esbuild-freebsd-arm64" "0.14.25"
-    "esbuild-linux-32" "0.14.25"
-    "esbuild-linux-64" "0.14.25"
-    "esbuild-linux-arm" "0.14.25"
-    "esbuild-linux-arm64" "0.14.25"
-    "esbuild-linux-mips64le" "0.14.25"
-    "esbuild-linux-ppc64le" "0.14.25"
-    "esbuild-linux-riscv64" "0.14.25"
-    "esbuild-linux-s390x" "0.14.25"
-    "esbuild-netbsd-64" "0.14.25"
-    "esbuild-openbsd-64" "0.14.25"
-    "esbuild-sunos-64" "0.14.25"
-    "esbuild-windows-32" "0.14.25"
-    "esbuild-windows-64" "0.14.25"
-    "esbuild-windows-arm64" "0.14.25"
-
-"estree-walker@^2.0.2":
-  "integrity" "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
-  "resolved" "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
-  "version" "2.0.2"
-
-"follow-redirects@^1.14.8":
-  "integrity" "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
-  "resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz"
-  "version" "1.14.9"
-
-"function-bind@^1.1.1":
-  "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
-  "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
-  "version" "1.1.1"
-
-"has@^1.0.3":
-  "integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw=="
-  "resolved" "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
-  "version" "1.0.3"
-  dependencies:
-    "function-bind" "^1.1.1"
-
-"is-core-module@^2.8.1":
-  "integrity" "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA=="
-  "resolved" "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz"
-  "version" "2.8.1"
-  dependencies:
-    "has" "^1.0.3"
-
-"magic-string@^0.25.7":
-  "integrity" "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="
-  "resolved" "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz"
-  "version" "0.25.9"
-  dependencies:
-    "sourcemap-codec" "^1.4.8"
-
-"nanoid@^3.3.1":
-  "integrity" "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw=="
-  "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz"
-  "version" "3.3.1"
-
-"path-parse@^1.0.7":
-  "integrity" "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
-  "resolved" "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
-  "version" "1.0.7"
-
-"picocolors@^1.0.0":
-  "integrity" "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-  "resolved" "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
-  "version" "1.0.0"
-
-"postcss@^8.1.10", "postcss@^8.4.6":
-  "integrity" "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ=="
-  "resolved" "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz"
-  "version" "8.4.8"
-  dependencies:
-    "nanoid" "^3.3.1"
-    "picocolors" "^1.0.0"
-    "source-map-js" "^1.0.2"
-
-"resolve@^1.22.0":
-  "integrity" "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw=="
-  "resolved" "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz"
-  "version" "1.22.0"
-  dependencies:
-    "is-core-module" "^2.8.1"
-    "path-parse" "^1.0.7"
-    "supports-preserve-symlinks-flag" "^1.0.0"
-
-"rollup@^2.59.0":
-  "integrity" "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ=="
-  "resolved" "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz"
-  "version" "2.63.0"
+    esbuild-android-64 "0.14.25"
+    esbuild-android-arm64 "0.14.25"
+    esbuild-darwin-64 "0.14.25"
+    esbuild-darwin-arm64 "0.14.25"
+    esbuild-freebsd-64 "0.14.25"
+    esbuild-freebsd-arm64 "0.14.25"
+    esbuild-linux-32 "0.14.25"
+    esbuild-linux-64 "0.14.25"
+    esbuild-linux-arm "0.14.25"
+    esbuild-linux-arm64 "0.14.25"
+    esbuild-linux-mips64le "0.14.25"
+    esbuild-linux-ppc64le "0.14.25"
+    esbuild-linux-riscv64 "0.14.25"
+    esbuild-linux-s390x "0.14.25"
+    esbuild-netbsd-64 "0.14.25"
+    esbuild-openbsd-64 "0.14.25"
+    esbuild-sunos-64 "0.14.25"
+    esbuild-windows-32 "0.14.25"
+    esbuild-windows-64 "0.14.25"
+    esbuild-windows-arm64 "0.14.25"
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
+
+follow-redirects@^1.15.4:
+  version "1.15.11"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
+
+form-data@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    es-set-tostringtag "^2.1.0"
+    hasown "^2.0.2"
+    mime-types "^2.1.12"
+
+fsevents@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
+
+function-bind@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+
+get-intrinsic@^1.2.6:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+  dependencies:
+    call-bind-apply-helpers "^1.0.2"
+    es-define-property "^1.0.1"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.1.1"
+    function-bind "^1.1.2"
+    get-proto "^1.0.1"
+    gopd "^1.2.0"
+    has-symbols "^1.1.0"
+    hasown "^2.0.2"
+    math-intrinsics "^1.1.0"
+
+get-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+  dependencies:
+    dunder-proto "^1.0.1"
+    es-object-atoms "^1.0.0"
+
+gopd@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+
+has-tostringtag@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+  dependencies:
+    has-symbols "^1.0.3"
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
+  dependencies:
+    function-bind "^1.1.1"
+
+hasown@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+  dependencies:
+    function-bind "^1.1.2"
+
+is-core-module@^2.8.1:
+  version "2.8.1"
+  resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz"
+  dependencies:
+    has "^1.0.3"
+
+magic-string@^0.25.7:
+  version "0.25.9"
+  resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz"
+  dependencies:
+    sourcemap-codec "^1.4.8"
+
+math-intrinsics@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  dependencies:
+    mime-db "1.52.0"
+
+nanoid@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz"
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
+
+postcss@^8.1.10, postcss@^8.4.6:
+  version "8.4.8"
+  resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz"
+  dependencies:
+    nanoid "^3.3.1"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+
+resolve@^1.22.0:
+  version "1.22.0"
+  resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz"
+  dependencies:
+    is-core-module "^2.8.1"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+rollup@^2.59.0:
+  version "2.63.0"
+  resolved "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz"
   optionalDependencies:
-    "fsevents" "~2.3.2"
+    fsevents "~2.3.2"
 
-"source-map-js@^1.0.2":
-  "integrity" "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
-  "resolved" "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
-  "version" "1.0.2"
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
 
-"source-map@^0.6.1", "source-map@0.6.1":
-  "integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
-  "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
-  "version" "0.6.1"
+source-map@0.6.1, source-map@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
 
-"sourcemap-codec@^1.4.8":
-  "integrity" "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
-  "resolved" "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
-  "version" "1.4.8"
+sourcemap-codec@^1.4.8:
+  version "1.4.8"
+  resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
 
-"supports-preserve-symlinks-flag@^1.0.0":
-  "integrity" "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
-  "resolved" "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
-  "version" "1.0.0"
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
 
-"vant@^3.4.5":
-  "integrity" "sha512-3an2v6ejW9pijKmaDbSCW7Hs1R45tqtTIcedfI8hEAA5zC2Bp8cXU6Z4gUCK6/RAGOJsB+vcyLINLb1cGS/SzQ=="
-  "resolved" "https://registry.npmjs.org/vant/-/vant-3.4.5.tgz"
-  "version" "3.4.5"
+vant@^3.4.5:
+  version "3.4.5"
+  resolved "https://registry.npmjs.org/vant/-/vant-3.4.5.tgz"
   dependencies:
     "@vant/icons" "^1.7.1"
     "@vant/popperjs" "^1.1.0"
     "@vant/use" "^1.3.5"
 
-"vite@^2.5.10", "vite@^2.8.6":
-  "integrity" "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug=="
-  "resolved" "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz"
-  "version" "2.8.6"
+vite@^2.8.6:
+  version "2.8.6"
+  resolved "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz"
   dependencies:
-    "esbuild" "^0.14.14"
-    "postcss" "^8.4.6"
-    "resolve" "^1.22.0"
-    "rollup" "^2.59.0"
+    esbuild "^0.14.14"
+    postcss "^8.4.6"
+    resolve "^1.22.0"
+    rollup "^2.59.0"
   optionalDependencies:
-    "fsevents" "~2.3.2"
+    fsevents "~2.3.2"
 
-"vue-i18n@^9.1.9":
-  "integrity" "sha512-JeRdNVxS2OGp1E+pye5XB6+M6BBkHwAv9C80Q7+kzoMdUDGRna06tjC0vCB/jDX9aWrl5swxOMFcyAr7or8XTA=="
-  "resolved" "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.9.tgz"
-  "version" "9.1.9"
+vue-i18n@^9.1.9:
+  version "9.1.9"
+  resolved "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.9.tgz"
   dependencies:
     "@intlify/core-base" "9.1.9"
     "@intlify/shared" "9.1.9"
     "@intlify/vue-devtools" "9.1.9"
     "@vue/devtools-api" "^6.0.0-beta.7"
 
-"vue-router@^4.0.14":
-  "integrity" "sha512-wAO6zF9zxA3u+7AkMPqw9LjoUCjSxfFvINQj3E/DceTt6uEz1XZLraDhdg2EYmvVwTBSGlLYsUw8bDmx0754Mw=="
-  "resolved" "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz"
-  "version" "4.0.14"
+vue-router@^4.0.14:
+  version "4.0.14"
+  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz"
   dependencies:
     "@vue/devtools-api" "^6.0.0"
 
-"vue@^3.0.0", "vue@^3.2.0", "vue@^3.2.25", "vue@^3.2.31", "vue@3.2.31", "vue@3.x":
-  "integrity" "sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw=="
-  "resolved" "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz"
-  "version" "3.2.31"
+vue@^3.2.31:
+  version "3.2.31"
+  resolved "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz"
   dependencies:
     "@vue/compiler-dom" "3.2.31"
     "@vue/compiler-sfc" "3.2.31"
@@ -378,7 +536,6 @@
     "@vue/server-renderer" "3.2.31"
     "@vue/shared" "3.2.31"
 
-"yarn@^1.22.17":
-  "integrity" "sha512-H0p241BXaH0UN9IeH//RT82tl5PfNraVpSpEoW+ET7lmopNC61eZ+A+IDvU8FM6Go5vx162SncDL8J1ZjRBriQ=="
-  "resolved" "https://registry.npmjs.org/yarn/-/yarn-1.22.17.tgz"
-  "version" "1.22.17"
+yarn@^1.22.17:
+  version "1.22.17"
+  resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.17.tgz"

From b0a41b5b30c91f4b1d2ac1caadb7a8a10e53266c Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Fri, 10 Oct 2025 16:38:42 +0800
Subject: [PATCH 16/30] chore(passport): bump python requirements version

---
 rootfs/requirements.txt | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index e007b6a..7f2b4a7 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,17 +1,17 @@
 # Drycc passport requirements
-django==5.2.6
+django==5.2.7
 pytz==2025.2
 django-auth-ldap==5.2.0
-django-cors-headers==4.8.0
+django-cors-headers==4.9.0
 djangorestframework==3.16.1
 gunicorn==23.0.0
-uvicorn==0.35.0
-asgiref==3.9.1
+uvicorn==0.37.0
+asgiref==3.10.0
 psycopg[binary]==3.2.10
 requests==2.32.5
 requests-toolbelt==1.0.0
 redis==6.4.0
 hiredis==3.2.1
 dj-database-url==3.0.1
-django-oauth-toolkit==3.0.1
-whitenoise==6.10.0
+django-oauth-toolkit==3.1.0
+whitenoise==6.11.0

From b2d4609424d95d52297281206b2f9207c4eca13c Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Fri, 10 Oct 2025 16:39:52 +0800
Subject: [PATCH 17/30] chore(git): add .venv to .gitignore

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index 6dd6722..124efaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ lib
 lib64
 .env
 .vscode
+.venv
 
 # coverage reports
 .coverage

From 15bdb83808588475850be402e0e350582278540c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 6 Nov 2025 15:33:29 +0800
Subject: [PATCH 18/30] chore(deps): bump django in /rootfs in the pip group
 across 1 directory (#16)

Bumps the pip group with 1 update in the /rootfs directory: [django](https://github.com/django/django).


Updates `django` from 5.2.7 to 5.2.8
- [Commits](https://github.com/django/django/compare/5.2.7...5.2.8)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.8
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 rootfs/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index 7f2b4a7..a8d7f47 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,5 +1,5 @@
 # Drycc passport requirements
-django==5.2.7
+django==5.2.8
 pytz==2025.2
 django-auth-ldap==5.2.0
 django-cors-headers==4.9.0

From cefaa2cfd131d07b702469014bb5f34296147da0 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 6 Nov 2025 15:36:02 +0800
Subject: [PATCH 19/30] chore(passport): change env to build arg

---
 rootfs/Dockerfile      | 2 +-
 rootfs/Dockerfile.test | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile
index c45ec6b..ee5bced 100644
--- a/rootfs/Dockerfile
+++ b/rootfs/Dockerfile
@@ -13,7 +13,7 @@ RUN install-stack node $NODE_VERSION && . init-stack \
 
 FROM registry.drycc.cc/drycc/base:${CODENAME}
 
-ENV DRYCC_UID=1001 \
+ARG DRYCC_UID=1001 \
   DRYCC_GID=1001 \
   DRYCC_HOME_DIR=/workspace \
   PYTHON_VERSION="3.13"
diff --git a/rootfs/Dockerfile.test b/rootfs/Dockerfile.test
index e7e2134..6ed5c60 100644
--- a/rootfs/Dockerfile.test
+++ b/rootfs/Dockerfile.test
@@ -1,12 +1,13 @@
 ARG CODENAME
 FROM registry.drycc.cc/drycc/base:${CODENAME}
 
-ENV DRYCC_HOME_DIR=/workspace \
-  PGDATA="/var/lib/postgresql/data" \
+ARG DRYCC_HOME_DIR=/workspace \
   PYTHON_VERSION="3.13" \
   POSTGRES_VERSION="17.6" \
   GOSU_VERSION="1.18"
 
+ENV PGDATA="/var/lib/postgresql/data"
+
 COPY requirements.txt ${DRYCC_HOME_DIR}/requirements.txt
 COPY dev_requirements.txt ${DRYCC_HOME_DIR}/dev_requirements.txt
 

From 8beae9e6d0d4f8f97d145f4c35a0df5dde4b5352 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 17 Dec 2025 13:05:45 +0800
Subject: [PATCH 20/30] feat(organization): add oauth2 login support

---
 .../passport/files/reserved-name-patterns.txt |   13 +
 .../templates/passport-configmap.yaml         |   12 +-
 rootfs/Dockerfile                             |    4 +-
 rootfs/Dockerfile.test                        |   33 +-
 rootfs/api/apps.py                            |   10 +
 rootfs/api/apps_extra/__init__.py             |    0
 rootfs/api/apps_extra/social_core/__init__.py |    0
 rootfs/api/apps_extra/social_core/backends.py |   82 +
 .../apps_extra/social_core/icons/feishu.svg   |   12 +
 .../apps_extra/social_core/icons/github.svg   |    3 +
 .../apps_extra/social_core/icons/google.svg   |    8 +
 .../apps_extra/social_core/icons/weixin.svg   |   14 +
 .../api/apps_extra/social_core/pipelines.py   |   59 +
 rootfs/api/exceptions.py                      |    1 -
 rootfs/api/settings/production.py             |   61 +-
 rootfs/api/signals.py                         |    0
 rootfs/api/static/css/main.css                |  243 +-
 rootfs/api/static/icons/logo.svg              |   14 +
 rootfs/api/templates/user/login.html          |   56 +-
 rootfs/api/templates/user/oauth_callback.html |   91 +
 rootfs/api/tests/__init__.py                  |    0
 rootfs/api/tests/test_views.py                |  163 +
 rootfs/api/urls.py                            |   77 +-
 rootfs/api/utils.py                           |   44 +
 rootfs/api/validators.py                      |   17 +-
 rootfs/api/views.py                           |  295 --
 rootfs/api/views/__init__.py                  |    2 +
 rootfs/api/views/api.py                       |  278 ++
 rootfs/api/views/web.py                       |  259 ++
 rootfs/api/viewset.py                         |   12 -
 rootfs/bin/test-unit                          |    7 +
 rootfs/passport/urls.py                       |    5 +-
 rootfs/requirements.txt                       |   27 +-
 rootfs/web/README.md                          |   83 +
 rootfs/web/index.html                         |    6 -
 rootfs/web/package-lock.json                  | 2996 +++++++++--------
 rootfs/web/package.json                       |   21 +-
 rootfs/web/src/.env                           |    1 -
 rootfs/web/src/assets/icons/drycc.svg         |   13 +
 rootfs/web/src/assets/icons/github.svg        |    8 +
 rootfs/web/src/assets/icons/google.svg        |    3 +
 .../web/src/components/AccessTokenDelete.js   |    4 +-
 .../web/src/components/AccessTokenDelete.vue  |  170 +-
 rootfs/web/src/components/Dropdown.vue        |  357 ++
 rootfs/web/src/components/MainFooter.vue      |  203 +-
 rootfs/web/src/components/MainNav.js          |    1 -
 rootfs/web/src/components/MainNav.vue         |   97 +-
 rootfs/web/src/components/NavBar.js           |    6 +
 rootfs/web/src/components/NavBar.vue          |  119 +-
 rootfs/web/src/components/NavBox.vue          |   32 +-
 rootfs/web/src/components/NavMenu.js          |   44 +-
 rootfs/web/src/components/NavMenu.vue         |   95 +-
 rootfs/web/src/components/UserMenu.js         |  101 +-
 rootfs/web/src/components/UserMenu.vue        |   64 +-
 rootfs/web/src/lang/index.js                  |    7 +-
 rootfs/web/src/main.js                        |   13 +-
 rootfs/web/src/services/identities.js         |   21 +
 rootfs/web/src/styles/unified.css             |  230 ++
 rootfs/web/src/utils/axios.js                 |   32 +-
 rootfs/web/src/views/AccessTokenList.vue      |  169 +-
 rootfs/web/src/views/AccountSetting.js        |   96 +-
 rootfs/web/src/views/AccountSetting.vue       |  623 +++-
 rootfs/web/vite.config.js                     |   65 +-
 rootfs/web/yarn.lock                          |  541 ---
 64 files changed, 5390 insertions(+), 2733 deletions(-)
 create mode 100644 charts/passport/files/reserved-name-patterns.txt
 create mode 100644 rootfs/api/apps.py
 create mode 100644 rootfs/api/apps_extra/__init__.py
 create mode 100644 rootfs/api/apps_extra/social_core/__init__.py
 create mode 100644 rootfs/api/apps_extra/social_core/backends.py
 create mode 100644 rootfs/api/apps_extra/social_core/icons/feishu.svg
 create mode 100644 rootfs/api/apps_extra/social_core/icons/github.svg
 create mode 100644 rootfs/api/apps_extra/social_core/icons/google.svg
 create mode 100644 rootfs/api/apps_extra/social_core/icons/weixin.svg
 create mode 100644 rootfs/api/apps_extra/social_core/pipelines.py
 create mode 100644 rootfs/api/signals.py
 create mode 100644 rootfs/api/static/icons/logo.svg
 create mode 100644 rootfs/api/templates/user/oauth_callback.html
 create mode 100644 rootfs/api/tests/__init__.py
 create mode 100644 rootfs/api/tests/test_views.py
 delete mode 100644 rootfs/api/views.py
 create mode 100644 rootfs/api/views/__init__.py
 create mode 100644 rootfs/api/views/api.py
 create mode 100644 rootfs/api/views/web.py
 delete mode 100644 rootfs/api/viewset.py
 create mode 100644 rootfs/web/README.md
 delete mode 100644 rootfs/web/src/.env
 create mode 100644 rootfs/web/src/assets/icons/drycc.svg
 create mode 100644 rootfs/web/src/assets/icons/github.svg
 create mode 100644 rootfs/web/src/assets/icons/google.svg
 create mode 100644 rootfs/web/src/components/Dropdown.vue
 create mode 100644 rootfs/web/src/services/identities.js
 create mode 100644 rootfs/web/src/styles/unified.css
 delete mode 100644 rootfs/web/yarn.lock

diff --git a/charts/passport/files/reserved-name-patterns.txt b/charts/passport/files/reserved-name-patterns.txt
new file mode 100644
index 0000000..1b6ca23
--- /dev/null
+++ b/charts/passport/files/reserved-name-patterns.txt
@@ -0,0 +1,13 @@
+^backup$
+^catalog$
+^cert-manager$
+^default$
+^drycc(?:-[\w-]+)?$
+^istio(?:-[\w-]+)?$
+^kube(?:-[\w-]+)?$
+^longhorn-system$
+^metallb$
+^mount-s3$
+^topolvm$
+^rook-ceph$
+^personal$
\ No newline at end of file
diff --git a/charts/passport/templates/passport-configmap.yaml b/charts/passport/templates/passport-configmap.yaml
index 62dab9b..804a95e 100644
--- a/charts/passport/templates/passport-configmap.yaml
+++ b/charts/passport/templates/passport-configmap.yaml
@@ -5,11 +5,11 @@ metadata:
   labels:
     heritage: drycc
 data:
-  reserved-usernames.txt: |-
-{{- if .Values.reservedUsernames }}
-{{- (tpl .Values.reservedUsernames $) | nindent 4 }}
-{{- else}}
-{{- include "passport.defaultReservedUsernames" . | nindent 4 }}
-{{- end }}
   init-applications.json: |-
 {{ toPrettyJson .Values.initApplications | indent 4 }}
+  reserved-name-patterns.txt: |-
+    {{- if .Values.config.reservedNames }}
+    {{- (tpl .Values.config.reservedNames $)  | nindent 4 }}
+    {{- else}}
+    {{- .Files.Get "files/reserved-name-patterns.txt" | nindent 4 }}
+    {{- end }}
\ No newline at end of file
diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile
index ee5bced..c08620f 100644
--- a/rootfs/Dockerfile
+++ b/rootfs/Dockerfile
@@ -4,7 +4,7 @@ FROM registry.drycc.cc/drycc/base:${CODENAME} as build-app
 ADD web /web
 WORKDIR /web
 
-ENV NODE_VERSION="22"
+ENV NODE_VERSION="24"
 
 RUN install-stack node $NODE_VERSION && . init-stack \
   && npm install --global yarn \
@@ -16,7 +16,7 @@ FROM registry.drycc.cc/drycc/base:${CODENAME}
 ARG DRYCC_UID=1001 \
   DRYCC_GID=1001 \
   DRYCC_HOME_DIR=/workspace \
-  PYTHON_VERSION="3.13"
+  PYTHON_VERSION="3.14"
 
 RUN groupadd drycc --gid ${DRYCC_GID} \
   && useradd drycc -u ${DRYCC_UID} -g ${DRYCC_GID} -s /bin/bash -m -d ${DRYCC_HOME_DIR}
diff --git a/rootfs/Dockerfile.test b/rootfs/Dockerfile.test
index 6ed5c60..ea284df 100644
--- a/rootfs/Dockerfile.test
+++ b/rootfs/Dockerfile.test
@@ -1,10 +1,31 @@
+ARG CODENAME
+FROM registry.drycc.cc/drycc/base:${CODENAME} as build-app
+
+ADD web /web
+WORKDIR /web
+
+ENV NODE_VERSION="24"
+
+RUN install-stack node $NODE_VERSION && . init-stack \
+  && npm install --global yarn \
+  && yarn install \
+  && yarn build
+
+
 ARG CODENAME
 FROM registry.drycc.cc/drycc/base:${CODENAME}
 
-ARG DRYCC_HOME_DIR=/workspace \
-  PYTHON_VERSION="3.13" \
-  POSTGRES_VERSION="17.6" \
-  GOSU_VERSION="1.18"
+ARG DRYCC_UID=1001 \
+  DRYCC_GID=1001 \
+  DRYCC_HOME_DIR=/workspace \
+  PYTHON_VERSION="3.14" \
+  VALKEY_VERSION="9.0.1" \
+  POSTGRES_VERSION="18.1" \
+  GOSU_VERSION="1.19"
+
+
+RUN groupadd drycc --gid ${DRYCC_GID} \
+  && useradd drycc -u ${DRYCC_UID} -g ${DRYCC_GID} -s /bin/bash -m -d ${DRYCC_HOME_DIR}
 
 ENV PGDATA="/var/lib/postgresql/data"
 
@@ -16,6 +37,7 @@ RUN buildDeps='gcc rustc cargo libffi-dev musl-dev libldap2-dev libsasl2-dev'; \
   && curl -SsL https://cli.codecov.io/latest/$([ $(dpkg --print-architecture) == "arm64" ] && echo linux-arm64 || echo linux)/codecov -o /usr/local/bin/codecov \
   && chmod +x /usr/local/bin/codecov \
   && install-stack python $PYTHON_VERSION \
+  && install-stack valkey $VALKEY_VERSION \
   && install-stack postgresql $POSTGRES_VERSION \
   && install-stack gosu $GOSU_VERSION && . init-stack \
   && python3 -m venv ${DRYCC_HOME_DIR}/.venv \
@@ -50,6 +72,9 @@ RUN buildDeps='gcc rustc cargo libffi-dev musl-dev libldap2-dev libsasl2-dev'; \
   && gosu postgres initdb -D $PGDATA
 
 COPY . ${DRYCC_HOME_DIR}
+COPY --chown=${DRYCC_UID}:${DRYCC_GID} . ${DRYCC_HOME_DIR}
+COPY --chown=${DRYCC_UID}:${DRYCC_GID} --from=build-app /web/dist ${DRYCC_HOME_DIR}/web/dist
+
 WORKDIR ${DRYCC_HOME_DIR}
 CMD ["bin/boot"]
 EXPOSE 8000
diff --git a/rootfs/api/apps.py b/rootfs/api/apps.py
new file mode 100644
index 0000000..85e1f47
--- /dev/null
+++ b/rootfs/api/apps.py
@@ -0,0 +1,10 @@
+from django import apps
+
+
+class AppConfig(apps.AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'api'
+
+    def ready(self):
+        super(AppConfig, self).ready()
+        __import__("api.signals")
diff --git a/rootfs/api/apps_extra/__init__.py b/rootfs/api/apps_extra/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rootfs/api/apps_extra/social_core/__init__.py b/rootfs/api/apps_extra/social_core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rootfs/api/apps_extra/social_core/backends.py b/rootfs/api/apps_extra/social_core/backends.py
new file mode 100644
index 0000000..34f8ab6
--- /dev/null
+++ b/rootfs/api/apps_extra/social_core/backends.py
@@ -0,0 +1,82 @@
+import json
+from os.path import join, dirname
+from pathlib import Path
+from social_core.backends.oauth import BaseOAuth2
+from social_core.backends.github import GithubOAuth2 as BaseGithubOAuth2
+from social_core.backends.google import GoogleOAuth2 as BaseGoogleOAuth2
+from social_core.backends.weixin import WeixinOAuth2 as BaseWeixinOAuth2
+
+
+def icon_path(name):
+    return Path(join(dirname(__file__), "icons", f"{name}.svg"))
+
+
+class GithubOAuth2(BaseGithubOAuth2):
+    icon = icon_path("github").read_text(encoding="utf-8")
+
+
+class GoogleOAuth2(BaseGoogleOAuth2):
+    name = "google"
+    icon = icon_path("google").read_text(encoding="utf-8")
+
+
+class WeixinOAuth2(BaseWeixinOAuth2):
+    icon = icon_path("weixin").read_text(encoding="utf-8")
+
+
+class FeiShuOAuth2(BaseOAuth2):
+    """FeiShu OAuth2 authentication backend"""
+    name = 'feishu'
+    icon = icon_path("feishu").read_text(encoding="utf-8")
+
+    AUTHORIZATION_URL = 'https://accounts.feishu.cn/open-apis/authen/v1/authorize'
+    ACCESS_TOKEN_URL = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token'
+    USER_INFO_URL = 'https://open.feishu.cn/open-apis/authen/v1/user_info'
+
+    DEFAULT_SCOPE = ['']
+    REDIRECT_STATE = False
+    ACCESS_TOKEN_METHOD = 'POST'
+    EXTRA_DATA = [
+        ('refresh_token', 'refresh_token'),
+        ('expires_in', 'expires'),
+        ('union_id', 'union_id'),
+        ('access_token', 'access_token'),
+    ]
+
+    def request_access_token(self, *args, **kwargs):
+        """Override to send JSON request provided by FeiShu"""
+        if 'data' in kwargs:
+            kwargs['data']['app_id'] = self.setting('KEY')
+            kwargs['data']['app_secret'] = self.setting('SECRET')
+            kwargs['data'] = json.dumps(kwargs['data'])
+            if 'headers' not in kwargs:
+                kwargs['headers'] = {}
+            kwargs['headers']['Content-Type'] = 'application/json; charset=utf-8'
+        return super().request_access_token(*args, **kwargs)
+
+    def get_user_details(self, response):
+        """Return user details from FeiShu account"""
+        return {
+            'username': response.get('name', ''),
+            'email': response.get('email', ''),
+        }
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from service"""
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+            'Content-Type': 'application/json; charset=utf-8'
+        }
+        response = self.get_json(
+            self.USER_INFO_URL,
+            headers=headers
+        )
+        # FeiShu returns data wrapped in 'data' field
+        if 'data' in response:
+            response.update(response['data'])
+
+        response['union_id'] = response.get('union_id', '')
+        return response
+
+
+__all__ = [GithubOAuth2, GoogleOAuth2, WeixinOAuth2, FeiShuOAuth2]
diff --git a/rootfs/api/apps_extra/social_core/icons/feishu.svg b/rootfs/api/apps_extra/social_core/icons/feishu.svg
new file mode 100644
index 0000000..7afdb94
--- /dev/null
+++ b/rootfs/api/apps_extra/social_core/icons/feishu.svg
@@ -0,0 +1,12 @@
+<svg width="24"
+     height="24"
+     viewBox="0 0 24 24"
+     fill="none"
+     xmlns="http://www.w3.org/2000/svg">
+  <path d="M12.9238 12.8029C12.9427 12.784 12.9616 12.7682 12.9806 12.7493C13.0184 12.7146 13.0563 12.6767 13.091 12.6389L13.1667 12.5631L13.397 12.336L14.7315 11.0173L15.0659 10.686C15.129 10.6229 15.1952 10.563 15.2615 10.5031C15.3845 10.3926 15.5076 10.2854 15.6369 10.1813C15.7536 10.0866 15.8767 9.99514 15.9997 9.9068C16.1732 9.78376 16.3499 9.67019 16.5329 9.55977C16.7127 9.45251 16.8957 9.35471 17.085 9.26322C17.2616 9.17804 17.4415 9.09917 17.6276 9.02661C17.7317 8.9856 17.8326 8.94774 17.9399 8.91304C17.9935 8.89411 18.044 8.87834 18.0977 8.86256C17.6276 7.00439 16.7632 5.3008 15.5991 3.84959C15.3719 3.56566 15.0249 3.40161 14.6589 3.40161H5.0084C4.83489 3.40161 4.76233 3.6256 4.90114 3.72656C8.18528 6.13997 10.9236 9.24114 12.9017 12.825C12.908 12.8187 12.9175 12.8124 12.9238 12.8029Z"
+        fill="#00D6B9" />
+  <path d="M9.09696 21.2986C14.0815 21.2986 18.4225 18.5476 20.6877 14.4843C20.7666 14.3423 20.8454 14.1972 20.918 14.052C20.8044 14.2729 20.6751 14.4811 20.5394 14.6767C20.4889 14.7461 20.4385 14.8155 20.388 14.8818C20.3217 14.9669 20.2555 15.049 20.1861 15.1278C20.1324 15.1909 20.0757 15.2509 20.0189 15.3108C19.9021 15.4307 19.7823 15.5474 19.6561 15.6547C19.5867 15.7146 19.5141 15.7714 19.4415 15.8282C19.3564 15.8944 19.268 15.9575 19.1797 16.0143C19.1229 16.0522 19.0661 16.09 19.0093 16.1247C18.9494 16.1626 18.8895 16.1973 18.8264 16.232C18.7002 16.3014 18.574 16.3645 18.4446 16.4245C18.3311 16.4749 18.2175 16.5223 18.1008 16.5633C17.9746 16.6106 17.8452 16.6516 17.7159 16.6863C17.5234 16.7399 17.3247 16.7809 17.1259 16.8125C16.9808 16.8346 16.8357 16.8504 16.6874 16.863C16.5328 16.8724 16.3751 16.8787 16.2173 16.8756C16.0438 16.8724 15.8703 16.863 15.6936 16.844C15.5643 16.8314 15.435 16.8125 15.3056 16.7873C15.192 16.7683 15.0785 16.7431 14.9649 16.7178C14.9049 16.7021 14.845 16.6895 14.7851 16.6737C14.6179 16.6295 14.4538 16.5822 14.2898 16.5349C14.2077 16.5096 14.1257 16.4875 14.0437 16.4623C13.9206 16.4245 13.7976 16.3897 13.6777 16.3519C13.5768 16.3203 13.479 16.2888 13.378 16.2572C13.2834 16.2257 13.1887 16.1942 13.0941 16.1626C13.031 16.1405 12.9647 16.1184 12.9016 16.0964C12.8228 16.0711 12.7471 16.0427 12.6682 16.0143C12.6114 15.9954 12.5578 15.9765 12.501 15.9544C12.3906 15.9134 12.2802 15.8755 12.1729 15.8345C12.1098 15.8093 12.0467 15.7872 11.9836 15.7619C11.8984 15.7304 11.8132 15.6957 11.7312 15.6641C11.6429 15.6294 11.5514 15.5947 11.4631 15.5569C11.4063 15.5348 11.3463 15.5096 11.2895 15.4875C11.217 15.4591 11.1476 15.4275 11.075 15.3991C11.0214 15.3771 10.9646 15.3518 10.911 15.3297C10.8542 15.3045 10.7974 15.2793 10.7406 15.254C10.6901 15.2319 10.6428 15.2099 10.5923 15.1878C10.5482 15.1688 10.5008 15.1468 10.4567 15.1278C10.4094 15.1057 10.3652 15.0868 10.3179 15.0647C10.2705 15.0427 10.2232 15.0206 10.1759 14.9985C10.116 14.9701 10.056 14.9417 9.99608 14.9165C9.93299 14.8881 9.87304 14.8565 9.80995 14.8281C9.7437 14.7966 9.67745 14.765 9.6112 14.7303C9.55441 14.7019 9.49762 14.6735 9.44084 14.6483C6.45324 13.1592 3.80321 11.1717 1.54438 8.76145C1.43081 8.64157 1.23206 8.72044 1.23206 8.88449L1.23836 18.0933C1.23836 18.494 1.43712 18.8726 1.77153 19.0934C3.86631 20.4878 6.38699 21.2986 9.09696 21.2986Z"
+        fill="#3370FF" />
+  <path d="M23.7322 9.29488C22.7226 8.79642 21.5838 8.5188 20.3818 8.5188C19.6688 8.5188 18.9747 8.6166 18.3217 8.80273C18.246 8.82481 18.1703 8.8469 18.0977 8.86898C18.0441 8.88476 17.9905 8.90368 17.94 8.91946C17.8359 8.95416 17.7318 8.99202 17.6276 9.03303C17.4447 9.10559 17.2617 9.18446 17.085 9.26964C16.8957 9.36113 16.7128 9.45893 16.5329 9.56619C16.35 9.67345 16.1701 9.79018 15.9998 9.91322C15.8767 10.0016 15.7569 10.093 15.637 10.1877C15.5076 10.2918 15.3846 10.3991 15.2616 10.5095C15.1953 10.5694 15.1322 10.6325 15.066 10.6925L14.7315 11.0206L13.3939 12.3424L13.1636 12.5696L13.0879 12.6453C13.05 12.6831 13.0122 12.7178 12.9775 12.7557C12.9586 12.7746 12.9396 12.7904 12.9207 12.8093C12.8923 12.8377 12.8639 12.863 12.8355 12.8882C12.804 12.9166 12.7724 12.9481 12.7409 12.9765C11.9143 13.7368 10.9931 14.3899 9.99304 14.923C10.053 14.9514 10.1129 14.9798 10.1729 15.0051C10.2202 15.0271 10.2675 15.0492 10.3148 15.0713C10.359 15.0934 10.4063 15.1123 10.4536 15.1344C10.4978 15.1533 10.5451 15.1754 10.5893 15.1943C10.6398 15.2164 10.6871 15.2385 10.7376 15.2606C10.7944 15.2858 10.8511 15.3111 10.9079 15.3363C10.9616 15.3584 11.0184 15.3836 11.072 15.4057C11.1445 15.4373 11.2139 15.4657 11.2865 15.4941C11.3433 15.5193 11.4032 15.5414 11.46 15.5635C11.5484 15.5982 11.6367 15.636 11.7282 15.6707C11.8134 15.7023 11.8954 15.737 11.9806 15.7685C12.0437 15.7938 12.1068 15.8158 12.1699 15.8411C12.2803 15.8821 12.3875 15.9231 12.498 15.961C12.5547 15.9799 12.6084 16.002 12.6652 16.0209C12.744 16.0493 12.8197 16.0745 12.8986 16.1029C12.9617 16.125 13.028 16.1471 13.0911 16.1692C13.1857 16.2007 13.2803 16.2323 13.375 16.2638C13.4728 16.2954 13.5737 16.3269 13.6747 16.3585C13.7977 16.3963 13.9176 16.4342 14.0406 16.4689C14.1227 16.4941 14.2047 16.5162 14.2867 16.5414C14.4508 16.5888 14.618 16.6361 14.782 16.6803C14.842 16.696 14.9019 16.7118 14.9618 16.7244C15.0754 16.7528 15.189 16.7749 15.3026 16.7938C15.4319 16.8159 15.5613 16.8348 15.6906 16.8506C15.8673 16.8695 16.0408 16.8822 16.2143 16.8822C16.372 16.8853 16.5298 16.879 16.6844 16.8695C16.8326 16.8601 16.9778 16.8412 17.1229 16.8191C17.3248 16.7875 17.5204 16.7465 17.7128 16.6929C17.8422 16.6582 17.9715 16.6172 18.0977 16.5698C18.2144 16.5257 18.328 16.4815 18.4416 16.4279C18.5709 16.3679 18.7003 16.3048 18.8233 16.2354C18.8833 16.2007 18.9464 16.166 19.0063 16.1282C19.0631 16.0935 19.1199 16.0556 19.1767 16.0178C19.265 15.9578 19.3533 15.8947 19.4385 15.8316C19.5111 15.7748 19.5836 15.718 19.653 15.6581C19.7792 15.5508 19.8991 15.4341 20.0158 15.3142C20.0726 15.2543 20.1294 15.1943 20.183 15.1313C20.2524 15.0524 20.3187 14.9704 20.3849 14.8852C20.4354 14.8189 20.4859 14.7495 20.5364 14.6801C20.672 14.4845 20.7982 14.2763 20.9118 14.0586L21.0411 13.7999L22.2084 11.4748L22.2053 11.4812C22.5807 10.6578 23.1012 9.91953 23.7322 9.29488Z"
+        fill="#133C9A" />
+</svg>
\ No newline at end of file
diff --git a/rootfs/api/apps_extra/social_core/icons/github.svg b/rootfs/api/apps_extra/social_core/icons/github.svg
new file mode 100644
index 0000000..c5858b0
--- /dev/null
+++ b/rootfs/api/apps_extra/social_core/icons/github.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
+    <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
+</svg>
\ No newline at end of file
diff --git a/rootfs/api/apps_extra/social_core/icons/google.svg b/rootfs/api/apps_extra/social_core/icons/google.svg
new file mode 100644
index 0000000..c2fd469
--- /dev/null
+++ b/rootfs/api/apps_extra/social_core/icons/google.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
+    <g fill="none" fill-rule="evenodd">
+        <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
+        <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
+        <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/>
+        <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/rootfs/api/apps_extra/social_core/icons/weixin.svg b/rootfs/api/apps_extra/social_core/icons/weixin.svg
new file mode 100644
index 0000000..fe20def
--- /dev/null
+++ b/rootfs/api/apps_extra/social_core/icons/weixin.svg
@@ -0,0 +1,14 @@
+<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+  <!-- 左上角大圆 -->
+  <path d="M7.5 2.5C3.36 2.5 0 5.41 0 9C0 11.12 1.15 13.03 2.94 14.28C2.5 15.92 1.5 17.43 1.5 17.43C1.5 17.43 3.94 17.67 6.04 16.31C6.51 16.39 7 16.43 7.5 16.43C11.64 16.43 15 13.52 15 9.93C15 6.34 11.64 2.5 7.5 2.5Z" fill="#07C160"/>
+  <!-- 左上角眼睛 -->
+  <circle cx="5.5" cy="7.5" r="1" fill="white"/>
+  <circle cx="9.5" cy="7.5" r="1" fill="white"/>
+  <!-- 右下角小圆 (带白色描边用于切割视觉) -->
+  <g>
+    <path d="M16.5 11.5C13.19 11.5 10.5 13.74 10.5 16.5C10.5 18.14 11.55 19.6 13.17 20.52C12.78 21.85 12 23 12 23C12 23 13.95 23.19 15.65 22.08C16.08 22.16 16.52 22.2 16.96 22.2C20.28 22.2 23 19.96 23 17.2C23 14.44 20.28 11.5 16.5 11.5Z" fill="#07C160" stroke="white" stroke-width="1.2" paint-order="stroke"/>
+    <!-- 右下角眼睛 -->
+    <circle cx="14.5" cy="15.5" r="0.8" fill="white"/>
+    <circle cx="18.5" cy="15.5" r="0.8" fill="white"/>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/rootfs/api/apps_extra/social_core/pipelines.py b/rootfs/api/apps_extra/social_core/pipelines.py
new file mode 100644
index 0000000..d8f24b2
--- /dev/null
+++ b/rootfs/api/apps_extra/social_core/pipelines.py
@@ -0,0 +1,59 @@
+"""
+Custom social auth pipeline for OAuth2 binding and creation guidance.
+"""
+
+import logging
+
+from django.shortcuts import redirect
+from social_django.models import UserSocialAuth
+
+from api.utils import get_oauth_callback
+
+logger = logging.getLogger(__name__)
+
+
+def handle_authenticated_binding(backend, uid, details, response, *args, **kwargs):
+    request = backend.strategy.request
+    if not request or not request.user.is_authenticated:
+        return None
+
+    current_user = request.user
+    try:
+        existing_social = UserSocialAuth.objects.filter(
+            provider=backend.name, uid=uid
+        ).select_related('user').first()
+        if existing_social and existing_social.user_id != current_user.id:
+            return redirect(get_oauth_callback(request, 'conflict', backend.name))
+        if existing_social and existing_social.user_id == current_user.id:
+            return redirect(get_oauth_callback(request, 'already_linked', backend.name))
+
+        UserSocialAuth.objects.create(
+            user=current_user,
+            provider=backend.name,
+            uid=uid,
+            extra_data=response or {}
+        )
+        return redirect(get_oauth_callback(request, 'linked', backend.name))
+    except Exception as exc:
+        logger.exception(exc)
+        return redirect(get_oauth_callback(request, 'error', backend.name))
+
+
+def require_username_password(backend, uid, details, response, user=None, *args, **kwargs):
+    request = backend.strategy.request
+    if request and request.user.is_authenticated:
+        return None
+    if user:
+        return None
+
+    email = details.get('email')
+    if not email:
+        return redirect(get_oauth_callback(request, 'missing_email', backend.name))
+
+    backend.strategy.session_set('oauth_pending', {
+        'provider': backend.name,
+        'uid': uid,
+        'email': email,
+        'extra_data': response or {},
+    })
+    return redirect(get_oauth_callback(request, 'pending', backend.name))
diff --git a/rootfs/api/exceptions.py b/rootfs/api/exceptions.py
index d409c39..dfd7d2e 100644
--- a/rootfs/api/exceptions.py
+++ b/rootfs/api/exceptions.py
@@ -43,7 +43,6 @@ def custom_exception_handler(exc, context):
 
     # No response means DRF couldn't handle it
     # Output a generic 500 in a JSON format
-    print(response)
     if response is None:
         logging.exception('Uncaught Exception', exc_info=exc)
         set_rollback()
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index e2960c4..c3536dc 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -116,6 +116,7 @@
     'gunicorn',
     'rest_framework',
     'oauth2_provider',
+    'social_django',
     # passport apps
     'api'
 )
@@ -124,6 +125,10 @@
 DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
 
 AUTHENTICATION_BACKENDS = (
+    "api.apps_extra.social_core.backends.FeiShuOAuth2",
+    "api.apps_extra.social_core.backends.WeixinOAuth2",
+    "api.apps_extra.social_core.backends.GithubOAuth2",
+    "api.apps_extra.social_core.backends.GoogleOAuth2",
     "django.contrib.auth.backends.AllowAllUsersModelBackend",
 )
 
@@ -168,6 +173,7 @@
 SECURE_BROWSER_XSS_FILTER = True
 CSRF_COOKIE_SECURE = os.environ.get('CSRF_COOKIE_SECURE', 'false').lower() == "true"
 SESSION_COOKIE_SECURE = os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == "true"
+CSRF_TRUSTED_ORIGINS = [r for r in os.environ.get('CSRF_TRUSTED_ORIGINS', '').split(',') if r]
 
 # Honor HTTPS from a trusted proxy
 # see https://docs.djangoproject.com/en/2.2/ref/settings/#secure-proxy-ssl-header
@@ -274,7 +280,7 @@
 SECRET_KEY = os.environ.get('DRYCC_SECRET_KEY', random_secret)
 
 # database default setting
-DRYCC_DATABASE_URL = os.environ.get('DRYCC_DATABASE_URL', 'postgres://postgres:123456@49.232.207.93:5432/drycc_passport')  # noqa
+DRYCC_DATABASE_URL = os.environ.get('DRYCC_DATABASE_URL', 'postgres://postgres:@:5432/passport')
 DATABASES = {
     'default': dj_database_url.config(default=DRYCC_DATABASE_URL)
 }
@@ -325,9 +331,6 @@
     "DEFAULT_CODE_CHALLENGE_METHOD": 'S256',
 }
 OAUTH2_PROVIDER_APPLICATION_MODEL = 'api.Application'
-# Valkey Configuration
-DRYCC_VALKEY_ADDRS = os.environ.get('DRYCC_VALKEY_ADDRS', '127.0.0.1:6379').split(",")
-DRYCC_VALKEY_PASSWORD = os.environ.get('DRYCC_VALKEY_PASSWORD', '')
 
 # Cache Valkey Configuration
 CACHES = {
@@ -349,6 +352,42 @@
 LDAP_STAFF_GROUP = os.environ.get('LDAP_STAFF_GROUP', '')
 LDAP_SUPERUSER_GROUP = os.environ.get('LDAP_SUPERUSER_GROUP', '')
 
+# Social Auth settings for OAuth2 providers
+SOCIAL_AUTH_FEISHU_KEY = os.environ.get('SOCIAL_AUTH_FEISHU_KEY', '')
+SOCIAL_AUTH_FEISHU_SECRET = os.environ.get('SOCIAL_AUTH_FEISHU_SECRET', '')
+
+SOCIAL_AUTH_WEIXIN_KEY = os.environ.get('SOCIAL_AUTH_WEIXIN_KEY', '')
+SOCIAL_AUTH_WEIXIN_SECRET = os.environ.get('SOCIAL_AUTH_WEIXIN_SECRET', '')
+
+SOCIAL_AUTH_GITHUB_KEY = os.environ.get('SOCIAL_AUTH_GITHUB_KEY', '')
+SOCIAL_AUTH_GITHUB_SECRET = os.environ.get('SOCIAL_AUTH_GITHUB_SECRET', '')
+
+SOCIAL_AUTH_GOOGLE_KEY = os.environ.get('SOCIAL_AUTH_GOOGLE_KEY', '')
+SOCIAL_AUTH_GOOGLE_SECRET = os.environ.get('SOCIAL_AUTH_GOOGLE_SECRET', '')
+
+# Social Auth Pipeline
+SOCIAL_AUTH_PIPELINE = (
+    'social_core.pipeline.social_auth.social_details',
+    'social_core.pipeline.social_auth.social_uid',
+    'social_core.pipeline.social_auth.auth_allowed',
+    'api.apps_extra.social_core.pipelines.handle_authenticated_binding',
+    'social_core.pipeline.social_auth.social_user',
+    'api.apps_extra.social_core.pipelines.require_username_password',
+    'social_core.pipeline.social_auth.associate_user',
+    'social_core.pipeline.social_auth.load_extra_data',
+    'social_core.pipeline.user.user_details',
+)
+
+# Social Auth settings
+SOCIAL_AUTH_URL_NAMESPACE = 'social'
+SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/access-tokens'
+SOCIAL_AUTH_LOGIN_CALLBACK_URL = '/oauth/callback'
+SOCIAL_AUTH_LOGIN_ERROR_URL = f'{SOCIAL_AUTH_LOGIN_CALLBACK_URL}?status=error'
+SOCIAL_AUTH_NEW_USER_REDIRECT_URL = f'{SOCIAL_AUTH_LOGIN_CALLBACK_URL}?status=pending'
+SOCIAL_AUTH_USER_MODEL = 'api.User'
+SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL = True
+SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['email',]
+
 # Django LDAP backend configuration.
 # See https://pythonhosted.org/django-auth-ldap/reference.html
 # for variables' details.
@@ -402,14 +441,14 @@
 # username regex
 USERNAME_REGEX = os.environ.get('USERNAME_REGEX', '^[a-z][a-z0-9]{4,}$')
 
-# reserved username
-RESERVED_USERNAMES_PATH = os.environ.get(
-    'RESERVED_USERNAMES_PATH', '/etc/drycc/passport/reserved-usernames.txt')
-if os.path.exists(RESERVED_USERNAMES_PATH):
-    with open(RESERVED_USERNAMES_PATH) as f:
-        RESERVED_USERNAMES = [line.strip() for line in f if line]
+# names which apps cannot reserve for routing
+RESERVED_NAME_PATTERNS_PATH = os.environ.get(
+    'RESERVED_NAME_PATTERNS_PATH', '/etc/controller/reserved-name-patterns.txt')
+if os.path.exists(RESERVED_NAME_PATTERNS_PATH):
+    with open(RESERVED_NAME_PATTERNS_PATH) as f:
+        RESERVED_NAME_PATTERNS = [line.strip() for line in f if line.strip()]
 else:
-    RESERVED_USERNAMES = ["drycc", "admin", "doopai"]
+    RESERVED_NAME_PATTERNS = [r"^drycc(?:-[\w-]+)?$", r"^kube(?:-[\w-]+)?$", r"^default$"]
 
 # hcaptcha config
 H_CAPTCHA_KEY = os.environ.get("H_CAPTCHA_KEY")
diff --git a/rootfs/api/signals.py b/rootfs/api/signals.py
new file mode 100644
index 0000000..e69de29
diff --git a/rootfs/api/static/css/main.css b/rootfs/api/static/css/main.css
index 432e6bb..43cd30d 100644
--- a/rootfs/api/static/css/main.css
+++ b/rootfs/api/static/css/main.css
@@ -1,13 +1,51 @@
+:root {
+    --ui-color-bg: #ffffff;
+    --ui-color-surface: #ffffff;
+    --ui-color-text: #3f3f44;
+    --ui-color-text-secondary: #62748e;
+    --ui-color-primary: #409eff;
+    --ui-color-primary-hover: #337ecc;
+    --ui-color-border: #e2e8f0;
+    --ui-color-muted-bg: #f1f5f9;
+    --ui-color-danger: #c20707;
+
+    --ui-font-family-base: benton-sans, 'Helvetica Neue', helvetica, arial, sans-serif;
+    --ui-font-size-base: 14px;
+    --ui-line-height-base: 1.42857;
+
+    --ui-radius-sm: 6px;
+    --ui-radius-md: 8px;
+    --ui-radius-lg: 12px;
+
+    --ui-shadow-sm: 0 2px 4px rgba(64, 158, 255, 0.3);
+    --ui-shadow-md: 0 4px 8px rgba(64, 158, 255, 0.4);
+    --ui-shadow-card: 0 4px 6px rgba(0, 0, 0, 0.1);
+
+    --ui-space-1: 4px;
+    --ui-space-2: 8px;
+    --ui-space-3: 12px;
+    --ui-space-4: 16px;
+    --ui-space-5: 20px;
+    --ui-space-6: 24px;
+    --ui-space-7: 32px;
+
+    --ui-breakpoint-xs: 320px;
+    --ui-breakpoint-sm: 576px;
+    --ui-breakpoint-md: 768px;
+    --ui-breakpoint-lg: 992px;
+    --ui-breakpoint-xl: 1200px;
+}
+
 html,body {
 	height: 100%;
 }
 
 body {
-    font-family: benton-sans,'Helvetica Neue',helvetica,arial,sans-serif;
-    font-size: 14px;
-    line-height: 1.42857;
-    color: #3f3f44;
-    background-color: #fff;
+    font-family: var(--ui-font-family-base);
+    font-size: var(--ui-font-size-base);
+    line-height: var(--ui-line-height-base);
+    color: var(--ui-color-text);
+    background-color: var(--ui-color-bg);
     margin: 0;
 }
 
@@ -16,22 +54,26 @@ body {
 }
 
 .page-wrap {
-    min-height: 100%;
+    min-height: 100vh;
     display: flex;
     flex-direction: column;
-    justify-content: space-between
 }
 
 .gradient-primary {
-    background-image: linear-gradient(to bottom,#409EFF, #337ecc);
-    background-color:  #337ecc
+    background-image: linear-gradient(to bottom, var(--ui-color-primary), var(--ui-color-primary-hover));
+    background-color: var(--ui-color-primary-hover)
 }
 
 .container {
     position: relative;
-    height: 100%;
-    text-align: center;
-    padding-top: 2%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    padding: 20px;
+    box-sizing: border-box;
+    flex: 1;
+    width: 100%;
 }
 
 h1.logo {
@@ -55,31 +97,29 @@ h1.logo a:before {
 }
 
 .panel {
-    max-width: 430px;
-    margin: 0 auto 20px;
-    border-radius: 8px;
-    background-color: rgb(255, 255, 255);
-    box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 1px;
-    border-width: 1px;
-    border-style: solid;
-    border-color: transparent;
-    border-image: initial;
+    min-width: 490px;
+    max-width: 520px;
+    margin: 0 auto;
+    border-radius: var(--ui-radius-lg);
+    background-color: var(--ui-color-surface);
+    box-shadow: var(--ui-shadow-card);
+    border: none;
 }
 
 .panel .h3 {
-    margin: 40px 20px 0;
+    margin: 40px 30px 20px;
     line-height: 1.5;
     font-size: 24px;
-    color: #409EFF;
+    color: var(--ui-color-primary);
 }
 
 h2 {
     font-family: inherit;
     font-weight: 200;
-    color: #409EFF;
+    color: var(--ui-color-primary);
 }
 .panel form {
-    padding: 30px
+    padding: 0px 30px 30px 30px
 }
 
 form {
@@ -102,7 +142,7 @@ label {
 form label {
     text-align: left;
     width: 100%;
-    color: rgb(98, 116, 142);
+    color: var(--ui-color-text-secondary);
     font-size: 12px;
     font-weight: bold;
 }
@@ -125,12 +165,20 @@ input:-webkit-autofill {
 .form-control {
     display: block;
     width: 100%;
-    padding: 6px 12px;
+    padding: 10px 16px;
     font-size: 14px;
     line-height: 1.42857;
-    border: 1px solid #cbcbd2;
-    border-radius: 4px;
+    border: 1px solid var(--ui-color-border);
+    border-radius: var(--ui-radius-sm);
     transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+    background-color: var(--ui-color-surface);
+    box-sizing: border-box;
+}
+
+.form-control:focus {
+    outline: none;
+    border-color: var(--ui-color-primary);
+    box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
 }
 
 input, button, select, textarea {
@@ -146,7 +194,7 @@ button, input, optgroup, select, textarea {
 }
 
 input {
-    -webkit-writing-mode: horizontal-tb !important;
+    writing-mode: horizontal-tb !important;
     text-rendering: auto;
     letter-spacing: normal;
     word-spacing: normal;
@@ -154,12 +202,17 @@ input {
     text-indent: 0px;
     text-shadow: none;
     text-align: start;
-    -webkit-appearance: textfield;
+    appearance: textfield;
     background-color: white;
     -webkit-rtl-ordering: logical;
     cursor: text;
 }
 
+input[disabled], input[readonly] {
+    background: var(--ui-color-muted-bg);
+    cursor: not-allowed;
+}
+
 form .btn {
     border: 1px solid transparent;
 }
@@ -173,16 +226,54 @@ form .btn {
 
 .btn-primary {
     color: #fff;
-    background-color:  #337ecc;
-    background-image: -webkit-linear-gradient(left bottom,rgba(159,88,150,0) 0,rgba(121,187,255,0.6) 100%);
+    background-color: var(--ui-color-primary);
+    background-image: none;
+    border: none;
+    box-shadow: var(--ui-shadow-sm);
+    transition: all 0.2s ease;
+}
+
+.btn-primary:hover {
+    background-color: var(--ui-color-primary-hover);
+    box-shadow: var(--ui-shadow-md);
+    transform: translateY(-1px);
+}
+
+.btn-primary:active {
+    transform: translateY(0);
+    box-shadow: 0 1px 2px rgba(64, 158, 255, 0.3);
 }
 
 .btn {
-    border-radius: 5px;
+    border-radius: var(--ui-radius-sm);
     padding-left: 18px;
     padding-right: 18px;
 }
 
+@media (max-width: 576px) {
+    .panel {
+        min-width: auto;
+        width: 100%;
+        max-width: 520px;
+    }
+
+    .panel .h3 {
+        margin: 30px 20px 16px;
+        font-size: 22px;
+    }
+
+    .panel form,
+    .oauth2-container {
+        padding-left: 20px;
+        padding-right: 20px;
+    }
+
+    .oauth2-btn {
+        flex: 1 0 100%;
+        width: 100%;
+    }
+}
+
 .btn-block {
     display: block;
     width: 100%;
@@ -199,6 +290,89 @@ form .btn {
     user-select: none;
 }
 
+/* OAuth2 buttons styles */
+.oauth2-container {
+    padding: 30px 30px 10px;
+}
+
+.oauth2-buttons {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 15px;
+    margin-bottom: 24px;
+    justify-content: space-between;
+}
+
+.oauth2-btn {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 12px 16px;
+    border: 1px solid #e2e8f0;
+    border-radius: 6px;
+    background-color: #ffffff;
+    color: #4a5568;
+    text-decoration: none;
+    font-size: 14px;
+    font-weight: 500;
+    transition: all 0.2s ease;
+    flex: 1 0 calc(50% - 10px);
+    width: calc(50% - 10px);
+    min-width: 140px;
+    max-width: none;
+    justify-content: center;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+    box-sizing: border-box;
+}
+
+.oauth2-btn:hover {
+    border-color: #cbd5e0;
+    background-color: #f7fafc;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
+    transform: translateY(-1px);
+}
+
+.oauth2-btn:focus {
+    outline: none;
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
+}
+
+.oauth2-icon {
+    width: 20px;
+    height: 20px;
+    display: inline-block;
+    flex-shrink: 0;
+}
+
+.oauth2-icon svg {
+    width: 100%;
+    height: 100%;
+    fill: currentColor;
+}
+
+.oauth2-email {
+    background:  #f1f5f9;
+}
+
+.oauth2-divider {
+    display: flex;
+    align-items: center;
+    text-align: center;
+}
+
+.oauth2-divider .divider-line {
+    flex: 1;
+    height: 1px;
+    background-color: #e2e8f0;
+}
+
+.oauth2-divider .divider-text {
+    padding: 0 16px;
+    color: #a0aec0;
+    font-size: 14px;
+    font-weight: 500;
+}
+
 .panel-footer {
     background-color: #f5f5f5;
     border-top: 1px solid #ddd;
@@ -211,6 +385,7 @@ form .btn {
     display: block;
     font-size: 17px;
     color: #4a5568;
+    text-align: center;
 }
 
 a.panel-footer {
diff --git a/rootfs/api/static/icons/logo.svg b/rootfs/api/static/icons/logo.svg
new file mode 100644
index 0000000..d970da0
--- /dev/null
+++ b/rootfs/api/static/icons/logo.svg
@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30px" height="30px" viewBox="0 0 16 16" version="1.1">
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <circle id="Oval" stroke="#FF2F57" fill="#FF2F57" cx="3" cy="3" r="2"/>
+        <circle id="Oval" stroke="#FF2F57" fill="#FF2F57" cx="3" cy="13" r="2"/>
+        <circle id="Oval" stroke="#FF2F57" fill="#FF2F57" cx="13" cy="13" r="2"/>
+        <circle id="Oval" stroke="#FF2F57" fill="#FF2F57" cx="13" cy="13" r="2"/>
+        <circle id="Oval" stroke="#FF2F57" fill="#FF2F57" cx="13" cy="3" r="2"/>
+        <rect id="Rectangle" stroke="#3FB1E5" fill="#3FB1E5" x="6.5" y="6.5" width="3" height="3"/>
+        <polygon id="Triangle" stroke="#38BB4C" fill="#38BB4C" stroke-linejoin="round" points="8 11 10 15 6 15"/>
+        <polygon id="Triangle" stroke="#38BB4C" fill="#38BB4C" stroke-linejoin="round" transform="translate(3.000000, 8.000000) rotate(90.000000) translate(-3.000000, -8.000000) " points="3 6 5 10 1 10"/>
+        <polygon id="Triangle" stroke="#38BB4C" fill="#38BB4C" stroke-linejoin="round" transform="translate(8.000000, 3.000000) rotate(180.000000) translate(-8.000000, -3.000000) " points="8 1 10 5 6 5"/>
+        <polygon id="Triangle" stroke="#38BB4C" fill="#38BB4C" stroke-linejoin="round" transform="translate(13.000000, 8.000000) rotate(270.000000) translate(-13.000000, -8.000000) " points="13 6 15 10 11 10"/>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/rootfs/api/templates/user/login.html b/rootfs/api/templates/user/login.html
index 2f81e5f..e2793ed 100644
--- a/rootfs/api/templates/user/login.html
+++ b/rootfs/api/templates/user/login.html
@@ -2,16 +2,34 @@
 <link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
 <head>
     <meta charset="UTF-8">
-    <title>Login</title>
+    <title>{% if identity_linking %}Identity Linking{% else %}Login{% endif %}</title>
 </head>
-<div class="page-wrap gradient-primary"> 
+<div class="page-wrap gradient-primary">
     <div class="container">
         <div class="panel">
-            <h2 class="h3">Log in to your account</h2>
+            <h2 class="h3">{% if identity_linking %}Identity Linking{% else %}Log in to your account{% endif %}</h2>
+
+            {% if not identity_linking %}
+            <div class="oauth2-container" id="oauth2-container">
+                <div class="oauth2-buttons" id="oauth2-buttons">
+                </div>
+                <div class="oauth2-divider" id="oauth2-divider">
+                    <span class="divider-line"></span>
+                    <span class="divider-text">or</span>
+                    <span class="divider-line"></span>
+                </div>
+            </div>
+            {% endif %}
+
             <form method="post" action="{% url 'user_login' %}">
                 {% csrf_token %}
                 {% if form.errors %}
-                {% for error in form.non_field_errors %} 
+                {% if identity_linking_error %}
+                <div class="alert alert-danger">
+                    <strong>{{ identity_linking_error|escape }}</strong>
+                </div>
+                {% endif %}
+                {% for error in form.non_field_errors %}
                 <div class="alert alert-danger">
                     <strong>{{ error|escape }}</strong>
                 </div>
@@ -27,6 +45,9 @@ <h2 class="h3">Log in to your account</h2>
                 </div>
                 <button class="btn btn-primary btn-lg btn-block" id="commit" type="submit">Log In</button>
                 <input type="hidden" name="next" value={{ next }}>
+                {% if identity_linking %}
+                <input type="hidden" name="identity_linking" value="1">
+                {% endif %}
             </form>
             {% if registration_enabled %}
             <a class="panel-footer" href="/user/registration/">New to Drycc?  <span>Sign Up</span></a>
@@ -39,4 +60,29 @@ <h2 class="h3">Log in to your account</h2>
         {% endif %}
     </div>
     {% include "base/footer.html" %}
-</div>
\ No newline at end of file
+</div>
+<script>
+{% if not identity_linking %}
+fetch('/user/identity-providers/')
+    .then(response => response.json())
+    .then(data => {
+        const oauth2Container = document.getElementById('oauth2-container');
+        const oauth2Buttons = document.getElementById('oauth2-buttons');
+        const oauth2Divider = document.getElementById('oauth2-divider');
+
+        if (data.results && data.results.length > 0) {
+            oauth2Buttons.innerHTML = data.results.map(provider => `
+                <a href="${provider.login_url}" class="oauth2-btn">
+                    <span class="oauth2-icon">${provider.icon}</span>
+                    <strong>Sign in with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}</strong>
+                </a>
+            `).join('');
+        } else {
+            oauth2Container.style.display = 'none';
+        }
+    })
+    .catch(error => {
+        console.error('Error fetching OAuth2 providers:', error);
+    });
+{% endif %}
+</script>
diff --git a/rootfs/api/templates/user/oauth_callback.html b/rootfs/api/templates/user/oauth_callback.html
new file mode 100644
index 0000000..39df210
--- /dev/null
+++ b/rootfs/api/templates/user/oauth_callback.html
@@ -0,0 +1,91 @@
+{% load static %}
+<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
+
+<head>
+    <meta charset="UTF-8">
+    <title>Identity Linking</title>
+</head>
+
+<div class="page-wrap gradient-primary">
+    <div class="container">
+        <div class="panel">
+            <h2 class="h3">Identity Linking</h2>
+
+            {% if status == 'pending' %}
+            <form method="post">
+                {% csrf_token %}
+                <div class="form-group">
+                    <p class="gray-90">
+                        This {{ display_name|default:'OAuth' }} identity is not linked yet. Create a new account to continue.
+                    </p>
+                </div>
+                {% if error %}
+                <div class="form-group">
+                    <div class="error">{{ error }}</div>
+                </div>
+                {% endif %}
+                <div class="form-group">
+                    <label for="oauth-username">Username</label>
+                    <input autofocus="off" class="form-control" id="oauth-username" name="username" placeholder="Username" type="text" required>
+                </div>
+                <div class="form-group">
+                    <label for="oauth-password">Password</label>
+                    <input class="form-control" id="oauth-password" name="password" placeholder="Password" type="password" required>
+                </div>
+                <div class="form-group">
+                    <label for="oauth-confirm-password">Confirm Password</label>
+                    <input class="form-control" id="oauth-confirm-password" name="confirm_password" placeholder="Confirm password" type="password" required>
+                </div>
+                <div class="form-group">
+                    <label for="oauth-email">Email (from provider)</label>
+                    <input autofocus="off" class="form-control" id="oauth-email"
+                           :placeholder="Email" type="email" disabled value="{{ email }}">
+                </div>
+                <button class="btn btn-primary btn-lg btn-block" type="submit">Create and Link</button>
+            </form>
+            <a class="panel-footer" href="/user/login/?identity_linking=1">I already have an account</a>
+
+            {% elif status == 'linked' %}
+            <form>
+                <div class="form-group">
+                    <p class="gray-90">Identity linked successfully.</p>
+                </div>
+                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/account-setting'">Go to Account Settings</button>
+            </form>
+
+            {% elif status == 'already_linked' %}
+            <form>
+                <div class="form-group">
+                    <p class="gray-90">This identity is already linked to your account.</p>
+                </div>
+                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/account-setting'">Go to Account Settings</button>
+            </form>
+
+            {% elif status == 'conflict' %}
+            <form>
+                <div class="form-group">
+                    <div class="error">This identity is linked to another account.</div>
+                </div>
+                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Login with another account</button>
+            </form>
+
+            {% elif status == 'missing_email' %}
+            <form>
+                <div class="form-group">
+                    <div class="error">The provider did not supply an email address.</div>
+                </div>
+                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Back to Login</button>
+            </form>
+
+            {% else %}
+            <form>
+                <div class="form-group">
+                    <div class="error">OAuth flow error. Please try again.</div>
+                </div>
+                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Back to Login</button>
+            </form>
+            {% endif %}
+        </div>
+    </div>
+    {% include "base/footer.html" %}
+</div>
\ No newline at end of file
diff --git a/rootfs/api/tests/__init__.py b/rootfs/api/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rootfs/api/tests/test_views.py b/rootfs/api/tests/test_views.py
new file mode 100644
index 0000000..91fe633
--- /dev/null
+++ b/rootfs/api/tests/test_views.py
@@ -0,0 +1,163 @@
+"""
+Test cases for API views
+"""
+from django.test import TestCase, RequestFactory
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+from rest_framework.test import APITestCase
+from rest_framework import status
+from oauth2_provider.models import AccessToken
+
+from api.views import (
+    ReadinessCheckView, LivenessCheckView, ActivateAccount
+)
+from api.forms import RegistrationForm
+
+User = get_user_model()
+
+
+class TestReadinessCheckView(TestCase):
+    """Test readiness check view"""
+
+    def test_readiness_check_get(self):
+        request = RequestFactory().get('/readiness/')
+        response = ReadinessCheckView.as_view()(request)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content.decode(), 'OK')
+
+    def test_readiness_check_head(self):
+        request = RequestFactory().head('/readiness/')
+        response = ReadinessCheckView.as_view()(request)
+        self.assertEqual(response.status_code, 200)
+
+
+class TestLivenessCheckView(TestCase):
+    """Test liveness check view"""
+
+    def test_liveness_check_get(self):
+        request = RequestFactory().get('/liveness/')
+        response = LivenessCheckView.as_view()(request)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content.decode(), 'OK')
+
+    def test_liveness_check_head(self):
+        request = RequestFactory().head('/liveness/')
+        response = LivenessCheckView.as_view()(request)
+        self.assertEqual(response.status_code, 200)
+
+
+class TestRegistrationView(TestCase):
+    """Test registration view"""
+
+    def setUp(self):
+        self.factory = RequestFactory()
+
+    def test_registration_form_valid(self):
+        form_data = {
+            'username': 'testuser',
+            'email': 'test@example.com',
+            'password1': 'testpassword123',
+            'password2': 'testpassword123'
+        }
+        form = RegistrationForm(data=form_data)
+        self.assertTrue(form.is_valid())
+
+
+class TestActivateAccount(TestCase):
+    """Test account activation view"""
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='testuser',
+            email='test@example.com',
+            password='testpass123',
+            is_active=False
+        )
+        self.factory = RequestFactory()
+
+    def test_activate_account_invalid_token(self):
+        from django.utils.http import urlsafe_base64_encode
+        from django.utils.encoding import force_bytes
+        from django.contrib.messages.storage.fallback import FallbackStorage
+
+        uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+        request = self.factory.get(f'/activate/{uid}/invalid-token/')
+        setattr(request, 'session', 'session')
+        messages = FallbackStorage(request)
+        setattr(request, '_messages', messages)
+        response = ActivateAccount.as_view()(request, uidb64=uid, token='invalid-token')
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response.url, '/user/activate/fail/')
+
+
+class TestUserDetailView(APITestCase):
+    """Test user detail viewset"""
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='testuser',
+            email='test@example.com',
+            password='testpass123'
+        )
+        self.client.force_authenticate(user=self.user)
+
+    def test_get_user_detail(self):
+        url = '/user/info'
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['username'], 'testuser')
+
+
+class TestUserTokensView(APITestCase):
+    """Test user tokens viewset"""
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='testuser',
+            email='test@example.com',
+            password='testpass123'
+        )
+        self.client.force_authenticate(user=self.user)
+        self.token = AccessToken.objects.create(
+            user=self.user,
+            token='tokentest',
+            application=None,
+            expires='9999-01-01T00:00:00Z',
+            scope='read write'
+        )
+
+    def test_list_user_tokens(self):
+        url = reverse('user_tokens')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_delete_user_token(self):
+        url = reverse('user_grants', args=[self.token.id])
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        self.assertFalse(AccessToken.objects.filter(id=self.token.id).exists())
+
+
+class TestUserAccountPasswordView(APITestCase):
+    """Test user account password view"""
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='testuser',
+            email='test@example.com',
+            password='oldpassword123'
+        )
+        self.client.force_authenticate(user=self.user)
+
+    def test_update_password_valid(self):
+        url = '/user/password'
+        data = {
+            'password': 'oldpassword123',
+            'new_password': 'newpassword123'
+        }
+        response = self.client.put(url, data)
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+        self.user.refresh_from_db()
+        self.assertTrue(self.user.check_password('newpassword123'))
diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py
index eabd62d..5f1b21b 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -1,50 +1,65 @@
 from django.urls import re_path, include
 from rest_framework.routers import DefaultRouter
 
-from api import views
+from api.views import web, api
+from passport.views import SettingsViewSet
 
 router = DefaultRouter(trailing_slash=False)
 
 urlpatterns = [
     re_path(r'^', include(router.urls)),
-    re_path(r'^info/?$',
-            views.UserDetailView.as_view({'get': 'retrieve', 'put': 'update'})),
-    re_path(r'update/(?P<uidb64>.+)/(?P<token>.+)/?$',
-            views.UpdateAccount.as_view(), name='user_update_account'),
-    re_path(r'^avatar/(?P<username>[-_\w]+)/?$',
-            views.UserAvatarViewSet.as_view({'get': 'avatar'})),
-    re_path(r'registration/?$', views.RegistrationView.as_view(), name='registration'),
-    re_path(r'activate/(?P<uidb64>.+)/(?P<token>.+)/?$',
-            views.ActivateAccount.as_view(), name='user_activate_account'),
-    re_path(r'registration/done/?$', views.RegistrationDoneView.as_view(),
+    # Settings URL (moved from main urls.py)
+    re_path(r'^settings/?$', SettingsViewSet.as_view({'get': 'retrieve'})),
+    # User URLs (add user prefix)
+    re_path(r'^user/info/?$',
+            api.UserDetailView.as_view({'get': 'retrieve', 'put': 'update'})),
+    re_path(r'^user/update/(?P<uidb64>.+)/(?P<token>.+)/?$',
+            web.UpdateAccount.as_view(), name='user_update_account'),
+    re_path(r'^user/avatar/(?P<username>[-_\w]+)/?$',
+            api.UserAvatarViewSet.as_view({'get': 'avatar'})),
+    re_path(r'^user/registration/?$', web.RegistrationView.as_view(), name='registration'),
+    re_path(r'^user/activate/(?P<uidb64>.+)/(?P<token>.+)/?$',
+            web.ActivateAccount.as_view(), name='user_activate_account'),
+    re_path(r'^user/registration/done/?$', web.RegistrationDoneView.as_view(),
             name='user_registration_done'),
-    re_path(r'activate/done/?$', views.ActivateAccountDoneView.as_view(),
+    re_path(r'^user/activate/done/?$', web.ActivateAccountDoneView.as_view(),
             name='user_activate_account_done'),
-    re_path(r'activate/fail/?$', views.ActivateAccountFailView.as_view(),
+    re_path(r'^user/activate/fail/?$', web.ActivateAccountFailView.as_view(),
             name='user_activate_account_done'),
-    re_path(r'password_reset/?$', views.UserPasswordResetView.as_view(),
+    re_path(r'^oauth/callback/?$', web.OAuthCallbackTemplateView.as_view(),
+            name='oauth_callback'),
+    re_path(r'^user/password_reset/?$', web.UserPasswordResetView.as_view(),
             name='user_password_reset'),
-    re_path(r'password_reset/done/?$',
-            views.UserPasswordResetDoneView.as_view(),
+    re_path(r'^user/password_reset/done/?$',
+            web.UserPasswordResetDoneView.as_view(),
             name='user_password_reset_done'),
-    re_path(r'reset/(?P<uidb64>.+)/(?P<token>.+)/?$',
-            views.UserPasswordResetConfirmView.as_view(),
+    re_path(r'^user/reset/(?P<uidb64>.+)/(?P<token>.+)/?$',
+            web.UserPasswordResetConfirmView.as_view(),
             name='user_password_reset_confirm'),
-    re_path(r'reset/done/?$',
-            views.UserPasswordResetCompleteView.as_view(),
+    re_path(r'^user/reset/done/?$',
+            web.UserPasswordResetCompleteView.as_view(),
             name='user_password_reset_complete'),
-    re_path(r'login/?$', views.UserLoginView.as_view(), name='user_login'),
-    re_path(r'login/done/?$', views.LoginDoneView.as_view(), name='login_done'),
-    re_path(r'logout/?$', views.UserLogoutView.as_view(), name='user_logout'),
-
-    re_path(r'tokens/?$',
-            views.UserTokensView.as_view({'get': 'list'}),
+    re_path(r'^user/login/?$', web.UserLoginView.as_view(), name='user_login'),
+    re_path(r'^user/login/done/?$', web.LoginDoneView.as_view(), name='login_done'),
+    re_path(r'^user/logout/?$', web.UserLogoutView.as_view(), name='user_logout'),
+    re_path(r'^user/tokens/?$',
+            api.UserTokensView.as_view({'get': 'list'}),
             name='user_tokens'),
-    re_path(r'tokens/(?P<pk>.+)/?$',
-            views.UserTokensView.as_view({'delete': 'destroy'}),
+    re_path(r'^user/tokens/(?P<pk>.+)/?$',
+            api.UserTokensView.as_view({'delete': 'destroy'}),
             name='user_grants'),
-    re_path(r'email/?$', views.UserEmailView.as_view({'get': 'retrieve'})),
-    re_path(r'password/?$',
-            views.UserAccountPasswordView.as_view({'put': 'update'}),
+    re_path(r'^user/email/?$', api.UserEmailView.as_view({'get': 'retrieve'})),
+    re_path(r'^user/password/?$',
+            api.UserAccountPasswordView.as_view({'put': 'update'}),
             name='user_account_update_password'),
+    re_path(r'^user/identity-providers/?$',
+            api.IdentityProviderView.as_view(), name='user_identity_providers'),
+    re_path(r'^user/identities/?$',
+            api.UserIdentityView.as_view(), name='user_identities'),
+    re_path(r'^user/identities/(?P<identity_id>\d+)/?$',
+            api.UserIdentityView.as_view(), name='user_identity_detail'),
+    re_path(r'^user/oauth/pending/?$',
+            api.OAuthPendingView.as_view(), name='user_oauth_pending'),
+    re_path(r'^user/oauth/create/?$',
+            api.OAuthCreateUserView.as_view(), name='user_oauth_create'),
 ]
diff --git a/rootfs/api/utils.py b/rootfs/api/utils.py
index b76040c..26b2a98 100644
--- a/rootfs/api/utils.py
+++ b/rootfs/api/utils.py
@@ -1,15 +1,21 @@
 """
 Helper functions used by the Drycc Passport server.
 """
+import re
 import logging
 import datetime
 
+from urllib.parse import urlencode
+from django.conf import settings
+
 from django.utils.encoding import force_bytes
 from django.template.loader import render_to_string
 from django.utils.http import urlsafe_base64_encode
 from django.contrib.auth.tokens import PasswordResetTokenGenerator
 from django.shortcuts import render
 
+from rest_framework.exceptions import ValidationError
+
 logger = logging.getLogger(__name__)
 
 
@@ -47,6 +53,44 @@ def get_local_host(request):
     return uri[0:uri.find(request.path)]
 
 
+def get_user_socials(user):
+    from api.apps_extra.social_core import backends
+    provider_map = {
+        backend_cls.name: backend_cls
+        for backend_cls in backends.__all__
+    }
+    results = []
+    for social in user.social_auth.all():
+        backend_cls = provider_map.get(social.provider)
+        email = None
+        if social.extra_data:
+            email = social.extra_data.get('email') or social.extra_data.get('user_email')
+        results.append({
+            'id': social.id,
+            'provider': social.provider,
+            'display_name': social.provider.title(),
+            'uid': social.uid,
+            'icon': backend_cls.icon if backend_cls else '',
+            'email': email or user.email,
+        })
+    return results
+
+
+def get_oauth_callback(request, status, provider=None):
+    query = {'status': status}
+    domain = get_local_host(request)
+    if provider:
+        query['provider'] = provider
+    return f"{domain}{settings.SOCIAL_AUTH_LOGIN_CALLBACK_URL}?{urlencode(query)}"
+
+
+def validate_reserved_names(value):
+    """A value cannot use some reserved names."""
+    for reserved_name_pattern in settings.RESERVED_NAME_PATTERNS:
+        if re.match(reserved_name_pattern, value):
+            raise ValidationError('{} is a reserved name.'.format(value))
+
+
 def send_activation_email(request, user):
     domain = get_local_host(request)
     mail_subject = 'Activate your account.'
diff --git a/rootfs/api/validators.py b/rootfs/api/validators.py
index e442287..b05be8a 100644
--- a/rootfs/api/validators.py
+++ b/rootfs/api/validators.py
@@ -1,20 +1,19 @@
-from django.contrib.auth import validators
+
+from django.contrib.auth.validators import UnicodeUsernameValidator
 from django.utils.translation import gettext_lazy as _
 from django.conf import settings
-from django.core.exceptions import ValidationError
+from django.utils.deconstruct import deconstructible
+
+from api.utils import validate_reserved_names
 
 
-class UsernameValidator(validators.UnicodeUsernameValidator):
+@deconstructible
+class UsernameValidator(UnicodeUsernameValidator):
     regex = settings.USERNAME_REGEX
     message = _(
         f"Enter a valid username. This value may match the regex {regex}."
     )
 
     def __call__(self, value):
-        if value in settings.RESERVED_USERNAMES:
-            raise ValidationError(
-                _("The current username is on the blocklist."),
-                code=self.code,
-                params={"value": value}
-            )
         super().__call__(value)
+        validate_reserved_names(value)
diff --git a/rootfs/api/views.py b/rootfs/api/views.py
deleted file mode 100644
index ac5ce61..0000000
--- a/rootfs/api/views.py
+++ /dev/null
@@ -1,295 +0,0 @@
-import logging
-import hashlib
-from django.conf import settings
-from django.core.cache import cache
-from django.utils.decorators import method_decorator
-from django.views.decorators.cache import cache_page
-from django.contrib import messages, auth
-from django.contrib.auth import login
-from django.contrib.auth import views
-from django.contrib.auth import get_user_model
-from django.db.models import Q
-from django.shortcuts import redirect, get_object_or_404, render
-from django.template.loader import render_to_string
-from django.http import HttpResponse, HttpResponseRedirect
-from django.utils.encoding import force_bytes, force_str
-from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
-from django.views.generic import View
-from django.views.generic.edit import CreateView
-from django.utils.translation import gettext_lazy as _
-from django.views.generic.base import TemplateView
-from django.urls import reverse_lazy
-from rest_framework import status
-from rest_framework.exceptions import AuthenticationFailed
-from rest_framework.response import Response
-from rest_framework.viewsets import ModelViewSet
-from oauth2_provider.models import AccessToken
-
-from api import serializers
-from api.forms import AuthenticationForm, RegistrationForm
-from api.exceptions import ServiceUnavailable, DryccException
-from api.utils import token_generator, get_local_host, send_activation_email
-from api.viewset import NormalUserViewSet
-
-User = get_user_model()
-logger = logging.getLogger(__name__)
-
-
-class ReadinessCheckView(View):
-    """
-    Simple readiness check view to determine DB connection / query.
-    """
-
-    def get(self, request):
-        try:
-            import django.db
-            with django.db.connection.cursor() as c:
-                c.execute("SELECT 0")
-        except django.db.Error as e:
-            raise ServiceUnavailable("Database health check failed") from e
-
-        return HttpResponse("OK")
-
-    head = get
-
-
-class LivenessCheckView(View):
-    """
-    Simple liveness check view to determine if the server
-    is responding to HTTP requests.
-    """
-
-    def get(self, request):
-        return HttpResponse("OK")
-
-    head = get
-
-
-class RegistrationView(CreateView):
-    form_class = RegistrationForm
-    template_name = 'user/registration.html'
-    success_url = reverse_lazy('user_registration_done')
-
-    def get(self, request, *args, **kwargs):
-        if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
-            return render(request, template_name='user/registration_disable.html')
-        return super().get(self, request, *args, **kwargs)
-
-    def post(self, request, *args, **kwargs):
-        if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
-            return render(request, template_name='user/registration_disable.html')
-        form = self.form_class(request.POST)
-        self.object = None
-        if form.is_valid():
-            user = form.save(commit=False)
-            if settings.EMAIL_HOST:
-                user.is_active = False
-                user.save()
-                send_activation_email(request, user)
-                messages.success(request, (
-                    'Please Confirm your email to complete registration.'))
-            else:
-                user.is_active = True
-                user.save()
-            return self.form_valid(form)
-        else:
-            return self.form_invalid(form)
-
-    def get_context_data(self, **kwargs):
-        context = super().get_context_data(**kwargs)
-        context["h_captcha_key"] = settings.H_CAPTCHA_KEY
-        return context
-
-
-class RegistrationDoneView(TemplateView):
-    template_name = 'user/registration_done.html'
-    title = _('Activate email sent')
-
-
-class ActivateAccount(View):
-
-    def get(self, request, uidb64, token, *args, **kwargs):
-        try:
-            uid = force_str(urlsafe_base64_decode(uidb64))
-            user = User.objects.get(pk=uid)
-        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
-            user = None
-        if user is not None and token_generator.check_token(
-                user, token):
-            user.is_active = True
-            user.save()
-            login(request, user, backend='django.contrib.auth.backends.ModelBackend')
-            messages.success(request, 'Your account have been confirmed.')
-            return redirect('/user/activate/done/')
-        else:
-            messages.warning(request, (
-                'The confirmation link was invalid, possibly because it has already been used.'))  # noqa
-            return redirect('/user/activate/fail/')
-
-
-class ActivateAccountDoneView(TemplateView):
-    template_name = 'user/account_activation_done.html'
-    title = _('Activate account done')
-
-
-class ActivateAccountFailView(TemplateView):
-    template_name = 'user/account_activation_fail.html'
-    title = _('Activate account fail')
-
-
-class UserLoginView(views.LoginView):
-    form_class = AuthenticationForm
-    extra_context = {
-        "registration_enabled": settings.REGISTRATION_ENABLED,
-        "password_reset_enabled": True if settings.EMAIL_HOST else False,
-    }
-    template_name = 'user/login.html'
-
-
-class UserDetailView(NormalUserViewSet):
-    serializer_class = serializers.UserSerializer
-    required_scopes = ['openid']
-
-    def get_object(self):
-        return self.request.user
-
-    def update(self, request, *args, **kwargs):
-        user_serializer = self.serializer_class(
-            data=request.data,
-            instance=request.user,
-            partial=True
-        )
-        if user_serializer.is_valid():
-            if settings.EMAIL_HOST:
-                user = self.get_object()
-                mail_subject = 'Update your account.'
-                uid = urlsafe_base64_encode(force_bytes(user.pk))
-                token = token_generator.make_token(user)
-                message = render_to_string(
-                    'user/account_update_email.html', {
-                        'uid': uid,
-                        'user': user,
-                        'token': token,
-                        'domain': get_local_host(request)
-                    })
-                cache_key = "user:serializer:%s" % user.pk
-                cache.set(cache_key, request.data, 60 * 30)
-                user.email_user(mail_subject, message, fail_silently=True)
-            else:
-                user_serializer.save()
-        return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class UpdateAccount(View):
-
-    def get(self, request, uidb64, token, *args, **kwargs):
-        user = get_object_or_404(User, pk=force_str(urlsafe_base64_decode(uidb64)))
-        if user is not None and token_generator.check_token(user, token):
-            cache_key = "user:serializer:%s" % user.pk
-            data = cache.get(cache_key, None)
-            if data:
-                user_serializer = serializers.UserSerializer(
-                    data=data, instance=user, partial=True)
-                if user_serializer.is_valid():
-                    user_serializer.save()
-                    login(request, user, backend='django.contrib.auth.backends.ModelBackend')
-                    cache.delete(cache_key)
-                    return render(request, template_name='user/account_update_done.html')
-        return render(request, template_name='user/account_update_fail.html')
-
-
-class UserAvatarViewSet(NormalUserViewSet):
-
-    @method_decorator(cache_page(60 * 5))
-    def avatar(self, request, *args, **kwargs):
-        user = User.objects.filter(username=kwargs["username"]).first()
-        size = request.GET.get("s", "80")
-        md5 = hashlib.md5()
-        if user:
-            md5.update(user.email.encode("utf8"))
-        return HttpResponseRedirect(settings.AVATAR_URL + md5.hexdigest() + "?s=" + size)
-
-
-class UserEmailView(NormalUserViewSet):
-    serializer_class = serializers.UserEmailSerializer
-    required_scopes = ['openid']
-
-    def get_object(self):
-        return self.request.user
-
-
-class LoginDoneView(TemplateView):
-    template_name = 'user/login_done.html'
-
-
-class UserPasswordResetView(views.PasswordResetView):
-    email_template_name = 'user/password_reset_email.html'
-    success_url = reverse_lazy('user_password_reset_done')
-    template_name = 'user/password_reset_form.html'
-
-
-class UserPasswordResetDoneView(views.PasswordResetDoneView):
-    template_name = 'user/password_reset_done.html'
-
-
-class UserPasswordResetConfirmView(views.PasswordResetConfirmView):
-    success_url = reverse_lazy('user_password_reset_complete')
-    template_name = 'user/password_reset_confirm.html'
-
-
-class UserPasswordResetCompleteView(views.PasswordResetCompleteView):
-    template_name = 'user/password_reset_complete.html'
-
-
-class UserLogoutView(views.LogoutView):
-    template_name = 'user/logout.html'
-
-
-class ListViewSet(ModelViewSet):
-
-    def get_queryset(self, *args, **kwargs):
-        serializer = self.serializer_class(data=self.request.query_params)
-        serializer.is_valid(raise_exception=True)
-
-        serializerlist = serializers.ListSerializer(
-            data=self.request.query_params)
-        serializerlist.is_valid(raise_exception=True)
-        q = Q(user=self.request.user)
-        if serializerlist.validated_data.get('section'):
-            q &= Q(created__range=serializerlist.validated_data.get('section'))
-        return self.model.objects.filter(
-            q, **serializer.validated_data).order_by(self.order_by)[0:100]
-
-
-class UserTokensView(ListViewSet):
-    model = AccessToken
-    serializer_class = serializers.UserTokensSerializer
-    order_by = '-created'
-
-    def destroy(self, request, *args, **kwargs):
-        token = get_object_or_404(self.model,
-                                  id=self.kwargs['pk'],
-                                  user=request.user)
-
-        token.delete()
-        return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class UserAccountPasswordView(ListViewSet):
-
-    def update(self, request, *args, **kwargs):
-        if settings.LDAP_ENDPOINT:
-            raise DryccException(
-                "You cannot change user info when ldap is enabled.")
-        if not request.data.get('new_password'):
-            raise DryccException("new_password is a required field")
-        if not request.data.get('password'):
-            raise DryccException("password is a required field")
-        if len(request.data.get('new_password')) < 8:
-            raise DryccException("password must be 8 or more characters. ")
-        if not request.user.check_password(request.data['password']):
-            raise AuthenticationFailed('Current password does not match')
-        request.user.set_password(request.data['new_password'])
-        request.user.save()
-        auth.logout(request)
-        return HttpResponse(status=204)
diff --git a/rootfs/api/views/__init__.py b/rootfs/api/views/__init__.py
new file mode 100644
index 0000000..5d77f0a
--- /dev/null
+++ b/rootfs/api/views/__init__.py
@@ -0,0 +1,2 @@
+from .api import *  # noqa
+from .web import *  # noqa
diff --git a/rootfs/api/views/api.py b/rootfs/api/views/api.py
new file mode 100644
index 0000000..dd84dca
--- /dev/null
+++ b/rootfs/api/views/api.py
@@ -0,0 +1,278 @@
+import hashlib
+
+from django.conf import settings
+from django.core.cache import cache
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
+from django.contrib import auth
+from django.contrib.auth import get_user_model
+from django.db import IntegrityError
+from django.db.models import Q
+from django.shortcuts import get_object_or_404
+from django.template.loader import render_to_string
+from django.http import HttpResponse, HttpResponseRedirect
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
+from django.urls import reverse_lazy
+from django.views.generic import View
+
+from rest_framework import status, viewsets
+from rest_framework.exceptions import AuthenticationFailed, ValidationError
+from rest_framework.permissions import AllowAny, IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework.viewsets import ModelViewSet
+
+from oauth2_provider.models import AccessToken
+from social_django.models import UserSocialAuth
+
+from api import serializers
+from api.exceptions import ServiceUnavailable, DryccException
+from api.utils import get_local_host, get_user_socials, token_generator
+
+
+User = get_user_model()
+
+
+class NormalUserViewSet(viewsets.ModelViewSet):
+    permission_classes = [IsAuthenticated]
+
+
+class ReadinessCheckView(View):
+    """Simple readiness check view to determine DB connection / query."""
+
+    def get(self, request):
+        try:
+            import django.db
+            with django.db.connection.cursor() as c:
+                c.execute("SELECT 0")
+        except django.db.Error as e:
+            raise ServiceUnavailable("Database health check failed") from e
+
+        return HttpResponse("OK")
+
+    head = get
+
+
+class LivenessCheckView(View):
+    """Simple liveness check view to determine if the server is alive."""
+
+    def get(self, request):
+        return HttpResponse("OK")
+
+    head = get
+
+
+class IdentityProviderView(APIView):
+    permission_classes = (AllowAny, )
+
+    def get(self, request, *args, **kwargs):
+        from api.apps_extra.social_core import backends
+
+        results = []
+        for backend_cls in backends.__all__:
+            key_setting = f'SOCIAL_AUTH_{backend_cls.name.upper()}_KEY'
+            secret_setting = f'SOCIAL_AUTH_{backend_cls.name.upper()}_SECRET'
+            if getattr(settings, key_setting, None) and getattr(settings, secret_setting, None):
+                results.append({
+                    'name': backend_cls.name,
+                    'icon': backend_cls.icon,
+                    'login_url': reverse_lazy('social:begin', args=(backend_cls.name,)),
+                })
+
+        return Response({'count': len(results), 'results': results})
+
+
+class UserIdentityView(APIView):
+
+    def get(self, request, *args, **kwargs):
+        results = get_user_socials(request.user)
+        return Response({'count': len(results), 'results': results})
+
+    def delete(self, request, identity_id, *args, **kwargs):
+        identity = get_object_or_404(UserSocialAuth, id=identity_id, user=request.user)
+        identity.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class OAuthPendingView(APIView):
+    permission_classes = (AllowAny, )
+
+    def get(self, request, *args, **kwargs):
+        pending = request.session.get('oauth_pending')
+        if not pending:
+            return Response(
+                {'detail': 'No pending OAuth request'}, status=status.HTTP_404_NOT_FOUND
+            )
+
+        from api.apps_extra.social_core import backends
+
+        backend_map = {backend_cls.name: backend_cls for backend_cls in backends.__all__}
+        backend_cls = backend_map.get(pending.get('provider'))
+        return Response({
+            'provider': pending.get('provider'),
+            'email': pending.get('email'),
+            'icon': backend_cls.icon if backend_cls else '',
+            'display_name': pending.get('provider', '').title(),
+        })
+
+
+class OAuthCreateUserView(APIView):
+    permission_classes = (AllowAny, )
+
+    def _create_oauth_user(self, request, username, password):
+        pending = request.session.get('oauth_pending')
+        if not pending:
+            raise ValidationError('No pending OAuth request')
+
+        if not username:
+            raise ValidationError('username is a required field')
+        if not password:
+            raise ValidationError('password is a required field')
+        if len(password) < 8:
+            raise ValidationError('password must be 8 or more characters')
+
+        email = pending.get('email')
+        if not email:
+            raise ValidationError('email is missing from oauth provider')
+        if User.objects.filter(email=email).exists():
+            raise ValidationError('email is already registered')
+
+        provider = pending.get('provider')
+        uid = pending.get('uid')
+        extra_data = pending.get('extra_data') or {}
+
+        if not provider or not uid:
+            request.session.pop('oauth_pending', None)
+            raise ValidationError('invalid oauth pending data')
+
+        existing = UserSocialAuth.objects.filter(provider=provider, uid=uid).first()
+        if existing:
+            request.session.pop('oauth_pending', None)
+            raise ValidationError('identity is already linked to another account')
+
+        try:
+            user = User.objects.create_user(username=username, email=email, password=password)
+        except IntegrityError:
+            raise ValidationError('username is already taken')
+
+        UserSocialAuth.objects.create(
+            user=user,
+            provider=provider,
+            uid=uid,
+            extra_data=extra_data,
+        )
+
+        auth.login(request, user, backend='django.contrib.auth.backends.ModelBackend')
+        request.session.pop('oauth_pending', None)
+        return user
+
+    def post(self, request, *args, **kwargs):
+        username = request.data.get('username')
+        password = request.data.get('password')
+        self._create_oauth_user(request, username, password)
+        return Response({'status': 'created'}, status=status.HTTP_201_CREATED)
+
+
+class UserDetailView(NormalUserViewSet):
+    serializer_class = serializers.UserSerializer
+    required_scopes = ['openid']
+    email_template_name = 'user/account_update_email.html'
+
+    def get_object(self):
+        return self.request.user
+
+    def update(self, request, *args, **kwargs):
+        user_serializer = self.serializer_class(
+            data=request.data,
+            instance=request.user,
+            partial=True,
+        )
+        if user_serializer.is_valid():
+            if settings.EMAIL_HOST:
+                user = self.get_object()
+                mail_subject = 'Update your account.'
+                uid = urlsafe_base64_encode(force_bytes(user.pk))
+                token = token_generator.make_token(user)
+                message = render_to_string(
+                    self.email_template_name,
+                    {
+                        'uid': uid,
+                        'user': user,
+                        'token': token,
+                        'domain': get_local_host(request),
+                    },
+                )
+                cache_key = "user:serializer:%s" % user.pk
+                cache.set(cache_key, request.data, 60 * 30)
+                user.email_user(mail_subject, message, fail_silently=True)
+            else:
+                user_serializer.save()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class UserAvatarViewSet(NormalUserViewSet):
+
+    @method_decorator(cache_page(60 * 5))
+    def avatar(self, request, *args, **kwargs):
+        user = User.objects.filter(username=kwargs["username"]).first()
+        size = request.GET.get("s", "80")
+        md5 = hashlib.md5()
+        if user:
+            md5.update(user.email.encode("utf8"))
+        return HttpResponseRedirect(settings.AVATAR_URL + md5.hexdigest() + "?s=" + size)
+
+
+class UserEmailView(NormalUserViewSet):
+    serializer_class = serializers.UserEmailSerializer
+    required_scopes = ['openid']
+
+    def get_object(self):
+        return self.request.user
+
+
+class ListViewSet(ModelViewSet):
+
+    def get_queryset(self, *args, **kwargs):
+        serializer = self.serializer_class(data=self.request.query_params)
+        serializer.is_valid(raise_exception=True)
+
+        serializerlist = serializers.ListSerializer(data=self.request.query_params)
+        serializerlist.is_valid(raise_exception=True)
+        q = Q(user=self.request.user)
+        if serializerlist.validated_data.get('section'):
+            q &= Q(created__range=serializerlist.validated_data.get('section'))
+
+        return self.model.objects.filter(
+            q, **serializer.validated_data
+        ).order_by(self.order_by)[0:100]
+
+
+class UserTokensView(ListViewSet):
+    model = AccessToken
+    serializer_class = serializers.UserTokensSerializer
+    order_by = '-created'
+
+    def destroy(self, request, *args, **kwargs):
+        token = get_object_or_404(self.model, id=self.kwargs['pk'], user=request.user)
+        token.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class UserAccountPasswordView(ListViewSet):
+
+    def update(self, request, *args, **kwargs):
+        if settings.LDAP_ENDPOINT:
+            raise DryccException("You cannot change user info when ldap is enabled.")
+        if not request.data.get('new_password'):
+            raise DryccException("new_password is a required field")
+        if not request.data.get('password'):
+            raise DryccException("password is a required field")
+        if len(request.data.get('new_password')) < 8:
+            raise DryccException("password must be 8 or more characters. ")
+        if not request.user.check_password(request.data['password']):
+            raise AuthenticationFailed('Current password does not match')
+        request.user.set_password(request.data['new_password'])
+        request.user.save()
+        auth.logout(request)
+        return HttpResponse(status=204)
diff --git a/rootfs/api/views/web.py b/rootfs/api/views/web.py
new file mode 100644
index 0000000..952f29c
--- /dev/null
+++ b/rootfs/api/views/web.py
@@ -0,0 +1,259 @@
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth import get_user_model, login, views
+from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.encoding import force_str
+from django.utils.http import urlsafe_base64_decode
+from django.views.generic import View
+from django.views.generic.base import TemplateView
+from django.views.generic.edit import CreateView
+from django.urls import reverse_lazy
+from django.utils.translation import gettext_lazy as _
+
+from rest_framework.exceptions import ValidationError
+from social_django.models import UserSocialAuth
+
+from api import serializers
+from api.forms import AuthenticationForm, RegistrationForm
+from api.utils import get_oauth_callback, send_activation_email, token_generator
+
+
+User = get_user_model()
+
+
+class RegistrationView(CreateView):
+    form_class = RegistrationForm
+    template_name = 'user/registration.html'
+    success_url = reverse_lazy('user_registration_done')
+
+    def get(self, request, *args, **kwargs):
+        if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
+            return render(request, template_name='user/registration_disable.html')
+        return super().get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
+            return render(request, template_name='user/registration_disable.html')
+        form = self.form_class(request.POST)
+        self.object = None
+        if form.is_valid():
+            user = form.save(commit=False)
+            if settings.EMAIL_HOST:
+                user.is_active = False
+                user.save()
+                send_activation_email(request, user)
+                messages.success(request, 'Please Confirm your email to complete registration.')
+            else:
+                user.is_active = True
+                user.save()
+            return self.form_valid(form)
+        return self.form_invalid(form)
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context["h_captcha_key"] = settings.H_CAPTCHA_KEY
+        return context
+
+
+class RegistrationDoneView(TemplateView):
+    template_name = 'user/registration_done.html'
+    title = _('Activate email sent')
+
+
+class ActivateAccount(View):
+
+    def get(self, request, uidb64, token, *args, **kwargs):
+        try:
+            uid = force_str(urlsafe_base64_decode(uidb64))
+            user = User.objects.get(pk=uid)
+        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
+            user = None
+        if user is not None and token_generator.check_token(user, token):
+            user.is_active = True
+            user.save()
+            login(request, user, backend='django.contrib.auth.backends.ModelBackend')
+            messages.success(request, 'Your account have been confirmed.')
+            return redirect('/user/activate/done/')
+
+        messages.warning(
+            request,
+            'The confirmation link was invalid, possibly because it has already been used.',
+        )
+        return redirect('/user/activate/fail/')
+
+
+class ActivateAccountDoneView(TemplateView):
+    template_name = 'user/account_activation_done.html'
+    title = _('Activate account done')
+
+
+class ActivateAccountFailView(TemplateView):
+    template_name = 'user/account_activation_fail.html'
+    title = _('Activate account fail')
+
+
+class UserLoginView(views.LoginView):
+    form_class = AuthenticationForm
+    template_name = 'user/login.html'
+
+    def _get_identity_linking(self):
+        identity_linking = self.request.POST.get('identity_linking')
+        if identity_linking is None:
+            identity_linking = self.request.GET.get('identity_linking')
+        return identity_linking == '1'
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        identity_linking = self._get_identity_linking()
+        form = kwargs.get('form')
+        context.update({
+            'identity_linking': identity_linking,
+            'registration_enabled': settings.REGISTRATION_ENABLED,
+            'password_reset_enabled': True if settings.EMAIL_HOST else False,
+            'identity_linking_error': (
+                _('Identity linking login failed. Please try again.')
+                if identity_linking and form is not None and form.errors else None
+            ),
+        })
+        return context
+
+    def form_valid(self, form):
+        response = super().form_valid(form)
+        pending = self.request.session.get('oauth_pending')
+        if not pending:
+            return response
+
+        provider = pending.get('provider')
+        uid = pending.get('uid')
+        extra_data = pending.get('extra_data') or {}
+        if not provider or not uid:
+            self.request.session.pop('oauth_pending', None)
+            raise ValidationError('invalid oauth pending data')
+
+        existing = UserSocialAuth.objects.filter(
+            provider=provider,
+            uid=uid,
+        ).select_related('user').first()
+        if existing and existing.user_id != self.request.user.id:
+            self.request.session.pop('oauth_pending', None)
+            return redirect(get_oauth_callback(self.request, 'conflict', provider))
+
+        if not existing:
+            UserSocialAuth.objects.create(
+                user=self.request.user,
+                provider=provider,
+                uid=uid,
+                extra_data=extra_data,
+            )
+
+        self.request.session.pop('oauth_pending', None)
+        return redirect(get_oauth_callback(self.request, 'linked', provider))
+
+
+class OAuthCallbackTemplateView(TemplateView):
+    template_name = 'user/oauth_callback.html'
+
+    def _build_context(self, request, status_value=None, error=None):
+        pending = request.session.get('oauth_pending')
+        provider = request.GET.get('provider')
+        email = None
+        display_name = None
+
+        if pending:
+            provider = pending.get('provider') or provider
+            email = pending.get('email')
+
+        if provider:
+            display_name = provider.title()
+
+        status_value = status_value or request.GET.get('status') or 'error'
+        if status_value == 'pending' and not pending:
+            status_value = 'error'
+            error = error or 'No pending OAuth request'
+
+        return {
+            'status': status_value,
+            'provider': provider,
+            'display_name': display_name,
+            'email': email,
+            'error': error,
+        }
+
+    def get(self, request, *args, **kwargs):
+        context = self._build_context(request)
+        return render(request, self.template_name, context)
+
+    def post(self, request, *args, **kwargs):
+        username = request.POST.get('username')
+        password = request.POST.get('password')
+        confirm_password = request.POST.get('confirm_password')
+
+        if password != confirm_password:
+            context = self._build_context(
+                request,
+                status_value='pending',
+                error='Password confirmation does not match',
+            )
+            return render(request, self.template_name, context)
+
+        from api.views.api import OAuthCreateUserView
+
+        try:
+            OAuthCreateUserView()._create_oauth_user(request, username, password)
+        except ValidationError as exc:
+            error_text = exc.detail
+            if isinstance(error_text, (list, tuple)):
+                error_text = error_text[0]
+            context = self._build_context(request, status_value='pending', error=error_text)
+            return render(request, self.template_name, context)
+
+        return redirect('/account-setting')
+
+
+class UpdateAccount(View):
+    fail_template_name = 'user/account_update_fail.html'
+    success_template_name = 'user/account_update_done.html'
+
+    def get(self, request, uidb64, token, *args, **kwargs):
+        user = get_object_or_404(User, pk=force_str(urlsafe_base64_decode(uidb64)))
+        if user is not None and token_generator.check_token(user, token):
+            cache_key = "user:serializer:%s" % user.pk
+            from django.core.cache import cache
+            data = cache.get(cache_key, None)
+            if data:
+                user_serializer = serializers.UserSerializer(
+                    data=data, instance=user, partial=True
+                )
+                if user_serializer.is_valid():
+                    user_serializer.save()
+                    login(request, user, backend='django.contrib.auth.backends.ModelBackend')
+                    cache.delete(cache_key)
+                    return render(request, template_name=self.success_template_name)
+        return render(request, template_name=self.fail_template_name)
+
+
+class LoginDoneView(TemplateView):
+    template_name = 'user/login_done.html'
+
+
+class UserPasswordResetView(views.PasswordResetView):
+    email_template_name = 'user/password_reset_email.html'
+    success_url = reverse_lazy('user_password_reset_done')
+    template_name = 'user/password_reset_form.html'
+
+
+class UserPasswordResetDoneView(views.PasswordResetDoneView):
+    template_name = 'user/password_reset_done.html'
+
+
+class UserPasswordResetConfirmView(views.PasswordResetConfirmView):
+    success_url = reverse_lazy('user_password_reset_complete')
+    template_name = 'user/password_reset_confirm.html'
+
+
+class UserPasswordResetCompleteView(views.PasswordResetCompleteView):
+    template_name = 'user/password_reset_complete.html'
+
+
+class UserLogoutView(views.LogoutView):
+    template_name = 'user/logout.html'
diff --git a/rootfs/api/viewset.py b/rootfs/api/viewset.py
deleted file mode 100644
index 9ccba36..0000000
--- a/rootfs/api/viewset.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from rest_framework import viewsets
-from rest_framework.permissions import IsAuthenticated
-
-
-class NormalUserViewSet(viewsets.ModelViewSet):
-    """
-    A simple ViewSet for objects filtered by their 'owner' attribute.
-
-    To use it, at minimum you'll need to provide the `serializer_class` attribute and
-    the `model` attribute shortcut.
-    """
-    permission_classes = [IsAuthenticated]
diff --git a/rootfs/bin/test-unit b/rootfs/bin/test-unit
index b1f9263..de37e50 100755
--- a/rootfs/bin/test-unit
+++ b/rootfs/bin/test-unit
@@ -6,12 +6,19 @@
 # fail hard and fast even on pipelines
 set -eou pipefail
 
+function start_valkey() {
+    cd /tmp
+    nohup valkey-server > /var/log/valkey.log 2>&1 &
+    cd -
+}
+
 function creating_rsa_key() {
     mkdir -p /var/run/secrets/drycc/passport/ \
       && openssl genrsa -out /var/run/secrets/drycc/passport/oidc-rsa-private-key 4096
 }
 
 gosu postgres pg_ctl -D "$PGDATA" start && ln -s /tmp/.s.PGSQL.5432 /var/run/postgresql/.s.PGSQL.5432
+start_valkey
 creating_rsa_key
 python3 manage.py check
 coverage run manage.py test --settings=api.settings.testing --noinput api
diff --git a/rootfs/passport/urls.py b/rootfs/passport/urls.py
index 6740132..a3a0dbf 100644
--- a/rootfs/passport/urls.py
+++ b/rootfs/passport/urls.py
@@ -18,7 +18,6 @@
 from django.contrib import admin
 from django.views.generic.base import TemplateView
 from api.views import LivenessCheckView, ReadinessCheckView
-from . import views
 
 if settings.ADMIN_ENABLED:
     urlpatterns = [path('admin/', admin.site.urls)]
@@ -28,9 +27,9 @@
 urlpatterns += [
     re_path(r'^healthz$', LivenessCheckView.as_view()),
     re_path(r'^readiness$', ReadinessCheckView.as_view()),
-    re_path(r"settings/?$", views.SettingsViewSet.as_view({'get': 'retrieve'})),
-    re_path(r"^user/", include('api.urls')),
+    re_path(r"^", include('api.urls')),
     re_path(r'^oauth/', include('oauth2_provider.urls', namespace='oauth2_provider')),
     re_path(r'^accounts/', include('django.contrib.auth.urls')),
+    re_path(r'^auth/', include('social_django.urls', namespace='social')),
     re_path(r'.*', TemplateView.as_view(template_name="index.html")),
 ]
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index a8d7f47..bfab3a3 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,17 +1,18 @@
 # Drycc passport requirements
-django==5.2.8
-pytz==2025.2
-django-auth-ldap==5.2.0
+django==5.2.12
+pytz==2026.1
+django-auth-ldap==5.3.0
 django-cors-headers==4.9.0
-djangorestframework==3.16.1
-gunicorn==23.0.0
-uvicorn==0.37.0
-asgiref==3.10.0
-psycopg[binary]==3.2.10
+djangorestframework==3.17.0
+gunicorn==25.1.0
+uvicorn==0.42.0
+asgiref==3.11.1
+psycopg[binary]==3.3.3
 requests==2.32.5
 requests-toolbelt==1.0.0
-redis==6.4.0
-hiredis==3.2.1
-dj-database-url==3.0.1
-django-oauth-toolkit==3.1.0
-whitenoise==6.11.0
+redis==7.4.0
+hiredis==3.3.1
+dj-database-url==3.1.2
+django-oauth-toolkit==3.2.0
+social-auth-app-django==5.7.0
+whitenoise==6.12.0
diff --git a/rootfs/web/README.md b/rootfs/web/README.md
new file mode 100644
index 0000000..3d51350
--- /dev/null
+++ b/rootfs/web/README.md
@@ -0,0 +1,83 @@
+# Passport Web Frontend
+
+This is the web frontend for the Passport authentication system, built with Vue.js and Vite.
+
+## Prerequisites
+
+Before running the web frontend, ensure you have the following installed:
+- Node.js (v23 or higher)
+- npm or yarn
+- Python environment with Django backend
+
+## Setup Instructions
+
+### 1. Backend Setup
+
+First, ensure the Django backend is properly set up:
+
+```bash
+# Install Django and dependencies
+pip install -r requirements.txt
+
+# Set environment variables
+export DRYCC_DATABASE_URL=postgres://postgres:@127.0.0.1:5432/passport
+export VUE_APP_BASE_URL=http://localhost:8000/
+export CSRF_TRUSTED_ORIGINS=http://localhost:5173
+
+# Start the Django development server
+python manage.py runserver
+```
+
+### 2. Frontend Setup
+
+Navigate to the web directory and install dependencies:
+
+```bash
+cd web
+npm install
+```
+
+### 3. Development Server
+
+Start the development server:
+
+```bash
+npm run dev -- --port 5173
+```
+
+The application will be available at `http://localhost:5173`.
+
+## Available Scripts
+
+- `npm run dev` - Start the development server
+- `npm run build` - Build the application for production
+- `npm run preview` - Preview the production build
+- `npm run lint` - Run ESLint
+
+## Environment Variables
+
+The following environment variables are required:
+
+- `VUE_APP_BASE_URL` - The base URL of the Django backend API
+- `CSRF_TRUSTED_ORIGINS` - Trusted origins for CSRF protection
+
+## Project Structure
+
+```
+web/
+├── public/          # Static assets
+├── src/
+│   ├── assets/      # Images and other assets
+│   ├── components/  # Vue components
+│   ├── views/       # Page components
+│   ├── services/    # API service modules
+│   ├── router/      # Vue Router configuration
+│   ├── utils/       # Utility functions
+│   └── lang/        # Internationalization
+├── package.json     # Project dependencies
+└── vite.config.js   # Vite configuration
+```
+
+## Contributing
+
+Please refer to the main project documentation for contribution guidelines.
diff --git a/rootfs/web/index.html b/rootfs/web/index.html
index 7c24d84..52ae093 100644
--- a/rootfs/web/index.html
+++ b/rootfs/web/index.html
@@ -2,15 +2,9 @@
 <html lang="en">
 <head>
   <meta charset="UTF-8">
-  <link rel='stylesheet' href='https://download.drycc.cc/static/bluecss/bulecss.css'>
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Drycc Passport</title>
 </head>
-<style>
-  body{
-    padding-top: 40px;
-  }
-</style>
 <body>
   <div id="app"></div>
   <script type="module" src="/src/main.js"></script>
diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index c8ac336..c112d7a 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -1,7 +1,7 @@
 {
   "name": "web",
   "version": "0.0.0",
-  "lockfileVersion": 2,
+  "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
@@ -9,736 +9,1095 @@
       "version": "0.0.0",
       "license": "Apache-2.0",
       "dependencies": {
-        "@icon-park/vue-next": "^1.3.6",
-        "axios": "^0.30.2",
-        "vant": "^3.4.5",
-        "vue": "^3.2.31",
-        "vue-i18n": "^9.1.9",
-        "vue-router": "^4.0.14",
-        "yarn": "^1.22.17"
+        "@element-plus/icons-vue": "^2.3.2",
+        "axios": "^1.13.6",
+        "element-plus": "^2.13.6",
+        "vant": "^4.9.22",
+        "vue": "^3.5.30",
+        "vue-i18n": "^11.3.0",
+        "vue-router": "^5.0.4",
+        "yarn": "^1.22.22"
       },
       "devDependencies": {
-        "@vitejs/plugin-vue": "^2.2.4",
-        "@vue/compiler-sfc": "^3.2.31",
-        "vite": "^2.8.6"
+        "@vitejs/plugin-vue": "^6.0.5",
+        "@vue/compiler-sfc": "^3.5.30",
+        "vite": "^8.0.2"
       }
     },
-    "node_modules/@babel/parser": {
-      "version": "7.16.7",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz",
-      "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==",
-      "bin": {
-        "parser": "bin/babel-parser.js"
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
       },
       "engines": {
-        "node": ">=6.0.0"
+        "node": ">=6.9.0"
       }
     },
-    "node_modules/@icon-park/vue-next": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.3.6.tgz",
-      "integrity": "sha512-AZaCcjRPU9vTNVfcrG03CYJM+uv0SXx5lC4njanlRorFo+TV/x0ZTYGYGpTR/l6ek6QmFu9THmrgqKl7i/8yHg==",
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
       "engines": {
-        "node": ">= 8.0.0",
-        "npm": ">= 5.0.0"
-      },
-      "peerDependencies": {
-        "vue": "3.x"
+        "node": ">=6.9.0"
       }
     },
-    "node_modules/@intlify/core-base": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
-      "integrity": "sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==",
-      "dependencies": {
-        "@intlify/devtools-if": "9.1.9",
-        "@intlify/message-compiler": "9.1.9",
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/runtime": "9.1.9",
-        "@intlify/shared": "9.1.9",
-        "@intlify/vue-devtools": "9.1.9"
-      },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
       "engines": {
-        "node": ">= 10"
+        "node": ">=6.9.0"
       }
     },
-    "node_modules/@intlify/devtools-if": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz",
-      "integrity": "sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==",
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "license": "MIT",
       "dependencies": {
-        "@intlify/shared": "9.1.9"
+        "@babel/types": "^7.29.0"
       },
-      "engines": {
-        "node": ">= 10"
-      }
-    },
-    "node_modules/@intlify/message-compiler": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz",
-      "integrity": "sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==",
-      "dependencies": {
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/shared": "9.1.9",
-        "source-map": "0.6.1"
+      "bin": {
+        "parser": "bin/babel-parser.js"
       },
       "engines": {
-        "node": ">= 10"
-      }
-    },
-    "node_modules/@intlify/message-resolver": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz",
-      "integrity": "sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA==",
-      "engines": {
-        "node": ">= 10"
+        "node": ">=6.0.0"
       }
     },
-    "node_modules/@intlify/runtime": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz",
-      "integrity": "sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==",
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
       "dependencies": {
-        "@intlify/message-compiler": "9.1.9",
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/shared": "9.1.9"
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
       },
       "engines": {
-        "node": ">= 10"
-      }
-    },
-    "node_modules/@intlify/shared": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz",
-      "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw==",
-      "engines": {
-        "node": ">= 10"
+        "node": ">=6.9.0"
       }
     },
-    "node_modules/@intlify/vue-devtools": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz",
-      "integrity": "sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==",
-      "dependencies": {
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/runtime": "9.1.9",
-        "@intlify/shared": "9.1.9"
-      },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
+      "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
+      "license": "MIT",
       "engines": {
-        "node": ">= 10"
-      }
-    },
-    "node_modules/@popperjs/core": {
-      "version": "2.11.2",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
-      "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
-    },
-    "node_modules/@vant/icons": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/@vant/icons/-/icons-1.7.3.tgz",
-      "integrity": "sha512-tW4EqzxN4kXw1rnlnQJQHofEifPbt/gECOWiibomht8QLyvoGuE4iUmDFS288dJ07ZjuTy0bhdABj0SENo2fmQ=="
-    },
-    "node_modules/@vant/popperjs": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.1.0.tgz",
-      "integrity": "sha512-8MD1gz146awV/uPxYjz4pet22f7a9YVKqk7T+gFkWFwT9mEcrIUEg/xPrdOnWKLP9puXyYtm7oVfSDSefZ/p/w==",
-      "dependencies": {
-        "@popperjs/core": "^2.9.2"
+        "node": ">=14"
       }
     },
-    "node_modules/@vant/use": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/@vant/use/-/use-1.3.6.tgz",
-      "integrity": "sha512-3z+nywPaV2F5BdJO7RQxWlgfzJeEOmViD2yHMb7Tg+R4NR/7iQskqW8v2Cnv9FWSJgTOSHlcr7UzeLpiTAP4HA=="
-    },
-    "node_modules/@vitejs/plugin-vue": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz",
-      "integrity": "sha512-ev9AOlp0ljCaDkFZF3JwC/pD2N4Hh+r5srl5JHM6BKg5+99jiiK0rE/XaRs3pVm1wzyKkjUy/StBSoXX5fFzcw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12.0.0"
-      },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "license": "MIT",
       "peerDependencies": {
-        "vite": "^2.5.10",
-        "vue": "^3.2.25"
-      }
-    },
-    "node_modules/@vue/compiler-core": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz",
-      "integrity": "sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==",
-      "dependencies": {
-        "@babel/parser": "^7.16.4",
-        "@vue/shared": "3.2.31",
-        "estree-walker": "^2.0.2",
-        "source-map": "^0.6.1"
+        "vue": "^3.2.0"
       }
     },
-    "node_modules/@vue/compiler-dom": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz",
-      "integrity": "sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==",
+    "node_modules/@emnapi/core": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+      "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
       "dependencies": {
-        "@vue/compiler-core": "3.2.31",
-        "@vue/shared": "3.2.31"
-      }
-    },
-    "node_modules/@vue/compiler-sfc": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz",
-      "integrity": "sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==",
-      "dependencies": {
-        "@babel/parser": "^7.16.4",
-        "@vue/compiler-core": "3.2.31",
-        "@vue/compiler-dom": "3.2.31",
-        "@vue/compiler-ssr": "3.2.31",
-        "@vue/reactivity-transform": "3.2.31",
-        "@vue/shared": "3.2.31",
-        "estree-walker": "^2.0.2",
-        "magic-string": "^0.25.7",
-        "postcss": "^8.1.10",
-        "source-map": "^0.6.1"
+        "@emnapi/wasi-threads": "1.2.0",
+        "tslib": "^2.4.0"
       }
     },
-    "node_modules/@vue/compiler-ssr": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz",
-      "integrity": "sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==",
+    "node_modules/@emnapi/runtime": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+      "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
       "dependencies": {
-        "@vue/compiler-dom": "3.2.31",
-        "@vue/shared": "3.2.31"
+        "tslib": "^2.4.0"
       }
     },
-    "node_modules/@vue/devtools-api": {
-      "version": "6.0.13",
-      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.13.tgz",
-      "integrity": "sha512-T34EjcArVqzANedEZe2kXQ+AZsld2z1ptJlkOGm87+blk+s6udnP4ze/NYqV8lz1o9AIivimN0xxteLlWiWQdg=="
-    },
-    "node_modules/@vue/reactivity": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz",
-      "integrity": "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==",
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+      "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
       "dependencies": {
-        "@vue/shared": "3.2.31"
+        "tslib": "^2.4.0"
       }
     },
-    "node_modules/@vue/reactivity-transform": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz",
-      "integrity": "sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA==",
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.5",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+      "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+      "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.16.4",
-        "@vue/compiler-core": "3.2.31",
-        "@vue/shared": "3.2.31",
-        "estree-walker": "^2.0.2",
-        "magic-string": "^0.25.7"
+        "@floating-ui/utils": "^0.2.11"
       }
     },
-    "node_modules/@vue/runtime-core": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz",
-      "integrity": "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==",
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+      "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+      "license": "MIT",
       "dependencies": {
-        "@vue/reactivity": "3.2.31",
-        "@vue/shared": "3.2.31"
+        "@floating-ui/core": "^1.7.5",
+        "@floating-ui/utils": "^0.2.11"
       }
     },
-    "node_modules/@vue/runtime-dom": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz",
-      "integrity": "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==",
-      "dependencies": {
-        "@vue/runtime-core": "3.2.31",
-        "@vue/shared": "3.2.31",
-        "csstype": "^2.6.8"
-      }
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+      "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+      "license": "MIT"
     },
-    "node_modules/@vue/server-renderer": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz",
-      "integrity": "sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==",
+    "node_modules/@intlify/core-base": {
+      "version": "11.3.0",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
+      "integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
+      "license": "MIT",
       "dependencies": {
-        "@vue/compiler-ssr": "3.2.31",
-        "@vue/shared": "3.2.31"
+        "@intlify/devtools-types": "11.3.0",
+        "@intlify/message-compiler": "11.3.0",
+        "@intlify/shared": "11.3.0"
       },
-      "peerDependencies": {
-        "vue": "3.2.31"
-      }
-    },
-    "node_modules/@vue/shared": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
-      "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
-    },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
-    },
-    "node_modules/axios": {
-      "version": "0.30.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.2.tgz",
-      "integrity": "sha512-0pE4RQ4UQi1jKY6p7u6i1Tkzqmu+d+/tHS7Q7rKunWLB9WyilBTpHHpXzPNMDj5hTbK0B0PTLSz07yqMBiF6xg==",
-      "dependencies": {
-        "follow-redirects": "^1.15.4",
-        "form-data": "^4.0.4",
-        "proxy-from-env": "^1.1.0"
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
       }
     },
-    "node_modules/call-bind-apply-helpers": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
-      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+    "node_modules/@intlify/devtools-types": {
+      "version": "11.3.0",
+      "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz",
+      "integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==",
+      "license": "MIT",
       "dependencies": {
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2"
+        "@intlify/core-base": "11.3.0",
+        "@intlify/shared": "11.3.0"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
       }
     },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+    "node_modules/@intlify/message-compiler": {
+      "version": "11.3.0",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
+      "integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
+      "license": "MIT",
       "dependencies": {
-        "delayed-stream": "~1.0.0"
+        "@intlify/shared": "11.3.0",
+        "source-map-js": "^1.0.2"
       },
       "engines": {
-        "node": ">= 0.8"
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
       }
     },
-    "node_modules/csstype": {
-      "version": "2.6.20",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
-      "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
-    },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+    "node_modules/@intlify/shared": {
+      "version": "11.3.0",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
+      "integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
+      "license": "MIT",
       "engines": {
-        "node": ">=0.4.0"
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
       }
     },
-    "node_modules/dunder-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
-      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "license": "MIT",
       "dependencies": {
-        "call-bind-apply-helpers": "^1.0.1",
-        "es-errors": "^1.3.0",
-        "gopd": "^1.2.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
       }
     },
-    "node_modules/es-define-property": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
-      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
-      "engines": {
-        "node": ">= 0.4"
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
       }
     },
-    "node_modules/es-errors": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
-      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "license": "MIT",
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=6.0.0"
       }
     },
-    "node_modules/es-object-atoms": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
-      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "license": "MIT",
       "dependencies": {
-        "es-errors": "^1.3.0"
-      },
-      "engines": {
-        "node": ">= 0.4"
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
-    "node_modules/es-set-tostringtag": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
-      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+      "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
       "dependencies": {
-        "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.6",
-        "has-tostringtag": "^1.0.2",
-        "hasown": "^2.0.2"
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1",
+        "@tybys/wasm-util": "^0.10.1"
       },
-      "engines": {
-        "node": ">= 0.4"
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
       }
     },
-    "node_modules/esbuild": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
-      "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "esbuild-android-64": "0.14.25",
-        "esbuild-android-arm64": "0.14.25",
-        "esbuild-darwin-64": "0.14.25",
-        "esbuild-darwin-arm64": "0.14.25",
-        "esbuild-freebsd-64": "0.14.25",
-        "esbuild-freebsd-arm64": "0.14.25",
-        "esbuild-linux-32": "0.14.25",
-        "esbuild-linux-64": "0.14.25",
-        "esbuild-linux-arm": "0.14.25",
-        "esbuild-linux-arm64": "0.14.25",
-        "esbuild-linux-mips64le": "0.14.25",
-        "esbuild-linux-ppc64le": "0.14.25",
-        "esbuild-linux-riscv64": "0.14.25",
-        "esbuild-linux-s390x": "0.14.25",
-        "esbuild-netbsd-64": "0.14.25",
-        "esbuild-openbsd-64": "0.14.25",
-        "esbuild-sunos-64": "0.14.25",
-        "esbuild-windows-32": "0.14.25",
-        "esbuild-windows-64": "0.14.25",
-        "esbuild-windows-arm64": "0.14.25"
-      }
-    },
-    "node_modules/esbuild-android-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz",
-      "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/@oxc-project/types": {
+      "version": "0.122.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+      "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+      "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
       }
     },
-    "node_modules/esbuild-android-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz",
-      "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==",
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz",
+      "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-darwin-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz",
-      "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==",
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz",
+      "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==",
       "cpu": [
-        "x64"
+        "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-darwin-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz",
-      "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==",
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz",
+      "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==",
       "cpu": [
-        "arm64"
+        "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-freebsd-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz",
-      "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==",
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz",
+      "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-freebsd-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz",
-      "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==",
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz",
+      "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==",
       "cpu": [
-        "arm64"
+        "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
-        "freebsd"
+        "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-32": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz",
-      "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==",
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz",
+      "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==",
       "cpu": [
-        "ia32"
+        "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz",
-      "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==",
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz",
+      "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==",
       "cpu": [
-        "x64"
+        "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-arm": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz",
-      "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==",
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz",
+      "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==",
       "cpu": [
-        "arm"
+        "ppc64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz",
-      "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==",
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz",
+      "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==",
       "cpu": [
-        "arm64"
+        "s390x"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-mips64le": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz",
-      "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==",
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz",
+      "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==",
       "cpu": [
-        "mips64el"
+        "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-ppc64le": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz",
-      "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==",
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz",
+      "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==",
       "cpu": [
-        "ppc64"
+        "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-riscv64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz",
-      "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==",
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz",
+      "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==",
       "cpu": [
-        "riscv64"
+        "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
-        "linux"
+        "openharmony"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-linux-s390x": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz",
-      "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==",
+    "node_modules/@rolldown/binding-wasm32-wasi": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz",
+      "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==",
       "cpu": [
-        "s390x"
+        "wasm32"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
-      "os": [
-        "linux"
-      ],
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^1.1.1"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">=14.0.0"
       }
     },
-    "node_modules/esbuild-netbsd-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz",
-      "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==",
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz",
+      "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==",
       "cpu": [
-        "x64"
+        "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
-        "netbsd"
+        "win32"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-openbsd-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz",
-      "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==",
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz",
+      "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==",
       "cpu": [
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
-        "openbsd"
+        "win32"
       ],
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/esbuild-sunos-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz",
-      "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+      "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
+      "license": "MIT"
     },
-    "node_modules/esbuild-windows-32": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz",
-      "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==",
-      "cpu": [
-        "ia32"
-      ],
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
       "dev": true,
+      "license": "MIT",
       "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "tslib": "^2.4.0"
       }
     },
-    "node_modules/esbuild-windows-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz",
-      "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/@types/lodash": {
+      "version": "4.17.24",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+      "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "license": "MIT"
+    },
+    "node_modules/@vant/popperjs": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.3.0.tgz",
+      "integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==",
+      "license": "MIT"
+    },
+    "node_modules/@vant/use": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@vant/use/-/use-1.6.0.tgz",
+      "integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
+      "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-rc.2"
+      },
       "engines": {
-        "node": ">=12"
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
       }
     },
-    "node_modules/esbuild-windows-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz",
-      "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/@vue-macros/common": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
+      "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-sfc": "^3.5.22",
+        "ast-kit": "^2.1.2",
+        "local-pkg": "^1.1.2",
+        "magic-string-ast": "^1.0.2",
+        "unplugin-utils": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/vue-macros"
+      },
+      "peerDependencies": {
+        "vue": "^2.7.0 || ^3.2.25"
+      },
+      "peerDependenciesMeta": {
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
+      "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@vue/shared": "3.5.30",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
+      "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.30",
+        "@vue/shared": "3.5.30"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
+      "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@vue/compiler-core": "3.5.30",
+        "@vue/compiler-dom": "3.5.30",
+        "@vue/compiler-ssr": "3.5.30",
+        "@vue/shared": "3.5.30",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.8",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
+      "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.30",
+        "@vue/shared": "3.5.30"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz",
+      "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^8.1.1",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "perfect-debounce": "^2.0.0"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz",
+      "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
+      "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.30"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
+      "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.30",
+        "@vue/shared": "3.5.30"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
+      "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.30",
+        "@vue/runtime-core": "3.5.30",
+        "@vue/shared": "3.5.30",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
+      "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.30",
+        "@vue/shared": "3.5.30"
+      },
+      "peerDependencies": {
+        "vue": "3.5.30"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
+      "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/core": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
+      "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "12.0.0",
+        "@vueuse/shared": "12.0.0",
+        "vue": "^3.5.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
+      "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
+      "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
+      "license": "MIT",
+      "dependencies": {
+        "vue": "^3.5.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/ast-kit": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
+      "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "pathe": "^2.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/ast-walker-scope": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
+      "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.4",
+        "ast-kit": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.6",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+      "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.20",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+      "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
+      "license": "Apache-2.0",
       "engines": {
-        "node": ">=12"
+        "node": ">=8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/element-plus": {
+      "version": "2.13.6",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.6.tgz",
+      "integrity": "sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^4.2.0",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+        "@types/lodash": "^4.17.20",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "12.0.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.19",
+        "lodash": "^4.17.23",
+        "lodash-es": "^4.17.23",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0",
+        "vue-component-type-helpers": "^3.2.4"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/estree-walker": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/exsolve": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+      "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
     },
     "node_modules/follow-redirects": {
       "version": "1.15.11",
@@ -750,6 +1109,7 @@
           "url": "https://github.com/sponsors/RubenVerborgh"
         }
       ],
+      "license": "MIT",
       "engines": {
         "node": ">=4.0"
       },
@@ -760,9 +1120,10 @@
       }
     },
     "node_modules/form-data": {
-      "version": "4.0.4",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
-      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
       "dependencies": {
         "asynckit": "^0.4.0",
         "combined-stream": "^1.0.8",
@@ -775,11 +1136,12 @@
       }
     },
     "node_modules/fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
       "dev": true,
       "hasInstallScript": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -792,6 +1154,7 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
       "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -800,6 +1163,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
       "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
       "dependencies": {
         "call-bind-apply-helpers": "^1.0.2",
         "es-define-property": "^1.0.1",
@@ -823,6 +1187,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
       "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
       "dependencies": {
         "dunder-proto": "^1.0.1",
         "es-object-atoms": "^1.0.0"
@@ -835,6 +1200,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
       "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4"
       },
@@ -842,22 +1208,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
-      "dependencies": {
-        "function-bind": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
     "node_modules/has-symbols": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
       "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
       "engines": {
         "node": ">= 0.4"
       },
@@ -869,6 +1224,7 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
       "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
       "dependencies": {
         "has-symbols": "^1.0.3"
       },
@@ -883,6 +1239,7 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
       "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
       "dependencies": {
         "function-bind": "^1.1.2"
       },
@@ -890,985 +1247,904 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/is-core-module": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
-      "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
-      "dev": true,
-      "dependencies": {
-        "has": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/magic-string": {
-      "version": "0.25.9",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
-      "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
-      "dependencies": {
-        "sourcemap-codec": "^1.4.8"
-      }
-    },
-    "node_modules/math-intrinsics": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
-      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "engines": {
-        "node": ">= 0.6"
-      }
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
     },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dependencies": {
-        "mime-db": "1.52.0"
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
       },
       "engines": {
-        "node": ">= 0.6"
+        "node": ">=6"
       }
     },
-    "node_modules/nanoid": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
-      "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "license": "MIT",
       "bin": {
-        "nanoid": "bin/nanoid.cjs"
+        "json5": "lib/cli.js"
       },
       "engines": {
-        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+        "node": ">=6"
       }
     },
-    "node_modules/path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
-    },
-    "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-    },
-    "node_modules/postcss": {
-      "version": "8.4.8",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz",
-      "integrity": "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==",
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+      "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "dev": true,
+      "license": "MPL-2.0",
       "dependencies": {
-        "nanoid": "^3.3.1",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
+        "detect-libc": "^2.0.3"
       },
       "engines": {
-        "node": "^10 || ^12 || >=14"
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+      "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
       },
       "funding": {
         "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
-    },
-    "node_modules/resolve": {
-      "version": "1.22.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
-      "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+      "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+      "cpu": [
+        "arm64"
+      ],
       "dev": true,
-      "dependencies": {
-        "is-core-module": "^2.8.1",
-        "path-parse": "^1.0.7",
-        "supports-preserve-symlinks-flag": "^1.0.0"
-      },
-      "bin": {
-        "resolve": "bin/resolve"
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/rollup": {
-      "version": "2.63.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz",
-      "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==",
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+      "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+      "cpu": [
+        "x64"
+      ],
       "dev": true,
-      "bin": {
-        "rollup": "dist/bin/rollup"
-      },
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
       "engines": {
-        "node": ">=10.0.0"
+        "node": ">= 12.0.0"
       },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+      "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+      "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/sourcemap-codec": {
-      "version": "1.4.8",
-      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
-      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
-    },
-    "node_modules/supports-preserve-symlinks-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+      "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+      "cpu": [
+        "arm64"
+      ],
       "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
       "engines": {
-        "node": ">= 0.4"
+        "node": ">= 12.0.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/vant": {
-      "version": "3.4.5",
-      "resolved": "https://registry.npmjs.org/vant/-/vant-3.4.5.tgz",
-      "integrity": "sha512-3an2v6ejW9pijKmaDbSCW7Hs1R45tqtTIcedfI8hEAA5zC2Bp8cXU6Z4gUCK6/RAGOJsB+vcyLINLb1cGS/SzQ==",
-      "dependencies": {
-        "@vant/icons": "^1.7.1",
-        "@vant/popperjs": "^1.1.0",
-        "@vant/use": "^1.3.5"
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+      "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
       },
-      "peerDependencies": {
-        "vue": "^3.0.0"
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/vite": {
-      "version": "2.8.6",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz",
-      "integrity": "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==",
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+      "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+      "cpu": [
+        "x64"
+      ],
       "dev": true,
-      "dependencies": {
-        "esbuild": "^0.14.14",
-        "postcss": "^8.4.6",
-        "resolve": "^1.22.0",
-        "rollup": "^2.59.0"
-      },
-      "bin": {
-        "vite": "bin/vite.js"
-      },
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
       "engines": {
-        "node": ">=12.2.0"
+        "node": ">= 12.0.0"
       },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
-      },
-      "peerDependencies": {
-        "less": "*",
-        "sass": "*",
-        "stylus": "*"
-      },
-      "peerDependenciesMeta": {
-        "less": {
-          "optional": true
-        },
-        "sass": {
-          "optional": true
-        },
-        "stylus": {
-          "optional": true
-        }
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/vue": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz",
-      "integrity": "sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==",
-      "dependencies": {
-        "@vue/compiler-dom": "3.2.31",
-        "@vue/compiler-sfc": "3.2.31",
-        "@vue/runtime-dom": "3.2.31",
-        "@vue/server-renderer": "3.2.31",
-        "@vue/shared": "3.2.31"
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+      "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/vue-i18n": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.9.tgz",
-      "integrity": "sha512-JeRdNVxS2OGp1E+pye5XB6+M6BBkHwAv9C80Q7+kzoMdUDGRna06tjC0vCB/jDX9aWrl5swxOMFcyAr7or8XTA==",
-      "dependencies": {
-        "@intlify/core-base": "9.1.9",
-        "@intlify/shared": "9.1.9",
-        "@intlify/vue-devtools": "9.1.9",
-        "@vue/devtools-api": "^6.0.0-beta.7"
-      },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+      "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
       "engines": {
-        "node": ">= 10"
+        "node": ">= 12.0.0"
       },
-      "peerDependencies": {
-        "vue": "^3.0.0"
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/vue-router": {
-      "version": "4.0.14",
-      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz",
-      "integrity": "sha512-wAO6zF9zxA3u+7AkMPqw9LjoUCjSxfFvINQj3E/DceTt6uEz1XZLraDhdg2EYmvVwTBSGlLYsUw8bDmx0754Mw==",
-      "dependencies": {
-        "@vue/devtools-api": "^6.0.0"
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+      "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/posva"
-      },
-      "peerDependencies": {
-        "vue": "^3.2.0"
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
       }
     },
-    "node_modules/yarn": {
-      "version": "1.22.17",
-      "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.17.tgz",
-      "integrity": "sha512-H0p241BXaH0UN9IeH//RT82tl5PfNraVpSpEoW+ET7lmopNC61eZ+A+IDvU8FM6Go5vx162SncDL8J1ZjRBriQ==",
-      "hasInstallScript": true,
-      "bin": {
-        "yarn": "bin/yarn.js",
-        "yarnpkg": "bin/yarn.js"
+    "node_modules/local-pkg": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
+      "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
+      "license": "MIT",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
       },
       "engines": {
-        "node": ">=4.0.0"
-      }
-    }
-  },
-  "dependencies": {
-    "@babel/parser": {
-      "version": "7.16.7",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz",
-      "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA=="
-    },
-    "@icon-park/vue-next": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.3.6.tgz",
-      "integrity": "sha512-AZaCcjRPU9vTNVfcrG03CYJM+uv0SXx5lC4njanlRorFo+TV/x0ZTYGYGpTR/l6ek6QmFu9THmrgqKl7i/8yHg==",
-      "requires": {}
-    },
-    "@intlify/core-base": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
-      "integrity": "sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==",
-      "requires": {
-        "@intlify/devtools-if": "9.1.9",
-        "@intlify/message-compiler": "9.1.9",
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/runtime": "9.1.9",
-        "@intlify/shared": "9.1.9",
-        "@intlify/vue-devtools": "9.1.9"
-      }
-    },
-    "@intlify/devtools-if": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz",
-      "integrity": "sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==",
-      "requires": {
-        "@intlify/shared": "9.1.9"
-      }
-    },
-    "@intlify/message-compiler": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz",
-      "integrity": "sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==",
-      "requires": {
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/shared": "9.1.9",
-        "source-map": "0.6.1"
-      }
-    },
-    "@intlify/message-resolver": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz",
-      "integrity": "sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA=="
-    },
-    "@intlify/runtime": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz",
-      "integrity": "sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==",
-      "requires": {
-        "@intlify/message-compiler": "9.1.9",
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/shared": "9.1.9"
-      }
-    },
-    "@intlify/shared": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz",
-      "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw=="
-    },
-    "@intlify/vue-devtools": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz",
-      "integrity": "sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==",
-      "requires": {
-        "@intlify/message-resolver": "9.1.9",
-        "@intlify/runtime": "9.1.9",
-        "@intlify/shared": "9.1.9"
-      }
-    },
-    "@popperjs/core": {
-      "version": "2.11.2",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
-      "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
-    },
-    "@vant/icons": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/@vant/icons/-/icons-1.7.3.tgz",
-      "integrity": "sha512-tW4EqzxN4kXw1rnlnQJQHofEifPbt/gECOWiibomht8QLyvoGuE4iUmDFS288dJ07ZjuTy0bhdABj0SENo2fmQ=="
-    },
-    "@vant/popperjs": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.1.0.tgz",
-      "integrity": "sha512-8MD1gz146awV/uPxYjz4pet22f7a9YVKqk7T+gFkWFwT9mEcrIUEg/xPrdOnWKLP9puXyYtm7oVfSDSefZ/p/w==",
-      "requires": {
-        "@popperjs/core": "^2.9.2"
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
       }
     },
-    "@vant/use": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/@vant/use/-/use-1.3.6.tgz",
-      "integrity": "sha512-3z+nywPaV2F5BdJO7RQxWlgfzJeEOmViD2yHMb7Tg+R4NR/7iQskqW8v2Cnv9FWSJgTOSHlcr7UzeLpiTAP4HA=="
+    "node_modules/lodash": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+      "license": "MIT"
     },
-    "@vitejs/plugin-vue": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz",
-      "integrity": "sha512-ev9AOlp0ljCaDkFZF3JwC/pD2N4Hh+r5srl5JHM6BKg5+99jiiK0rE/XaRs3pVm1wzyKkjUy/StBSoXX5fFzcw==",
-      "dev": true,
-      "requires": {}
-    },
-    "@vue/compiler-core": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz",
-      "integrity": "sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==",
-      "requires": {
-        "@babel/parser": "^7.16.4",
-        "@vue/shared": "3.2.31",
-        "estree-walker": "^2.0.2",
-        "source-map": "^0.6.1"
-      }
-    },
-    "@vue/compiler-dom": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz",
-      "integrity": "sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==",
-      "requires": {
-        "@vue/compiler-core": "3.2.31",
-        "@vue/shared": "3.2.31"
-      }
-    },
-    "@vue/compiler-sfc": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz",
-      "integrity": "sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==",
-      "requires": {
-        "@babel/parser": "^7.16.4",
-        "@vue/compiler-core": "3.2.31",
-        "@vue/compiler-dom": "3.2.31",
-        "@vue/compiler-ssr": "3.2.31",
-        "@vue/reactivity-transform": "3.2.31",
-        "@vue/shared": "3.2.31",
-        "estree-walker": "^2.0.2",
-        "magic-string": "^0.25.7",
-        "postcss": "^8.1.10",
-        "source-map": "^0.6.1"
-      }
-    },
-    "@vue/compiler-ssr": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz",
-      "integrity": "sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==",
-      "requires": {
-        "@vue/compiler-dom": "3.2.31",
-        "@vue/shared": "3.2.31"
-      }
-    },
-    "@vue/devtools-api": {
-      "version": "6.0.13",
-      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.13.tgz",
-      "integrity": "sha512-T34EjcArVqzANedEZe2kXQ+AZsld2z1ptJlkOGm87+blk+s6udnP4ze/NYqV8lz1o9AIivimN0xxteLlWiWQdg=="
-    },
-    "@vue/reactivity": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz",
-      "integrity": "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==",
-      "requires": {
-        "@vue/shared": "3.2.31"
-      }
-    },
-    "@vue/reactivity-transform": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz",
-      "integrity": "sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA==",
-      "requires": {
-        "@babel/parser": "^7.16.4",
-        "@vue/compiler-core": "3.2.31",
-        "@vue/shared": "3.2.31",
-        "estree-walker": "^2.0.2",
-        "magic-string": "^0.25.7"
+    "node_modules/lodash-es": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
+      "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
       }
     },
-    "@vue/runtime-core": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz",
-      "integrity": "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==",
-      "requires": {
-        "@vue/reactivity": "3.2.31",
-        "@vue/shared": "3.2.31"
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
       }
     },
-    "@vue/runtime-dom": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz",
-      "integrity": "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==",
-      "requires": {
-        "@vue/runtime-core": "3.2.31",
-        "@vue/shared": "3.2.31",
-        "csstype": "^2.6.8"
+    "node_modules/magic-string-ast": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
+      "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
+      "license": "MIT",
+      "dependencies": {
+        "magic-string": "^0.30.19"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
       }
     },
-    "@vue/server-renderer": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz",
-      "integrity": "sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==",
-      "requires": {
-        "@vue/compiler-ssr": "3.2.31",
-        "@vue/shared": "3.2.31"
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
       }
     },
-    "@vue/shared": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
-      "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
     },
-    "asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
-    },
-    "axios": {
-      "version": "0.30.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.2.tgz",
-      "integrity": "sha512-0pE4RQ4UQi1jKY6p7u6i1Tkzqmu+d+/tHS7Q7rKunWLB9WyilBTpHHpXzPNMDj5hTbK0B0PTLSz07yqMBiF6xg==",
-      "requires": {
-        "follow-redirects": "^1.15.4",
-        "form-data": "^4.0.4",
-        "proxy-from-env": "^1.1.0"
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
       }
     },
-    "call-bind-apply-helpers": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
-      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
-      "requires": {
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2"
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
       }
     },
-    "combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "requires": {
-        "delayed-stream": "~1.0.0"
+    "node_modules/mlly": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.3"
+      }
+    },
+    "node_modules/mlly/node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+      "license": "MIT"
+    },
+    "node_modules/mlly/node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
       }
     },
-    "csstype": {
-      "version": "2.6.20",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
-      "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "license": "MIT"
     },
-    "delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
-    },
-    "dunder-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
-      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
-      "requires": {
-        "call-bind-apply-helpers": "^1.0.1",
-        "es-errors": "^1.3.0",
-        "gopd": "^1.2.0"
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
-    "es-define-property": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
-      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
-    },
-    "es-errors": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
-      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+      "license": "BSD-3-Clause"
     },
-    "es-object-atoms": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
-      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
-      "requires": {
-        "es-errors": "^1.3.0"
-      }
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "license": "MIT"
     },
-    "es-set-tostringtag": {
+    "node_modules/perfect-debounce": {
       "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
-      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
-      "requires": {
-        "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.6",
-        "has-tostringtag": "^1.0.2",
-        "hasown": "^2.0.2"
-      }
-    },
-    "esbuild": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
-      "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==",
-      "dev": true,
-      "requires": {
-        "esbuild-android-64": "0.14.25",
-        "esbuild-android-arm64": "0.14.25",
-        "esbuild-darwin-64": "0.14.25",
-        "esbuild-darwin-arm64": "0.14.25",
-        "esbuild-freebsd-64": "0.14.25",
-        "esbuild-freebsd-arm64": "0.14.25",
-        "esbuild-linux-32": "0.14.25",
-        "esbuild-linux-64": "0.14.25",
-        "esbuild-linux-arm": "0.14.25",
-        "esbuild-linux-arm64": "0.14.25",
-        "esbuild-linux-mips64le": "0.14.25",
-        "esbuild-linux-ppc64le": "0.14.25",
-        "esbuild-linux-riscv64": "0.14.25",
-        "esbuild-linux-s390x": "0.14.25",
-        "esbuild-netbsd-64": "0.14.25",
-        "esbuild-openbsd-64": "0.14.25",
-        "esbuild-sunos-64": "0.14.25",
-        "esbuild-windows-32": "0.14.25",
-        "esbuild-windows-64": "0.14.25",
-        "esbuild-windows-arm64": "0.14.25"
-      }
-    },
-    "esbuild-android-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz",
-      "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-android-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz",
-      "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-darwin-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz",
-      "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-darwin-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz",
-      "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-freebsd-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz",
-      "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-freebsd-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz",
-      "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-linux-32": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz",
-      "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-linux-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz",
-      "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-linux-arm": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz",
-      "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-linux-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz",
-      "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==",
-      "dev": true,
-      "optional": true
-    },
-    "esbuild-linux-mips64le": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz",
-      "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==",
-      "dev": true,
-      "optional": true
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+      "license": "MIT"
     },
-    "esbuild-linux-ppc64le": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz",
-      "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==",
-      "dev": true,
-      "optional": true
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
     },
-    "esbuild-linux-riscv64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz",
-      "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==",
-      "dev": true,
-      "optional": true
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
     },
-    "esbuild-linux-s390x": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz",
-      "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==",
-      "dev": true,
-      "optional": true
+    "node_modules/pkg-types": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+      "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.2",
+        "exsolve": "^1.0.7",
+        "pathe": "^2.0.3"
+      }
     },
-    "esbuild-netbsd-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz",
-      "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==",
-      "dev": true,
-      "optional": true
+    "node_modules/postcss": {
+      "version": "8.5.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
     },
-    "esbuild-openbsd-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz",
-      "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==",
-      "dev": true,
-      "optional": true
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
     },
-    "esbuild-sunos-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz",
-      "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==",
-      "dev": true,
-      "optional": true
+    "node_modules/quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/antfu"
+        },
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/sxzz"
+        }
+      ],
+      "license": "MIT"
     },
-    "esbuild-windows-32": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz",
-      "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==",
-      "dev": true,
-      "optional": true
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
     },
-    "esbuild-windows-64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz",
-      "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==",
+    "node_modules/rolldown": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
+      "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==",
       "dev": true,
-      "optional": true
-    },
-    "esbuild-windows-arm64": {
-      "version": "0.14.25",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz",
-      "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==",
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.122.0",
+        "@rolldown/pluginutils": "1.0.0-rc.11"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.0-rc.11",
+        "@rolldown/binding-darwin-arm64": "1.0.0-rc.11",
+        "@rolldown/binding-darwin-x64": "1.0.0-rc.11",
+        "@rolldown/binding-freebsd-x64": "1.0.0-rc.11",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11",
+        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11",
+        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11",
+        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11",
+        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11",
+        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11",
+        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11"
+      }
+    },
+    "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.11",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz",
+      "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==",
       "dev": true,
-      "optional": true
+      "license": "MIT"
     },
-    "estree-walker": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    "node_modules/scule": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+      "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+      "license": "MIT"
     },
-    "follow-redirects": {
-      "version": "1.15.11",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
-      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
-    "form-data": {
-      "version": "4.0.4",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
-      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
-      "requires": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "es-set-tostringtag": "^2.1.0",
-        "hasown": "^2.0.2",
-        "mime-types": "^2.1.12"
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
       }
     },
-    "fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
       "dev": true,
+      "license": "0BSD",
       "optional": true
     },
-    "function-bind": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
-    },
-    "get-intrinsic": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
-      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
-      "requires": {
-        "call-bind-apply-helpers": "^1.0.2",
-        "es-define-property": "^1.0.1",
-        "es-errors": "^1.3.0",
-        "es-object-atoms": "^1.1.1",
-        "function-bind": "^1.1.2",
-        "get-proto": "^1.0.1",
-        "gopd": "^1.2.0",
-        "has-symbols": "^1.1.0",
-        "hasown": "^2.0.2",
-        "math-intrinsics": "^1.1.0"
-      }
-    },
-    "get-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
-      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
-      "requires": {
-        "dunder-proto": "^1.0.1",
-        "es-object-atoms": "^1.0.0"
-      }
-    },
-    "gopd": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
-      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
-    },
-    "has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
-      "requires": {
-        "function-bind": "^1.1.1"
+    "node_modules/ufo": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
+      "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
+      "license": "MIT"
+    },
+    "node_modules/unplugin": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
+      "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "picomatch": "^4.0.3",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "has-symbols": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
-      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
-    },
-    "has-tostringtag": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
-      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
-      "requires": {
-        "has-symbols": "^1.0.3"
+    "node_modules/unplugin-utils": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
+      "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
       }
     },
-    "hasown": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
-      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
-      "requires": {
-        "function-bind": "^1.1.2"
+    "node_modules/vant": {
+      "version": "4.9.22",
+      "resolved": "https://registry.npmjs.org/vant/-/vant-4.9.22.tgz",
+      "integrity": "sha512-P2PDSj3oB6l3W1OpVlQpapeLmI6bXMSvPqPdrw5rutslC0Y6tSkrVB/iSD57weD7K92GsjGkvgDK0eZlOsXGqw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vant/popperjs": "^1.3.0",
+        "@vant/use": "^1.6.0",
+        "@vue/shared": "^3.5.25"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
       }
     },
-    "is-core-module": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
-      "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+    "node_modules/vite": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
+      "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==",
       "dev": true,
-      "requires": {
-        "has": "^1.0.3"
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.8",
+        "rolldown": "1.0.0-rc.11",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.1.0",
+        "esbuild": "^0.27.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
       }
     },
-    "magic-string": {
-      "version": "0.25.9",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
-      "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
-      "requires": {
-        "sourcemap-codec": "^1.4.8"
+    "node_modules/vue": {
+      "version": "3.5.30",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
+      "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.30",
+        "@vue/compiler-sfc": "3.5.30",
+        "@vue/runtime-dom": "3.5.30",
+        "@vue/server-renderer": "3.5.30",
+        "@vue/shared": "3.5.30"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
       }
     },
-    "math-intrinsics": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
-      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
+    "node_modules/vue-component-type-helpers": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz",
+      "integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==",
+      "license": "MIT"
     },
-    "mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
-    },
-    "mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "requires": {
-        "mime-db": "1.52.0"
+    "node_modules/vue-i18n": {
+      "version": "11.3.0",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
+      "integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
+      "license": "MIT",
+      "dependencies": {
+        "@intlify/core-base": "11.3.0",
+        "@intlify/devtools-types": "11.3.0",
+        "@intlify/shared": "11.3.0",
+        "@vue/devtools-api": "^6.5.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
       }
     },
-    "nanoid": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
-      "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw=="
-    },
-    "path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
+    "node_modules/vue-router": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz",
+      "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/generator": "^7.28.6",
+        "@vue-macros/common": "^3.1.1",
+        "@vue/devtools-api": "^8.0.6",
+        "ast-walker-scope": "^0.8.3",
+        "chokidar": "^5.0.0",
+        "json5": "^2.2.3",
+        "local-pkg": "^1.1.2",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.0",
+        "muggle-string": "^0.4.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "scule": "^1.3.0",
+        "tinyglobby": "^0.2.15",
+        "unplugin": "^3.0.0",
+        "unplugin-utils": "^0.3.1",
+        "yaml": "^2.8.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@pinia/colada": ">=0.21.2",
+        "@vue/compiler-sfc": "^3.5.17",
+        "pinia": "^3.0.4",
+        "vue": "^3.5.0"
+      },
+      "peerDependenciesMeta": {
+        "@pinia/colada": {
+          "optional": true
+        },
+        "@vue/compiler-sfc": {
+          "optional": true
+        },
+        "pinia": {
+          "optional": true
+        }
+      }
     },
-    "picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
-    },
-    "postcss": {
-      "version": "8.4.8",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz",
-      "integrity": "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==",
-      "requires": {
-        "nanoid": "^3.3.1",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
+    "node_modules/vue-router/node_modules/@vue/devtools-api": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz",
+      "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^8.1.1"
       }
     },
-    "proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+      "license": "MIT"
     },
-    "resolve": {
-      "version": "1.22.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
-      "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
-      "dev": true,
-      "requires": {
-        "is-core-module": "^2.8.1",
-        "path-parse": "^1.0.7",
-        "supports-preserve-symlinks-flag": "^1.0.0"
+    "node_modules/yaml": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+      "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/eemeli"
       }
     },
-    "rollup": {
-      "version": "2.63.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz",
-      "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==",
-      "dev": true,
-      "requires": {
-        "fsevents": "~2.3.2"
+    "node_modules/yarn": {
+      "version": "1.22.22",
+      "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz",
+      "integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==",
+      "hasInstallScript": true,
+      "license": "BSD-2-Clause",
+      "bin": {
+        "yarn": "bin/yarn.js",
+        "yarnpkg": "bin/yarn.js"
+      },
+      "engines": {
+        "node": ">=4.0.0"
       }
-    },
-    "source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
-    },
-    "source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
-    },
-    "sourcemap-codec": {
-      "version": "1.4.8",
-      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
-      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
-    },
-    "supports-preserve-symlinks-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-      "dev": true
-    },
-    "vant": {
-      "version": "3.4.5",
-      "resolved": "https://registry.npmjs.org/vant/-/vant-3.4.5.tgz",
-      "integrity": "sha512-3an2v6ejW9pijKmaDbSCW7Hs1R45tqtTIcedfI8hEAA5zC2Bp8cXU6Z4gUCK6/RAGOJsB+vcyLINLb1cGS/SzQ==",
-      "requires": {
-        "@vant/icons": "^1.7.1",
-        "@vant/popperjs": "^1.1.0",
-        "@vant/use": "^1.3.5"
-      }
-    },
-    "vite": {
-      "version": "2.8.6",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz",
-      "integrity": "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==",
-      "dev": true,
-      "requires": {
-        "esbuild": "^0.14.14",
-        "fsevents": "~2.3.2",
-        "postcss": "^8.4.6",
-        "resolve": "^1.22.0",
-        "rollup": "^2.59.0"
-      }
-    },
-    "vue": {
-      "version": "3.2.31",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz",
-      "integrity": "sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==",
-      "requires": {
-        "@vue/compiler-dom": "3.2.31",
-        "@vue/compiler-sfc": "3.2.31",
-        "@vue/runtime-dom": "3.2.31",
-        "@vue/server-renderer": "3.2.31",
-        "@vue/shared": "3.2.31"
-      }
-    },
-    "vue-i18n": {
-      "version": "9.1.9",
-      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.9.tgz",
-      "integrity": "sha512-JeRdNVxS2OGp1E+pye5XB6+M6BBkHwAv9C80Q7+kzoMdUDGRna06tjC0vCB/jDX9aWrl5swxOMFcyAr7or8XTA==",
-      "requires": {
-        "@intlify/core-base": "9.1.9",
-        "@intlify/shared": "9.1.9",
-        "@intlify/vue-devtools": "9.1.9",
-        "@vue/devtools-api": "^6.0.0-beta.7"
-      }
-    },
-    "vue-router": {
-      "version": "4.0.14",
-      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz",
-      "integrity": "sha512-wAO6zF9zxA3u+7AkMPqw9LjoUCjSxfFvINQj3E/DceTt6uEz1XZLraDhdg2EYmvVwTBSGlLYsUw8bDmx0754Mw==",
-      "requires": {
-        "@vue/devtools-api": "^6.0.0"
-      }
-    },
-    "yarn": {
-      "version": "1.22.17",
-      "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.17.tgz",
-      "integrity": "sha512-H0p241BXaH0UN9IeH//RT82tl5PfNraVpSpEoW+ET7lmopNC61eZ+A+IDvU8FM6Go5vx162SncDL8J1ZjRBriQ=="
     }
   }
 }
diff --git a/rootfs/web/package.json b/rootfs/web/package.json
index 178c079..f08aa64 100644
--- a/rootfs/web/package.json
+++ b/rootfs/web/package.json
@@ -8,17 +8,18 @@
     "serve": "vite preview"
   },
   "dependencies": {
-    "@icon-park/vue-next": "^1.3.6",
-    "axios": "^0.30.2",
-    "vant": "^3.4.5",
-    "vue": "^3.2.31",
-    "vue-i18n": "^9.1.9",
-    "vue-router": "^4.0.14",
-    "yarn": "^1.22.17"
+    "@element-plus/icons-vue": "^2.3.2",
+    "axios": "^1.13.6",
+    "element-plus": "^2.13.6",
+    "vant": "^4.9.22",
+    "vue": "^3.5.30",
+    "vue-i18n": "^11.3.0",
+    "vue-router": "^5.0.4",
+    "yarn": "^1.22.22"
   },
   "devDependencies": {
-    "@vitejs/plugin-vue": "^2.2.4",
-    "@vue/compiler-sfc": "^3.2.31",
-    "vite": "^2.8.6"
+    "@vitejs/plugin-vue": "^6.0.5",
+    "@vue/compiler-sfc": "^3.5.30",
+    "vite": "^8.0.2"
   }
 }
diff --git a/rootfs/web/src/.env b/rootfs/web/src/.env
deleted file mode 100644
index cba474f..0000000
--- a/rootfs/web/src/.env
+++ /dev/null
@@ -1 +0,0 @@
-VITE_APP_BASE_API=/
\ No newline at end of file
diff --git a/rootfs/web/src/assets/icons/drycc.svg b/rootfs/web/src/assets/icons/drycc.svg
new file mode 100644
index 0000000..5a7cdca
--- /dev/null
+++ b/rootfs/web/src/assets/icons/drycc.svg
@@ -0,0 +1,13 @@
+<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="38" cy="38" r="32" fill="#FD2C55"/>
+  <circle cx="162" cy="38" r="32" fill="#FD2C55"/>
+  <circle cx="38" cy="162" r="32" fill="#FD2C55"/>
+  <circle cx="162" cy="162" r="32" fill="#FD2C55"/>
+  <g fill="#3BC569" stroke="#3BC569" stroke-width="12" stroke-linejoin="round">
+    <path d="M72 16 L128 16 L100 62 Z"/>
+    <path d="M72 184 L128 184 L100 138 Z"/>
+    <path d="M16 72 L16 128 L62 100 Z"/>
+    <path d="M184 72 L184 128 L138 100 Z"/>
+  </g>
+  <rect x="73" y="73" width="54" height="54" rx="12" fill="#3EBAF0"/>
+</svg>
diff --git a/rootfs/web/src/assets/icons/github.svg b/rootfs/web/src/assets/icons/github.svg
new file mode 100644
index 0000000..dc2f59a
--- /dev/null
+++ b/rootfs/web/src/assets/icons/github.svg
@@ -0,0 +1,8 @@
+<svg width="20" height="20" fill="#000000" viewBox="-0.48 -0.48 24.96 24.96" role="img" xmlns="http://www.w3.org/2000/svg">
+  <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
+  <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
+  <g id="SVGRepo_iconCarrier">
+    <title>GitHub icon</title>
+    <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/rootfs/web/src/assets/icons/google.svg b/rootfs/web/src/assets/icons/google.svg
new file mode 100644
index 0000000..e9f9e65
--- /dev/null
+++ b/rootfs/web/src/assets/icons/google.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="20" height="20">
+  <path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/><path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/><path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/><path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/>
+</svg>
\ No newline at end of file
diff --git a/rootfs/web/src/components/AccessTokenDelete.js b/rootfs/web/src/components/AccessTokenDelete.js
index 0b1eb41..987cae4 100644
--- a/rootfs/web/src/components/AccessTokenDelete.js
+++ b/rootfs/web/src/components/AccessTokenDelete.js
@@ -1,5 +1,5 @@
 import { reactive, toRefs } from 'vue'
-import { Toast } from "vant";
+import { showSuccessToast } from "vant";
 import {deleteAccessToken} from "../services/tokens";
 
 export default {
@@ -19,7 +19,7 @@ export default {
         const deleteToken = () => {
             deleteAccessToken(state.token.id).then(res=>{
                 if (res.status == 204) {
-                    Toast.success("OK")
+                    showSuccessToast("OK")
                     context.emit('closeDelete', { hasAccessTokenDeleted: true })
                 }
             })
diff --git a/rootfs/web/src/components/AccessTokenDelete.vue b/rootfs/web/src/components/AccessTokenDelete.vue
index 5420465..fe7c67d 100644
--- a/rootfs/web/src/components/AccessTokenDelete.vue
+++ b/rootfs/web/src/components/AccessTokenDelete.vue
@@ -1,31 +1,30 @@
 <template>
-    <div id="hk-slide-panels">
-        <div class="hk-slide-panel-overlay is-visible ember-view">
-            <div class="hk-slide-panel-container shadow-outer-2 flex flex-column fixed bg-white overflow-y-hidden ma2 br2 from-right standard" >
-                <div class="bg-gradient-silver hk-slide-panel-header relative flex justify-center tc items-center relative bb b--light-silver bg-white z-2 ember-view">
-                    <div class="ml4 flex-auto f3 dark-gray truncate lh-copy" >
+    <div id="ui-slide-panels">
+        <div class="ui-modal-overlay">
+            <div class="ui-modal-panel" >
+                <div class="ui-modal-header">
+                    <div class="ui-modal-title" >
                         Delete Token
                     </div>
-                    <button @click="canelDelete" class="flex bg-transparent bn mr2 mt0 pa1 pointer" type="button">
-                        <svg style="height: 16px; width: 16px;" class="icon malibu-icon fill-gray" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1158" width="200" height="200"><path d="M574.55 522.35L904.4 192.5c16.65-16.65 16.65-44.1 0-60.75l-1.8-1.8c-16.65-16.65-44.1-16.65-60.75 0L512 460.25l-329.85-330.3c-16.65-16.65-44.1-16.65-60.75 0l-1.8 1.8c-17.1 16.65-17.1 44.1 0 60.75l329.85 329.85L119.6 852.2c-16.65 16.65-16.65 44.1 0 60.75l1.8 1.8c16.65 16.65 44.1 16.65 60.75 0L512 584.9l329.85 329.85c16.65 16.65 44.1 16.65 60.75 0l1.8-1.8c16.65-16.65 16.65-44.1 0-60.75L574.55 522.35z" p-id="1159"></path></svg>
+                    <button @click="canelDelete" class="ui-modal-close" type="button">
+                        <svg style="height: 16px; width: 16px;" class="ui-modal-close__icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1158" width="200" height="200"><path d="M574.55 522.35L904.4 192.5c16.65-16.65 16.65-44.1 0-60.75l-1.8-1.8c-16.65-16.65-44.1-16.65-60.75 0L512 460.25l-329.85-330.3c-16.65-16.65-44.1-16.65-60.75 0l-1.8 1.8c-17.1 16.65-17.1 44.1 0 60.75l329.85 329.85L119.6 852.2c-16.65 16.65-16.65 44.1 0 60.75l1.8 1.8c16.65 16.65 44.1 16.65 60.75 0L512 584.9l329.85 329.85c16.65 16.65 44.1 16.65 60.75 0l1.8-1.8c16.65-16.65 16.65-44.1 0-60.75L574.55 522.35z" p-id="1159"></path></svg>
                     </button>
                 </div>
 
-                <div class="hk-slide-panel-content relative overflow-y-auto flex flex-column z-1 ember-view">
-                    <div class="flex flex-column h-100">
-                        <div class="slide-panel-shadow-cover-top z-5"></div>
-                        <div class="slide-panel-content flex-grow-1 flex-shrink-0 pa4">
-                            <div class="pb3">
-                                <div class="ember-view">
-                                    <div class="collaborator-edit">
-                                        <div class="form-group">
-                                            <div class="row">
-                                                <div class="col-sm-12">
-                                                    <label class="f5 b gray mb2">Do you want delete the Access Token of
+                <div class="ui-modal-content">
+                    <div class="ui-modal-body">
+                        <div class="ui-modal-message-wrap">
+                            <div>
+                                <div>
+                                    <div>
+                                        <div>
+                                            <div>
+                                                <div>
+                                                    <label class="ui-modal-message">Do you want delete the Access Token of
                                                       {{ token.token }}
                                                     </label>
-                                                    <div class="cp-validating-gravatar-input form-group ember-view">
-                                                        <div class="flex flex-auto">
+                                                    <div>
+                                                        <div>
                                                         </div>
                                                     </div>
                                                 </div>
@@ -34,15 +33,13 @@
                                     </div>
                                 </div>
                             </div>
-                            <button v-if="editAccess" @click="deleteToken" class="w-100 async-button default hk-button--danger ember-view" type="submit">Delete Token</button>
+                            <button v-if="editAccess" @click="deleteToken" class="ui-btn ui-btn--danger ui-btn--block" type="submit">Delete Token</button>
                         </div>
-                        <div class="slide-panel-shadow-cover-bottom z-5"></div>
                     </div>
                 </div>
-                <div class="mh4 ember-view"></div>
-                <div class="hk-slide-panel-footer relative shadow-outer-1 flex justify-center items-center z-2 pa4 ember-view">
-                    <button @click="canelDelete" class="async-button w-50 mr2 default hk-button--secondary ember-view" type="button">Cancel</button>
-                    <button @click="deleteToken" class="async-button w-50 default hk-button--primary ember-view" type="submit">Yes</button>
+                <div class="ui-modal-footer">
+                    <button @click="canelDelete" class="ui-btn ui-btn--secondary ui-btn--half" type="button">Cancel</button>
+                    <button @click="deleteToken" class="ui-btn ui-btn--primary ui-btn--half" type="submit">Yes</button>
                 </div>
             </div>
         </div>
@@ -55,5 +52,126 @@ export default AccessTokenDelete
 </script>
 
 <style scoped>
+.ui-modal-overlay {
+    position: fixed;
+    inset: 0;
+    z-index: 1000;
+    background: rgba(15, 23, 42, 0.42);
+    display: flex;
+    justify-content: flex-end;
+}
+
+.ui-modal-panel {
+    margin: 12px;
+    width: min(520px, calc(100vw - 24px));
+    background: #fff;
+    border-radius: var(--ui-radius-md);
+    box-shadow: var(--ui-shadow-card);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.ui-modal-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 14px 16px;
+    border-bottom: 1px solid var(--ui-color-border);
+}
+
+.ui-modal-title {
+    font-size: var(--ui-font-size-3xl);
+    font-weight: var(--ui-font-weight-bold);
+    color: var(--ui-color-text);
+}
+
+.ui-modal-close {
+    border: none;
+    background: transparent;
+    cursor: pointer;
+    border-radius: var(--ui-radius-sm);
+    padding: 4px;
+}
+
+.ui-modal-close__icon {
+    fill: #8a94a6;
+}
+
+.ui-modal-content {
+    padding: 16px;
+}
+
+.ui-modal-message {
+    display: block;
+    color: var(--ui-color-text);
+    font-size: var(--ui-font-size-lg);
+    font-weight: var(--ui-font-weight-semibold);
+    margin-bottom: 10px;
+}
+
+.ui-modal-footer {
+    display: flex;
+    gap: 8px;
+    padding: 12px 16px 16px;
+    border-top: 1px solid var(--ui-color-border);
+}
+
+.ui-btn {
+    border-radius: var(--ui-radius-sm);
+    border: 1px solid transparent;
+    height: 34px;
+    padding: 0 14px;
+    cursor: pointer;
+}
+
+.ui-btn--half {
+    width: 50%;
+}
+
+.ui-btn--block {
+    width: 100%;
+}
+
+.ui-btn--primary {
+    background: var(--ui-color-primary);
+    border-color: var(--ui-color-primary);
+    color: #fff;
+}
+
+.ui-btn--primary:hover {
+    background: var(--ui-color-primary-hover);
+    border-color: var(--ui-color-primary-hover);
+}
+
+.ui-btn--secondary {
+    background: #fff;
+    border-color: var(--ui-color-border);
+    color: var(--ui-color-text);
+}
+
+.ui-btn--secondary:hover {
+    border-color: var(--ui-color-primary);
+    color: var(--ui-color-primary);
+}
+
+.ui-btn--danger {
+    background: #fff5f4;
+    border-color: #fda29b;
+    color: #b42318;
+}
+
+.ui-btn--danger:hover {
+    background: #d92d20;
+    border-color: #d92d20;
+    color: #fff;
+}
+
+@media (max-width: 900px) {
+    .ui-modal-overlay {
+        justify-content: center;
+        align-items: center;
+    }
+}
 
 </style>
diff --git a/rootfs/web/src/components/Dropdown.vue b/rootfs/web/src/components/Dropdown.vue
new file mode 100644
index 0000000..8ff2770
--- /dev/null
+++ b/rootfs/web/src/components/Dropdown.vue
@@ -0,0 +1,357 @@
+<template>
+    <div
+        class="ui-top-dropdown"
+        ref="root"
+        @focusin="openMenu"
+        @focusout="handleFocusOut"
+        @mouseenter="handleMouseEnter"
+        @mouseleave="handleMouseLeave"
+        @keydown.esc="closeMenu"
+    >
+        <button
+            :id="triggerId || undefined"
+            class="ui-top-dropdown__trigger-button"
+            type="button"
+            @focus="openMenu"
+            @click.stop="openMenu"
+            :aria-expanded="isOpen ? 'true' : 'false'"
+            :aria-controls="menuId || undefined"
+        >
+            <el-icon class="ui-top-dropdown__trigger-icon" aria-hidden="true">
+                <component :is="triggerIcon" />
+            </el-icon>
+            <span class="ui-top-dropdown__trigger-label" :class="{ 'is-mobile-hidden': hideLabelOnMobile }">
+                {{ triggerLabel }}
+            </span>
+        </button>
+
+        <ul
+            :id="menuId || undefined"
+            class="ui-top-dropdown__menu"
+            :class="{ 'ui-top-dropdown__menu--open': isOpen }"
+            :style="{ width: normalizedMenuWidth }"
+        >
+            <li v-if="metaValue" class="ui-top-dropdown__item-row">
+                <div class="ui-top-dropdown__meta">
+                    <span class="ui-top-dropdown__meta-label">{{ metaLabel || 'Signed in as' }}</span>
+                    <span class="ui-top-dropdown__meta-value">{{ metaValue }}</span>
+                </div>
+            </li>
+
+            <li
+                v-for="item in items"
+                :key="item.key || item.label"
+                class="ui-top-dropdown__item-row"
+            >
+                <a
+                    v-if="item.href"
+                    class="ui-top-dropdown__link"
+                    :href="item.href"
+                    :target="item.external ? '_blank' : undefined"
+                    :rel="item.external ? 'noopener noreferrer' : undefined"
+                    @click="handleItemClick(item, $event)"
+                >
+                    <el-icon v-if="item.icon" class="ui-top-dropdown__item-icon" aria-hidden="true">
+                        <component :is="item.icon" />
+                    </el-icon>
+                    <span>{{ item.label }}</span>
+                </a>
+
+                <button
+                    v-else
+                    class="ui-top-dropdown__link ui-top-dropdown__link--button"
+                    type="button"
+                    @click="handleItemClick(item, $event)"
+                >
+                    <el-icon v-if="item.icon" class="ui-top-dropdown__item-icon" aria-hidden="true">
+                        <component :is="item.icon" />
+                    </el-icon>
+                    <span>{{ item.label }}</span>
+                </button>
+            </li>
+        </ul>
+    </div>
+</template>
+
+<script>
+export default {
+    name: "Dropdown",
+    props: {
+        triggerId: {
+            type: String,
+            default: ""
+        },
+        menuId: {
+            type: String,
+            default: ""
+        },
+        triggerLabel: {
+            type: String,
+            required: true
+        },
+        triggerIcon: {
+            type: [Object, Function],
+            required: true
+        },
+        items: {
+            type: Array,
+            default: () => []
+        },
+        metaLabel: {
+            type: String,
+            default: ""
+        },
+        metaValue: {
+            type: String,
+            default: ""
+        },
+        menuWidth: {
+            type: [Number, String],
+            default: 226
+        },
+        hideLabelOnMobile: {
+            type: Boolean,
+            default: false
+        },
+        closeOnSelect: {
+            type: Boolean,
+            default: true
+        }
+    },
+    emits: ["action"],
+    data() {
+        return {
+            isOpen: false,
+            closeTimer: null
+        }
+    },
+    computed: {
+        normalizedMenuWidth() {
+            return typeof this.menuWidth === "number" ? `${this.menuWidth}px` : this.menuWidth
+        }
+    },
+    methods: {
+        openMenu() {
+            this.clearCloseTimer()
+            this.isOpen = true
+        },
+        closeMenu() {
+            this.clearCloseTimer()
+            this.isOpen = false
+        },
+        clearCloseTimer() {
+            if (this.closeTimer) {
+                clearTimeout(this.closeTimer)
+                this.closeTimer = null
+            }
+        },
+        handleMouseEnter() {
+            this.openMenu()
+        },
+        handleMouseLeave() {
+            this.clearCloseTimer()
+            this.closeTimer = setTimeout(() => {
+                this.isOpen = false
+                this.closeTimer = null
+            }, 100)
+        },
+        handleFocusOut(event) {
+            const root = this.$refs.root
+            const nextFocused = event.relatedTarget
+            if (!root || !nextFocused || !root.contains(nextFocused)) {
+                this.closeMenu()
+            }
+        },
+        handleItemClick(item, event) {
+            if (item.actionKey) {
+                event.preventDefault()
+                this.$emit("action", item.actionKey, item)
+            }
+            if (this.closeOnSelect) {
+                this.closeMenu()
+            }
+        }
+    },
+    beforeUnmount() {
+        this.clearCloseTimer()
+    }
+}
+</script>
+
+<style scoped>
+.ui-top-dropdown {
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+}
+
+.ui-top-dropdown__trigger-button {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    gap: 6px;
+    height: 38px;
+    padding: 0 12px;
+    border-radius: 9px;
+    border: 1px solid transparent;
+    background: transparent;
+    box-sizing: border-box;
+    cursor: pointer;
+    text-decoration: none;
+    font: inherit;
+}
+
+.ui-top-dropdown__trigger-button:hover {
+    background: #f5f8fd;
+    border-color: #e1e9f5;
+}
+
+.ui-top-dropdown__trigger-icon {
+    font-size: var(--ui-font-size-3xl);
+    color: var(--ui-color-primary);
+}
+
+.ui-top-dropdown__trigger-icon :deep(svg),
+.ui-top-dropdown__item-icon :deep(svg) {
+    stroke-width: 2.4;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+}
+
+.ui-top-dropdown__trigger-label {
+    font-size: var(--ui-font-size-xl);
+    font-weight: var(--ui-font-weight-semibold);
+    color: #4e5e76;
+    line-height: 1;
+}
+
+.ui-top-dropdown__menu {
+    position: absolute;
+    right: 0;
+    top: calc(100% + 8px);
+    padding: 0;
+    margin: 0;
+    list-style: none;
+    background: #fff;
+    border: 1px solid rgba(217, 226, 239, 0.96);
+    border-radius: 14px;
+    box-shadow: 0 16px 36px rgba(16, 42, 72, 0.18);
+    overflow: hidden;
+    opacity: 0;
+    pointer-events: none;
+    transform: translateY(-6px);
+    transition: all 0.2s ease;
+}
+
+.ui-top-dropdown__menu--open {
+    opacity: 1;
+    pointer-events: auto;
+    transform: translateY(0);
+}
+
+.ui-top-dropdown__item-row {
+    border-bottom: 1px solid var(--ui-color-border);
+}
+
+.ui-top-dropdown__item-row:last-child {
+    border-bottom: none;
+}
+
+.ui-top-dropdown__meta,
+.ui-top-dropdown__link {
+    display: block;
+    padding: 11px 13px;
+    text-decoration: none;
+    color: var(--ui-color-text);
+}
+
+.ui-top-dropdown__meta {
+    background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+}
+
+.ui-top-dropdown__meta-label {
+    display: block;
+    color: #6a7b95;
+    font-size: var(--ui-font-size-sm);
+}
+
+.ui-top-dropdown__meta-value {
+    display: block;
+    margin-top: 2px;
+    color: #344a66;
+    font-size: var(--ui-font-size-md);
+    font-weight: var(--ui-font-weight-semibold);
+    max-width: 100%;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.ui-top-dropdown__link {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: var(--ui-font-size-lg);
+    font-weight: var(--ui-font-weight-medium);
+    line-height: var(--ui-line-height-tight);
+}
+
+.ui-top-dropdown__link--button {
+    width: 100%;
+    border: 0;
+    background: transparent;
+    text-align: left;
+    font: inherit;
+    cursor: pointer;
+}
+
+.ui-top-dropdown__item-icon {
+    width: 16px;
+    flex: 0 0 16px;
+    font-size: var(--ui-font-size-2xl);
+    color: var(--ui-color-primary);
+}
+
+.ui-top-dropdown__link:hover {
+    background: #f4f8fe;
+    color: var(--ui-color-primary);
+}
+
+.ui-top-dropdown__link:hover .ui-top-dropdown__item-icon {
+    color: var(--ui-color-primary);
+}
+
+.ui-top-dropdown__item-row:last-child .ui-top-dropdown__link {
+    border-radius: 0 0 14px 14px;
+}
+
+@media (max-width: 900px) {
+    .ui-top-dropdown__trigger-button {
+        padding: 0 10px;
+        gap: 5px;
+    }
+
+    .ui-top-dropdown__trigger-label {
+        font-size: var(--ui-font-size-md);
+    }
+
+    .ui-top-dropdown__menu {
+        width: 208px !important;
+    }
+}
+
+@media (max-width: 720px) {
+    .ui-top-dropdown__trigger-button {
+        padding: 0 6px;
+    }
+
+    .ui-top-dropdown__trigger-label.is-mobile-hidden {
+        display: none;
+    }
+}
+</style>
diff --git a/rootfs/web/src/components/MainFooter.vue b/rootfs/web/src/components/MainFooter.vue
index 5860a2e..1c1848d 100644
--- a/rootfs/web/src/components/MainFooter.vue
+++ b/rootfs/web/src/components/MainFooter.vue
@@ -1,78 +1,78 @@
 <template>
-    <div class="outer-footer">
-        <div app bottom fixed class="allow-if-delinquent ember-view footer" padless>
-            <div class="w-100 flex flex-column flex-row-l items-center bg-white bt b--light-silver ph3-l">
+    <div class="ui-footer-shell">
+        <div class="ui-footer">
+            <div class="ui-footer__content">
                 <template v-if="legalEnabled">
-                    <ul class="list flex ma0 pa0 f6 order-0 pv2">
-                        <li class="dn flex-l mr3">
-                            <a class="hk-link no-underline gray" href="http://www.drycc.com">drycc.com</a>
+                    <ul class="ui-footer__links ui-footer__links--left">
+                        <li class="ui-footer__item ui-footer__item--desktop">
+                            <a class="ui-footer__link" href="http://www.drycc.com">drycc.com</a>
                         </li>
-                        <li class="mr3">
-                            <a class="hk-link no-underline gray" href="https://github.com/drycc">Open Source</a>
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link" href="https://github.com/drycc">Open Source</a>
                         </li>
-                        <li class="mr3">
-                            <a class="hk-link no-underline gray" href="https://drycc.slack.com/">Community</a>
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link" href="https://drycc.slack.com/">Community</a>
                         </li>
-                        <li class="">
-                            <a class="hk-link no-underline gray" href="https://www.drycc.cc/">Documentation</a>
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link" href="https://www.drycc.cc/">Documentation</a>
                         </li>
                     </ul>
 
-                    <div class="bt b--light-silver w-100 dn-l"></div>
+                    <div class="ui-footer__divider ui-footer__divider--mobile"></div>
 
-                    <div class="flex pv2 ph3 f6 order-5 order-1-l">
-                        <a class="hk-button-sm--primary" href="https://tawk.to/chat/6234b5491ffac05b1d7f4669/1fueu8a9t"
-                            target="_blank">Support</a>
+                    <div class="ui-footer__support">
+                        <a class="ui-footer__support-btn" href="https://tawk.to/chat/6234b5491ffac05b1d7f4669/1fueu8a9t"
+                            target="_blank" rel="noopener noreferrer">Support</a>
                     </div>
 
-                    <div class="dn flex-l flex-auto order-3"></div>
+                    <div class="ui-footer__spacer"></div>
 
-                    <ul class="list flex ma0 pa0 pv2 f6 order-2 order-4-l">
-                        <li class="">
-                            <a class="hk-link no-underline gray"
+                    <ul class="ui-footer__links ui-footer__links--right">
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link"
                                 href="https://legal.doopai.com/#/cloud-services-terms">Terms of Service</a>
                         </li>
-                        <li class="ml3">
-                            <a class="hk-link no-underline gray"
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link"
                                 href="https://legal.doopai.com/#/doopai-privacy-policy">Privacy</a>
                         </li>
-                        <li class="ml3">
-                            <a class="hk-link no-underline gray"
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link"
                                 href="https://legal.doopai.com/#/doopai-cookie-policy">Cookies</a>
                         </li>
-                        <li class="ml3 gray">© {{year}} drycc.com</li>
+                        <li class="ui-footer__item ui-footer__copyright">© {{year}} drycc.com</li>
                     </ul>
                 </template>
                 <template v-else>
-                    <ul class="list flex ma0 pa0 f6 order-0 pv2">
-                        <li class="mr3">
-                            <a class="hk-link no-underline gray" href="https://github.com/drycc">Open Source</a>
+                    <ul class="ui-footer__links ui-footer__links--left">
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link" href="https://github.com/drycc">Open Source</a>
                         </li>
-                        <li class="mr3">
-                            <a class="hk-link no-underline gray" href="https://drycc.slack.com/">Community</a>
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link" href="https://drycc.slack.com/">Community</a>
                         </li>
-                        <li class="">
-                            <a class="hk-link no-underline gray" href="https://www.drycc.cc/">Documentation</a>
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link" href="https://www.drycc.cc/">Documentation</a>
                         </li>
                     </ul>
 
-                    <div class="bt b--light-silver w-100 dn-l"></div>
+                    <div class="ui-footer__divider ui-footer__divider--mobile"></div>
 
-                    <div class="dn flex-l flex-auto order-3"></div>
+                    <div class="ui-footer__spacer"></div>
 
-                    <ul class="list flex ma0 pa0 pv2 f6 order-2 order-4-l">
-                        <li class="">
-                            <a class="hk-link no-underline gray"
+                    <ul class="ui-footer__links ui-footer__links--right">
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link"
                                 href="https://tawk.to/chat/6234b5491ffac05b1d7f4669/1fueu8a9t">Support</a>
                         </li>
-                        <li class="ml3">
-                            <a class="hk-link no-underline gray"
+                        <li class="ui-footer__item">
+                            <a class="ui-footer__link"
                                 href="https://github.com/drycc/workflow/issues">Issues</a>
                         </li>
-                        <li class="ml3 gray">© {{year}} drycc.cc</li>
+                        <li class="ui-footer__item ui-footer__copyright">© {{year}} drycc.cc</li>
                     </ul>
                 </template>
-                <div class="bt b--light-silver w-100 order-4 dn-l"></div>
+                <div class="ui-footer__divider ui-footer__divider--mobile"></div>
             </div>
         </div>
     </div>
@@ -83,13 +83,124 @@ import MainFooter from "./MainFooter"
 export default MainFooter
 </script>
 <style scoped>
-.footer {
+.ui-footer {
     width: 100%;
-    position: fixed;
-    bottom: 0;
+    position: static;
+    border-top: 1px solid rgba(217, 226, 239, 0.95);
+    background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(249, 252, 255, 0.98) 100%);
+    backdrop-filter: blur(4px);
 }
 
-.outer-footer{
-    min-height: 68px;
+.ui-footer-shell {
+    min-height: 56px;
+    margin-top: auto;
+    padding-top: 6px;
+}
+
+.ui-footer__content {
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 0 16px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    min-height: 56px;
+    flex-wrap: wrap;
+}
+
+.ui-footer__links {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    flex-wrap: wrap;
+}
+
+.ui-footer__link {
+    color: var(--ui-color-text-secondary);
+    text-decoration: none;
+    font-size: var(--ui-font-size-sm);
+    line-height: 1.35;
+    font-weight: var(--ui-font-weight-medium);
+    transition: color 0.15s ease;
+}
+
+.ui-footer__link:hover {
+    color: var(--ui-color-primary);
+}
+
+.ui-footer__support {
+    margin-left: auto;
+}
+
+.ui-footer__support-btn {
+    display: inline-block;
+    border-radius: 999px;
+    padding: 5px 11px;
+    font-size: var(--ui-font-size-sm);
+    line-height: 1.2;
+    font-weight: var(--ui-font-weight-semibold);
+    text-decoration: none;
+    border: 1px solid var(--ui-color-primary);
+    background: var(--ui-color-primary);
+    color: #fff;
+}
+
+.ui-footer__support-btn:hover {
+    background: var(--ui-color-primary-hover);
+    border-color: var(--ui-color-primary-hover);
+    box-shadow: 0 8px 18px rgba(47, 128, 237, 0.3);
+}
+
+.ui-footer__spacer {
+    flex: 1;
+}
+
+.ui-footer__copyright {
+    color: #8a94a6;
+    font-size: var(--ui-font-size-sm);
+    line-height: 1.35;
+}
+
+.ui-footer__divider {
+    display: none;
+    width: 100%;
+    border-top: 1px solid var(--ui-color-border);
+}
+
+@media (max-width: 900px) {
+    .ui-footer__content {
+        min-height: unset;
+        padding: 8px 12px;
+        gap: 8px;
+    }
+
+    .ui-footer__links {
+        gap: 12px;
+    }
+
+    .ui-footer__link,
+    .ui-footer__support-btn,
+    .ui-footer__copyright {
+        font-size: var(--ui-font-size-xs);
+    }
+
+    .ui-footer__support {
+        margin-left: 0;
+    }
+
+    .ui-footer__spacer {
+        display: none;
+    }
+
+    .ui-footer__divider--mobile {
+        display: block;
+    }
+
+    .ui-footer__item--desktop {
+        display: none;
+    }
 }
 </style>
\ No newline at end of file
diff --git a/rootfs/web/src/components/MainNav.js b/rootfs/web/src/components/MainNav.js
index 095e494..9682823 100644
--- a/rootfs/web/src/components/MainNav.js
+++ b/rootfs/web/src/components/MainNav.js
@@ -1,4 +1,3 @@
-import { reactive, toRefs} from 'vue'
 import { useRouter } from 'vue-router'
 
 export default {
diff --git a/rootfs/web/src/components/MainNav.vue b/rootfs/web/src/components/MainNav.vue
index 1175c29..8ba028f 100644
--- a/rootfs/web/src/components/MainNav.vue
+++ b/rootfs/web/src/components/MainNav.vue
@@ -1,35 +1,82 @@
 <template>
-    <nav class="app-nav nav nav-tabs sub-nav ember-view">
-        <div class="limit-width">
-            <div class="sub-nav-item ember-view">
-                <a @click="goToAccessToken" class="ember-view" :class="{'active': isAccessTokenActive}">
-                    <svg style="height: 24px; width: 24px;"  class="icon malibu-icon fill-purple" >
-                        <title id="malibu-icon-ember486">Access Tokens</title>
-                        <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#overview-28"></use>
-                    </svg>
-                    <span class="sub-nav-item-name gray">Access Tokens</span>
-                </a>
-            </div>
-            <div class="sub-nav-item ember-view">
-                <a @click="goToAccountSetting" class="ember-view" :class="{'active': isAccountSettingActive}">
-                    <svg style="height: 24px; width: 24px;" class="icon malibu-icon fill-gray" >
-                        <title id="malibu-icon-ember489">Account Setting</title>
-                        <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#formation-28"></use>
-                    </svg>
-                    <span class="sub-nav-item-name gray">Account Setting</span>
-                </a>
-            </div>
+    <nav class="ui-subnav">
+        <div class="ui-subnav__inner">
+            <button @click="goToAccessToken" class="ui-subnav__item" :class="{'is-active': isAccessTokenActive}" type="button">
+                <span>Access Tokens</span>
+            </button>
+            <button @click="goToAccountSetting" class="ui-subnav__item" :class="{'is-active': isAccountSettingActive}" type="button">
+                <span>Account Setting</span>
+            </button>
         </div>
     </nav>
 </template>
 
 <script>
-import MainNav from "./MainNav"
-export default MainNav
+import { useRouter } from 'vue-router'
+
+export default {
+    props: {
+        isAccessTokenActive: {
+            type: Boolean,
+            default: false
+        },
+        isAccountSettingActive: {
+            type: Boolean,
+            default: false
+        },
+    },
+    setup(props) {
+        const router = useRouter()
+
+        const goToAccessToken = () => {
+            router.push('/access-tokens')
+        }
+
+        const goToAccountSetting = () => {
+            router.push('/account-setting')
+        }
+
+        return {
+            goToAccessToken,
+            goToAccountSetting
+        }
+    }
+}
 </script>
 
 <style scoped>
-    a:hover {
-        cursor: pointer;
-    }
+.ui-subnav {
+    padding: 14px 16px 8px;
+    border-bottom: 1px solid rgba(217, 226, 239, 0.75);
+    background: linear-gradient(180deg, rgba(247, 250, 255, 0.95) 0%, rgba(255, 255, 255, 1) 100%);
+}
+
+.ui-subnav__inner {
+    display: flex;
+    gap: 8px;
+    padding: 0;
+    flex-wrap: wrap;
+}
+
+.ui-subnav__item {
+    border: none;
+    background: #edf2fa;
+    color: var(--ui-color-text-secondary);
+    padding: 9px 16px;
+    border-radius: 999px;
+    cursor: pointer;
+    font-weight: var(--ui-font-weight-semibold);
+    transition: all 0.18s ease;
+}
+
+.ui-subnav__item:hover {
+    color: var(--ui-color-primary-hover);
+    background: var(--ui-color-primary-soft);
+}
+
+.ui-subnav__item.is-active {
+    color: #fff;
+    background: var(--ui-color-primary);
+    box-shadow: 0 8px 20px rgba(64, 158, 255, 0.34);
+}
 </style>
diff --git a/rootfs/web/src/components/NavBar.js b/rootfs/web/src/components/NavBar.js
index 821a924..b10471a 100644
--- a/rootfs/web/src/components/NavBar.js
+++ b/rootfs/web/src/components/NavBar.js
@@ -3,6 +3,12 @@ import UserMenu from "./UserMenu.vue";
 
 export default {
     name: "NavBar",
+    props: {
+        user: {
+            type: Object,
+            default: null
+        }
+    },
     components: {
         'nav-menu': NavMenu,
         'user-menu': UserMenu
diff --git a/rootfs/web/src/components/NavBar.vue b/rootfs/web/src/components/NavBar.vue
index d89c4b0..31044d8 100755
--- a/rootfs/web/src/components/NavBar.vue
+++ b/rootfs/web/src/components/NavBar.vue
@@ -1,18 +1,26 @@
 <!--导航栏-->
 <template>
-  <nav class="glostick">
-    <div class="glostick__property">
-      <a class="glostick__property__name" href="/">
-        <icon-app-store size="3rem" class="icon-logo"/>
-        <span>DRYCC</span>
-      </a>
-    </div>
-    <div class="glostick__user">
-      <nav-menu />
-      <user-menu />
-    </div>
-
-  </nav>
+  <header class="ui-top-area">
+    <nav class="ui-topbar">
+      <div class="ui-topbar__layer">
+        <div class="ui-topbar__brand-wrap">
+          <a class="ui-topbar__brand" href="/">
+            <el-icon size="2rem" class="ui-topbar__logo">
+              <img src="../../src/assets/icons/drycc.svg" />
+            </el-icon>
+          </a>
+        </div>
+        <div class="ui-topbar__right">
+          <div class="ui-topbar__center">
+            <nav-menu />
+          </div>
+          <div class="ui-topbar__actions">
+            <user-menu :user="user" />
+          </div>
+        </div>
+      </div>
+    </nav>
+  </header>
 
 </template>
 
@@ -20,3 +28,88 @@
 import NavBar from "./NavBar"
 export default NavBar
 </script>
+
+<style scoped>
+.ui-top-area {
+  position: sticky;
+  top: 0;
+  z-index: 40;
+}
+
+.ui-topbar {
+  height: 68px;
+  background: rgba(255, 255, 255, 0.95);
+  border-bottom: 1px solid rgba(217, 226, 239, 0.88);
+  backdrop-filter: blur(8px);
+  box-shadow: 0 4px 14px rgba(16, 42, 72, 0.06);
+}
+
+.ui-topbar__layer {
+  width: min(var(--ui-shell-max-width), calc(100% - (var(--ui-shell-gutter) * 2)));
+  max-width: none;
+  margin: 0 auto;
+  height: 100%;
+  padding: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  box-sizing: border-box;
+}
+
+.ui-topbar__brand {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: auto;
+  height: auto;
+  padding: 0;
+  border: none;
+  background: transparent;
+  text-decoration: none;
+  line-height: 0;
+}
+
+.ui-topbar__brand-wrap {
+  flex: 0 0 auto;
+}
+
+.ui-topbar__right {
+  display: inline-flex;
+  align-items: center;
+  gap: 2px;
+}
+
+.ui-topbar__center {
+  display: inline-flex;
+  align-items: center;
+}
+
+.ui-topbar__actions {
+  display: flex;
+  align-items: center;
+}
+
+.ui-topbar__logo img {
+  width: 40px;
+  height: 40px;
+  display: block;
+}
+
+@media (max-width: 900px) {
+  .ui-topbar__layer {
+    gap: 8px;
+  }
+
+  .ui-topbar {
+    height: 60px;
+  }
+
+  .ui-topbar__center {
+    justify-self: end;
+  }
+
+  .ui-topbar__right {
+    gap: 0;
+  }
+}
+</style>
diff --git a/rootfs/web/src/components/NavBox.vue b/rootfs/web/src/components/NavBox.vue
index 957087c..770035c 100644
--- a/rootfs/web/src/components/NavBox.vue
+++ b/rootfs/web/src/components/NavBox.vue
@@ -1,15 +1,15 @@
 <!--子导航插槽组件-->
 <template>
-    <div class="top-nav flex flex-column justify-center">
-        <div class="limit-width flex items-center flex-wrap">
-            <div class="flex-auto f4">
-                <nav class="crumb-trail flex items-center flex-auto f3 lh-copy near-black nudge-left--5 pv--3 ember-view">
+    <div class="ui-nav-box">
+        <div class="ui-nav-box__inner">
+            <div class="ui-nav-box__cluster">
+                <nav class="ui-nav-box__trail">
                     <slot name="nav-cluster"></slot>
                 </nav>
             </div>
             <slot name="create-new-app"></slot>
         </div>
-        <div class="limit-width flex items-center flex-wrap"></div>
+        <div class="ui-nav-box__inner"></div>
     </div>
 </template>
 
@@ -19,4 +19,26 @@ export default NavBox
 </script>
 
 <style scoped>
+.ui-nav-box {
+    padding: 8px 0;
+}
+
+.ui-nav-box__inner {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 8px;
+}
+
+.ui-nav-box__cluster {
+    flex: 1;
+}
+
+.ui-nav-box__trail {
+    display: flex;
+    align-items: center;
+    color: var(--ui-color-text);
+    font-size: var(--ui-font-size-2xl);
+    line-height: 1.4;
+}
 </style>
diff --git a/rootfs/web/src/components/NavMenu.js b/rootfs/web/src/components/NavMenu.js
index a55826d..5089186 100644
--- a/rootfs/web/src/components/NavMenu.js
+++ b/rootfs/web/src/components/NavMenu.js
@@ -1,23 +1,35 @@
+import { markRaw } from "vue";
+import { Comment, Document, List, Notebook } from "@element-plus/icons-vue";
+
 export default {
     name: "NavMenu",
     data() {
         return {
-            isMenuActived: false
-        }
-    },
-    methods: {
-        openOrCloseMenu() {
-            this.isMenuActived = !this.isMenuActived;
+            isMenuActived: false,
+            resourcesTriggerIcon: markRaw(List),
+            resourceItems: [
+                {
+                    key: "docs",
+                    label: "Documentation",
+                    href: "https://www.drycc.com",
+                    icon: markRaw(Document),
+                    external: true
+                },
+                {
+                    key: "community",
+                    label: "Community Support",
+                    href: "https://www.drycc.cc/community/",
+                    icon: markRaw(Comment),
+                    external: true
+                },
+                {
+                    key: "blog",
+                    label: "Drycc.cc Blog",
+                    href: "https://www.drycc.cc/blog/",
+                    icon: markRaw(Notebook),
+                    external: true
+                },
+            ]
         }
-    },
-    mounted() {
-        let _this = this
-        document.addEventListener('click', function (e) {
-            // 下面这句代码是获取 点击的区域是否包含你的菜单，如果包含，说明点击的是菜单以外，不包含则为菜单以内
-            if (e.target.id !== 'menu-nav') {
-                _this.isMenuActived = false
-            }
-
-        })
     }
 }
diff --git a/rootfs/web/src/components/NavMenu.vue b/rootfs/web/src/components/NavMenu.vue
index dbc70a9..0a52432 100644
--- a/rootfs/web/src/components/NavMenu.vue
+++ b/rootfs/web/src/components/NavMenu.vue
@@ -1,21 +1,102 @@
 <template>
-    <ul id="glostick__menu--navigator" class="glostick__menu glostick__menu--navigator" :class="{'glostick__menu--open': isMenuActived}">
-        <li class="glostick__menu__listitem">
-            <a class="glostick__menu__item glostick__menu__item--databases" href="https://github.com/drycc/">Github</a>
+    <ul id="menu-navigator" class="ui-nav-menu" :class="{'ui-nav-menu--open': isMenuActived}">
+        <li class="ui-nav-menu__item">
+            <a class="ui-nav-menu__link" href="/">
+                <el-icon class="ui-nav-menu__icon" aria-hidden="true"><Grid /></el-icon>
+                <span>Dashboard</span>
+            </a>
         </li>
-        <li class="glostick__menu__listitem">
-            <a class="glostick__menu__item glostick__menu__item--dataclips" href="https://www.drycc.com">Docs</a>
+        <li class="ui-nav-menu__item ui-nav-menu__item--dropdown">
+            <dropdown
+                trigger-id="menu-nav"
+                menu-id="ui-nav-menu-resources"
+                trigger-label="Resources"
+                :trigger-icon="resourcesTriggerIcon"
+                :items="resourceItems"
+                :menu-width="226"
+            />
         </li>
     </ul>
 </template>
 
 <script>
+import { Connection, Document, Grid } from "@element-plus/icons-vue"
 import NavMenu from "./NavMenu"
+import Dropdown from "./Dropdown.vue"
+
+NavMenu.components = {
+    ...(NavMenu.components || {}),
+    Dropdown,
+    Grid,
+    Connection,
+    Document,
+}
+
 export default NavMenu
 </script>
 
 <style scoped>
-.glostick__menu--navigator {
-    top: 42px
+.ui-nav-menu {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.ui-nav-menu__item {
+    position: relative;
+}
+
+.ui-nav-menu__link {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    text-decoration: none;
+    color: #4e5e76;
+    font-size: var(--ui-font-size-xl);
+    font-weight: var(--ui-font-weight-semibold);
+    border: 1px solid transparent;
+    border-radius: 8px;
+    padding: 8px 10px;
+    background: transparent;
+    transition: all 0.16s ease;
+}
+
+.ui-nav-menu__icon {
+    font-size: var(--ui-font-size-2xl);
+    color: var(--ui-color-primary);
+}
+
+.ui-nav-menu__icon :deep(svg) {
+    stroke-width: 2.4;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+}
+
+.ui-nav-menu__link:hover {
+    color: #394e6b;
+    background: #f5f8fd;
+    border-color: #e1e9f5;
+}
+
+.ui-nav-menu__link:hover .ui-nav-menu__icon {
+    color: var(--ui-color-primary);
+}
+
+@media (max-width: 900px) {
+    .ui-nav-menu {
+        gap: 4px;
+    }
+
+    .ui-nav-menu__link {
+        padding: 6px;
+        font-size: var(--ui-font-size-md);
+    }
+
+    .ui-nav-menu__icon {
+        font-size: var(--ui-font-size-xl);
+    }
 }
 </style>
diff --git a/rootfs/web/src/components/UserMenu.js b/rootfs/web/src/components/UserMenu.js
index d018cc0..1775713 100644
--- a/rootfs/web/src/components/UserMenu.js
+++ b/rootfs/web/src/components/UserMenu.js
@@ -1,21 +1,37 @@
-import {onMounted, reactive, toRefs} from "vue";
+import { computed, markRaw, onBeforeMount, reactive, toRefs, watch } from "vue";
 import { useRouter } from 'vue-router'
 import {dealUser, getUser, postLogout} from "../services/user";
+import { Key, Setting, SwitchButton, UserFilled } from "@element-plus/icons-vue";
 
 export default {
     name: "UserMenu",
-    data() {
-        return {
-            isMenuActived: false
-        }
-    },
-    methods: {
-        openOrCloseMenu() {
-            this.isMenuActived = !this.isMenuActived;
+    props: {
+        user: {
+            type: Object,
+            default: null
         }
     },
-    setup() {
+    setup(props) {
         const router = useRouter()
+        const normalizeUser = (input) => {
+            if (!input) {
+                return {
+                    username: null,
+                    email: null,
+                    first_name: null,
+                    last_name: null
+                }
+            }
+            if (input.data) {
+                return dealUser(input)
+            }
+            return {
+                username: input.username ?? null,
+                email: input.email ?? null,
+                first_name: input.first_name ?? null,
+                last_name: input.last_name ?? null
+            }
+        }
         const state = reactive({
             user :{
                 username: null,
@@ -32,31 +48,58 @@ export default {
             })
             location.reload(true)
         }
-        onMounted(async () => {
-            let user = sessionStorage.getItem('user')
-            if (user){
-                state.user = JSON.parse(user)
-            }else {
-                const res = await getUser()
-                state.user = dealUser(res)
-                if (state.user){
-                    sessionStorage.setItem('user', JSON.stringify(state.user))
+
+        const accountTriggerIcon = markRaw(UserFilled)
+
+        const menuItems = computed(() => [
+            {
+                key: "settings",
+                label: "Settings",
+                href: "/account-setting",
+                icon: markRaw(Setting)
+            },
+            {
+                key: "access-tokens",
+                label: "Access Tokens",
+                href: "/access-tokens",
+                icon: markRaw(Key)
+            },
+            {
+                key: "sign-out",
+                label: "Sign Out",
+                actionKey: "logout",
+                icon: markRaw(SwitchButton)
+            }
+        ])
+
+        const handleMenuAction = (actionKey) => {
+            if (actionKey === "logout") {
+                logout()
+            }
+        }
+
+        watch(
+            () => props.user,
+            (newUser) => {
+                if (newUser) {
+                    state.user = normalizeUser(newUser)
                 }
+            },
+            { immediate: true }
+        )
+
+        onBeforeMount(async () => {
+            if (!props.user) {
+                const res = await getUser()
+                state.user = normalizeUser(res)
             }
         })
 
         return {
             ...toRefs(state),
-            logout
+            accountTriggerIcon,
+            menuItems,
+            handleMenuAction
         }
-    },
-    mounted() {
-        let _this = this
-        document.addEventListener('click', function (e) {
-            // 下面这句代码是获取 点击的区域是否包含你的菜单，如果包含，说明点击的是菜单以外，不包含则为菜单以内
-            if (e.target.id !== 'menu-account') {
-                _this.isMenuActived = false
-            }
-        })
     }
 }
diff --git a/rootfs/web/src/components/UserMenu.vue b/rootfs/web/src/components/UserMenu.vue
index cec87a6..72a65cd 100644
--- a/rootfs/web/src/components/UserMenu.vue
+++ b/rootfs/web/src/components/UserMenu.vue
@@ -1,45 +1,29 @@
 <!--用户菜单-->
-<template>
-    <a class="glostick__menu-icon glostick__menu-icon--account" @click="openOrCloseMenu">
-        <div class="glostick__user__avatar--container">
-            <img class="glostick__user__avatar" src="../../src/assets/images/avatar.png" id="menu-account">
-        </div>
-        <div class="glostick__user__notification "></div>
-    </a>
-    <ul id="glostick__menu--account" class="glostick__menu glostick__menu--account" :class="{'glostick__menu--open': isMenuActived}">
-        <li class="glostick__menu__listitem">
-            <a class="glostick__menu__item glostick__menu__item--account-details" href="/account-setting">
-                <div class="glostick__user__avatar--container">
-                    <img class="glostick__account-details__avatar" :src="'/user/avatar/'+user.username" >
-                </div>
-                <div v-if="user.first_name || user.last_name" class="glostick__account-details__name">
-                  {{user.first_name}} {{user.last_name}}
-                </div>
-                <div v-else class="glostick__account-details__name">
-                  {{user.username}}
-                </div>
-                <div class="glostick__account-details__email">
-                  {{user.email}}
-                </div>
-            </a>
-        </li>
-        <li class="glostick__menu__listitem">
-            <a class="glostick__menu__item glostick__menu__item--settings" href="/account-setting">Account settings</a>
-        </li>
-
-        <li class="glostick__menu__listitem">
-            <a class="glostick__menu__item glostick__menu__item--docs" @click="logout">Sign out</a>
-        </li>
-    </ul>
+<template v-if="user.username != null">
+    <dropdown
+        trigger-id="menu-account"
+        menu-id="ui-user-menu--account"
+        trigger-label="Account"
+        :trigger-icon="accountTriggerIcon"
+        :items="menuItems"
+        meta-label="Signed in as"
+        :meta-value="user.email || user.username"
+        :menu-width="226"
+        :hide-label-on-mobile="true"
+        @action="handleMenuAction"
+    />
 </template>
 
 <script>
-import UserMenu from "./UserMenu"
-export default UserMenu
-</script>
+import { UserFilled } from "@element-plus/icons-vue"
+import Dropdown from "./Dropdown.vue"
+import UserMenu from "./UserMenu";
 
-<style scoped>
-.glostick__menu--account {
-    top: 42px
-}
-</style>
+UserMenu.components = {
+    ...(UserMenu.components || {}),
+    Dropdown,
+    UserFilled,
+};
+
+export default UserMenu;
+</script>
diff --git a/rootfs/web/src/lang/index.js b/rootfs/web/src/lang/index.js
index 786a17e..eb2bda9 100644
--- a/rootfs/web/src/lang/index.js
+++ b/rootfs/web/src/lang/index.js
@@ -11,6 +11,7 @@ const ENUM_LANG = {
 
 
 export const i18n = createI18n({
+  legacy: false,
   locale: ENUM_LANG.zhCN,
   messages: {
     [ENUM_LANG.enUS]: {
@@ -23,13 +24,11 @@ export const i18n = createI18n({
 })
 
 export const setLang = (lang) => {
-  // i18n.global.locale.value = lang
-  i18n.global.locale = lang
+  i18n.global.locale.value = lang
 }
 
 export const getLang = () => {
-  // return i18n && i18n.global.locale.value
-  return i18n && i18n.global.locale
+  return i18n && i18n.global.locale.value
 }
 
 // 获取UA语言类型
diff --git a/rootfs/web/src/main.js b/rootfs/web/src/main.js
index a357889..99f04d4 100644
--- a/rootfs/web/src/main.js
+++ b/rootfs/web/src/main.js
@@ -1,10 +1,12 @@
 import App from './App.vue'
 import router from './router'
 import { createApp } from 'vue'
-import { Toast } from 'vant';
 import { i18n, setLang, getUAgentLang } from './lang'
+import './styles/unified.css'
 import 'vant/lib/index.css';
-import {install} from '@icon-park/vue-next/es/all';
+import ElementPlus from "element-plus";
+import "element-plus/dist/index.css";
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 
 /**
  * 时间对象的格式化;
@@ -46,8 +48,11 @@ function init () {
 init()
 
 const app = createApp(App)
-install(app);
 app.use(router)
 app.use(i18n)
-app.use(Toast);
+app.use(ElementPlus);
+// registry global components for Element Plus icons
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
 app.mount('#app')
diff --git a/rootfs/web/src/services/identities.js b/rootfs/web/src/services/identities.js
new file mode 100644
index 0000000..3081dea
--- /dev/null
+++ b/rootfs/web/src/services/identities.js
@@ -0,0 +1,21 @@
+import axios from "../utils/axios";
+
+export const getIdentityProviders = () => {
+    return axios.get('/user/identity-providers/')
+}
+
+export const getLinkedIdentities = () => {
+    return axios.get('/user/identities/')
+}
+
+export const unlinkIdentity = (identityId) => {
+    return axios.delete(`/user/identities/${identityId}`)
+}
+
+export const getOAuthPending = () => {
+    return axios.get('/user/oauth/pending/')
+}
+
+export const createOAuthUser = (payload) => {
+    return axios.post('/user/oauth/create/', payload)
+}
\ No newline at end of file
diff --git a/rootfs/web/src/styles/unified.css b/rootfs/web/src/styles/unified.css
new file mode 100644
index 0000000..b55fedc
--- /dev/null
+++ b/rootfs/web/src/styles/unified.css
@@ -0,0 +1,230 @@
+:root {
+    --ui-color-bg: #eff5ff;
+    --ui-color-surface: #ffffff;
+    --ui-color-text: #192539;
+    --ui-color-text-secondary: #5f6f86;
+    --ui-color-primary: #409eff;
+    --ui-color-primary-hover: #337ecc;
+    --ui-color-primary-soft: #eaf3ff;
+    --ui-color-primary-softer: #f4f9ff;
+    --ui-color-border: #d9e2ef;
+    --ui-color-muted-bg: #eef3fa;
+    --ui-color-accent: #337ecc;
+    --ui-shell-max-width: 1240px;
+    --ui-shell-gutter: 18px;
+
+    --ui-font-family-base: "Manrope", "IBM Plex Sans", "Helvetica Neue", helvetica, arial, sans-serif;
+    --ui-font-size-base: 14px;
+    --ui-font-size-xs: 12px;
+    --ui-font-size-sm: 13px;
+    --ui-font-size-md: 14px;
+    --ui-font-size-lg: 15px;
+    --ui-font-size-xl: 16px;
+    --ui-font-size-2xl: 18px;
+    --ui-font-size-3xl: 20px;
+    --ui-font-size-4xl: 24px;
+    --ui-font-size-5xl: 30px;
+    --ui-line-height-base: 1.42857;
+    --ui-line-height-tight: 1.25;
+    --ui-line-height-normal: 1.4;
+    --ui-line-height-relaxed: 1.6;
+    --ui-font-weight-regular: 400;
+    --ui-font-weight-medium: 500;
+    --ui-font-weight-semibold: 600;
+    --ui-font-weight-bold: 700;
+
+    --ui-radius-sm: 6px;
+    --ui-radius-md: 12px;
+    --ui-radius-lg: 18px;
+
+    --ui-shadow-sm: 0 8px 18px rgba(64, 158, 255, 0.16);
+    --ui-shadow-md: 0 12px 28px rgba(64, 158, 255, 0.22);
+    --ui-shadow-card: 0 14px 32px rgba(64, 158, 255, 0.14);
+
+    /* Element Plus theme mapping */
+    --el-color-primary: var(--ui-color-primary);
+    --el-color-primary-light-3: #79bbff;
+    --el-color-primary-light-5: #a0cfff;
+    --el-color-primary-light-7: #c6e2ff;
+    --el-color-primary-light-8: #d9ecff;
+    --el-color-primary-light-9: #ecf5ff;
+    --el-color-primary-dark-2: var(--ui-color-primary-hover);
+    --el-text-color-primary: var(--ui-color-text);
+    --el-text-color-regular: var(--ui-color-text-secondary);
+    --el-border-color: var(--ui-color-border);
+    --el-border-color-light: var(--ui-color-border);
+    --el-fill-color-blank: var(--ui-color-surface);
+    --el-bg-color: var(--ui-color-surface);
+    --el-mask-color: rgba(15, 23, 42, 0.42);
+}
+
+html,
+body,
+#app {
+    min-height: 100%;
+}
+
+#app {
+    display: flex;
+    flex-direction: column;
+}
+
+#app > div {
+    min-height: 100vh;
+    display: flex;
+    flex-direction: column;
+}
+
+body {
+    margin: 0;
+    font-family: var(--ui-font-family-base);
+    font-size: var(--ui-font-size-base);
+    line-height: var(--ui-line-height-base);
+    color: var(--ui-color-text);
+    background-color: #eef2f8;
+    background-image:
+        radial-gradient(110% 68% at -8% 44%, rgba(181, 164, 232, 0.24) 0%, rgba(181, 164, 232, 0.14) 34%, rgba(181, 164, 232, 0) 72%),
+        radial-gradient(110% 68% at 108% 44%, rgba(164, 206, 241, 0.26) 0%, rgba(164, 206, 241, 0.14) 34%, rgba(164, 206, 241, 0) 72%),
+        radial-gradient(84% 36% at 50% -6%, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 78%),
+        linear-gradient(180deg, #f1f4fa 0%, #f4f1fa 52%, #edf3f9 100%);
+    background-repeat: no-repeat;
+}
+
+a {
+    color: var(--ui-color-primary);
+}
+
+a:hover {
+    color: var(--ui-color-primary-hover);
+}
+
+.main-panel {
+    width: min(var(--ui-shell-max-width), calc(100% - (var(--ui-shell-gutter) * 2)));
+    max-width: none;
+    margin: 18px auto 0;
+    padding: 0 0 24px;
+    box-sizing: border-box;
+    background: transparent;
+    flex: 1;
+}
+
+.main-content {
+    background: var(--ui-color-surface);
+    border: 1px solid rgba(217, 226, 239, 0.9);
+    border-radius: var(--ui-radius-lg);
+    box-shadow: var(--ui-shadow-card);
+    overflow: hidden;
+    animation: ui-fade-up 0.34s ease;
+}
+
+@keyframes ui-fade-up {
+    from {
+        opacity: 0;
+        transform: translateY(6px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.form-control,
+.el-input__wrapper {
+    border-radius: var(--ui-radius-sm);
+}
+
+.el-button--primary {
+    --el-button-bg-color: var(--ui-color-primary);
+    --el-button-border-color: var(--ui-color-primary);
+    --el-button-hover-bg-color: var(--ui-color-primary-hover);
+    --el-button-hover-border-color: var(--ui-color-primary-hover);
+}
+
+.el-button {
+    border-radius: var(--ui-radius-sm);
+    font-weight: var(--ui-font-weight-semibold);
+}
+
+.el-button--danger.is-plain {
+    --el-button-text-color: #b42318;
+    --el-button-border-color: #fda29b;
+    --el-button-bg-color: #fff5f4;
+    --el-button-hover-text-color: #ffffff;
+    --el-button-hover-border-color: #d92d20;
+    --el-button-hover-bg-color: #d92d20;
+}
+
+.el-table {
+    --el-table-header-bg-color: var(--ui-color-primary-softer);
+    --el-table-header-text-color: var(--ui-color-text);
+    --el-table-row-hover-bg-color: var(--ui-color-primary-softer);
+    border: 1px solid var(--ui-color-border);
+    border-radius: var(--ui-radius-md);
+    overflow: hidden;
+}
+
+.el-table th.el-table__cell {
+    font-weight: var(--ui-font-weight-bold);
+}
+
+.el-empty__description p {
+    color: var(--ui-color-text-secondary);
+}
+
+.el-message {
+    border-radius: var(--ui-radius-sm);
+    box-shadow: var(--ui-shadow-card);
+}
+
+a:focus-visible,
+button:focus-visible,
+input:focus-visible,
+.el-button:focus-visible,
+[role="button"]:focus-visible {
+    outline: 2px solid color-mix(in srgb, var(--ui-color-primary) 70%, #ffffff);
+    outline-offset: 2px;
+}
+
+@media (prefers-reduced-motion: reduce) {
+    *,
+    *::before,
+    *::after {
+        animation-duration: 0.01ms !important;
+        animation-iteration-count: 1 !important;
+        transition-duration: 0.01ms !important;
+        scroll-behavior: auto !important;
+    }
+}
+
+/* Keep Vant only for Toast usage and align its visual tone. */
+.van-toast {
+    border-radius: var(--ui-radius-sm);
+    background: rgba(15, 23, 42, 0.88);
+    color: #fff;
+    box-shadow: var(--ui-shadow-card);
+}
+
+.van-toast--loading {
+    padding: 12px;
+}
+
+@media (max-width: 576px) {
+    :root {
+        --ui-shell-gutter: 10px;
+    }
+
+    body {
+        font-size: var(--ui-font-size-sm);
+    }
+
+    .main-panel {
+        margin-top: 10px;
+        padding: 0 0 16px;
+    }
+
+    .main-content {
+        border-radius: var(--ui-radius-md);
+        box-shadow: var(--ui-shadow-sm);
+    }
+}
diff --git a/rootfs/web/src/utils/axios.js b/rootfs/web/src/utils/axios.js
index d6a2d14..607924c 100755
--- a/rootfs/web/src/utils/axios.js
+++ b/rootfs/web/src/utils/axios.js
@@ -1,5 +1,5 @@
 import axios from 'axios'
-import { Toast } from 'vant'
+import { closeToast, showLoadingToast, showToast } from 'vant'
 import {getCookie} from "./array";
 
 axios.defaults.baseURL = process.env.VUE_APP_BASE_URL
@@ -9,7 +9,7 @@ const loadingToast = [];
 
 axios.interceptors.request.use(
     (config)=>{
-        loadingToast.push(Toast.loading({
+    loadingToast.push(showLoadingToast({
             duration: 0,
             forbidClick: true,
             message: "Loading..."
@@ -25,7 +25,12 @@ axios.interceptors.request.use(
 axios.interceptors.response.use(
     response => {
       if (loadingToast.length > 0) {
-        loadingToast.pop().clear();
+        const toast = loadingToast.pop();
+        if (toast && typeof toast.close === 'function') {
+          toast.close();
+        } else {
+          closeToast();
+        }
       }
       if ([200, 201, 204].indexOf(response.status) >= 0) {
         return Promise.resolve(response);
@@ -35,9 +40,14 @@ axios.interceptors.response.use(
     },
     error => {
       if (loadingToast.length > 0) {
-        loadingToast.pop().clear();
+        const toast = loadingToast.pop();
+        if (toast && typeof toast.close === 'function') {
+          toast.close();
+        } else {
+          closeToast();
+        }
       }
-      if (error.response.status) {
+      if (error.response && error.response.status) {
         switch (error.response.status) {
           case 401:
             window.location.replace("/user/login/");
@@ -54,16 +64,17 @@ axios.interceptors.response.use(
                       forbidClick: true
                   });
               }
+              break;
           case 404:
             console.log('error.response: ', error.response)
               if(error.response.data){
-                  Toast({
+                showToast({
                       message: error.response.data.replace("\"","").replace("\"",""),
                       duration: 1500,
                       forbidClick: true
                   });
               }else {
-                  Toast({
+                showToast({
                       message: 'The request does not exist.',
                       duration: 1500,
                       forbidClick: true
@@ -73,13 +84,13 @@ axios.interceptors.response.use(
           case 400:
             console.log('error.response: ', error.response)
               if(error.response.data.detail){
-                  Toast({
+                showToast({
                       message: error.response.data.detail.replace("\"","").replace("\"",""),
                       duration: 1500,
                       forbidClick: true
                   });
               }else {
-                  Toast({
+                showToast({
                       message: 'The request parameter error',
                       duration: 1500,
                       forbidClick: true
@@ -94,8 +105,9 @@ axios.interceptors.response.use(
               forbidClick: true
             });
         }
-        return Promise.reject(error.response);
+        return Promise.reject(error);
       }
+      return Promise.reject(error);
     }
 );
 
diff --git a/rootfs/web/src/views/AccessTokenList.vue b/rootfs/web/src/views/AccessTokenList.vue
index c6daba0..679c85e 100644
--- a/rootfs/web/src/views/AccessTokenList.vue
+++ b/rootfs/web/src/views/AccessTokenList.vue
@@ -2,46 +2,55 @@
 <template>
 <div :id="'vue-content-'+Math.random().toString(36).substring(2)">
     <nav-bar />
-    <div class="main-panel bg-lightest-silver relative mt5">
+    <div class="main-panel ui-page-shell">
         <div class="main-content">
             <main-nav :is-access-token-active="true"/>
             <access-token-delete ref="accessTokenDelete" v-if="isShowDelete"
               :token="token"
               @closeDelete="closeDelete"
             />
-            <div class="w-100 mt4 ml2 mb2 mr2 limit-width" style="margin-top: 0px">
-                <table class="w-100">
+            <div class="ui-table-wrap">
+                <table class="ui-table">
                     <tbody>
-                        <tr class="w-100 f5">
-                            <th class="pl1 pr1 pv2 bb b--light-gray b">
+                        <tr class="ui-table__head-row">
+                            <th>
                                 application
                             </th>
-                            <th class="pv2 pr1 bb b--light-gray b">
+                            <th>
                                 token
                             </th>
-                            <th class="pv2 pr1 bb b--light-gray b">
+                            <th>
                                 created
                             </th>
-                            <th class="pv2 pr1 bb b--light-gray b">
+                            <th>
                                 expires
                             </th>
-                            <th class="pv2 pr1 bb b--light-gray b">
+                            <th>
                             </th>
                         </tr>
                     <template v-for="(token, index) in tokens">
-                        <tr class="ember-view">
-                            <td class="bb b--light-silver pv2 pr1 gray">{{ token.application }}</td>
-                            <td class="bb b--light-silver pv2 pr1 gray">{{ token.token }}</td>
-                            <td class="bb b--light-silver pv2 pr1 gray">{{ token.created }}</td>
-                            <td class="bb b--light-silver pv2 pr1 gray">{{ token.expires }}</td>
-                            <td class="bb b--light-silver action-cell ember-view">
-                              <button @click="showDelete(index)" class="bg-transparent hk-focus-ring--blue:focus cursor-hand br1 ba0 b--none pa--1 mr2" title="Delete" type="button" >
+                        <tr class="ui-table__body-row">
+                            <td>{{ token.application }}</td>
+                            <td>{{ token.token }}</td>
+                            <td>{{ token.created }}</td>
+                            <td>{{ token.expires }}</td>
+                            <td class="ui-table__action-cell">
+                              <button @click="showDelete(index)" class="ui-icon-btn" title="Delete" type="button" >
                                 <span class="clip">Delete</span>
-                                <svg style="height: 16px; width: 16px;" class="icon malibu-icon fill-gray hover-fill-red" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M574.55 522.35L904.4 192.5c16.65-16.65 16.65-44.1 0-60.75l-1.8-1.8c-16.65-16.65-44.1-16.65-60.75 0L512 460.25l-329.85-330.3c-16.65-16.65-44.1-16.65-60.75 0l-1.8 1.8c-17.1 16.65-17.1 44.1 0 60.75l329.85 329.85L119.6 852.2c-16.65 16.65-16.65 44.1 0 60.75l1.8 1.8c16.65 16.65 44.1 16.65 60.75 0L512 584.9l329.85 329.85c16.65 16.65 44.1 16.65 60.75 0l1.8-1.8c16.65-16.65 16.65-44.1 0-60.75L574.55 522.35z" ></path></svg>
+                                <svg style="height: 16px; width: 16px;" class="ui-icon-btn__icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M574.55 522.35L904.4 192.5c16.65-16.65 16.65-44.1 0-60.75l-1.8-1.8c-16.65-16.65-44.1-16.65-60.75 0L512 460.25l-329.85-330.3c-16.65-16.65-44.1-16.65-60.75 0l-1.8 1.8c-17.1 16.65-17.1 44.1 0 60.75l329.85 329.85L119.6 852.2c-16.65 16.65-16.65 44.1 0 60.75l1.8 1.8c16.65 16.65 44.1 16.65 60.75 0L512 584.9l329.85 329.85c16.65 16.65 44.1 16.65 60.75 0l1.8-1.8c16.65-16.65 16.65-44.1 0-60.75L574.55 522.35z" ></path></svg>
                               </button>
                             </td>
                         </tr>
                     </template>
+                    <tr v-if="!tokens || tokens.length === 0" class="ui-table__empty-row">
+                        <td class="ui-table__empty-cell" colspan="5">
+                            <div class="ui-table-empty-state">
+                                <div class="ui-table-empty-state__icon" aria-hidden="true"></div>
+                                <p class="ui-table-empty-state__title">No access tokens</p>
+                                <p class="ui-table-empty-state__desc">Create a token when an integration needs API access.</p>
+                            </div>
+                        </td>
+                    </tr>
                     </tbody>
                 </table>
             </div>
@@ -57,7 +66,129 @@ export default AccessTokenList
 </script>
 
 <style scoped>
-    .pr1 {
-        padding-right: 15px;
+.ui-page-shell {
+    padding: 14px 0 22px;
+}
+
+.ui-table-wrap {
+    margin: 14px 16px 18px;
+    border: 1px solid rgba(217, 226, 239, 0.95);
+    border-radius: 14px;
+    background: #ffffff;
+    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.65);
+    overflow-x: auto;
+}
+
+.ui-table {
+    width: 100%;
+    border-collapse: collapse;
+}
+
+.ui-table th,
+.ui-table td {
+    padding: 12px 14px;
+    border-bottom: 1px solid var(--ui-color-border);
+    text-align: left;
+    color: var(--ui-color-text);
+}
+
+.ui-table th {
+    text-transform: uppercase;
+    letter-spacing: 0.06em;
+    font-size: var(--ui-font-size-xs);
+    background: var(--ui-color-primary-softer);
+    color: #47607f;
+        font-weight: var(--ui-font-weight-bold);
+}
+
+.ui-table td:nth-child(2) {
+    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
+    font-size: var(--ui-font-size-xs);
+    color: #3b4f69;
+}
+
+.ui-table__body-row:hover td {
+    background: var(--ui-color-primary-softer);
+}
+
+.ui-table__action-cell {
+    width: 52px;
+}
+
+.ui-table__empty-cell {
+    padding: 0;
+    border-bottom: none;
+}
+
+.ui-table-empty-state {
+    min-height: 128px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 6px;
+    color: var(--ui-color-text-secondary);
+    background: transparent;
+}
+
+.ui-table-empty-state__icon {
+    width: 26px;
+    height: 26px;
+    border-radius: 999px;
+    border: 2px solid #b9cae3;
+    position: relative;
+}
+
+.ui-table-empty-state__icon::after {
+    content: "";
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    width: 8px;
+    height: 2px;
+    margin-left: -4px;
+    margin-top: -1px;
+    border-radius: 2px;
+    background: #8fa8c9;
+}
+
+.ui-table-empty-state__title {
+    margin: 0;
+    font-size: var(--ui-font-size-lg);
+        font-weight: var(--ui-font-weight-semibold);
+    color: #4f6280;
+}
+
+.ui-table-empty-state__desc {
+    margin: 0;
+    font-size: var(--ui-font-size-sm);
+}
+
+.ui-icon-btn {
+    border: 1px solid transparent;
+    background: #f4f7fc;
+    border-radius: var(--ui-radius-sm);
+    padding: 5px;
+    cursor: pointer;
+    transition: all 0.16s ease;
+}
+
+.ui-icon-btn__icon {
+    fill: #8a94a6;
+}
+
+.ui-icon-btn:hover {
+    border-color: #fda29b;
+    background: #fff5f4;
+}
+
+.ui-icon-btn:hover .ui-icon-btn__icon {
+    fill: #de0a0a;
+}
+
+@media (max-width: 900px) {
+    .ui-table-wrap {
+        margin: 10px;
     }
+}
 </style>
diff --git a/rootfs/web/src/views/AccountSetting.js b/rootfs/web/src/views/AccountSetting.js
index 7999b50..be8dbb8 100644
--- a/rootfs/web/src/views/AccountSetting.js
+++ b/rootfs/web/src/views/AccountSetting.js
@@ -1,11 +1,12 @@
 import NavBar from "../components/NavBar.vue";
 import NavBox from "../components/NavBox.vue";
-import {onBeforeMount, reactive, toRefs} from 'vue'
+import {onMounted, reactive, toRefs} from 'vue'
 import MainNav from "../components/MainNav.vue";
 import MainFooter from "../components/MainFooter.vue";
 import { useRouter } from 'vue-router'
-import { putAccount, putAccountPassword } from "../services/user";
-import { Toast } from "vant"
+import { dealUser, getUser, putAccount, putAccountPassword } from "../services/user";
+import { showFailToast, showSuccessToast } from "vant"
+import { getIdentityProviders, getLinkedIdentities, unlinkIdentity } from "../services/identities";
 
 export default {
     name: "AccountSetting",
@@ -16,32 +17,85 @@ export default {
         'main-footer': MainFooter,
     },
     setup() {
-        const showBTN = "flex items-center mb2"
-        const hiddenBTN = "flex items-center mb2 clip"
-        const hiddenUpdateBTN = "async-button default hk-button--disabled-primary ember-view"
-        const showUpdateBTN = "async-button default hk-button--primary ember-view"
+        const showBTN = "ui-inline-actions"
+        const hiddenBTN = "ui-inline-actions is-hidden"
+        const hiddenUpdateBTN = "ui-btn ui-btn--primary is-disabled"
+        const showUpdateBTN = "ui-btn ui-btn--primary"
 
         var currentPassword, newPassword, confirmNewPassword
 
         const router = useRouter()
         const state = reactive({
-            user: Object,
-            originUser: Object,
+            user: {
+                username: null,
+                email: null,
+                first_name: null,
+                last_name: null
+            },
+            originUser: {
+                username: null,
+                email: null,
+                first_name: null,
+                last_name: null
+            },
             emailBTN: hiddenBTN,
             nameBTN: hiddenBTN,
             firstNameBTN: hiddenBTN,
             lastNameBTN: hiddenBTN,
-            updateBTN: hiddenUpdateBTN
+            updateBTN: hiddenUpdateBTN,
+            providers: [],
+            identities: [],
+            loading: false,
         })
 
-        onBeforeMount(async () => {
-            state.user = JSON.parse(sessionStorage.getItem('user'))
-            state.originUser = JSON.parse(sessionStorage.getItem('user'))
+        onMounted(async () => {
+            const res = await getUser()
+            const userData = dealUser(res)
+            state.user = { ...userData }
+            state.originUser = { ...userData }
+            await fetchProviders()
+            await fetchIdentities()
         })
 
+        const fetchProviders = async () => {
+            state.loading = true
+            try {
+                const res = await getIdentityProviders()
+                state.providers = res.data?.results || []
+            } finally {
+                state.loading = false
+            }
+        }
+
+        const fetchIdentities = async () => {
+            state.loading = true
+            try {
+                const res = await getLinkedIdentities()
+                state.identities = res.data?.results || []
+            } finally {
+                state.loading = false
+            }
+        }
+
+        const handleLink = (provider) => {
+            if (!provider?.login_url) {
+                showFailToast("Provider login URL missing.")
+                return
+            }
+            window.location.href = provider.login_url
+        }
+
+        const handleUnlink = async (identity) => {
+            if (!identity?.id) {
+                return
+            }
+            await unlinkIdentity(identity.id)
+            showSuccessToast("Unlinked successfully.")
+            await fetchIdentities()
+        }
+
         const updateUser = () => {
-            sessionStorage.setItem('user', JSON.stringify(state.user))
-            state.originUser = state.user
+            state.originUser = { ...state.user }
         }
 
         const emailInputChange = (event) => {
@@ -101,7 +155,7 @@ export default {
         const submitEmail = () => {
             var re = /^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$/;
             if(!re.test(state.user.email)) {
-                Toast.fail("this email is not valid.")
+                showFailToast("this email is not valid.")
             } else {
                 putAccount({email: state.user.email}).then(res=>{
                     if (res.status == 204) {
@@ -132,11 +186,11 @@ export default {
 
         const submitPassowrd = () => {
             if (currentPassword == newPassword){
-                Toast.fail("Current Password and New Password are the same.")
+                showFailToast("Current Password and New Password are the same.")
             } else if( newPassword.length < 8){
-                Toast.fail("this New Password is not valid.")
+                showFailToast("this New Password is not valid.")
             } else if (newPassword != confirmNewPassword){
-                Toast.fail("New Password Confirm and New Password are different.")
+                showFailToast("New Password Confirm and New Password are different.")
             }else {
                 putAccountPassword({password: currentPassword, new_password: newPassword}).then(res=>{
                     if (res.status == 204) {
@@ -175,7 +229,9 @@ export default {
             currentPasswordChange,
             newPasswordChange,
             confirmNewPasswordChange,
-            submitPassowrd
+            submitPassowrd,
+            handleLink,
+            handleUnlink
         }
     },
 }
diff --git a/rootfs/web/src/views/AccountSetting.vue b/rootfs/web/src/views/AccountSetting.vue
index 675efb1..3f39c0a 100644
--- a/rootfs/web/src/views/AccountSetting.vue
+++ b/rootfs/web/src/views/AccountSetting.vue
@@ -1,163 +1,166 @@
 <!--app列表页-->
 <template>
 <div :id="'vue-content-'+Math.random().toString(36).substring(2)">
-    <nav-bar />
-    <div class="main-panel bg-lightest-silver relative mt5">
+    <nav-bar :user="user" />
+    <div class="main-panel ui-page-shell">
         <div class="main-content">
             <main-nav :is-account-setting-active="true"/>
-        <ul class="list-group list-group-lg">
-            <li class="list-group-item ember-view">
-                <div class="panel-section">
-                    <div class="section-description">
-                        <div class="section-title f3 purple" role="heading" aria-level="3">
+        <ul class="ui-settings-list">
+            <li class="ui-settings-card">
+                <div class="ui-settings-section">
+                    <div class="ui-settings-head">
+                        <div class="ui-settings-title" role="heading" aria-level="3">
                             Profile
                         </div>
-                        <div class="mt2 f5 lh-copy dark-gray">
+                        <div class="ui-settings-desc">
                             Your username is your identity on drycc and is used to log in, update account may require email confirmation.
                         </div>
                     </div>
 
-                    <div class="panel-content">
-                        <div class="avatar">
+                    <div class="ui-settings-body">
+                        <div class="ui-avatar-placeholder">
                         </div>
-                        <div class="edit-first">
-                            <div class="ember-view">
-                                <div class="ember-view">
-                                    <label class="hk-label db mb2" for="name">Username
-                                    </label>
-                                    <div class="flex flex-column flex-row-ns items-center-ns w-100">
-                                        <input autocomplete="off" placeholder="Your name"  id="username" :value=user.username
-                                                class="mr2 mb2 w-100 mw6 ember-text-field hk-input ember-view"
-                                                disabled="disabled"
-                                                type="text">
+                        <div class="ui-settings-content-wrap">
+                            <div class="ui-form-grid">
+                                <div>
+                                    <div>
+                                        <label class="ui-field-label" for="name">Username
+                                        </label>
+                                        <div class="ui-inline-field">
+                                            <input autocomplete="off" placeholder="Your name"  id="username" :value=user.username
+                                                    class="ui-input"
+                                                    disabled="disabled"
+                                                    type="text">
+                                        </div>
                                     </div>
                                 </div>
-                            </div>
-                            <div class="ember-view">
-                                <div class="ember-view">
-                                    <form role="form">
-                                        <label class="hk-label db mb2"
-                                               for="new-e-mail">Email
-                                            Address</label>
-                                        <div class="flex flex-column flex-row-ns items-center-ns w-100">
-                                            <input autocomplete="off" placeholder="Your email address" @input="emailInputChange($event)"  id="new-e-mail" :value=user.email
-                                                   class="mr2 mb2 w-100 mw6 ember-text-field hk-input ember-view"
-                                                   type="text">
-
-                                            <div :class=emailBTN data-test-target="hk-inline-buttons">
-                                                <button class="mr1 async-button default hk-button--primary ember-view" @click="submitEmail()"
-                                                        type="button"> Save
-                                                </button>
-                                                <button class="async-button default hk-button--secondary ember-view" @click="cancelEmail()"
-                                                        type="button"> Cancel
-                                                </button>
+                                <div>
+                                    <div>
+                                        <form role="form">
+                                            <label class="ui-field-label"
+                                                   for="new-e-mail">Email
+                                                Address</label>
+                                            <div class="ui-inline-field">
+                                                <input autocomplete="off" placeholder="Your email address" @input="emailInputChange($event)"  id="new-e-mail" :value=user.email
+                                                       class="ui-input"
+                                                       type="text">
+
+                                                <div :class=emailBTN>
+                                                    <button class="ui-btn ui-btn--primary" @click="submitEmail()"
+                                                            type="button"> Save
+                                                    </button>
+                                                    <button class="ui-btn ui-btn--secondary" @click="cancelEmail()"
+                                                            type="button"> Cancel
+                                                    </button>
+                                                </div>
                                             </div>
-                                        </div>
-                                        <div style="display: none" class="email_error">提示信息</div>
-                                    </form>
+                                            <div style="display: none" class="email_error">提示信息</div>
+                                        </form>
+                                    </div>
+                                    <span class="confirmable-action"> </span>
                                 </div>
-                                <span class="confirmable-action ember-view"> </span>
-                            </div>
-                            <div class="ember-view">
-                                <div class="ember-view">
-                                    <form role="form">
-                                        <label class="hk-label db mb2" for="name">Last Name
-                                        </label>
-                                        <div class="flex flex-column flex-row-ns items-center-ns w-100">
-                                            <input autocomplete="off" placeholder="Your last name" @input="lastNameInputChange($event)" id="last-name" :value=user.last_name
-                                                   class="mr2 mb2 w-100 mw6 ember-text-field hk-input ember-view"
-                                                   type="text">
-                                            <div :class=lastNameBTN data-test-target="hk-inline-buttons">
-                                                <button
-                                                        class="mr1 async-button default hk-button--primary ember-view" @click="submitLastName()"
-                                                        type="button"> Save
-                                                </button>
-                                                <button
-                                                        class="async-button default hk-button--secondary ember-view" @click="cancelLastName()"
-                                                        type="button"> Cancel
-                                                </button>
+                                <div>
+                                    <div>
+                                        <form role="form">
+                                            <label class="ui-field-label" for="name">Last Name
+                                            </label>
+                                            <div class="ui-inline-field">
+                                                <input autocomplete="off" placeholder="Your last name" @input="lastNameInputChange($event)" id="last-name" :value=user.last_name
+                                                       class="ui-input"
+                                                       type="text">
+                                                <div :class=lastNameBTN>
+                                                    <button
+                                                            class="ui-btn ui-btn--primary" @click="submitLastName()"
+                                                            type="button"> Save
+                                                    </button>
+                                                    <button
+                                                            class="ui-btn ui-btn--secondary" @click="cancelLastName()"
+                                                            type="button"> Cancel
+                                                    </button>
+                                                </div>
                                             </div>
-                                        </div>
-                                    </form>
+                                        </form>
+                                    </div>
                                 </div>
-                            </div>
-                            <div class="ember-view">
-                                <div class="ember-view">
-                                    <form role="form">
-                                        <label class="hk-label db mb2" for="name">First Name
-                                        </label>
-                                        <div class="flex flex-column flex-row-ns items-center-ns w-100">
-                                            <input autocomplete="off" placeholder="Your first name" @input="firstNameInputChange($event)" id="first-name" :value=user.first_name
-                                                   class="mr2 mb2 w-100 mw6 ember-text-field hk-input ember-view"
-                                                   type="text">
-                                            <div :class=firstNameBTN data-test-target="hk-inline-buttons">
-                                                <button
-                                                        class="mr1 async-button default hk-button--primary ember-view" @click="submitFirstName()"
-                                                        type="button"> Save
-                                                </button>
-                                                <button
-                                                        class="async-button default hk-button--secondary ember-view" @click="cancelFirstName()"
-                                                        type="button"> Cancel
-                                                </button>
+                                <div>
+                                    <div>
+                                        <form role="form">
+                                            <label class="ui-field-label" for="name">First Name
+                                            </label>
+                                            <div class="ui-inline-field">
+                                                <input autocomplete="off" placeholder="Your first name" @input="firstNameInputChange($event)" id="first-name" :value=user.first_name
+                                                       class="ui-input"
+                                                       type="text">
+                                                <div :class=firstNameBTN>
+                                                    <button
+                                                            class="ui-btn ui-btn--primary" @click="submitFirstName()"
+                                                            type="button"> Save
+                                                    </button>
+                                                    <button
+                                                            class="ui-btn ui-btn--secondary" @click="cancelFirstName()"
+                                                            type="button"> Cancel
+                                                    </button>
+                                                </div>
                                             </div>
-                                        </div>
-                                    </form>
+                                        </form>
+                                    </div>
                                 </div>
+                                <p class="ui-settings-note">
+                                Manage your avatar using <a href="https://gravatar.com" target="_blank" rel="noopener noreferrer" class="ui-inline-link">Gravatar</a>.
+                                </p>
                             </div>
-                            <p class="f5 dark-gray">
-                            Manage your avatar using <a href="https://gravatar.com" target="_blank" class="hk-link">Gravatar</a>.
-                            </p>
                         </div>
                     </div>
                 </div>
             </li>
-            <li class="list-group-item ember-view">
-                <div class="panel-section">
-                    <div class="section-description">
-                        <div class="section-title f3 purple" role="heading" aria-level="3">
+            <li class="ui-settings-card">
+                <div class="ui-settings-section">
+                    <div class="ui-settings-head">
+                        <div class="ui-settings-title" role="heading" aria-level="3">
                             Password
                         </div>
 
-                        <div class="mt2 f5 lh-copy dark-gray">
+                        <div class="ui-settings-desc">
                             Change the password here.
                         </div>
                     </div>
 
-                    <div class="panel-content">
-                        <div class="ember-view">
-                            <div class="edit-first edit-password">
-                                <form role="form" method="POST">
-                                    <div class="form-group current-password-field">
+                    <div class="ui-settings-body">
+                        <div>
+                            <div class="ui-settings-content-wrap">
+                            <div class="ui-form-grid ui-form-grid--password">
+                                <form role="form" method="POST" class="ui-password-form">
+                                    <div class="ui-form-group">
                                         <label for="current-password">Current Password</label>
                                         <input name="password" @input="currentPasswordChange($event)"
                                                placeholder="enter your current password"
                                                id="current-password"
-                                               class="mr2 mb2 w-100 mw6 form-control ember-text-field ember-view"
+                                               class="ui-input"
                                                type="password">
                                     </div>
 
-                                    <div class="form-group">
+                                    <div class="ui-form-group">
                                         <label for="new-password">New
                                             Password</label>
                                         <input placeholder="enter a new password" @input="newPasswordChange($event)"
                                                id="new-password"
-                                               class="mr2 mb2 w-100 mw6 form-control ember-text-field ember-view"
+                                               class="ui-input"
                                                type="password">
-                                        <p class="f5 dark-gray pt2">
+                                        <p class="ui-settings-note">
                                             Password must be 8 or more characters.
                                         </p>
                                     </div>
 
-                                    <div class="form-group">
+                                    <div class="ui-form-group">
                                         <label for="confirm-new-password">Confirm
                                             New Password</label>
                                         <input placeholder="enter the password again"  @input="confirmNewPasswordChange($event)"
                                                id="confirm-new-password"
-                                               class="mr2 mb2 w-100 mw6 form-control ember-text-field ember-view"
+                                               class="ui-input"
                                                type="password">
                                     </div>
 
-                                    <div class="form-group">
+                                    <div class="ui-form-group">
                                         <button @click="submitPassowrd()"
                                                 :class=updateBTN
                                                 type="button">
@@ -166,6 +169,77 @@
                                     </div>
                                 </form>
                             </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </li>
+            <li class="ui-settings-card">
+                <div class="ui-settings-section">
+                    <div class="ui-settings-head">
+                        <div class="ui-settings-title" role="heading" aria-level="3">
+                            Linked SSO
+                        </div>
+                        <div class="ui-settings-desc">
+                            Manage linked OAuth identities for your account.
+                        </div>
+                    </div>
+                    <div class="ui-settings-body">
+                        <div class="ui-identities-table-wrap">
+                            <el-table class="ui-identities-table" :data="identities" v-loading="loading" style="width: 100%">
+                                <el-table-column label="Provider">
+                                    <template #default="scope">
+                                        <el-space alignment="center" :size="8">
+                                            <span v-html="scope.row.icon"></span>
+                                            <span>{{ scope.row.display_name }}</span>
+                                        </el-space>
+                                    </template>
+                                </el-table-column>
+                                <el-table-column prop="email" label="Email" />
+                                <el-table-column label="Action" width="140">
+                                    <template #default="scope">
+                                        <el-button type="danger" plain @click="handleUnlink(scope.row)">Unlink</el-button>
+                                    </template>
+                                </el-table-column>
+                                <template #empty>
+                                    <div class="ui-table-empty-state">
+                                        <div class="ui-table-empty-state__icon" aria-hidden="true"></div>
+                                        <p class="ui-table-empty-state__title">No linked identities</p>
+                                        <p class="ui-table-empty-state__desc">Link an OAuth provider below to get started.</p>
+                                    </div>
+                                </template>
+                            </el-table>
+                        </div>
+                    </div>
+                </div>
+            </li>
+            <li class="ui-settings-card">
+                <div class="ui-settings-section">
+                    <div class="ui-settings-head">
+                        <div class="ui-settings-title" role="heading" aria-level="3">
+                            Link New SSO
+                        </div>
+                        <div class="ui-settings-desc">
+                            Choose a provider to link a new OAuth identity.
+                        </div>
+                    </div>
+                    <div class="ui-settings-body">
+                        <div v-if="providers.length === 0" class="ui-left-align">
+                            <el-empty description="No providers available." :image-size="80" />
+                        </div>
+                        <div v-else class="ui-sso-providers-wrap">
+                            <el-space wrap class="ui-sso-providers-list">
+                                <el-button
+                                    v-for="provider in providers"
+                                    :key="provider.name"
+                                    type="primary"
+                                    plain
+                                    @click.prevent="handleLink(provider)"
+                                >
+                                    <span v-html="provider.icon"></span>
+                                    <strong>Link {{ provider.name.charAt(0).toUpperCase() + provider.name.slice(1) }}</strong>
+                                </el-button>
+                            </el-space>
                         </div>
                     </div>
                 </div>
@@ -176,7 +250,346 @@
   <main-footer />
 </div>
 </template>
+<style lang="css" scoped>
+.ui-page-shell {
+    padding: 14px 0 22px;
+}
+
+.ui-settings-list {
+    list-style: none;
+    margin: 0;
+    padding: 14px;
+    display: flex;
+    flex-direction: column;
+    gap: 14px;
+}
+
+.ui-settings-card {
+    border: 1px solid rgba(217, 226, 239, 0.92);
+    border-radius: 14px;
+    padding: 22px;
+    background: linear-gradient(180deg, #ffffff 0%, #fcfdff 100%);
+    box-shadow: 0 10px 22px rgba(16, 42, 72, 0.06);
+}
+
+.ui-settings-section {
+    display: flex;
+    gap: 24px;
+}
+
+.ui-settings-head {
+    flex: 0 0 230px;
+}
+
+.ui-settings-title {
+    font-size: var(--ui-font-size-5xl);
+    line-height: 1.3;
+    color: var(--ui-color-primary);
+    font-weight: var(--ui-font-weight-bold);
+    letter-spacing: -0.01em;
+}
+
+.ui-settings-desc {
+    margin-top: 12px;
+    color: var(--ui-color-text-secondary);
+    font-size: var(--ui-font-size-xl);
+    line-height: 1.35;
+    max-width: 220px;
+}
+
+.ui-settings-body {
+    flex: 1;
+}
+
+.ui-avatar-placeholder {
+    display: none;
+}
+
+.ui-form-grid {
+    max-width: 760px;
+}
+
+.ui-form-grid > div {
+    margin-bottom: 14px;
+}
+
+.ui-form-grid > div:last-of-type {
+    margin-bottom: 0;
+}
+
+.ui-settings-content-wrap {
+    width: 100%;
+    max-width: none;
+    border: 1px solid var(--ui-color-border);
+    border-radius: 14px;
+    background: #fff;
+    padding: 14px;
+    box-sizing: border-box;
+}
+
+.ui-field-label {
+    display: block;
+    margin-bottom: 8px;
+    font-weight: var(--ui-font-weight-bold);
+    color: #2a3f5e;
+    font-size: var(--ui-font-size-lg);
+}
+
+.ui-inline-field {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    flex-wrap: wrap;
+}
+
+.ui-input {
+    width: 100%;
+    max-width: 500px;
+    height: 42px;
+    border: 1px solid var(--ui-color-border);
+    border-radius: 10px;
+    padding: 0 13px;
+    color: var(--ui-color-text);
+    background: #fbfcff;
+    box-sizing: border-box;
+    transition: all 0.16s ease;
+}
+
+.ui-input:focus {
+    outline: 2px solid rgba(47, 128, 237, 0.2);
+    border-color: var(--ui-color-primary);
+    background: #fff;
+    box-shadow: 0 6px 16px rgba(47, 128, 237, 0.16);
+}
+
+.ui-inline-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.is-hidden {
+    display: none;
+}
+
+.ui-btn {
+    border-radius: 10px;
+    border: 1px solid transparent;
+    height: 36px;
+    padding: 0 15px;
+    cursor: pointer;
+    font-weight: var(--ui-font-weight-bold);
+    transition: all 0.18s ease;
+}
+
+.ui-btn--primary {
+    background: var(--ui-color-primary);
+    border-color: var(--ui-color-primary);
+    color: #fff;
+}
+
+.ui-btn--primary:hover {
+    background: var(--ui-color-primary-hover);
+    border-color: var(--ui-color-primary-hover);
+    box-shadow: 0 8px 18px rgba(47, 128, 237, 0.28);
+}
+
+.ui-btn--secondary {
+    background: #fff;
+    border-color: var(--ui-color-border);
+    color: var(--ui-color-text);
+}
+
+.ui-btn--secondary:hover {
+    border-color: var(--ui-color-primary);
+    color: var(--ui-color-primary);
+}
+
+.is-disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+}
+
+.ui-form-group {
+    margin-bottom: 16px;
+}
+
+.ui-password-form {
+    width: 100%;
+}
+
+.ui-form-grid--password .ui-form-group {
+    display: block;
+    margin-bottom: 14px;
+}
+
+.ui-form-grid--password .ui-form-group > label {
+    display: block;
+    margin-bottom: 8px;
+    color: #2f3f56;
+    font-weight: var(--ui-font-weight-semibold);
+}
+
+.ui-form-grid--password .ui-form-group > .ui-input {
+    width: 100%;
+    max-width: 500px;
+}
+
+.ui-form-grid--password .ui-form-group > .ui-settings-note {
+    margin: 8px 0 0;
+    font-size: var(--ui-font-size-md);
+}
+
+.ui-form-grid--password .ui-form-group > .ui-btn {
+    margin-top: 4px;
+}
+
+.ui-identities-table-wrap {
+    width: 100%;
+    border: 1px solid var(--ui-color-border);
+    border-radius: 14px;
+    overflow: hidden;
+    background: #fff;
+    box-sizing: border-box;
+}
+
+:deep(.ui-identities-table) {
+    border: none;
+    border-radius: 0;
+    overflow: visible;
+}
+
+:deep(.ui-identities-table .el-table__inner-wrapper),
+:deep(.ui-identities-table .el-table__header-wrapper),
+:deep(.ui-identities-table .el-table__body-wrapper) {
+    border-radius: 0;
+}
+
+.ui-sso-providers-wrap {
+    width: 100%;
+    border: 1px solid var(--ui-color-border);
+    border-radius: 14px;
+    background: #fff;
+    min-height: 92px;
+    padding: 14px;
+    box-sizing: border-box;
+}
+
+.ui-sso-providers-list {
+    width: 100%;
+}
+
+.ui-table-empty-state {
+    min-height: 120px;
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 6px;
+    color: var(--ui-color-text-secondary);
+    background: transparent;
+}
+
+.ui-table-empty-state__icon {
+    width: 26px;
+    height: 26px;
+    border-radius: 999px;
+    border: 2px solid #b9cae3;
+    position: relative;
+}
+
+.ui-table-empty-state__icon::after {
+    content: "";
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    width: 8px;
+    height: 2px;
+    margin-left: -4px;
+    margin-top: -1px;
+    border-radius: 2px;
+    background: #8fa8c9;
+}
+
+.ui-table-empty-state__title {
+    margin: 0;
+    font-size: var(--ui-font-size-lg);
+    font-weight: var(--ui-font-weight-semibold);
+    color: #4f6280;
+}
+
+.ui-table-empty-state__desc {
+    margin: 0;
+    font-size: var(--ui-font-size-sm);
+}
+
+:deep(.ui-identities-table .el-table__empty-block) {
+    width: 100%;
+    min-height: 120px;
+}
+
+:deep(.ui-identities-table .el-table__empty-text) {
+    width: 100%;
+    margin: 0;
+    line-height: normal;
+}
+
+.ui-settings-note {
+    color: var(--ui-color-text-secondary);
+    margin: 10px 0 0;
+    font-size: var(--ui-font-size-lg);
+}
+
+.ui-inline-link {
+    color: var(--ui-color-primary);
+    text-decoration: none;
+}
+
+.ui-inline-link:hover {
+    color: var(--ui-color-primary-hover);
+}
+
+.ui-left-align {
+    text-align: left;
+}
+
+.el-empty-custom {
+    --el-empty-padding: 0px;
+}
+
+@media (max-width: 900px) {
+    .ui-settings-card {
+        padding: 16px;
+    }
+
+    .ui-settings-section {
+        flex-direction: column;
+        gap: 16px;
+    }
+
+    .ui-settings-head {
+        flex: 1 1 auto;
+    }
+
+    .ui-settings-title {
+        font-size: var(--ui-font-size-4xl);
+    }
+
+    .ui-settings-desc {
+        font-size: var(--ui-font-size-lg);
+        max-width: none;
+    }
+
+    .ui-settings-content-wrap {
+        padding: 12px;
+    }
 
+    .ui-form-grid--password .ui-form-group {
+        margin-bottom: 12px;
+    }
+}
+</style>
 <script>
 import AccountSetting from  "./AccountSetting"
 export default AccountSetting
diff --git a/rootfs/web/vite.config.js b/rootfs/web/vite.config.js
index 04b0adc..2dbbb43 100644
--- a/rootfs/web/vite.config.js
+++ b/rootfs/web/vite.config.js
@@ -1,38 +1,69 @@
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
 
-VUE_APP_BASE_URL = process.env.VUE_APP_BASE_URL ? process.env.VUE_APP_BASE_URL : ''
+VUE_APP_BASE_URL = process.env.VUE_APP_BASE_URL
+  ? process.env.VUE_APP_BASE_URL
+  : "";
 
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [vue()],
   build: {
     minify: true,
-    chunkSizeWarningLimit: 3000
+    chunkSizeWarningLimit: 3000,
+    rollupOptions: {
+      output: {
+        manualChunks(id) {
+          if (id.includes('element-plus')) {
+            return 'element-plus';
+          }
+        },
+      },
+    },
   },
-  server:{
-    proxy: { // 代理配置
-      '/avatar': {
+  server: {
+    proxy: {
+      // 代理配置
+      "/avatar": {
         target: VUE_APP_BASE_URL,
-        changeOrigin: true,
+        changeOrigin: false,
+        xfwd: true,
       },
-      '/user': {
+      "/orgs": {
         target: VUE_APP_BASE_URL,
-        changeOrigin: true,
+        changeOrigin: false,
+        xfwd: true,
       },
-      '/assets': {
+      "/user": {
         target: VUE_APP_BASE_URL,
-        changeOrigin: true,
+        changeOrigin: false,
+        xfwd: true,
       },
-      '/settings': {
+      "/auth": {
         target: VUE_APP_BASE_URL,
-        changeOrigin: true,
+        changeOrigin: false,
+        xfwd: true,
+      },
+      "/oauth": {
+        target: VUE_APP_BASE_URL,
+        changeOrigin: false,
+        xfwd: true,
+      },
+      "/assets": {
+        target: VUE_APP_BASE_URL,
+        changeOrigin: false,
+        xfwd: true,
+      },
+      "/settings": {
+        target: VUE_APP_BASE_URL,
+        changeOrigin: false,
+        xfwd: true,
       },
     },
   },
   define: {
-    'process.env': {
+    "process.env": {
       VUE_APP_BASE_URL: VUE_APP_BASE_URL,
     },
-  }
-})
+  },
+});
diff --git a/rootfs/web/yarn.lock b/rootfs/web/yarn.lock
deleted file mode 100644
index dfd0be5..0000000
--- a/rootfs/web/yarn.lock
+++ /dev/null
@@ -1,541 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@babel/parser@^7.16.4":
-  version "7.16.7"
-  resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz"
-
-"@icon-park/vue-next@^1.3.6":
-  version "1.3.6"
-  resolved "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.3.6.tgz"
-
-"@intlify/core-base@9.1.9":
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz"
-  dependencies:
-    "@intlify/devtools-if" "9.1.9"
-    "@intlify/message-compiler" "9.1.9"
-    "@intlify/message-resolver" "9.1.9"
-    "@intlify/runtime" "9.1.9"
-    "@intlify/shared" "9.1.9"
-    "@intlify/vue-devtools" "9.1.9"
-
-"@intlify/devtools-if@9.1.9":
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz"
-  dependencies:
-    "@intlify/shared" "9.1.9"
-
-"@intlify/message-compiler@9.1.9":
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz"
-  dependencies:
-    "@intlify/message-resolver" "9.1.9"
-    "@intlify/shared" "9.1.9"
-    source-map "0.6.1"
-
-"@intlify/message-resolver@9.1.9":
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz"
-
-"@intlify/runtime@9.1.9":
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz"
-  dependencies:
-    "@intlify/message-compiler" "9.1.9"
-    "@intlify/message-resolver" "9.1.9"
-    "@intlify/shared" "9.1.9"
-
-"@intlify/shared@9.1.9":
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz"
-
-"@intlify/vue-devtools@9.1.9":
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz"
-  dependencies:
-    "@intlify/message-resolver" "9.1.9"
-    "@intlify/runtime" "9.1.9"
-    "@intlify/shared" "9.1.9"
-
-"@popperjs/core@^2.9.2":
-  version "2.11.2"
-  resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz"
-
-"@vant/icons@^1.7.1":
-  version "1.7.3"
-  resolved "https://registry.npmjs.org/@vant/icons/-/icons-1.7.3.tgz"
-
-"@vant/popperjs@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.1.0.tgz"
-  dependencies:
-    "@popperjs/core" "^2.9.2"
-
-"@vant/use@^1.3.5":
-  version "1.3.6"
-  resolved "https://registry.npmjs.org/@vant/use/-/use-1.3.6.tgz"
-
-"@vitejs/plugin-vue@^2.2.4":
-  version "2.2.4"
-  resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz"
-
-"@vue/compiler-core@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz"
-  dependencies:
-    "@babel/parser" "^7.16.4"
-    "@vue/shared" "3.2.31"
-    estree-walker "^2.0.2"
-    source-map "^0.6.1"
-
-"@vue/compiler-dom@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz"
-  dependencies:
-    "@vue/compiler-core" "3.2.31"
-    "@vue/shared" "3.2.31"
-
-"@vue/compiler-sfc@3.2.31", "@vue/compiler-sfc@^3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz"
-  dependencies:
-    "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.31"
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/compiler-ssr" "3.2.31"
-    "@vue/reactivity-transform" "3.2.31"
-    "@vue/shared" "3.2.31"
-    estree-walker "^2.0.2"
-    magic-string "^0.25.7"
-    postcss "^8.1.10"
-    source-map "^0.6.1"
-
-"@vue/compiler-ssr@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz"
-  dependencies:
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/shared" "3.2.31"
-
-"@vue/devtools-api@^6.0.0", "@vue/devtools-api@^6.0.0-beta.7":
-  version "6.0.13"
-  resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.13.tgz"
-
-"@vue/reactivity-transform@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz"
-  dependencies:
-    "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.31"
-    "@vue/shared" "3.2.31"
-    estree-walker "^2.0.2"
-    magic-string "^0.25.7"
-
-"@vue/reactivity@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz"
-  dependencies:
-    "@vue/shared" "3.2.31"
-
-"@vue/runtime-core@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz"
-  dependencies:
-    "@vue/reactivity" "3.2.31"
-    "@vue/shared" "3.2.31"
-
-"@vue/runtime-dom@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz"
-  dependencies:
-    "@vue/runtime-core" "3.2.31"
-    "@vue/shared" "3.2.31"
-    csstype "^2.6.8"
-
-"@vue/server-renderer@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz"
-  dependencies:
-    "@vue/compiler-ssr" "3.2.31"
-    "@vue/shared" "3.2.31"
-
-"@vue/shared@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz"
-
-asynckit@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
-
-axios@^0.30.2:
-  version "0.30.2"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.30.2.tgz#256d3a8ee765cc27188d08b8b545a5f5a0c77dad"
-  dependencies:
-    follow-redirects "^1.15.4"
-    form-data "^4.0.4"
-    proxy-from-env "^1.1.0"
-
-call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
-  dependencies:
-    es-errors "^1.3.0"
-    function-bind "^1.1.2"
-
-combined-stream@^1.0.8:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
-  dependencies:
-    delayed-stream "~1.0.0"
-
-csstype@^2.6.8:
-  version "2.6.20"
-  resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz"
-
-delayed-stream@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
-
-dunder-proto@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
-  dependencies:
-    call-bind-apply-helpers "^1.0.1"
-    es-errors "^1.3.0"
-    gopd "^1.2.0"
-
-es-define-property@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
-
-es-errors@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
-
-es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
-  dependencies:
-    es-errors "^1.3.0"
-
-es-set-tostringtag@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
-  dependencies:
-    es-errors "^1.3.0"
-    get-intrinsic "^1.2.6"
-    has-tostringtag "^1.0.2"
-    hasown "^2.0.2"
-
-esbuild-android-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz#d532d38cb5fe0ae45167ce35f4bbc784c636be40"
-
-esbuild-android-arm64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz#9c5bb3366aabfd14a1c726d36978b79441dfcb6e"
-
-esbuild-darwin-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz#05dcdb6d884f427039ffee5e92ff97527e56c26d"
-
-esbuild-darwin-arm64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz#28e080da4ea0cfe9498071e7f8060498caee1a95"
-
-esbuild-freebsd-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz#200d3664a3b945bc9fdcba73614b49a11ebd1cfa"
-
-esbuild-freebsd-arm64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz#624b08c5da6013bdc312aaa23c4ff409580f5c3c"
-
-esbuild-linux-32@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz#0238e597eb0b60aa06c7e98fccbbfd6bb9a0d6c5"
-
-esbuild-linux-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz"
-
-esbuild-linux-arm64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz#7ac94371418a2640ba413bc1700aaedeb2794e52"
-
-esbuild-linux-arm@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz#034bd18e9310b9f010c89f90ef7f05706689600b"
-
-esbuild-linux-mips64le@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz#05f98a8cf6b578eab6b4e6b0ab094f37530934f4"
-
-esbuild-linux-ppc64le@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz#46fd0add8d8535678439d7a9c2876ad20042d952"
-
-esbuild-linux-riscv64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz#ea2e986f0f3e5df73c635135dd778051734fc605"
-
-esbuild-linux-s390x@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz#efe89486e9a1b1508925048076e3f3a6698aa6a3"
-
-esbuild-netbsd-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz#439fe27d8ee3b5887501ee63988e85f920107db6"
-
-esbuild-openbsd-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz#31ebf616aadf6e60674469f2b92cec92280d9930"
-
-esbuild-sunos-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz#815e4f936d74970292a63ccfd5791fe5e3569f5f"
-
-esbuild-windows-32@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz#189e14df2478f2c193c86968ab1fb54e1ceaafd2"
-
-esbuild-windows-64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz#3d5fbfdc3856850bb47439299e3b60dd18be111f"
-
-esbuild-windows-arm64@0.14.25:
-  version "0.14.25"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz#8b243cbbad8a86cf98697da9ccb88c05df2ef458"
-
-esbuild@^0.14.14:
-  version "0.14.25"
-  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz"
-  optionalDependencies:
-    esbuild-android-64 "0.14.25"
-    esbuild-android-arm64 "0.14.25"
-    esbuild-darwin-64 "0.14.25"
-    esbuild-darwin-arm64 "0.14.25"
-    esbuild-freebsd-64 "0.14.25"
-    esbuild-freebsd-arm64 "0.14.25"
-    esbuild-linux-32 "0.14.25"
-    esbuild-linux-64 "0.14.25"
-    esbuild-linux-arm "0.14.25"
-    esbuild-linux-arm64 "0.14.25"
-    esbuild-linux-mips64le "0.14.25"
-    esbuild-linux-ppc64le "0.14.25"
-    esbuild-linux-riscv64 "0.14.25"
-    esbuild-linux-s390x "0.14.25"
-    esbuild-netbsd-64 "0.14.25"
-    esbuild-openbsd-64 "0.14.25"
-    esbuild-sunos-64 "0.14.25"
-    esbuild-windows-32 "0.14.25"
-    esbuild-windows-64 "0.14.25"
-    esbuild-windows-arm64 "0.14.25"
-
-estree-walker@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
-
-follow-redirects@^1.15.4:
-  version "1.15.11"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
-
-form-data@^4.0.4:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
-  dependencies:
-    asynckit "^0.4.0"
-    combined-stream "^1.0.8"
-    es-set-tostringtag "^2.1.0"
-    hasown "^2.0.2"
-    mime-types "^2.1.12"
-
-fsevents@~2.3.2:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
-
-function-bind@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
-
-function-bind@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
-
-get-intrinsic@^1.2.6:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
-  dependencies:
-    call-bind-apply-helpers "^1.0.2"
-    es-define-property "^1.0.1"
-    es-errors "^1.3.0"
-    es-object-atoms "^1.1.1"
-    function-bind "^1.1.2"
-    get-proto "^1.0.1"
-    gopd "^1.2.0"
-    has-symbols "^1.1.0"
-    hasown "^2.0.2"
-    math-intrinsics "^1.1.0"
-
-get-proto@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
-  dependencies:
-    dunder-proto "^1.0.1"
-    es-object-atoms "^1.0.0"
-
-gopd@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
-
-has-symbols@^1.0.3, has-symbols@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
-
-has-tostringtag@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
-  dependencies:
-    has-symbols "^1.0.3"
-
-has@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
-  dependencies:
-    function-bind "^1.1.1"
-
-hasown@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
-  dependencies:
-    function-bind "^1.1.2"
-
-is-core-module@^2.8.1:
-  version "2.8.1"
-  resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz"
-  dependencies:
-    has "^1.0.3"
-
-magic-string@^0.25.7:
-  version "0.25.9"
-  resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz"
-  dependencies:
-    sourcemap-codec "^1.4.8"
-
-math-intrinsics@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
-
-mime-db@1.52.0:
-  version "1.52.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
-
-mime-types@^2.1.12:
-  version "2.1.35"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
-  dependencies:
-    mime-db "1.52.0"
-
-nanoid@^3.3.1:
-  version "3.3.1"
-  resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz"
-
-path-parse@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
-
-picocolors@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
-
-postcss@^8.1.10, postcss@^8.4.6:
-  version "8.4.8"
-  resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz"
-  dependencies:
-    nanoid "^3.3.1"
-    picocolors "^1.0.0"
-    source-map-js "^1.0.2"
-
-proxy-from-env@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
-
-resolve@^1.22.0:
-  version "1.22.0"
-  resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz"
-  dependencies:
-    is-core-module "^2.8.1"
-    path-parse "^1.0.7"
-    supports-preserve-symlinks-flag "^1.0.0"
-
-rollup@^2.59.0:
-  version "2.63.0"
-  resolved "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz"
-  optionalDependencies:
-    fsevents "~2.3.2"
-
-source-map-js@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
-
-source-map@0.6.1, source-map@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
-
-sourcemap-codec@^1.4.8:
-  version "1.4.8"
-  resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
-
-supports-preserve-symlinks-flag@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
-
-vant@^3.4.5:
-  version "3.4.5"
-  resolved "https://registry.npmjs.org/vant/-/vant-3.4.5.tgz"
-  dependencies:
-    "@vant/icons" "^1.7.1"
-    "@vant/popperjs" "^1.1.0"
-    "@vant/use" "^1.3.5"
-
-vite@^2.8.6:
-  version "2.8.6"
-  resolved "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz"
-  dependencies:
-    esbuild "^0.14.14"
-    postcss "^8.4.6"
-    resolve "^1.22.0"
-    rollup "^2.59.0"
-  optionalDependencies:
-    fsevents "~2.3.2"
-
-vue-i18n@^9.1.9:
-  version "9.1.9"
-  resolved "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.9.tgz"
-  dependencies:
-    "@intlify/core-base" "9.1.9"
-    "@intlify/shared" "9.1.9"
-    "@intlify/vue-devtools" "9.1.9"
-    "@vue/devtools-api" "^6.0.0-beta.7"
-
-vue-router@^4.0.14:
-  version "4.0.14"
-  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz"
-  dependencies:
-    "@vue/devtools-api" "^6.0.0"
-
-vue@^3.2.31:
-  version "3.2.31"
-  resolved "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz"
-  dependencies:
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/compiler-sfc" "3.2.31"
-    "@vue/runtime-dom" "3.2.31"
-    "@vue/server-renderer" "3.2.31"
-    "@vue/shared" "3.2.31"
-
-yarn@^1.22.17:
-  version "1.22.17"
-  resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.17.tgz"

From cca4b2dc36b340553d622c511582d5fbd7b0b6f2 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 25 Mar 2026 09:18:24 +0800
Subject: [PATCH 21/30] chore(web): remove vant package

---
 rootfs/api/static/css/main.css                |  53 ++++--
 rootfs/api/templates/base/footer.html         |   4 +-
 rootfs/api/templates/base/layout.html         |  20 +++
 .../templates/oauth2_provider/authorize.html  |  79 ++++-----
 .../user/account_activation_done.html         |   4 +-
 .../user/account_activation_fail.html         |   2 +-
 .../templates/user/account_update_done.html   |   4 +-
 .../templates/user/account_update_fail.html   |   2 +-
 rootfs/api/templates/user/login.html          | 109 ++++++------
 rootfs/api/templates/user/login_done.html     |   2 +-
 rootfs/api/templates/user/logout.html         |   4 +-
 rootfs/api/templates/user/message.html        |  28 +--
 rootfs/api/templates/user/oauth_callback.html | 159 +++++++++---------
 .../user/password_reset_complete.html         |   2 +-
 .../user/password_reset_confirm.html          |  57 +++----
 .../templates/user/password_reset_done.html   |   2 +-
 .../templates/user/password_reset_fail.html   |   2 +-
 .../templates/user/password_reset_form.html   |  40 ++---
 rootfs/api/templates/user/registration.html   | 118 ++++++-------
 .../templates/user/registration_disable.html  |   2 +-
 .../api/templates/user/registration_done.html |   2 +-
 rootfs/api/urls.py                            |   4 +-
 rootfs/requirements.txt                       |   2 +-
 rootfs/web/package-lock.json                  |  30 ----
 rootfs/web/package.json                       |   1 -
 .../web/src/components/AccessTokenDelete.js   |   4 +-
 rootfs/web/src/lang/en_US.js                  |  94 +----------
 rootfs/web/src/lang/zh_CN.js                  |  95 +----------
 rootfs/web/src/main.js                        |   1 -
 rootfs/web/src/utils/axios.js                 |  66 +++-----
 rootfs/web/src/views/AccountSetting.js        |  14 +-
 31 files changed, 382 insertions(+), 624 deletions(-)
 create mode 100644 rootfs/api/templates/base/layout.html

diff --git a/rootfs/api/static/css/main.css b/rootfs/api/static/css/main.css
index 43cd30d..685b23d 100644
--- a/rootfs/api/static/css/main.css
+++ b/rootfs/api/static/css/main.css
@@ -28,12 +28,10 @@
     --ui-space-5: 20px;
     --ui-space-6: 24px;
     --ui-space-7: 32px;
+}
 
-    --ui-breakpoint-xs: 320px;
-    --ui-breakpoint-sm: 576px;
-    --ui-breakpoint-md: 768px;
-    --ui-breakpoint-lg: 992px;
-    --ui-breakpoint-xl: 1200px;
+*, *::before, *::after {
+    box-sizing: border-box;
 }
 
 html,body {
@@ -50,7 +48,7 @@ body {
 }
 
 .privacy {
-    text-align: justify!important;
+    text-align: justify;
 }
 
 .page-wrap {
@@ -97,7 +95,8 @@ h1.logo a:before {
 }
 
 .panel {
-    min-width: 490px;
+    min-width: 0;
+    width: 100%;
     max-width: 520px;
     margin: 0 auto;
     border-radius: var(--ui-radius-lg);
@@ -158,10 +157,6 @@ input:-webkit-autofill {
     box-shadow: inset 0 1px 2px rgba(203,203,210,0.4), inset 0 0 10px 1000px #fffedb;
 }
 
-.form-control {
-    padding-right: 8px;
-}
-
 .form-control {
     display: block;
     width: 100%;
@@ -172,7 +167,6 @@ input:-webkit-autofill {
     border-radius: var(--ui-radius-sm);
     transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
     background-color: var(--ui-color-surface);
-    box-sizing: border-box;
 }
 
 .form-control:focus {
@@ -252,9 +246,7 @@ form .btn {
 
 @media (max-width: 576px) {
     .panel {
-        min-width: auto;
-        width: 100%;
-        max-width: 520px;
+        max-width: 100%;
     }
 
     .panel .h3 {
@@ -272,6 +264,36 @@ form .btn {
         flex: 1 0 100%;
         width: 100%;
     }
+
+    .panel-footer {
+        font-size: 15px;
+        padding: 16px;
+    }
+
+    .inverted-wrapper {
+        padding: 20px 20px 0 20px;
+    }
+
+    .legal li {
+        display: block;
+        margin: 8px 0;
+    }
+}
+
+@media (min-width: 577px) and (max-width: 768px) {
+    .panel {
+        max-width: 490px;
+    }
+}
+
+/* hCaptcha responsive */
+.h-captcha {
+    display: flex;
+    justify-content: center;
+}
+
+.h-captcha iframe {
+    max-width: 100%;
 }
 
 .btn-block {
@@ -322,7 +344,6 @@ form .btn {
     max-width: none;
     justify-content: center;
     box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-    box-sizing: border-box;
 }
 
 .oauth2-btn:hover {
diff --git a/rootfs/api/templates/base/footer.html b/rootfs/api/templates/base/footer.html
index 6dbaf4e..3b3a62c 100644
--- a/rootfs/api/templates/base/footer.html
+++ b/rootfs/api/templates/base/footer.html
@@ -1,4 +1,5 @@
-<footer class="logo-sfdc">
+<footer class="logo-sfdc" role="contentinfo">
+    <nav aria-label="Footer links">
     <ul class="legal">
         {% if LEGAL_ENABLED %}
         <li><a href="https://www.drycc.cc" rel="nofollow">Drycc Documents</a></li>
@@ -14,4 +15,5 @@
         <li>© {% now 'Y' %} drycc.cc</li>
         {% endif %}
     </ul>
+    </nav>
 </footer>
\ No newline at end of file
diff --git a/rootfs/api/templates/base/layout.html b/rootfs/api/templates/base/layout.html
new file mode 100644
index 0000000..7a6af75
--- /dev/null
+++ b/rootfs/api/templates/base/layout.html
@@ -0,0 +1,20 @@
+{% load static %}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{% block title %}Drycc Passport{% endblock %}</title>
+    <link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
+    {% block extra_head %}{% endblock %}
+</head>
+<body>
+    <div class="page-wrap gradient-primary">
+        <div class="container">
+            {% block content %}{% endblock %}
+        </div>
+        {% include "base/footer.html" %}
+    </div>
+    {% block extra_scripts %}{% endblock %}
+</body>
+</html>
diff --git a/rootfs/api/templates/oauth2_provider/authorize.html b/rootfs/api/templates/oauth2_provider/authorize.html
index b440717..90b98bb 100644
--- a/rootfs/api/templates/oauth2_provider/authorize.html
+++ b/rootfs/api/templates/oauth2_provider/authorize.html
@@ -1,44 +1,39 @@
+{% extends 'base/layout.html' %}
 {% load i18n %}
-{% load static %}
-<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
-<head>
-    <meta charset="UTF-8">
-    <title>Authorize</title>
-    <script type="text/javascript">
-        window.onload=function(){
-            const element = document.getElementById("logout-link")
-            element.href += encodeURIComponent(window.location.pathname + window.location.search)
+{% block title %}Authorize{% endblock %}
+{% block content %}
+<div class="panel">
+    {% if not error %}
+    <h2 class="h3">{% trans "Authorize" %} {{ application.name }}?</h2>
+    {{ updated }}
+    <div>{{ application.name }} wants to access your {{ request.user.username }} account</div>
+    <form id="authorizationForm" method="post">
+        {% csrf_token %}
+        {% for field in form %}
+            {% if field.is_hidden %}
+                {{ field }}
+            {% endif %}
+        {% endfor %}
+        {{ form.errors }}
+        {{ form.non_field_errors }}
+        <br>
+        <input type="submit" class="btn btn-primary btn-lg btn-block" name="allow" value="{% trans 'Authorize' %}"/>
+    </form>
+    <a id="logout-link" class="panel-footer" href="/user/logout?next=">With another account? &nbsp;<span>Sign In</span></a>
+    {% else %}
+    <h2 class="h3">{{ error.error }}</h2>
+    <p class="tc gray-90 inverted-wrapper">{{ error.description }}</p>
+    <div class="inverted-wrapper"></div>
+    {% endif %}
+</div>
+{% endblock %}
+{% block extra_scripts %}
+<script>
+    document.addEventListener('DOMContentLoaded', function() {
+        const element = document.getElementById("logout-link");
+        if (element) {
+            element.href += encodeURIComponent(window.location.pathname + window.location.search);
         }
-    </script>
-</head>
-<body>
-    <div class="page-wrap gradient-primary">
-        <div class="container">
-            <div class="panel">
-                {% if not error %}
-                <h2 class="h3">{% trans "Authorize" %} {{ application.name }}?</h2>
-                {{ updated }}
-                <div>{{ application.name }} wants to access your {{ request.user.username }} account</div>
-                <form id="authorizationForm" method="post">
-                    {% csrf_token %}
-                    {% for field in form %}
-                        {% if field.is_hidden %}
-                            {{ field }}
-                        {% endif %}
-                    {% endfor %}
-                    {{ form.errors }}
-                    {{ form.non_field_errors }}
-                    <br>
-                    <input type="submit" class="btn btn-primary btn-lg btn-block" name="allow" value="{% trans 'Authorize' %}"/>
-                </form>
-                <a id="logout-link" class="panel-footer" href="/user/logout?next=">With another account? &nbsp;<span>Sigin In</span></a>
-                {% else %}
-                <h2 class="h3">{{ error.error }}</h2>
-                <p class="tc gray-90 inverted-wrapper">{{ error.description }}</p>
-                <div class="inverted-wrapper"></div>
-                {% endif %}
-            </div>
-        </div>
-        {% include "base/footer.html" %}
-    </div>
-</body>
\ No newline at end of file
+    });
+</script>
+{% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/account_activation_done.html b/rootfs/api/templates/user/account_activation_done.html
index d6c8bc1..e9c7095 100644
--- a/rootfs/api/templates/user/account_activation_done.html
+++ b/rootfs/api/templates/user/account_activation_done.html
@@ -1,6 +1,6 @@
 {% extends 'user/message.html' %}
-{% block title %}Account activite successful{% endblock %}
+{% block heading %}Account activate successful{% endblock %}
 {% block message %}
 Your account has been activated successfully.
-<a href="/user/login/">please go to the login page.</a>
+<a href="/user/login/">Please go to the login page.</a>
 {% endblock %}
diff --git a/rootfs/api/templates/user/account_activation_fail.html b/rootfs/api/templates/user/account_activation_fail.html
index b2e6e43..15235d5 100644
--- a/rootfs/api/templates/user/account_activation_fail.html
+++ b/rootfs/api/templates/user/account_activation_fail.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}Account activite fail{% endblock %}
+{% block heading %}Account activate fail{% endblock %}
 {% block message %}
 The confirmation link was invalid, possibly it has already been used, go back <a href="/user/registration/">Sign Up</a>
 {% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/account_update_done.html b/rootfs/api/templates/user/account_update_done.html
index 2d1c946..7991945 100644
--- a/rootfs/api/templates/user/account_update_done.html
+++ b/rootfs/api/templates/user/account_update_done.html
@@ -1,6 +1,6 @@
 {% extends 'user/message.html' %}
-{% block title %}Account update successful{% endblock %}
+{% block heading %}Account update successful{% endblock %}
 {% block message %}
-Your account has been update successfully.
+Your account has been updated successfully.
 Please go to the <a href="/">main</a> page.
 {% endblock %}
diff --git a/rootfs/api/templates/user/account_update_fail.html b/rootfs/api/templates/user/account_update_fail.html
index f50175e..cec93ea 100644
--- a/rootfs/api/templates/user/account_update_fail.html
+++ b/rootfs/api/templates/user/account_update_fail.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}Account update fail{% endblock %}
+{% block heading %}Account update fail{% endblock %}
 {% block message %}
 The confirmation link was invalid, possibly it has already been expired.
 Please go to the <a href="/">main</a> page.
diff --git a/rootfs/api/templates/user/login.html b/rootfs/api/templates/user/login.html
index e2793ed..1fa860d 100644
--- a/rootfs/api/templates/user/login.html
+++ b/rootfs/api/templates/user/login.html
@@ -1,66 +1,60 @@
-{% load static %}
-<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
-<head>
-    <meta charset="UTF-8">
-    <title>{% if identity_linking %}Identity Linking{% else %}Login{% endif %}</title>
-</head>
-<div class="page-wrap gradient-primary">
-    <div class="container">
-        <div class="panel">
-            <h2 class="h3">{% if identity_linking %}Identity Linking{% else %}Log in to your account{% endif %}</h2>
+{% extends 'base/layout.html' %}
+{% block title %}{% if identity_linking %}Identity Linking{% else %}Login{% endif %}{% endblock %}
+{% block content %}
+<div class="panel">
+    <h2 class="h3">{% if identity_linking %}Identity Linking{% else %}Log in to your account{% endif %}</h2>
 
-            {% if not identity_linking %}
-            <div class="oauth2-container" id="oauth2-container">
-                <div class="oauth2-buttons" id="oauth2-buttons">
-                </div>
-                <div class="oauth2-divider" id="oauth2-divider">
-                    <span class="divider-line"></span>
-                    <span class="divider-text">or</span>
-                    <span class="divider-line"></span>
-                </div>
-            </div>
-            {% endif %}
+    {% if not identity_linking %}
+    <div class="oauth2-container" id="oauth2-container">
+        <div class="oauth2-buttons" id="oauth2-buttons">
+        </div>
+        <div class="oauth2-divider" id="oauth2-divider">
+            <span class="divider-line"></span>
+            <span class="divider-text">or</span>
+            <span class="divider-line"></span>
+        </div>
+    </div>
+    {% endif %}
 
-            <form method="post" action="{% url 'user_login' %}">
-                {% csrf_token %}
-                {% if form.errors %}
-                {% if identity_linking_error %}
-                <div class="alert alert-danger">
-                    <strong>{{ identity_linking_error|escape }}</strong>
-                </div>
-                {% endif %}
-                {% for error in form.non_field_errors %}
-                <div class="alert alert-danger">
-                    <strong>{{ error|escape }}</strong>
-                </div>
-                {% endfor %}
-                {% endif %}
-                <div class="form-group">
-                    <label for="username">{{ form.username.label_tag }}</label>
-                    <input autofocus="off" class="form-control" id="username" name="username" :placeholder="Username" type="text" required>
-                </div>
-                <div class="form-group">
-                    <label for="password">{{ form.password.label_tag }}</label>
-                    <input autocomplete="off" class="form-control password" id="password" name="password" :placeholder="Password" type="password" required>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" id="commit" type="submit">Log In</button>
-                <input type="hidden" name="next" value={{ next }}>
-                {% if identity_linking %}
-                <input type="hidden" name="identity_linking" value="1">
-                {% endif %}
-            </form>
-            {% if registration_enabled %}
-            <a class="panel-footer" href="/user/registration/">New to Drycc?  <span>Sign Up</span></a>
-            {% endif %}
+    <form method="post" action="{% url 'user_login' %}">
+        {% csrf_token %}
+        {% if form.errors %}
+        {% if identity_linking_error %}
+        <div class="alert alert-danger" role="alert">
+            <strong>{{ identity_linking_error|escape }}</strong>
         </div>
-        {% if password_reset_enabled %}
-        <div>
-            <a href="/user/password_reset/" class="white-link">Forgot your password?</a>
+        {% endif %}
+        {% for error in form.non_field_errors %}
+        <div class="alert alert-danger" role="alert">
+            <strong>{{ error|escape }}</strong>
         </div>
+        {% endfor %}
         {% endif %}
-    </div>
-    {% include "base/footer.html" %}
+        <div class="form-group">
+            <label for="username">{{ form.username.label_tag }}</label>
+            <input autocomplete="username" class="form-control" id="username" name="username" placeholder="Username" type="text" required>
+        </div>
+        <div class="form-group">
+            <label for="password">{{ form.password.label_tag }}</label>
+            <input autocomplete="current-password" class="form-control password" id="password" name="password" placeholder="Password" type="password" required>
+        </div>
+        <button class="btn btn-primary btn-lg btn-block" id="commit" type="submit">Log In</button>
+        <input type="hidden" name="next" value="{{ next|escape }}">
+        {% if identity_linking %}
+        <input type="hidden" name="identity_linking" value="1">
+        {% endif %}
+    </form>
+    {% if registration_enabled %}
+    <a class="panel-footer" href="/user/registration/">New to Drycc?  <span>Sign Up</span></a>
+    {% endif %}
 </div>
+{% if password_reset_enabled %}
+<div>
+    <a href="/user/password_reset/" class="white-link">Forgot your password?</a>
+</div>
+{% endif %}
+{% endblock %}
+{% block extra_scripts %}
 <script>
 {% if not identity_linking %}
 fetch('/user/identity-providers/')
@@ -86,3 +80,4 @@ <h2 class="h3">{% if identity_linking %}Identity Linking{% else %}Log in to your
     });
 {% endif %}
 </script>
+{% endblock %}
diff --git a/rootfs/api/templates/user/login_done.html b/rootfs/api/templates/user/login_done.html
index 357cb84..1b738fa 100644
--- a/rootfs/api/templates/user/login_done.html
+++ b/rootfs/api/templates/user/login_done.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}Login done{% endblock %}
+{% block heading %}Login done{% endblock %}
 {% block message %}
 You can close this page and return to your CLI. It should now be logged in.
 {% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/logout.html b/rootfs/api/templates/user/logout.html
index 0c25d7f..7840f14 100644
--- a/rootfs/api/templates/user/logout.html
+++ b/rootfs/api/templates/user/logout.html
@@ -1,8 +1,8 @@
 {% extends 'user/message.html' %}
-{% block title %}Logout{% endblock %}
+{% block heading %}Logout{% endblock %}
 {% block message %}
 Logout successfully. Return to the login later.
 <script>
-  window.setTimeout("window.location='/'", 2000);
+  setTimeout(function() { window.location = '/'; }, 2000);
 </script>
 {% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/message.html b/rootfs/api/templates/user/message.html
index 3215758..d600ddc 100644
--- a/rootfs/api/templates/user/message.html
+++ b/rootfs/api/templates/user/message.html
@@ -1,19 +1,9 @@
-{% load static %}
-
-<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
-
-<head>
-    <meta charset="UTF-8">
-    <title>Drycc | Message</title>
-</head>
-
-<div class="page-wrap gradient-primary">
-    <div class="container">
-        <div class="panel">
-            <h2 class="h3">{% block title %}{% endblock %}</h2>
-            <p class="tc gray-90 inverted-wrapper">{% block message %}{% endblock %}</p>
-            <div class="inverted-wrapper"></div>
-        </div>
-    </div>
-    {% include "base/footer.html" %}
-</div>
\ No newline at end of file
+{% extends 'base/layout.html' %}
+{% block title %}Drycc | Message{% endblock %}
+{% block content %}
+<div class="panel">
+    <h2 class="h3">{% block heading %}{% endblock %}</h2>
+    <p class="tc gray-90 inverted-wrapper">{% block message %}{% endblock %}</p>
+    <div class="inverted-wrapper"></div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/oauth_callback.html b/rootfs/api/templates/user/oauth_callback.html
index 39df210..48a04d5 100644
--- a/rootfs/api/templates/user/oauth_callback.html
+++ b/rootfs/api/templates/user/oauth_callback.html
@@ -1,91 +1,82 @@
-{% load static %}
-<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
+{% extends 'base/layout.html' %}
+{% block title %}Identity Linking{% endblock %}
+{% block content %}
+<div class="panel">
+    <h2 class="h3">Identity Linking</h2>
 
-<head>
-    <meta charset="UTF-8">
-    <title>Identity Linking</title>
-</head>
-
-<div class="page-wrap gradient-primary">
-    <div class="container">
-        <div class="panel">
-            <h2 class="h3">Identity Linking</h2>
-
-            {% if status == 'pending' %}
-            <form method="post">
-                {% csrf_token %}
-                <div class="form-group">
-                    <p class="gray-90">
-                        This {{ display_name|default:'OAuth' }} identity is not linked yet. Create a new account to continue.
-                    </p>
-                </div>
-                {% if error %}
-                <div class="form-group">
-                    <div class="error">{{ error }}</div>
-                </div>
-                {% endif %}
-                <div class="form-group">
-                    <label for="oauth-username">Username</label>
-                    <input autofocus="off" class="form-control" id="oauth-username" name="username" placeholder="Username" type="text" required>
-                </div>
-                <div class="form-group">
-                    <label for="oauth-password">Password</label>
-                    <input class="form-control" id="oauth-password" name="password" placeholder="Password" type="password" required>
-                </div>
-                <div class="form-group">
-                    <label for="oauth-confirm-password">Confirm Password</label>
-                    <input class="form-control" id="oauth-confirm-password" name="confirm_password" placeholder="Confirm password" type="password" required>
-                </div>
-                <div class="form-group">
-                    <label for="oauth-email">Email (from provider)</label>
-                    <input autofocus="off" class="form-control" id="oauth-email"
-                           :placeholder="Email" type="email" disabled value="{{ email }}">
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" type="submit">Create and Link</button>
-            </form>
-            <a class="panel-footer" href="/user/login/?identity_linking=1">I already have an account</a>
+    {% if status == 'pending' %}
+    <form method="post">
+        {% csrf_token %}
+        <div class="form-group">
+            <p class="gray-90">
+                This {{ display_name|default:'OAuth' }} identity is not linked yet. Create a new account to continue.
+            </p>
+        </div>
+        {% if error %}
+        <div class="form-group">
+            <div class="error" role="alert">{{ error }}</div>
+        </div>
+        {% endif %}
+        <div class="form-group">
+            <label for="oauth-username">Username</label>
+            <input autocomplete="username" class="form-control" id="oauth-username" name="username" placeholder="Username" type="text" required>
+        </div>
+        <div class="form-group">
+            <label for="oauth-password">Password</label>
+            <input autocomplete="new-password" class="form-control" id="oauth-password" name="password" placeholder="Password" type="password" required>
+        </div>
+        <div class="form-group">
+            <label for="oauth-confirm-password">Confirm Password</label>
+            <input autocomplete="new-password" class="form-control" id="oauth-confirm-password" name="confirm_password" placeholder="Confirm password" type="password" required>
+        </div>
+        <div class="form-group">
+            <label for="oauth-email">Email (from provider)</label>
+            <input autocomplete="email" class="form-control" id="oauth-email"
+                   placeholder="Email" type="email" disabled value="{{ email }}">
+        </div>
+        <button class="btn btn-primary btn-lg btn-block" type="submit">Create and Link</button>
+    </form>
+    <a class="panel-footer" href="/user/login/?identity_linking=1">I already have an account</a>
 
-            {% elif status == 'linked' %}
-            <form>
-                <div class="form-group">
-                    <p class="gray-90">Identity linked successfully.</p>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/account-setting'">Go to Account Settings</button>
-            </form>
+    {% elif status == 'linked' %}
+    <form>
+        <div class="form-group">
+            <p class="gray-90">Identity linked successfully.</p>
+        </div>
+        <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/account-setting'">Go to Account Settings</button>
+    </form>
 
-            {% elif status == 'already_linked' %}
-            <form>
-                <div class="form-group">
-                    <p class="gray-90">This identity is already linked to your account.</p>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/account-setting'">Go to Account Settings</button>
-            </form>
+    {% elif status == 'already_linked' %}
+    <form>
+        <div class="form-group">
+            <p class="gray-90">This identity is already linked to your account.</p>
+        </div>
+        <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/account-setting'">Go to Account Settings</button>
+    </form>
 
-            {% elif status == 'conflict' %}
-            <form>
-                <div class="form-group">
-                    <div class="error">This identity is linked to another account.</div>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Login with another account</button>
-            </form>
+    {% elif status == 'conflict' %}
+    <form>
+        <div class="form-group">
+            <div class="error" role="alert">This identity is linked to another account.</div>
+        </div>
+        <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Login with another account</button>
+    </form>
 
-            {% elif status == 'missing_email' %}
-            <form>
-                <div class="form-group">
-                    <div class="error">The provider did not supply an email address.</div>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Back to Login</button>
-            </form>
+    {% elif status == 'missing_email' %}
+    <form>
+        <div class="form-group">
+            <div class="error" role="alert">The provider did not supply an email address.</div>
+        </div>
+        <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Back to Login</button>
+    </form>
 
-            {% else %}
-            <form>
-                <div class="form-group">
-                    <div class="error">OAuth flow error. Please try again.</div>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Back to Login</button>
-            </form>
-            {% endif %}
+    {% else %}
+    <form>
+        <div class="form-group">
+            <div class="error" role="alert">OAuth flow error. Please try again.</div>
         </div>
-    </div>
-    {% include "base/footer.html" %}
-</div>
\ No newline at end of file
+        <button class="btn btn-primary btn-lg btn-block" type="button" onclick="location.href='/user/login/'">Back to Login</button>
+    </form>
+    {% endif %}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/password_reset_complete.html b/rootfs/api/templates/user/password_reset_complete.html
index 7903242..2a0afc4 100644
--- a/rootfs/api/templates/user/password_reset_complete.html
+++ b/rootfs/api/templates/user/password_reset_complete.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}Password reset successful{% endblock %}
+{% block heading %}Password reset successful{% endblock %}
 {% block message %}
 Your password has been set.  You may go ahead and log in now.
 {% endblock %}
diff --git a/rootfs/api/templates/user/password_reset_confirm.html b/rootfs/api/templates/user/password_reset_confirm.html
index 343eea0..794cfa7 100644
--- a/rootfs/api/templates/user/password_reset_confirm.html
+++ b/rootfs/api/templates/user/password_reset_confirm.html
@@ -1,35 +1,32 @@
-{% load static %}
+{% extends 'base/layout.html' %}
+{% block title %}Reset Password{% endblock %}
+{% block content %}
 {% if validlink %}
-<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
-<head>
-    <meta charset="UTF-8">
-    <title>Reset Password</title>
-</head>
-<div class="page-wrap gradient-primary">
-    <div class="container">
-        <div class="panel">
-            <h2 class="h3">{{ title }}</h2>
-            <p class="tc gray-90 inverted-wrapper">Please enter your new password twice so we can verify you typed it in correctly.</p>
-            <form method="post">
-                {% csrf_token %}
-                <div class="form-group">
-                    <label for="id_new_password1">{{ form.new_password1.label_tag }}</label>
-                    <input autofocus="off" class="form-control" id="id_new_password1" name="new_password1"
-                       :placeholder="Password" type="password">
-                    <div class="error">{{ form.new_password1.errors }}</div>
-                </div>
-                <div class="form-group">
-                    <label for="id_new_password2">{{ form.new_password2.label_tag }}</label>
-                    <input autofocus="off" class="form-control" id="id_new_password2" name="new_password2"
-                       :placeholder="Password" type="password">
-                    <div class="error">{{ form.new_password2.errors }}</div>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" name="commit" type="submit">Submit</button>
-            </form>
+<div class="panel">
+    <h2 class="h3">{{ title }}</h2>
+    <p class="tc gray-90 inverted-wrapper">Please enter your new password twice so we can verify you typed it in correctly.</p>
+    <form method="post">
+        {% csrf_token %}
+        <div class="form-group">
+            <label for="id_new_password1">{{ form.new_password1.label_tag }}</label>
+            <input autocomplete="new-password" class="form-control" id="id_new_password1" name="new_password1"
+               placeholder="Password" type="password">
+            <div class="error" role="alert">{{ form.new_password1.errors }}</div>
         </div>
-    </div>
-    {% include "base/footer.html" %}
+        <div class="form-group">
+            <label for="id_new_password2">{{ form.new_password2.label_tag }}</label>
+            <input autocomplete="new-password" class="form-control" id="id_new_password2" name="new_password2"
+               placeholder="Password" type="password">
+            <div class="error" role="alert">{{ form.new_password2.errors }}</div>
+        </div>
+        <button class="btn btn-primary btn-lg btn-block" name="commit" type="submit">Submit</button>
+    </form>
 </div>
 {% else %}
-    {% include 'user/password_reset_fail.html' %}
+<div class="panel">
+    <h2 class="h3">The password reset link was invalid</h2>
+    <p class="tc gray-90 inverted-wrapper">Possibly because it has already been used. Please request a new password reset.</p>
+    <div class="inverted-wrapper"></div>
+</div>
 {% endif %}
+{% endblock %}
diff --git a/rootfs/api/templates/user/password_reset_done.html b/rootfs/api/templates/user/password_reset_done.html
index e1b0931..04f49c5 100644
--- a/rootfs/api/templates/user/password_reset_done.html
+++ b/rootfs/api/templates/user/password_reset_done.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}Email send successful{% endblock %}
+{% block heading %}Email send successful{% endblock %}
 {% block message %}
 Please check your inbox. It may be in spam.
 {% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/password_reset_fail.html b/rootfs/api/templates/user/password_reset_fail.html
index 2f95291..ec506eb 100644
--- a/rootfs/api/templates/user/password_reset_fail.html
+++ b/rootfs/api/templates/user/password_reset_fail.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}The password reset link was invalid{% endblock %}
+{% block heading %}The password reset link was invalid{% endblock %}
 {% block message %}
 Possibly because it has already been used. Please request a new password reset.
 {% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/password_reset_form.html b/rootfs/api/templates/user/password_reset_form.html
index d9790e5..00803a0 100644
--- a/rootfs/api/templates/user/password_reset_form.html
+++ b/rootfs/api/templates/user/password_reset_form.html
@@ -1,26 +1,18 @@
-{% load static %}
-<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
-<head>
-    <meta charset="UTF-8">
-    <title>Reset Password</title>
-</head>
-<div class="page-wrap gradient-primary">
-    <div class="container">
-        <div class="panel">
-            <h2 class="h3">{{ title }}</h2>
-            <p class="tc gray-90 inverted-wrapper">Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one.</p>
-            <form method="post">
-                {% csrf_token %}
-                <div class="form-group">
-                    <label for="email">{{ form.email.label_tag }}</label>
-                    <input autofocus="off" class="form-control" id="email" name="email"
-                           :placeholder="Email" type="email">
-                    <div class="error">{{ form.email.errors }}</div>
-                </div>
-                <button class="btn btn-primary btn-lg btn-block" name="commit" type="submit">Submit</button>
-            </form>
+{% extends 'base/layout.html' %}
+{% block title %}Reset Password{% endblock %}
+{% block content %}
+<div class="panel">
+    <h2 class="h3">{{ title }}</h2>
+    <p class="tc gray-90 inverted-wrapper">Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one.</p>
+    <form method="post">
+        {% csrf_token %}
+        <div class="form-group">
+            <label for="email">{{ form.email.label_tag }}</label>
+            <input autocomplete="email" class="form-control" id="email" name="email"
+                   placeholder="Email" type="email">
+            <div class="error" role="alert">{{ form.email.errors }}</div>
         </div>
-    </div>
-    {% include "base/footer.html" %}
+        <button class="btn btn-primary btn-lg btn-block" name="commit" type="submit">Submit</button>
+    </form>
 </div>
-
+{% endblock %}
diff --git a/rootfs/api/templates/user/registration.html b/rootfs/api/templates/user/registration.html
index f994f74..058ff41 100644
--- a/rootfs/api/templates/user/registration.html
+++ b/rootfs/api/templates/user/registration.html
@@ -1,70 +1,52 @@
-{% load static %}
-
-<link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
-
-<head>
-    <meta charset="UTF-8">
-    <title>Registration</title>
-</head>
-
-<div class="page-wrap gradient-primary">
-    <div class="container">
-        <div class="panel">
-            <h2 class="h3">Registration</h2>
-            <form method="post">
-                {% csrf_token %}
-                <div class="form-group">
-                    <label for="username">{{ form.username.label_tag }}</label>
-                    <input autofocus="off" class="form-control" id="username" name="username" :placeholder="Username" type="text" required>
-                    <div class="error">{{ form.username.errors }}</div>
-                </div>
-                <div class="form-group">
-                    <label for="password1">{{ form.password1.label_tag }}</label>
-                    <input autofocus="off"
-                        class="form-control"
-                        id="password1"
-                        name="password1"
-                        placeholder="Password"
-                        type="password" required>
-                    <div class="error">{{ form.password1.errors }}</div>
-                </div>
-
-                <div class="form-group">
-                    <label for="password2">{{ form.password2.label_tag }}</label>
-                    <input autofocus="off"
-                        class="form-control" id="password2" name="password2"
-                        placeholder="Confirm password"
-                        type="password" required>
-                    <div class="error">{{ form.password2.errors }}</div>
-                </div>
-
-                <div class="form-group">
-                    <label for="email">{{ form.email.label_tag }}</label>
-                    <input autofocus="off" class="form-control" id="email" name="email"
-                           :placeholder="Email" type="email" required>
-                    <div class="error">{{ form.email.errors }}</div>
-                </div>
-                {% if h_captcha_key %}
-                <style>
-                    .h-captcha iframe {
-                        transform: scaleX(1.22);
-                    }
-                </style>
-                <div class="form-group">
-                    <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
-                    <div class="h-captcha" data-sitekey="{{ h_captcha_key }}"></div>
-                    <div class="error">{{ form.h_captcha_token.errors }}</div>
-                </div>
-                {% endif %}
-                <button class="btn btn-primary btn-lg btn-block" id="submit" type="submit">Submit</button>
-                {% if LEGAL_ENABLED %}
-                <div class="form-group">
-                    <p class="privacy">
-                        Signing up signifies that you have read and agree to the <a target="_blank" href="https://legal.doopai.com/#/cloud-services-terms">Terms of Service</a> and our <a target="_blank" href="https://legal.doopai.com/#/doopai-privacy-policy">Privacy Policy</a>.
-                    </p>
-                </div>
-                {% endif %}
-            </form>
+{% extends 'base/layout.html' %}
+{% block title %}Registration{% endblock %}
+{% block content %}
+<div class="panel">
+    <h2 class="h3">Registration</h2>
+    <form method="post">
+        {% csrf_token %}
+        <div class="form-group">
+            <label for="username">{{ form.username.label_tag }}</label>
+            <input autocomplete="username" class="form-control" id="username" name="username" placeholder="Username" type="text" required>
+            <div class="error" role="alert">{{ form.username.errors }}</div>
         </div>
-    </div>
+        <div class="form-group">
+            <label for="password1">{{ form.password1.label_tag }}</label>
+            <input autocomplete="new-password" class="form-control" id="password1" name="password1"
+                placeholder="Password" type="password" required>
+            <div class="error" role="alert">{{ form.password1.errors }}</div>
+        </div>
+        <div class="form-group">
+            <label for="password2">{{ form.password2.label_tag }}</label>
+            <input autocomplete="new-password" class="form-control" id="password2" name="password2"
+                placeholder="Confirm password" type="password" required>
+            <div class="error" role="alert">{{ form.password2.errors }}</div>
+        </div>
+        <div class="form-group">
+            <label for="email">{{ form.email.label_tag }}</label>
+            <input autocomplete="email" class="form-control" id="email" name="email"
+                   placeholder="Email" type="email" required>
+            <div class="error" role="alert">{{ form.email.errors }}</div>
+        </div>
+        {% if h_captcha_key %}
+        <div class="form-group">
+            <div class="h-captcha" data-sitekey="{{ h_captcha_key }}"></div>
+            <div class="error" role="alert">{{ form.h_captcha_token.errors }}</div>
+        </div>
+        {% endif %}
+        <button class="btn btn-primary btn-lg btn-block" id="submit" type="submit">Submit</button>
+        {% if LEGAL_ENABLED %}
+        <div class="form-group">
+            <p class="privacy">
+                Signing up signifies that you have read and agree to the <a target="_blank" href="https://legal.doopai.com/#/cloud-services-terms">Terms of Service</a> and our <a target="_blank" href="https://legal.doopai.com/#/doopai-privacy-policy">Privacy Policy</a>.
+            </p>
+        </div>
+        {% endif %}
+    </form>
 </div>
+{% endblock %}
+{% block extra_head %}
+{% if h_captcha_key %}
+<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
+{% endif %}
+{% endblock %}
diff --git a/rootfs/api/templates/user/registration_disable.html b/rootfs/api/templates/user/registration_disable.html
index 8dfc922..4f4bbaf 100644
--- a/rootfs/api/templates/user/registration_disable.html
+++ b/rootfs/api/templates/user/registration_disable.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}Registration fail{% endblock %}
+{% block heading %}Registration fail{% endblock %}
 {% block message %}
 The registration function of drycc passport is not enabled. Please contact the administrator to sign up.
 {% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/registration_done.html b/rootfs/api/templates/user/registration_done.html
index e6467c5..29497d0 100644
--- a/rootfs/api/templates/user/registration_done.html
+++ b/rootfs/api/templates/user/registration_done.html
@@ -1,5 +1,5 @@
 {% extends 'user/message.html' %}
-{% block title %}Registration successful{% endblock %}
+{% block heading %}Registration successful{% endblock %}
 {% block message %}
 We have sent an email to your mailbox, please log in the email to activate your account.
 {% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py
index 5f1b21b..a5e8a68 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -1,10 +1,10 @@
 from django.urls import re_path, include
-from rest_framework.routers import DefaultRouter
+from rest_framework.routers import SimpleRouter
 
 from api.views import web, api
 from passport.views import SettingsViewSet
 
-router = DefaultRouter(trailing_slash=False)
+router = SimpleRouter(trailing_slash=False)
 
 urlpatterns = [
     re_path(r'^', include(router.urls)),
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index bfab3a3..3b38b52 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -8,7 +8,7 @@ gunicorn==25.1.0
 uvicorn==0.42.0
 asgiref==3.11.1
 psycopg[binary]==3.3.3
-requests==2.32.5
+requests==2.33.0
 requests-toolbelt==1.0.0
 redis==7.4.0
 hiredis==3.3.1
diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index c112d7a..c698ee1 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -12,7 +12,6 @@
         "@element-plus/icons-vue": "^2.3.2",
         "axios": "^1.13.6",
         "element-plus": "^2.13.6",
-        "vant": "^4.9.22",
         "vue": "^3.5.30",
         "vue-i18n": "^11.3.0",
         "vue-router": "^5.0.4",
@@ -601,21 +600,6 @@
       "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
       "license": "MIT"
     },
-    "node_modules/@vant/popperjs": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.3.0.tgz",
-      "integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==",
-      "license": "MIT"
-    },
-    "node_modules/@vant/use": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/@vant/use/-/use-1.6.0.tgz",
-      "integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==",
-      "license": "MIT",
-      "peerDependencies": {
-        "vue": "^3.0.0"
-      }
-    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "6.0.5",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
@@ -1917,20 +1901,6 @@
         "url": "https://github.com/sponsors/sxzz"
       }
     },
-    "node_modules/vant": {
-      "version": "4.9.22",
-      "resolved": "https://registry.npmjs.org/vant/-/vant-4.9.22.tgz",
-      "integrity": "sha512-P2PDSj3oB6l3W1OpVlQpapeLmI6bXMSvPqPdrw5rutslC0Y6tSkrVB/iSD57weD7K92GsjGkvgDK0eZlOsXGqw==",
-      "license": "MIT",
-      "dependencies": {
-        "@vant/popperjs": "^1.3.0",
-        "@vant/use": "^1.6.0",
-        "@vue/shared": "^3.5.25"
-      },
-      "peerDependencies": {
-        "vue": "^3.0.0"
-      }
-    },
     "node_modules/vite": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
diff --git a/rootfs/web/package.json b/rootfs/web/package.json
index f08aa64..30a83ca 100644
--- a/rootfs/web/package.json
+++ b/rootfs/web/package.json
@@ -11,7 +11,6 @@
     "@element-plus/icons-vue": "^2.3.2",
     "axios": "^1.13.6",
     "element-plus": "^2.13.6",
-    "vant": "^4.9.22",
     "vue": "^3.5.30",
     "vue-i18n": "^11.3.0",
     "vue-router": "^5.0.4",
diff --git a/rootfs/web/src/components/AccessTokenDelete.js b/rootfs/web/src/components/AccessTokenDelete.js
index 987cae4..2cf6e0b 100644
--- a/rootfs/web/src/components/AccessTokenDelete.js
+++ b/rootfs/web/src/components/AccessTokenDelete.js
@@ -1,5 +1,5 @@
 import { reactive, toRefs } from 'vue'
-import { showSuccessToast } from "vant";
+import { ElMessage } from "element-plus";
 import {deleteAccessToken} from "../services/tokens";
 
 export default {
@@ -19,7 +19,7 @@ export default {
         const deleteToken = () => {
             deleteAccessToken(state.token.id).then(res=>{
                 if (res.status == 204) {
-                    showSuccessToast("OK")
+                    ElMessage.success("OK")
                     context.emit('closeDelete', { hasAccessTokenDeleted: true })
                 }
             })
diff --git a/rootfs/web/src/lang/en_US.js b/rootfs/web/src/lang/en_US.js
index f3ccb24..c8ff4e2 100644
--- a/rootfs/web/src/lang/en_US.js
+++ b/rootfs/web/src/lang/en_US.js
@@ -7,97 +7,5 @@ const customEnUS = {
   newToDrycc: "New To Drycc? Please contact your adminstrator",
 
 }
-
-const vantEnUS = {
-  // vant default
-  name: 'Name',
-  tel: 'Phone',
-  save: 'Save',
-  confirm: 'Confirm',
-  cancel: 'Cancel',
-  delete: 'Delete',
-  complete: 'Complete',
-  loading: 'Loading...',
-  telEmpty: 'Please fill in the tel',
-  nameEmpty: 'Please fill in the name',
-  nameInvalid: 'Malformed name',
-  confirmDelete: 'Are you sure you want to delete?',
-  telInvalid: 'Malformed phone number',
-  vanCalendar: {
-    end: 'End',
-    start: 'Start',
-    title: 'Calendar',
-    startEnd: 'Start/End',
-    weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
-    monthTitle: function monthTitle(year, month) {
-      return year + "/" + month;
-    },
-    rangePrompt: function rangePrompt(maxRange) {
-      return "Choose no more than " + maxRange + " days";
-    }
-  },
-  vanContactCard: {
-    addText: 'Add contact info'
-  },
-  vanContactList: {
-    addText: 'Add new contact'
-  },
-  vanPagination: {
-    prev: 'Previous',
-    next: 'Next'
-  },
-  vanPullRefresh: {
-    pulling: 'Pull to refresh...',
-    loosing: 'Loose to refresh...'
-  },
-  vanSubmitBar: {
-    label: 'Total：'
-  },
-  vanCoupon: {
-    unlimited: 'Unlimited',
-    discount: function discount(_discount) {
-      return _discount * 10 + "% off";
-    },
-    condition: function condition(_condition) {
-      return "At least " + _condition;
-    }
-  },
-  vanCouponCell: {
-    title: 'Coupon',
-    tips: 'No coupons',
-    count: function count(_count) {
-      return "You have " + _count + " coupons";
-    }
-  },
-  vanCouponList: {
-    empty: 'No coupons',
-    exchange: 'Exchange',
-    close: 'Close',
-    enable: 'Available',
-    disabled: 'Unavailable',
-    placeholder: 'Coupon code'
-  },
-  vanAddressEdit: {
-    area: 'Area',
-    postal: 'Postal',
-    areaEmpty: 'Please select a receiving area',
-    addressEmpty: 'Address can not be empty',
-    postalEmpty: 'Wrong postal code',
-    defaultAddress: 'Set as the default address',
-    telPlaceholder: 'Phone',
-    namePlaceholder: 'Name',
-    areaPlaceholder: 'Area'
-  },
-  vanAddressEditDetail: {
-    label: 'Address',
-    placeholder: 'Address'
-  },
-  vanAddressList: {
-    add: 'Add new address'
-  }
-}
-
-const message = Object.assign(customEnUS, vantEnUS)
-
-export default message
+export default customEnUS
 
diff --git a/rootfs/web/src/lang/zh_CN.js b/rootfs/web/src/lang/zh_CN.js
index 2c34c83..4a5ad29 100644
--- a/rootfs/web/src/lang/zh_CN.js
+++ b/rootfs/web/src/lang/zh_CN.js
@@ -7,97 +7,4 @@ const customZhCN = {
   newToDrycc: "如果您是Drycc的新用户，请联系管理员",
 
 }
-
-const vantZhCN = {
-  // vant default
-  name: '姓名',
-  tel: '电话',
-  save: '保存',
-  confirm: '确认',
-  cancel: '取消',
-  delete: '删除',
-  complete: '完成',
-  loading: '加载中...',
-  telEmpty: '请填写电话',
-  nameEmpty: '请填写姓名',
-  nameInvalid: '请输入正确的姓名',
-  confirmDelete: '确定要删除吗',
-  telInvalid: '请输入正确的手机号',
-  vanCalendar: {
-    end: '结束',
-    start: '开始',
-    title: '日期选择',
-    confirm: '确定',
-    startEnd: '开始/结束',
-    weekdays: ['日', '一', '二', '三', '四', '五', '六'],
-    monthTitle: function monthTitle(year, month) {
-      return year + "\u5E74" + month + "\u6708";
-    },
-    rangePrompt: function rangePrompt(maxRange) {
-      return "\u9009\u62E9\u5929\u6570\u4E0D\u80FD\u8D85\u8FC7 " + maxRange + " \u5929";
-    }
-  },
-  vanContactCard: {
-    addText: '添加联系人'
-  },
-  vanContactList: {
-    addText: '新建联系人'
-  },
-  vanPagination: {
-    prev: '上一页',
-    next: '下一页'
-  },
-  vanPullRefresh: {
-    pulling: '下拉即可刷新...',
-    loosing: '释放即可刷新...'
-  },
-  vanSubmitBar: {
-    label: '合计：'
-  },
-  vanCoupon: {
-    unlimited: '无使用门槛',
-    discount: function discount(_discount) {
-      return _discount + "\u6298";
-    },
-    condition: function condition(_condition) {
-      return "\u6EE1" + _condition + "\u5143\u53EF\u7528";
-    }
-  },
-  vanCouponCell: {
-    title: '优惠券',
-    tips: '暂无可用',
-    count: function count(_count) {
-      return _count + "\u5F20\u53EF\u7528";
-    }
-  },
-  vanCouponList: {
-    empty: '暂无优惠券',
-    exchange: '兑换',
-    close: '不使用优惠券',
-    enable: '可用',
-    disabled: '不可用',
-    placeholder: '请输入优惠码'
-  },
-  vanAddressEdit: {
-    area: '地区',
-    postal: '邮政编码',
-    areaEmpty: '请选择地区',
-    addressEmpty: '请填写详细地址',
-    postalEmpty: '邮政编码格式不正确',
-    defaultAddress: '设为默认收货地址',
-    telPlaceholder: '收货人手机号',
-    namePlaceholder: '收货人姓名',
-    areaPlaceholder: '选择省 / 市 / 区'
-  },
-  vanAddressEditDetail: {
-    label: '详细地址',
-    placeholder: '街道门牌、楼层房间号等信息'
-  },
-  vanAddressList: {
-    add: '新增地址'
-  }
-}
-
-const message = Object.assign(customZhCN, vantZhCN)
-
-export default message
+export default customZhCN
diff --git a/rootfs/web/src/main.js b/rootfs/web/src/main.js
index 99f04d4..c59cd43 100644
--- a/rootfs/web/src/main.js
+++ b/rootfs/web/src/main.js
@@ -3,7 +3,6 @@ import router from './router'
 import { createApp } from 'vue'
 import { i18n, setLang, getUAgentLang } from './lang'
 import './styles/unified.css'
-import 'vant/lib/index.css';
 import ElementPlus from "element-plus";
 import "element-plus/dist/index.css";
 import * as ElementPlusIconsVue from '@element-plus/icons-vue'
diff --git a/rootfs/web/src/utils/axios.js b/rootfs/web/src/utils/axios.js
index 607924c..a6ddbdb 100755
--- a/rootfs/web/src/utils/axios.js
+++ b/rootfs/web/src/utils/axios.js
@@ -1,5 +1,5 @@
 import axios from 'axios'
-import { closeToast, showLoadingToast, showToast } from 'vant'
+import { ElLoading, ElMessage } from 'element-plus'
 import {getCookie} from "./array";
 
 axios.defaults.baseURL = process.env.VUE_APP_BASE_URL
@@ -9,11 +9,11 @@ const loadingToast = [];
 
 axios.interceptors.request.use(
     (config)=>{
-    loadingToast.push(showLoadingToast({
-            duration: 0,
-            forbidClick: true,
-            message: "Loading..."
-        }));
+    loadingToast.push(ElLoading.service({
+      lock: true,
+      text: "Loading...",
+      background: "rgba(0, 0, 0, 0.2)",
+    }));
         let csrftoken = getCookie('csrftoken')
         if(csrftoken){
             config.headers['X-CSRFToken'] = csrftoken;
@@ -25,12 +25,7 @@ axios.interceptors.request.use(
 axios.interceptors.response.use(
     response => {
       if (loadingToast.length > 0) {
-        const toast = loadingToast.pop();
-        if (toast && typeof toast.close === 'function') {
-          toast.close();
-        } else {
-          closeToast();
-        }
+        loadingToast.pop().close();
       }
       if ([200, 201, 204].indexOf(response.status) >= 0) {
         return Promise.resolve(response);
@@ -40,12 +35,7 @@ axios.interceptors.response.use(
     },
     error => {
       if (loadingToast.length > 0) {
-        const toast = loadingToast.pop();
-        if (toast && typeof toast.close === 'function') {
-          toast.close();
-        } else {
-          closeToast();
-        }
+        loadingToast.pop().close();
       }
       if (error.response && error.response.status) {
         switch (error.response.status) {
@@ -68,33 +58,33 @@ axios.interceptors.response.use(
           case 404:
             console.log('error.response: ', error.response)
               if(error.response.data){
-                showToast({
-                      message: error.response.data.replace("\"","").replace("\"",""),
-                      duration: 1500,
-                      forbidClick: true
-                  });
+                ElMessage({
+                    message: error.response.data.replace("\"","").replace("\"",""),
+                    type: "error",
+                    duration: 1500,
+                });
               }else {
-                showToast({
-                      message: 'The request does not exist.',
-                      duration: 1500,
-                      forbidClick: true
-                  });
+                ElMessage({
+                    message: 'The request does not exist.',
+                    type: "error",
+                    duration: 1500,
+                });
               }
             break;
           case 400:
             console.log('error.response: ', error.response)
               if(error.response.data.detail){
-                showToast({
-                      message: error.response.data.detail.replace("\"","").replace("\"",""),
-                      duration: 1500,
-                      forbidClick: true
-                  });
+                ElMessage({
+                    message: error.response.data.detail.replace("\"","").replace("\"",""),
+                    type: "error",
+                    duration: 1500,
+                });
               }else {
-                showToast({
-                      message: 'The request parameter error',
-                      duration: 1500,
-                      forbidClick: true
-                  });
+                ElMessage({
+                    message: 'The request parameter error',
+                    type: "error",
+                    duration: 1500,
+                });
               }
             break;
           // other error
diff --git a/rootfs/web/src/views/AccountSetting.js b/rootfs/web/src/views/AccountSetting.js
index be8dbb8..f7c4412 100644
--- a/rootfs/web/src/views/AccountSetting.js
+++ b/rootfs/web/src/views/AccountSetting.js
@@ -5,7 +5,7 @@ import MainNav from "../components/MainNav.vue";
 import MainFooter from "../components/MainFooter.vue";
 import { useRouter } from 'vue-router'
 import { dealUser, getUser, putAccount, putAccountPassword } from "../services/user";
-import { showFailToast, showSuccessToast } from "vant"
+import { ElMessage } from "element-plus"
 import { getIdentityProviders, getLinkedIdentities, unlinkIdentity } from "../services/identities";
 
 export default {
@@ -79,7 +79,7 @@ export default {
 
         const handleLink = (provider) => {
             if (!provider?.login_url) {
-                showFailToast("Provider login URL missing.")
+                ElMessage.error("Provider login URL missing.")
                 return
             }
             window.location.href = provider.login_url
@@ -90,7 +90,7 @@ export default {
                 return
             }
             await unlinkIdentity(identity.id)
-            showSuccessToast("Unlinked successfully.")
+            ElMessage.success("Unlinked successfully.")
             await fetchIdentities()
         }
 
@@ -155,7 +155,7 @@ export default {
         const submitEmail = () => {
             var re = /^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$/;
             if(!re.test(state.user.email)) {
-                showFailToast("this email is not valid.")
+                ElMessage.error("this email is not valid.")
             } else {
                 putAccount({email: state.user.email}).then(res=>{
                     if (res.status == 204) {
@@ -186,11 +186,11 @@ export default {
 
         const submitPassowrd = () => {
             if (currentPassword == newPassword){
-                showFailToast("Current Password and New Password are the same.")
+                ElMessage.error("Current Password and New Password are the same.")
             } else if( newPassword.length < 8){
-                showFailToast("this New Password is not valid.")
+                ElMessage.error("this New Password is not valid.")
             } else if (newPassword != confirmNewPassword){
-                showFailToast("New Password Confirm and New Password are different.")
+                ElMessage.error("New Password Confirm and New Password are different.")
             }else {
                 putAccountPassword({password: currentPassword, new_password: newPassword}).then(res=>{
                     if (res.status == 204) {

From bea64ad291610cf6844735d55c8ab392bdc91a50 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 1 Apr 2026 11:12:47 +0800
Subject: [PATCH 22/30] fix(charts): reserved names miss

---
 charts/passport/templates/_helpers.tpl        | 74 -------------------
 .../templates/passport-configmap.yaml         |  4 +-
 charts/passport/values.yaml                   |  4 +-
 3 files changed, 4 insertions(+), 78 deletions(-)

diff --git a/charts/passport/templates/_helpers.tpl b/charts/passport/templates/_helpers.tpl
index 4d429bb..834cb5a 100644
--- a/charts/passport/templates/_helpers.tpl
+++ b/charts/passport/templates/_helpers.tpl
@@ -100,77 +100,3 @@ volumes:
     configMap:
       name: passport-config
 {{- end }}
-
-{{/* Generate passport default reserved usernames */}}
-{{- define "passport.defaultReservedUsernames" }}
-admin
-administrator
-anonymous
-asshole
-bastard
-billing
-callback
-cancer
-cocaine
-contact
-coronavirus
-cracker
-database
-developer
-doopai
-drycc
-email
-explore
-faggot
-feedback
-hacker
-helpdesk
-hentai
-heroin
-hitler
-homophobic
-horny
-idiot
-killer
-login
-logout
-moderator
-murder
-nigger
-nigga
-official
-payment
-pedophile
-pornhub
-profile
-racist
-rapist
-recovery
-register
-retard
-scammer
-security
-service
-settings
-sexist
-signup
-signin
-slave
-spammer
-staff
-suicide
-support
-system
-terrorism
-trending
-undefined
-update
-username
-verification
-verify
-webmaster
-webhook
-wetback
-whore
-xvideos
-{{- end }}
diff --git a/charts/passport/templates/passport-configmap.yaml b/charts/passport/templates/passport-configmap.yaml
index 804a95e..8280dcf 100644
--- a/charts/passport/templates/passport-configmap.yaml
+++ b/charts/passport/templates/passport-configmap.yaml
@@ -8,8 +8,8 @@ data:
   init-applications.json: |-
 {{ toPrettyJson .Values.initApplications | indent 4 }}
   reserved-name-patterns.txt: |-
-    {{- if .Values.config.reservedNames }}
-    {{- (tpl .Values.config.reservedNames $)  | nindent 4 }}
+    {{- if .Values.reservedNames }}
+    {{- (tpl .Values.reservedNames $)  | nindent 4 }}
     {{- else}}
     {{- .Files.Get "files/reserved-name-patterns.txt" | nindent 4 }}
     {{- end }}
\ No newline at end of file
diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index 8fd8778..22986bb 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -86,8 +86,8 @@ environment:
 adminUsername: "admin"
 adminPassword: "admin"
 adminEmail: "admin@email.com"
-# Reserved usernames
-reservedUsernames: ""
+# Reserved names
+reservedNames: ""
 # The following configurations to initialize oauth2 application
 # Names are all lowercase letters
 # The key and secret are generated automatically if they are empty

From 7fedd91c9b9cc9bb25d62686e6a2e5b90d2d0852 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Fri, 10 Apr 2026 14:25:54 +0800
Subject: [PATCH 23/30] fix(ui): fix layout alignment on OAuth2 authorize page

---
 rootfs/api/templates/oauth2_provider/authorize.html | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/rootfs/api/templates/oauth2_provider/authorize.html b/rootfs/api/templates/oauth2_provider/authorize.html
index 90b98bb..36c33e6 100644
--- a/rootfs/api/templates/oauth2_provider/authorize.html
+++ b/rootfs/api/templates/oauth2_provider/authorize.html
@@ -6,8 +6,10 @@
     {% if not error %}
     <h2 class="h3">{% trans "Authorize" %} {{ application.name }}?</h2>
     {{ updated }}
-    <div>{{ application.name }} wants to access your {{ request.user.username }} account</div>
     <form id="authorizationForm" method="post">
+        <div class="form-group">
+            <p class="gray-90">{{ application.name }} wants to access your {{ request.user.username }} account</p>
+        </div>
         {% csrf_token %}
         {% for field in form %}
             {% if field.is_hidden %}

From ed33ae71f07d38a85d256d67cb002c746acfdb4d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Apr 2026 14:29:43 +0800
Subject: [PATCH 24/30] chore(deps): bump lodash-es (#20)

Bumps the npm_and_yarn group with 1 update in the /rootfs/web directory: [lodash-es](https://github.com/lodash/lodash).


Updates `lodash-es` from 4.17.23 to 4.18.1
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.18.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 rootfs/web/package-lock.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index c698ee1..a128153 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -1546,9 +1546,9 @@
       "license": "MIT"
     },
     "node_modules/lodash-es": {
-      "version": "4.17.23",
-      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
-      "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
       "license": "MIT"
     },
     "node_modules/lodash-unified": {

From bc365fa70daa149a3a9da78c89ae834981c23ef4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Apr 2026 14:33:11 +0800
Subject: [PATCH 25/30] chore(deps): bump the npm_and_yarn group across 1
 directory with 3 updates (#21)

Bumps the npm_and_yarn group with 3 updates in the /rootfs/web directory: [axios](https://github.com/axios/axios), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [lodash](https://github.com/lodash/lodash).


Updates `axios` from 1.13.6 to 1.15.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.6...v1.15.0)

Updates `vite` from 8.0.2 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `lodash` from 4.17.23 to 4.18.1
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 rootfs/web/package-lock.json | 219 ++++++++++++++++++-----------------
 rootfs/web/package.json      |   4 +-
 2 files changed, 115 insertions(+), 108 deletions(-)

diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index a128153..43623f2 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -10,7 +10,7 @@
       "license": "Apache-2.0",
       "dependencies": {
         "@element-plus/icons-vue": "^2.3.2",
-        "axios": "^1.13.6",
+        "axios": "^1.15.0",
         "element-plus": "^2.13.6",
         "vue": "^3.5.30",
         "vue-i18n": "^11.3.0",
@@ -20,7 +20,7 @@
       "devDependencies": {
         "@vitejs/plugin-vue": "^6.0.5",
         "@vue/compiler-sfc": "^3.5.30",
-        "vite": "^8.0.2"
+        "vite": "^8.0.8"
       }
     },
     "node_modules/@babel/generator": {
@@ -104,21 +104,21 @@
       }
     },
     "node_modules/@emnapi/core": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
-      "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+      "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@emnapi/wasi-threads": "1.2.0",
+        "@emnapi/wasi-threads": "1.2.1",
         "tslib": "^2.4.0"
       }
     },
     "node_modules/@emnapi/runtime": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
-      "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+      "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
       "dev": true,
       "license": "MIT",
       "optional": true,
@@ -127,9 +127,9 @@
       }
     },
     "node_modules/@emnapi/wasi-threads": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
-      "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+      "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
       "dev": true,
       "license": "MIT",
       "optional": true,
@@ -269,26 +269,28 @@
       }
     },
     "node_modules/@napi-rs/wasm-runtime": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
-      "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
+      "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@emnapi/core": "^1.7.1",
-        "@emnapi/runtime": "^1.7.1",
         "@tybys/wasm-util": "^0.10.1"
       },
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1"
       }
     },
     "node_modules/@oxc-project/types": {
-      "version": "0.122.0",
-      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
-      "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+      "version": "0.124.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
+      "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
       "dev": true,
       "license": "MIT",
       "funding": {
@@ -307,9 +309,9 @@
       }
     },
     "node_modules/@rolldown/binding-android-arm64": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz",
-      "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
       "cpu": [
         "arm64"
       ],
@@ -324,9 +326,9 @@
       }
     },
     "node_modules/@rolldown/binding-darwin-arm64": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz",
-      "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
       "cpu": [
         "arm64"
       ],
@@ -341,9 +343,9 @@
       }
     },
     "node_modules/@rolldown/binding-darwin-x64": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz",
-      "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
       "cpu": [
         "x64"
       ],
@@ -358,9 +360,9 @@
       }
     },
     "node_modules/@rolldown/binding-freebsd-x64": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz",
-      "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
       "cpu": [
         "x64"
       ],
@@ -375,9 +377,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz",
-      "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
+      "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
       "cpu": [
         "arm"
       ],
@@ -392,9 +394,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm64-gnu": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz",
-      "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
       "cpu": [
         "arm64"
       ],
@@ -409,9 +411,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm64-musl": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz",
-      "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
+      "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
       "cpu": [
         "arm64"
       ],
@@ -426,9 +428,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-ppc64-gnu": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz",
-      "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
       "cpu": [
         "ppc64"
       ],
@@ -443,9 +445,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-s390x-gnu": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz",
-      "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
       "cpu": [
         "s390x"
       ],
@@ -460,9 +462,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-x64-gnu": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz",
-      "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
       "cpu": [
         "x64"
       ],
@@ -477,9 +479,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-x64-musl": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz",
-      "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
+      "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
       "cpu": [
         "x64"
       ],
@@ -494,9 +496,9 @@
       }
     },
     "node_modules/@rolldown/binding-openharmony-arm64": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz",
-      "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
       "cpu": [
         "arm64"
       ],
@@ -511,9 +513,9 @@
       }
     },
     "node_modules/@rolldown/binding-wasm32-wasi": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz",
-      "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
+      "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
       "cpu": [
         "wasm32"
       ],
@@ -521,16 +523,18 @@
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@napi-rs/wasm-runtime": "^1.1.1"
+        "@emnapi/core": "1.9.2",
+        "@emnapi/runtime": "1.9.2",
+        "@napi-rs/wasm-runtime": "^1.1.3"
       },
       "engines": {
         "node": ">=14.0.0"
       }
     },
     "node_modules/@rolldown/binding-win32-arm64-msvc": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz",
-      "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
+      "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
       "cpu": [
         "arm64"
       ],
@@ -545,9 +549,9 @@
       }
     },
     "node_modules/@rolldown/binding-win32-x64-msvc": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz",
-      "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
+      "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
       "cpu": [
         "x64"
       ],
@@ -861,14 +865,14 @@
       "license": "MIT"
     },
     "node_modules/axios": {
-      "version": "1.13.6",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
-      "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+      "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
       "license": "MIT",
       "dependencies": {
         "follow-redirects": "^1.15.11",
         "form-data": "^4.0.5",
-        "proxy-from-env": "^1.1.0"
+        "proxy-from-env": "^2.1.0"
       }
     },
     "node_modules/birpc": {
@@ -1540,9 +1544,9 @@
       }
     },
     "node_modules/lodash": {
-      "version": "4.17.23",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
-      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
       "license": "MIT"
     },
     "node_modules/lodash-es": {
@@ -1751,10 +1755,13 @@
       }
     },
     "node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
-      "license": "MIT"
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
     },
     "node_modules/quansync": {
       "version": "0.2.11",
@@ -1786,14 +1793,14 @@
       }
     },
     "node_modules/rolldown": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
-      "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
+      "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@oxc-project/types": "=0.122.0",
-        "@rolldown/pluginutils": "1.0.0-rc.11"
+        "@oxc-project/types": "=0.124.0",
+        "@rolldown/pluginutils": "1.0.0-rc.15"
       },
       "bin": {
         "rolldown": "bin/cli.mjs"
@@ -1802,27 +1809,27 @@
         "node": "^20.19.0 || >=22.12.0"
       },
       "optionalDependencies": {
-        "@rolldown/binding-android-arm64": "1.0.0-rc.11",
-        "@rolldown/binding-darwin-arm64": "1.0.0-rc.11",
-        "@rolldown/binding-darwin-x64": "1.0.0-rc.11",
-        "@rolldown/binding-freebsd-x64": "1.0.0-rc.11",
-        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11",
-        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11",
-        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11",
-        "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11",
-        "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11",
-        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11",
-        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11",
-        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11",
-        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11",
-        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11",
-        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11"
+        "@rolldown/binding-android-arm64": "1.0.0-rc.15",
+        "@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
+        "@rolldown/binding-darwin-x64": "1.0.0-rc.15",
+        "@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
+        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
+        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
+        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
       }
     },
     "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
-      "version": "1.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz",
-      "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==",
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
+      "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
       "dev": true,
       "license": "MIT"
     },
@@ -1902,16 +1909,16 @@
       }
     },
     "node_modules/vite": {
-      "version": "8.0.2",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
-      "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==",
+      "version": "8.0.8",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
+      "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "lightningcss": "^1.32.0",
-        "picomatch": "^4.0.3",
+        "picomatch": "^4.0.4",
         "postcss": "^8.5.8",
-        "rolldown": "1.0.0-rc.11",
+        "rolldown": "1.0.0-rc.15",
         "tinyglobby": "^0.2.15"
       },
       "bin": {
@@ -1929,7 +1936,7 @@
       "peerDependencies": {
         "@types/node": "^20.19.0 || >=22.12.0",
         "@vitejs/devtools": "^0.1.0",
-        "esbuild": "^0.27.0",
+        "esbuild": "^0.27.0 || ^0.28.0",
         "jiti": ">=1.21.0",
         "less": "^4.0.0",
         "sass": "^1.70.0",
diff --git a/rootfs/web/package.json b/rootfs/web/package.json
index 30a83ca..b01bd32 100644
--- a/rootfs/web/package.json
+++ b/rootfs/web/package.json
@@ -9,7 +9,7 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.2",
-    "axios": "^1.13.6",
+    "axios": "^1.15.0",
     "element-plus": "^2.13.6",
     "vue": "^3.5.30",
     "vue-i18n": "^11.3.0",
@@ -19,6 +19,6 @@
   "devDependencies": {
     "@vitejs/plugin-vue": "^6.0.5",
     "@vue/compiler-sfc": "^3.5.30",
-    "vite": "^8.0.2"
+    "vite": "^8.0.8"
   }
 }

From bc7510a0ce36289d378ae140858423628946e38e Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Fri, 10 Apr 2026 14:38:27 +0800
Subject: [PATCH 26/30] chore(deps): bump django from 5.2.12 to 5.2.13 in
 /rootfs in the pip group across 1 directory

---
 rootfs/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index 3b38b52..65ca2c5 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,5 +1,5 @@
 # Drycc passport requirements
-django==5.2.12
+django==5.2.13
 pytz==2026.1
 django-auth-ldap==5.3.0
 django-cors-headers==4.9.0

From db063cf338829123dd8e8c406bddd9b54714d7d8 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 13 Apr 2026 11:18:03 +0800
Subject: [PATCH 27/30] fix(ui): access token delete button

---
 rootfs/web/src/views/AccessTokenList.vue | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/rootfs/web/src/views/AccessTokenList.vue b/rootfs/web/src/views/AccessTokenList.vue
index 679c85e..0f96eea 100644
--- a/rootfs/web/src/views/AccessTokenList.vue
+++ b/rootfs/web/src/views/AccessTokenList.vue
@@ -36,8 +36,7 @@
                             <td>{{ token.expires }}</td>
                             <td class="ui-table__action-cell">
                               <button @click="showDelete(index)" class="ui-icon-btn" title="Delete" type="button" >
-                                <span class="clip">Delete</span>
-                                <svg style="height: 16px; width: 16px;" class="ui-icon-btn__icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M574.55 522.35L904.4 192.5c16.65-16.65 16.65-44.1 0-60.75l-1.8-1.8c-16.65-16.65-44.1-16.65-60.75 0L512 460.25l-329.85-330.3c-16.65-16.65-44.1-16.65-60.75 0l-1.8 1.8c-17.1 16.65-17.1 44.1 0 60.75l329.85 329.85L119.6 852.2c-16.65 16.65-16.65 44.1 0 60.75l1.8 1.8c16.65 16.65 44.1 16.65 60.75 0L512 584.9l329.85 329.85c16.65 16.65 44.1 16.65 60.75 0l1.8-1.8c16.65-16.65 16.65-44.1 0-60.75L574.55 522.35z" ></path></svg>
+                                <el-icon class="ui-icon-btn__icon" :size="16"><Delete /></el-icon>
                               </button>
                             </td>
                         </tr>
@@ -165,16 +164,22 @@ export default AccessTokenList
 }
 
 .ui-icon-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 28px;
+    height: 28px;
     border: 1px solid transparent;
-    background: #f4f7fc;
-    border-radius: var(--ui-radius-sm);
-    padding: 5px;
+    background: transparent;
+    border-radius: var(--ui-radius-sm, 6px);
+    padding: 0;
     cursor: pointer;
     transition: all 0.16s ease;
 }
 
 .ui-icon-btn__icon {
-    fill: #8a94a6;
+    color: #8a94a6;
+    transition: color 0.16s ease;
 }
 
 .ui-icon-btn:hover {
@@ -183,7 +188,7 @@ export default AccessTokenList
 }
 
 .ui-icon-btn:hover .ui-icon-btn__icon {
-    fill: #de0a0a;
+    color: #de0a0a;
 }
 
 @media (max-width: 900px) {

From 1bea8c8aec137b0c9bd83b655be0c4ff6f00d8d2 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Fri, 17 Apr 2026 23:42:30 +0800
Subject: [PATCH 28/30] chore(ui): use tailadmin css replace vue-element-plus

---
 .woodpecker/build-linux.yml                   |   1 +
 .woodpecker/chart.yaml                        |   1 +
 .woodpecker/manifest.yml                      |   1 +
 .woodpecker/test-linux.yml                    |   1 +
 rootfs/api/templates/base/layout.html         |   2 +-
 rootfs/web/index.html                         |   6 +-
 rootfs/web/package-lock.json                  | 620 ++++++++----
 rootfs/web/package.json                       |   9 +-
 rootfs/web/src/assets/account-setting.js      |   8 +
 .../web/src/components/AccessTokenDelete.js   |  33 -
 .../web/src/components/AccessTokenDelete.vue  | 187 +---
 rootfs/web/src/components/Dropdown.vue        | 229 +----
 rootfs/web/src/components/MainFooter.js       |  21 -
 rootfs/web/src/components/MainFooter.vue      | 236 +----
 rootfs/web/src/components/MainNav.js          |  31 -
 rootfs/web/src/components/MainNav.vue         |  73 +-
 rootfs/web/src/components/NavBar.js           |  16 -
 rootfs/web/src/components/NavBar.vue          | 137 +--
 rootfs/web/src/components/NavBox.js           |   3 -
 rootfs/web/src/components/NavBox.vue          |   8 +-
 rootfs/web/src/components/NavMenu.js          |  35 -
 rootfs/web/src/components/NavMenu.vue         | 127 +--
 rootfs/web/src/components/UserMenu.js         | 105 --
 rootfs/web/src/components/UserMenu.vue        | 141 ++-
 rootfs/web/src/env.d.ts                       |   6 +
 rootfs/web/src/main.js                        |  57 --
 rootfs/web/src/main.ts                        |  51 +
 rootfs/web/src/router/{index.js => index.ts}  |   4 +-
 rootfs/web/src/styles/unified.css             | 242 +----
 rootfs/web/src/utils/axios.js                 |   2 +-
 rootfs/web/src/views/AccessTokenList.js       |  53 -
 rootfs/web/src/views/AccessTokenList.vue      | 339 +++----
 rootfs/web/src/views/AccountSetting.js        | 237 -----
 rootfs/web/src/views/AccountSetting.vue       | 923 ++++++++----------
 rootfs/web/tsconfig.json                      |  18 +
 rootfs/web/tsconfig.node.json                 |  16 +
 rootfs/web/vite.config.js                     |  69 --
 rootfs/web/vite.config.ts                     |  28 +
 38 files changed, 1542 insertions(+), 2534 deletions(-)
 create mode 100644 rootfs/web/src/assets/account-setting.js
 delete mode 100644 rootfs/web/src/components/AccessTokenDelete.js
 delete mode 100644 rootfs/web/src/components/MainFooter.js
 delete mode 100644 rootfs/web/src/components/MainNav.js
 delete mode 100644 rootfs/web/src/components/NavBar.js
 delete mode 100644 rootfs/web/src/components/NavBox.js
 delete mode 100644 rootfs/web/src/components/NavMenu.js
 delete mode 100644 rootfs/web/src/components/UserMenu.js
 create mode 100644 rootfs/web/src/env.d.ts
 delete mode 100644 rootfs/web/src/main.js
 create mode 100644 rootfs/web/src/main.ts
 rename rootfs/web/src/router/{index.js => index.ts} (81%)
 delete mode 100644 rootfs/web/src/views/AccessTokenList.js
 delete mode 100644 rootfs/web/src/views/AccountSetting.js
 create mode 100644 rootfs/web/tsconfig.json
 create mode 100644 rootfs/web/tsconfig.node.json
 delete mode 100644 rootfs/web/vite.config.js
 create mode 100644 rootfs/web/vite.config.ts

diff --git a/.woodpecker/build-linux.yml b/.woodpecker/build-linux.yml
index 3424a97..ef4ecfd 100644
--- a/.woodpecker/build-linux.yml
+++ b/.woodpecker/build-linux.yml
@@ -29,6 +29,7 @@ steps:
     event:
     - push
     - tag
+    - cron
 
 depends_on:
 - test-linux
\ No newline at end of file
diff --git a/.woodpecker/chart.yaml b/.woodpecker/chart.yaml
index 40397e0..cfcd3ec 100644
--- a/.woodpecker/chart.yaml
+++ b/.woodpecker/chart.yaml
@@ -27,6 +27,7 @@ steps:
     event:
     - push
     - tag
+    - cron
 
 depends_on:
 - manifest
diff --git a/.woodpecker/manifest.yml b/.woodpecker/manifest.yml
index 6a91aa3..825ea37 100644
--- a/.woodpecker/manifest.yml
+++ b/.woodpecker/manifest.yml
@@ -36,6 +36,7 @@ steps:
     event:
     - tag
     - push
+    - cron
 
 depends_on:
 - build-linux
diff --git a/.woodpecker/test-linux.yml b/.woodpecker/test-linux.yml
index ded2712..f22a5f0 100644
--- a/.woodpecker/test-linux.yml
+++ b/.woodpecker/test-linux.yml
@@ -23,3 +23,4 @@ steps:
     event:
     - push
     - tag
+    - cron
diff --git a/rootfs/api/templates/base/layout.html b/rootfs/api/templates/base/layout.html
index 7a6af75..11e1c35 100644
--- a/rootfs/api/templates/base/layout.html
+++ b/rootfs/api/templates/base/layout.html
@@ -4,7 +4,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% block title %}Drycc Passport{% endblock %}</title>
+    <title>{% block title %}Drycc CaaS{% endblock %}</title>
     <link rel="stylesheet" href="{% static 'css/main.css' %}" type="text/css">
     {% block extra_head %}{% endblock %}
 </head>
diff --git a/rootfs/web/index.html b/rootfs/web/index.html
index 52ae093..369b5fa 100644
--- a/rootfs/web/index.html
+++ b/rootfs/web/index.html
@@ -3,10 +3,10 @@
 <head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title>Drycc Passport</title>
+  <title>Drycc CaaS</title>
 </head>
-<body>
+<body class="overflow-y-scroll">
   <div id="app"></div>
-  <script type="module" src="/src/main.js"></script>
+  <script type="module" src="/src/main.ts"></script>
 </body>
 </html>
diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index 43623f2..69100bd 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -9,18 +9,21 @@
       "version": "0.0.0",
       "license": "Apache-2.0",
       "dependencies": {
-        "@element-plus/icons-vue": "^2.3.2",
         "axios": "^1.15.0",
-        "element-plus": "^2.13.6",
         "vue": "^3.5.30",
         "vue-i18n": "^11.3.0",
         "vue-router": "^5.0.4",
         "yarn": "^1.22.22"
       },
       "devDependencies": {
+        "@tailwindcss/vite": "^4.2.2",
         "@vitejs/plugin-vue": "^6.0.5",
         "@vue/compiler-sfc": "^3.5.30",
-        "vite": "^8.0.8"
+        "lucide-vue-next": "^1.0.0",
+        "tailwindcss": "^4.2.2",
+        "typescript": "^6.0.3",
+        "vite": "^8.0.8",
+        "vue-tsc": "^3.2.6"
       }
     },
     "node_modules/@babel/generator": {
@@ -85,24 +88,6 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@ctrl/tinycolor": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
-      "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/@element-plus/icons-vue": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
-      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
-      "license": "MIT",
-      "peerDependencies": {
-        "vue": "^3.2.0"
-      }
-    },
     "node_modules/@emnapi/core": {
       "version": "1.9.2",
       "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
@@ -137,31 +122,6 @@
         "tslib": "^2.4.0"
       }
     },
-    "node_modules/@floating-ui/core": {
-      "version": "1.7.5",
-      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
-      "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@floating-ui/utils": "^0.2.11"
-      }
-    },
-    "node_modules/@floating-ui/dom": {
-      "version": "1.7.6",
-      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
-      "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@floating-ui/core": "^1.7.5",
-        "@floating-ui/utils": "^0.2.11"
-      }
-    },
-    "node_modules/@floating-ui/utils": {
-      "version": "0.2.11",
-      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
-      "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
-      "license": "MIT"
-    },
     "node_modules/@intlify/core-base": {
       "version": "11.3.0",
       "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
@@ -297,17 +257,6 @@
         "url": "https://github.com/sponsors/Boshen"
       }
     },
-    "node_modules/@popperjs/core": {
-      "name": "@sxzz/popperjs-es",
-      "version": "2.11.8",
-      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
-      "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
-      "license": "MIT",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
-    },
     "node_modules/@rolldown/binding-android-arm64": {
       "version": "1.0.0-rc.15",
       "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
@@ -572,37 +521,288 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@tybys/wasm-util": {
-      "version": "0.10.1",
-      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
-      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+    "node_modules/@tailwindcss/node": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
+      "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "enhanced-resolve": "^5.19.0",
+        "jiti": "^2.6.1",
+        "lightningcss": "1.32.0",
+        "magic-string": "^0.30.21",
+        "source-map-js": "^1.2.1",
+        "tailwindcss": "4.2.2"
+      }
+    },
+    "node_modules/@tailwindcss/oxide": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
+      "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20"
+      },
+      "optionalDependencies": {
+        "@tailwindcss/oxide-android-arm64": "4.2.2",
+        "@tailwindcss/oxide-darwin-arm64": "4.2.2",
+        "@tailwindcss/oxide-darwin-x64": "4.2.2",
+        "@tailwindcss/oxide-freebsd-x64": "4.2.2",
+        "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
+        "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
+        "@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
+        "@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
+        "@tailwindcss/oxide-linux-x64-musl": "4.2.2",
+        "@tailwindcss/oxide-wasm32-wasi": "4.2.2",
+        "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
+        "@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-android-arm64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
+      "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-arm64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
+      "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-x64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
+      "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-freebsd-x64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
+      "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
+      "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
+      "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
+      "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
+      "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
+      "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
+      "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
+      "bundleDependencies": [
+        "@napi-rs/wasm-runtime",
+        "@emnapi/core",
+        "@emnapi/runtime",
+        "@tybys/wasm-util",
+        "@emnapi/wasi-threads",
+        "tslib"
+      ],
+      "cpu": [
+        "wasm32"
+      ],
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "tslib": "^2.4.0"
+        "@emnapi/core": "^1.8.1",
+        "@emnapi/runtime": "^1.8.1",
+        "@emnapi/wasi-threads": "^1.1.0",
+        "@napi-rs/wasm-runtime": "^1.1.1",
+        "@tybys/wasm-util": "^0.10.1",
+        "tslib": "^2.8.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
       }
     },
-    "node_modules/@types/lodash": {
-      "version": "4.17.24",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
-      "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
-      "license": "MIT"
+    "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
+      "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
     },
-    "node_modules/@types/lodash-es": {
-      "version": "4.17.12",
-      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
-      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+    "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
+      "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/vite": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
+      "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@types/lodash": "*"
+        "@tailwindcss/node": "4.2.2",
+        "@tailwindcss/oxide": "4.2.2",
+        "tailwindcss": "4.2.2"
+      },
+      "peerDependencies": {
+        "vite": "^5.2.0 || ^6 || ^7 || ^8"
       }
     },
-    "node_modules/@types/web-bluetooth": {
-      "version": "0.0.20",
-      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
-      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
-      "license": "MIT"
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
     },
     "node_modules/@vitejs/plugin-vue": {
       "version": "6.0.5",
@@ -621,6 +821,35 @@
         "vue": "^3.2.25"
       }
     },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
     "node_modules/@vue-macros/common": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
@@ -722,6 +951,22 @@
       "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==",
       "license": "MIT"
     },
+    "node_modules/@vue/language-core": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
+      "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.0.0",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.2"
+      }
+    },
     "node_modules/@vue/reactivity": {
       "version": "3.5.30",
       "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
@@ -772,42 +1017,6 @@
       "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
       "license": "MIT"
     },
-    "node_modules/@vueuse/core": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
-      "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/web-bluetooth": "^0.0.20",
-        "@vueuse/metadata": "12.0.0",
-        "@vueuse/shared": "12.0.0",
-        "vue": "^3.5.13"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
-    "node_modules/@vueuse/metadata": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
-      "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
-    "node_modules/@vueuse/shared": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
-      "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
-      "license": "MIT",
-      "dependencies": {
-        "vue": "^3.5.13"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
     "node_modules/acorn": {
       "version": "8.16.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -820,6 +1029,13 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/alien-signals": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
+      "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/ast-kit": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
@@ -852,12 +1068,6 @@
         "url": "https://github.com/sponsors/sxzz"
       }
     },
-    "node_modules/async-validator": {
-      "version": "4.2.5",
-      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
-      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
-      "license": "MIT"
-    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -936,12 +1146,6 @@
       "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
       "license": "MIT"
     },
-    "node_modules/dayjs": {
-      "version": "1.11.20",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
-      "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
-      "license": "MIT"
-    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -975,30 +1179,18 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/element-plus": {
-      "version": "2.13.6",
-      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.6.tgz",
-      "integrity": "sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==",
-      "license": "MIT",
-      "dependencies": {
-        "@ctrl/tinycolor": "^4.2.0",
-        "@element-plus/icons-vue": "^2.3.2",
-        "@floating-ui/dom": "^1.0.1",
-        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
-        "@types/lodash": "^4.17.20",
-        "@types/lodash-es": "^4.17.12",
-        "@vueuse/core": "12.0.0",
-        "async-validator": "^4.2.5",
-        "dayjs": "^1.11.19",
-        "lodash": "^4.17.23",
-        "lodash-es": "^4.17.23",
-        "lodash-unified": "^1.0.3",
-        "memoize-one": "^6.0.0",
-        "normalize-wheel-es": "^1.2.0",
-        "vue-component-type-helpers": "^3.2.4"
+    "node_modules/enhanced-resolve": {
+      "version": "5.20.1",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
+      "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.3.0"
       },
-      "peerDependencies": {
-        "vue": "^3.3.0"
+      "engines": {
+        "node": ">=10.13.0"
       }
     },
     "node_modules/entities": {
@@ -1088,9 +1280,9 @@
       }
     },
     "node_modules/follow-redirects": {
-      "version": "1.15.11",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
-      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
       "funding": [
         {
           "type": "individual",
@@ -1196,6 +1388,13 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/has-symbols": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -1241,6 +1440,16 @@
       "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
       "license": "MIT"
     },
+    "node_modules/jiti": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+      "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
     "node_modules/jsesc": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -1543,27 +1752,14 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
-    "node_modules/lodash": {
-      "version": "4.18.1",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
-      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
-      "license": "MIT"
-    },
-    "node_modules/lodash-es": {
-      "version": "4.18.1",
-      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
-      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
-      "license": "MIT"
-    },
-    "node_modules/lodash-unified": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
-      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
-      "license": "MIT",
+    "node_modules/lucide-vue-next": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz",
+      "integrity": "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==",
+      "dev": true,
+      "license": "ISC",
       "peerDependencies": {
-        "@types/lodash-es": "*",
-        "lodash": "*",
-        "lodash-es": "*"
+        "vue": ">=3.0.1"
       }
     },
     "node_modules/magic-string": {
@@ -1599,12 +1795,6 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/memoize-one": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
-      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
-      "license": "MIT"
-    },
     "node_modules/mime-db": {
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1679,11 +1869,12 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
-    "node_modules/normalize-wheel-es": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
-      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
-      "license": "BSD-3-Clause"
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/pathe": {
       "version": "2.0.3",
@@ -1848,6 +2039,27 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/tailwindcss": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
+      "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tapable": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
+      "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
     "node_modules/tinyglobby": {
       "version": "0.2.15",
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -1872,6 +2084,20 @@
       "license": "0BSD",
       "optional": true
     },
+    "node_modules/typescript": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+      "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
     "node_modules/ufo": {
       "version": "1.6.3",
       "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
@@ -1986,6 +2212,13 @@
         }
       }
     },
+    "node_modules/vscode-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+      "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/vue": {
       "version": "3.5.30",
       "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
@@ -2007,12 +2240,6 @@
         }
       }
     },
-    "node_modules/vue-component-type-helpers": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz",
-      "integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==",
-      "license": "MIT"
-    },
     "node_modules/vue-i18n": {
       "version": "11.3.0",
       "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
@@ -2088,6 +2315,23 @@
         "@vue/devtools-kit": "^8.1.1"
       }
     },
+    "node_modules/vue-tsc": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
+      "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.2.6"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
     "node_modules/webpack-virtual-modules": {
       "version": "0.6.2",
       "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
diff --git a/rootfs/web/package.json b/rootfs/web/package.json
index b01bd32..cddffca 100644
--- a/rootfs/web/package.json
+++ b/rootfs/web/package.json
@@ -8,17 +8,20 @@
     "serve": "vite preview"
   },
   "dependencies": {
-    "@element-plus/icons-vue": "^2.3.2",
     "axios": "^1.15.0",
-    "element-plus": "^2.13.6",
     "vue": "^3.5.30",
     "vue-i18n": "^11.3.0",
     "vue-router": "^5.0.4",
     "yarn": "^1.22.22"
   },
   "devDependencies": {
+    "@tailwindcss/vite": "^4.2.2",
     "@vitejs/plugin-vue": "^6.0.5",
     "@vue/compiler-sfc": "^3.5.30",
-    "vite": "^8.0.8"
+    "lucide-vue-next": "^1.0.0",
+    "tailwindcss": "^4.2.2",
+    "typescript": "^6.0.3",
+    "vite": "^8.0.8",
+    "vue-tsc": "^3.2.6"
   }
 }
diff --git a/rootfs/web/src/assets/account-setting.js b/rootfs/web/src/assets/account-setting.js
new file mode 100644
index 0000000..7dc6e22
--- /dev/null
+++ b/rootfs/web/src/assets/account-setting.js
@@ -0,0 +1,8 @@
+const fs = require('fs');
+const content = fs.readFileSync('/home/duanhongyi/Sources/passport/rootfs/web/src/views/AccountSetting.vue', 'utf8');
+
+const regex = /<style lang="css" scoped>[\s\S]*<\/style>/g;
+const newContent = content.replace(regex, '<style scoped>\n/* Removed legacy styles */\n</style>');
+
+// We also need to rewrite the template part to use tailwind classes.
+
diff --git a/rootfs/web/src/components/AccessTokenDelete.js b/rootfs/web/src/components/AccessTokenDelete.js
deleted file mode 100644
index 2cf6e0b..0000000
--- a/rootfs/web/src/components/AccessTokenDelete.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { reactive, toRefs } from 'vue'
-import { ElMessage } from "element-plus";
-import {deleteAccessToken} from "../services/tokens";
-
-export default {
-    name: "AccessTokenDelete",
-    props: {
-        token: [Object, Function],
-    },
-    setup(props, context) {
-        const state = reactive({
-            token: props.token,
-        })
-
-        const canelDelete = () => {
-            context.emit('closeDelete', { hasAccessTokenDeleted: false })
-        }
-
-        const deleteToken = () => {
-            deleteAccessToken(state.token.id).then(res=>{
-                if (res.status == 204) {
-                    ElMessage.success("OK")
-                    context.emit('closeDelete', { hasAccessTokenDeleted: true })
-                }
-            })
-        }
-        return {
-            ...toRefs(state),
-            canelDelete,
-            deleteToken
-        }
-    }
-}
diff --git a/rootfs/web/src/components/AccessTokenDelete.vue b/rootfs/web/src/components/AccessTokenDelete.vue
index fe7c67d..f7473cf 100644
--- a/rootfs/web/src/components/AccessTokenDelete.vue
+++ b/rootfs/web/src/components/AccessTokenDelete.vue
@@ -1,17 +1,17 @@
 <template>
     <div id="ui-slide-panels">
-        <div class="ui-modal-overlay">
-            <div class="ui-modal-panel" >
-                <div class="ui-modal-header">
-                    <div class="ui-modal-title" >
+        <div class="fixed inset-0 z-50 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center sm:justify-end">
+            <div class="m-3 w-full max-w-lg bg-white rounded-xl shadow-xl flex flex-col overflow-hidden" >
+                <div class="flex items-center justify-between px-6 py-4 border-b border-slate-200">
+                    <div class="text-lg font-semibold text-slate-800" >
                         Delete Token
                     </div>
-                    <button @click="canelDelete" class="ui-modal-close" type="button">
-                        <svg style="height: 16px; width: 16px;" class="ui-modal-close__icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1158" width="200" height="200"><path d="M574.55 522.35L904.4 192.5c16.65-16.65 16.65-44.1 0-60.75l-1.8-1.8c-16.65-16.65-44.1-16.65-60.75 0L512 460.25l-329.85-330.3c-16.65-16.65-44.1-16.65-60.75 0l-1.8 1.8c-17.1 16.65-17.1 44.1 0 60.75l329.85 329.85L119.6 852.2c-16.65 16.65-16.65 44.1 0 60.75l1.8 1.8c16.65 16.65 44.1 16.65 60.75 0L512 584.9l329.85 329.85c16.65 16.65 44.1 16.65 60.75 0l1.8-1.8c16.65-16.65 16.65-44.1 0-60.75L574.55 522.35z" p-id="1159"></path></svg>
+                    <button @click="canelDelete" class="p-2 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors" type="button">
+                        <svg style="height: 16px; width: 16px;" class="w-5 h-5 fill-current" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1158" width="200" height="200"><path d="M574.55 522.35L904.4 192.5c16.65-16.65 16.65-44.1 0-60.75l-1.8-1.8c-16.65-16.65-44.1-16.65-60.75 0L512 460.25l-329.85-330.3c-16.65-16.65-44.1-16.65-60.75 0l-1.8 1.8c-17.1 16.65-17.1 44.1 0 60.75l329.85 329.85L119.6 852.2c-16.65 16.65-16.65 44.1 0 60.75l1.8 1.8c16.65 16.65 44.1 16.65 60.75 0L512 584.9l329.85 329.85c16.65 16.65 44.1 16.65 60.75 0l1.8-1.8c16.65-16.65 16.65-44.1 0-60.75L574.55 522.35z" p-id="1159"></path></svg>
                     </button>
                 </div>
 
-                <div class="ui-modal-content">
+                <div class="p-6">
                     <div class="ui-modal-body">
                         <div class="ui-modal-message-wrap">
                             <div>
@@ -20,8 +20,8 @@
                                         <div>
                                             <div>
                                                 <div>
-                                                    <label class="ui-modal-message">Do you want delete the Access Token of
-                                                      {{ token.token }}
+                                                    <label class="block text-base font-medium text-slate-800 mb-6">Are you sure you want to delete the access token:
+                                                      <span class="font-mono text-sm text-slate-500 break-all bg-slate-100 px-2 py-1 rounded">{{ (token as any)?.token }}</span>?
                                                     </label>
                                                     <div>
                                                         <div>
@@ -33,145 +33,56 @@
                                     </div>
                                 </div>
                             </div>
-                            <button v-if="editAccess" @click="deleteToken" class="ui-btn ui-btn--danger ui-btn--block" type="submit">Delete Token</button>
                         </div>
                     </div>
                 </div>
-                <div class="ui-modal-footer">
-                    <button @click="canelDelete" class="ui-btn ui-btn--secondary ui-btn--half" type="button">Cancel</button>
-                    <button @click="deleteToken" class="ui-btn ui-btn--primary ui-btn--half" type="submit">Yes</button>
+                <div class="flex items-center gap-3 px-6 py-4 border-t border-slate-200 bg-slate-50">
+                    <button @click="canelDelete" class="flex-1 inline-flex justify-center items-center px-4 py-2 bg-white text-slate-700 border border-slate-300 rounded-lg hover:bg-slate-50 font-medium transition-colors" type="button">Cancel</button>
+                    <button @click="deleteToken" class="flex-1 inline-flex justify-center items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-600 font-medium transition-colors" type="submit">Yes</button>
                 </div>
             </div>
         </div>
     </div>
 </template>
 
-<script>
-import AccessTokenDelete from "./AccessTokenDelete"
-export default AccessTokenDelete
-</script>
-
-<style scoped>
-.ui-modal-overlay {
-    position: fixed;
-    inset: 0;
-    z-index: 1000;
-    background: rgba(15, 23, 42, 0.42);
-    display: flex;
-    justify-content: flex-end;
-}
-
-.ui-modal-panel {
-    margin: 12px;
-    width: min(520px, calc(100vw - 24px));
-    background: #fff;
-    border-radius: var(--ui-radius-md);
-    box-shadow: var(--ui-shadow-card);
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-}
-
-.ui-modal-header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 14px 16px;
-    border-bottom: 1px solid var(--ui-color-border);
-}
-
-.ui-modal-title {
-    font-size: var(--ui-font-size-3xl);
-    font-weight: var(--ui-font-weight-bold);
-    color: var(--ui-color-text);
-}
-
-.ui-modal-close {
-    border: none;
-    background: transparent;
-    cursor: pointer;
-    border-radius: var(--ui-radius-sm);
-    padding: 4px;
-}
-
-.ui-modal-close__icon {
-    fill: #8a94a6;
-}
-
-.ui-modal-content {
-    padding: 16px;
-}
-
-.ui-modal-message {
-    display: block;
-    color: var(--ui-color-text);
-    font-size: var(--ui-font-size-lg);
-    font-weight: var(--ui-font-weight-semibold);
-    margin-bottom: 10px;
-}
-
-.ui-modal-footer {
-    display: flex;
-    gap: 8px;
-    padding: 12px 16px 16px;
-    border-top: 1px solid var(--ui-color-border);
-}
-
-.ui-btn {
-    border-radius: var(--ui-radius-sm);
-    border: 1px solid transparent;
-    height: 34px;
-    padding: 0 14px;
-    cursor: pointer;
-}
-
-.ui-btn--half {
-    width: 50%;
-}
-
-.ui-btn--block {
-    width: 100%;
-}
-
-.ui-btn--primary {
-    background: var(--ui-color-primary);
-    border-color: var(--ui-color-primary);
-    color: #fff;
-}
-
-.ui-btn--primary:hover {
-    background: var(--ui-color-primary-hover);
-    border-color: var(--ui-color-primary-hover);
-}
-
-.ui-btn--secondary {
-    background: #fff;
-    border-color: var(--ui-color-border);
-    color: var(--ui-color-text);
-}
-
-.ui-btn--secondary:hover {
-    border-color: var(--ui-color-primary);
-    color: var(--ui-color-primary);
-}
-
-.ui-btn--danger {
-    background: #fff5f4;
-    border-color: #fda29b;
-    color: #b42318;
-}
-
-.ui-btn--danger:hover {
-    background: #d92d20;
-    border-color: #d92d20;
-    color: #fff;
-}
-
-@media (max-width: 900px) {
-    .ui-modal-overlay {
-        justify-content: center;
-        align-items: center;
+<script lang="ts">
+import { reactive, toRefs } from 'vue'
+const ElMessage = { success: alert, error: alert };
+import {deleteAccessToken} from "../services/tokens";
+
+export default {
+    name: "AccessTokenDelete",
+    props: {
+        token: [Object, Function],
+    },
+    setup(props, context) {
+        const state = reactive({
+            token: props.token,
+        })
+
+        const canelDelete = () => {
+            context.emit('closeDelete', { hasAccessTokenDeleted: false })
+        }
+
+        const deleteToken = () => {
+            if (!state.token || typeof state.token === 'function' || !state.token.id) return;
+            deleteAccessToken(state.token.id).then(res=>{
+                if (res.status == 204) {
+                    ElMessage.success("OK")
+                    context.emit('closeDelete', { hasAccessTokenDeleted: true })
+                }
+            })
+        }
+        return {
+            ...toRefs(state),
+            canelDelete,
+            deleteToken
+        }
     }
 }
 
+</script>
+
+<style scoped>
+/* Tailwind handles styles */
 </style>
diff --git a/rootfs/web/src/components/Dropdown.vue b/rootfs/web/src/components/Dropdown.vue
index 8ff2770..7d8a926 100644
--- a/rootfs/web/src/components/Dropdown.vue
+++ b/rootfs/web/src/components/Dropdown.vue
@@ -1,6 +1,6 @@
 <template>
     <div
-        class="ui-top-dropdown"
+        class="relative inline-block text-left"
         ref="root"
         @focusin="openMenu"
         @focusout="handleFocusOut"
@@ -8,64 +8,65 @@
         @mouseleave="handleMouseLeave"
         @keydown.esc="closeMenu"
     >
-        <button
-            :id="triggerId || undefined"
-            class="ui-top-dropdown__trigger-button"
-            type="button"
+        <div
             @focus="openMenu"
             @click.stop="openMenu"
+            :id="triggerId || undefined"
             :aria-expanded="isOpen ? 'true' : 'false'"
             :aria-controls="menuId || undefined"
+            class="outline-none"
+            tabindex="0"
         >
-            <el-icon class="ui-top-dropdown__trigger-icon" aria-hidden="true">
-                <component :is="triggerIcon" />
-            </el-icon>
-            <span class="ui-top-dropdown__trigger-label" :class="{ 'is-mobile-hidden': hideLabelOnMobile }">
-                {{ triggerLabel }}
-            </span>
-        </button>
+            <slot name="trigger">
+                <button
+                    class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group"
+                    type="button"
+                >
+                    <component class="w-4 h-4 text-primary" aria-hidden="true" :is="triggerIcon" />
+                    <span :class="{ 'is-mobile-hidden': hideLabelOnMobile }">
+                        {{ triggerLabel }}
+                    </span>
+                </button>
+            </slot>
+        </div>
 
         <ul
             :id="menuId || undefined"
-            class="ui-top-dropdown__menu"
+            v-show="isOpen" class="absolute right-0 mt-2 z-50 w-56 origin-top-right rounded-xl bg-white shadow-lg ring-1 ring-black/5 focus:outline-none py-1 list-none m-0"
             :class="{ 'ui-top-dropdown__menu--open': isOpen }"
             :style="{ width: normalizedMenuWidth }"
         >
-            <li v-if="metaValue" class="ui-top-dropdown__item-row">
-                <div class="ui-top-dropdown__meta">
-                    <span class="ui-top-dropdown__meta-label">{{ metaLabel || 'Signed in as' }}</span>
-                    <span class="ui-top-dropdown__meta-value">{{ metaValue }}</span>
+            <li v-if="metaValue" class="">
+                <div class="px-4 py-3 border-b border-slate-100">
+                    <span class="block text-xs font-medium text-slate-500 mb-0.5">{{ metaLabel || 'Signed in as' }}</span>
+                    <span class="block text-sm font-semibold text-slate-800 truncate">{{ metaValue }}</span>
                 </div>
             </li>
 
             <li
                 v-for="item in items"
                 :key="item.key || item.label"
-                class="ui-top-dropdown__item-row"
+                class=""
             >
                 <a
                     v-if="item.href"
-                    class="ui-top-dropdown__link"
+                    class="group flex items-center gap-3 px-4 py-2.5 text-sm text-slate-700 hover:bg-slate-50 hover:text-primary transition-colors w-full text-left"
                     :href="item.href"
                     :target="item.external ? '_blank' : undefined"
                     :rel="item.external ? 'noopener noreferrer' : undefined"
                     @click="handleItemClick(item, $event)"
                 >
-                    <el-icon v-if="item.icon" class="ui-top-dropdown__item-icon" aria-hidden="true">
-                        <component :is="item.icon" />
-                    </el-icon>
+                    <component v-if="item.icon" class="w-4 h-4 text-slate-400 group-hover:text-primary transition-colors" aria-hidden="true" :is="item.icon" />
                     <span>{{ item.label }}</span>
                 </a>
 
                 <button
                     v-else
-                    class="ui-top-dropdown__link ui-top-dropdown__link--button"
+                    class="group flex items-center gap-3 px-4 py-2.5 text-sm text-slate-700 hover:bg-slate-50 hover:text-primary transition-colors w-full text-left"
                     type="button"
                     @click="handleItemClick(item, $event)"
                 >
-                    <el-icon v-if="item.icon" class="ui-top-dropdown__item-icon" aria-hidden="true">
-                        <component :is="item.icon" />
-                    </el-icon>
+                    <component v-if="item.icon" class="w-4 h-4 text-slate-400 group-hover:text-primary transition-colors" aria-hidden="true" :is="item.icon" />
                     <span>{{ item.label }}</span>
                 </button>
             </li>
@@ -179,179 +180,5 @@ export default {
 </script>
 
 <style scoped>
-.ui-top-dropdown {
-    position: relative;
-    display: inline-flex;
-    align-items: center;
-}
-
-.ui-top-dropdown__trigger-button {
-    display: inline-flex;
-    align-items: center;
-    justify-content: center;
-    gap: 6px;
-    height: 38px;
-    padding: 0 12px;
-    border-radius: 9px;
-    border: 1px solid transparent;
-    background: transparent;
-    box-sizing: border-box;
-    cursor: pointer;
-    text-decoration: none;
-    font: inherit;
-}
-
-.ui-top-dropdown__trigger-button:hover {
-    background: #f5f8fd;
-    border-color: #e1e9f5;
-}
-
-.ui-top-dropdown__trigger-icon {
-    font-size: var(--ui-font-size-3xl);
-    color: var(--ui-color-primary);
-}
-
-.ui-top-dropdown__trigger-icon :deep(svg),
-.ui-top-dropdown__item-icon :deep(svg) {
-    stroke-width: 2.4;
-    stroke-linecap: round;
-    stroke-linejoin: round;
-}
-
-.ui-top-dropdown__trigger-label {
-    font-size: var(--ui-font-size-xl);
-    font-weight: var(--ui-font-weight-semibold);
-    color: #4e5e76;
-    line-height: 1;
-}
-
-.ui-top-dropdown__menu {
-    position: absolute;
-    right: 0;
-    top: calc(100% + 8px);
-    padding: 0;
-    margin: 0;
-    list-style: none;
-    background: #fff;
-    border: 1px solid rgba(217, 226, 239, 0.96);
-    border-radius: 14px;
-    box-shadow: 0 16px 36px rgba(16, 42, 72, 0.18);
-    overflow: hidden;
-    opacity: 0;
-    pointer-events: none;
-    transform: translateY(-6px);
-    transition: all 0.2s ease;
-}
-
-.ui-top-dropdown__menu--open {
-    opacity: 1;
-    pointer-events: auto;
-    transform: translateY(0);
-}
-
-.ui-top-dropdown__item-row {
-    border-bottom: 1px solid var(--ui-color-border);
-}
-
-.ui-top-dropdown__item-row:last-child {
-    border-bottom: none;
-}
-
-.ui-top-dropdown__meta,
-.ui-top-dropdown__link {
-    display: block;
-    padding: 11px 13px;
-    text-decoration: none;
-    color: var(--ui-color-text);
-}
-
-.ui-top-dropdown__meta {
-    background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%);
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    text-align: center;
-}
-
-.ui-top-dropdown__meta-label {
-    display: block;
-    color: #6a7b95;
-    font-size: var(--ui-font-size-sm);
-}
-
-.ui-top-dropdown__meta-value {
-    display: block;
-    margin-top: 2px;
-    color: #344a66;
-    font-size: var(--ui-font-size-md);
-    font-weight: var(--ui-font-weight-semibold);
-    max-width: 100%;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-.ui-top-dropdown__link {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    font-size: var(--ui-font-size-lg);
-    font-weight: var(--ui-font-weight-medium);
-    line-height: var(--ui-line-height-tight);
-}
-
-.ui-top-dropdown__link--button {
-    width: 100%;
-    border: 0;
-    background: transparent;
-    text-align: left;
-    font: inherit;
-    cursor: pointer;
-}
-
-.ui-top-dropdown__item-icon {
-    width: 16px;
-    flex: 0 0 16px;
-    font-size: var(--ui-font-size-2xl);
-    color: var(--ui-color-primary);
-}
-
-.ui-top-dropdown__link:hover {
-    background: #f4f8fe;
-    color: var(--ui-color-primary);
-}
-
-.ui-top-dropdown__link:hover .ui-top-dropdown__item-icon {
-    color: var(--ui-color-primary);
-}
-
-.ui-top-dropdown__item-row:last-child .ui-top-dropdown__link {
-    border-radius: 0 0 14px 14px;
-}
-
-@media (max-width: 900px) {
-    .ui-top-dropdown__trigger-button {
-        padding: 0 10px;
-        gap: 5px;
-    }
-
-    .ui-top-dropdown__trigger-label {
-        font-size: var(--ui-font-size-md);
-    }
-
-    .ui-top-dropdown__menu {
-        width: 208px !important;
-    }
-}
-
-@media (max-width: 720px) {
-    .ui-top-dropdown__trigger-button {
-        padding: 0 6px;
-    }
-
-    .ui-top-dropdown__trigger-label.is-mobile-hidden {
-        display: none;
-    }
-}
+/* Tailwind handles styles */
 </style>
diff --git a/rootfs/web/src/components/MainFooter.js b/rootfs/web/src/components/MainFooter.js
deleted file mode 100644
index bfae243..0000000
--- a/rootfs/web/src/components/MainFooter.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import {toRefs, reactive, onMounted} from 'vue'
-
-import { getSettings } from '../services/settings'
-
-export default {
-    name: "Footer",
-    setup(props) {
-        const state = reactive({
-            year: new Date().getFullYear(),
-            legalEnabled: false,
-        })
-        onMounted(async () => {
-            var res =  await getSettings()
-            state.legalEnabled = res.data.legal
-        })
-        return {
-            ...toRefs(state),
-        }
-    },
-
-}
diff --git a/rootfs/web/src/components/MainFooter.vue b/rootfs/web/src/components/MainFooter.vue
index 1c1848d..c8931d8 100644
--- a/rootfs/web/src/components/MainFooter.vue
+++ b/rootfs/web/src/components/MainFooter.vue
@@ -1,206 +1,52 @@
 <template>
-    <div class="ui-footer-shell">
-        <div class="ui-footer">
-            <div class="ui-footer__content">
-                <template v-if="legalEnabled">
-                    <ul class="ui-footer__links ui-footer__links--left">
-                        <li class="ui-footer__item ui-footer__item--desktop">
-                            <a class="ui-footer__link" href="http://www.drycc.com">drycc.com</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link" href="https://github.com/drycc">Open Source</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link" href="https://drycc.slack.com/">Community</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link" href="https://www.drycc.cc/">Documentation</a>
-                        </li>
-                    </ul>
-
-                    <div class="ui-footer__divider ui-footer__divider--mobile"></div>
-
-                    <div class="ui-footer__support">
-                        <a class="ui-footer__support-btn" href="https://tawk.to/chat/6234b5491ffac05b1d7f4669/1fueu8a9t"
-                            target="_blank" rel="noopener noreferrer">Support</a>
-                    </div>
-
-                    <div class="ui-footer__spacer"></div>
-
-                    <ul class="ui-footer__links ui-footer__links--right">
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link"
-                                href="https://legal.doopai.com/#/cloud-services-terms">Terms of Service</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link"
-                                href="https://legal.doopai.com/#/doopai-privacy-policy">Privacy</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link"
-                                href="https://legal.doopai.com/#/doopai-cookie-policy">Cookies</a>
-                        </li>
-                        <li class="ui-footer__item ui-footer__copyright">© {{year}} drycc.com</li>
-                    </ul>
-                </template>
-                <template v-else>
-                    <ul class="ui-footer__links ui-footer__links--left">
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link" href="https://github.com/drycc">Open Source</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link" href="https://drycc.slack.com/">Community</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link" href="https://www.drycc.cc/">Documentation</a>
-                        </li>
-                    </ul>
-
-                    <div class="ui-footer__divider ui-footer__divider--mobile"></div>
-
-                    <div class="ui-footer__spacer"></div>
-
-                    <ul class="ui-footer__links ui-footer__links--right">
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link"
-                                href="https://tawk.to/chat/6234b5491ffac05b1d7f4669/1fueu8a9t">Support</a>
-                        </li>
-                        <li class="ui-footer__item">
-                            <a class="ui-footer__link"
-                                href="https://github.com/drycc/workflow/issues">Issues</a>
-                        </li>
-                        <li class="ui-footer__item ui-footer__copyright">© {{year}} drycc.cc</li>
-                    </ul>
-                </template>
-                <div class="ui-footer__divider ui-footer__divider--mobile"></div>
+    <footer class="mt-auto py-8">
+        <div class="max-w-[1600px] mx-auto px-6 w-full">
+            <div v-if="legalEnabled" class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-xs text-slate-500">
+                <span class="text-slate-400">© {{year}} drycc.com</span>
+                <a class="hover:text-primary transition-colors" href="https://legal.doopai.com/#/cloud-services-terms" target="_blank" rel="noopener noreferrer">Terms</a>
+                <a class="hover:text-primary transition-colors" href="https://legal.doopai.com/#/doopai-privacy-policy" target="_blank" rel="noopener noreferrer">Privacy</a>
+                <a class="hover:text-primary transition-colors" href="https://github.com/drycc" target="_blank" rel="noopener noreferrer">Open Source</a>
+                <a class="hover:text-primary transition-colors" href="https://drycc.slack.com/" target="_blank" rel="noopener noreferrer">Community</a>
+                <a class="hover:text-primary transition-colors" href="https://www.drycc.cc/" target="_blank" rel="noopener noreferrer">Documentation</a>
+                <a class="hover:text-primary transition-colors" href="https://tawk.to/chat/6234b5491ffac05b1d7f4669/1fueu8a9t" target="_blank" rel="noopener noreferrer">Support</a>
+            </div>
+            
+            <div v-else class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-xs text-slate-500">
+                <span class="text-slate-400">© {{year}} drycc.cc</span>
+                <a class="hover:text-primary transition-colors" href="https://github.com/drycc" target="_blank" rel="noopener noreferrer">Open Source</a>
+                <a class="hover:text-primary transition-colors" href="https://drycc.slack.com/" target="_blank" rel="noopener noreferrer">Community</a>
+                <a class="hover:text-primary transition-colors" href="https://www.drycc.cc/" target="_blank" rel="noopener noreferrer">Documentation</a>
+                <a class="hover:text-primary transition-colors" href="https://tawk.to/chat/6234b5491ffac05b1d7f4669/1fueu8a9t" target="_blank" rel="noopener noreferrer">Support</a>
+                <a class="hover:text-primary transition-colors" href="https://github.com/drycc/workflow/issues" target="_blank" rel="noopener noreferrer">Issues</a>
             </div>
         </div>
-    </div>
+    </footer>
 </template>
 
-<script>
-import MainFooter from "./MainFooter"
-export default MainFooter
-</script>
-<style scoped>
-.ui-footer {
-    width: 100%;
-    position: static;
-    border-top: 1px solid rgba(217, 226, 239, 0.95);
-    background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(249, 252, 255, 0.98) 100%);
-    backdrop-filter: blur(4px);
-}
-
-.ui-footer-shell {
-    min-height: 56px;
-    margin-top: auto;
-    padding-top: 6px;
-}
-
-.ui-footer__content {
-    max-width: 1200px;
-    margin: 0 auto;
-    padding: 0 16px;
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    min-height: 56px;
-    flex-wrap: wrap;
-}
-
-.ui-footer__links {
-    list-style: none;
-    margin: 0;
-    padding: 0;
-    display: flex;
-    align-items: center;
-    gap: 16px;
-    flex-wrap: wrap;
-}
-
-.ui-footer__link {
-    color: var(--ui-color-text-secondary);
-    text-decoration: none;
-    font-size: var(--ui-font-size-sm);
-    line-height: 1.35;
-    font-weight: var(--ui-font-weight-medium);
-    transition: color 0.15s ease;
-}
-
-.ui-footer__link:hover {
-    color: var(--ui-color-primary);
-}
-
-.ui-footer__support {
-    margin-left: auto;
-}
-
-.ui-footer__support-btn {
-    display: inline-block;
-    border-radius: 999px;
-    padding: 5px 11px;
-    font-size: var(--ui-font-size-sm);
-    line-height: 1.2;
-    font-weight: var(--ui-font-weight-semibold);
-    text-decoration: none;
-    border: 1px solid var(--ui-color-primary);
-    background: var(--ui-color-primary);
-    color: #fff;
-}
+<script lang="ts">
+import {toRefs, reactive, onMounted} from 'vue'
 
-.ui-footer__support-btn:hover {
-    background: var(--ui-color-primary-hover);
-    border-color: var(--ui-color-primary-hover);
-    box-shadow: 0 8px 18px rgba(47, 128, 237, 0.3);
-}
+import { getSettings } from '../services/settings'
 
-.ui-footer__spacer {
-    flex: 1;
-}
-
-.ui-footer__copyright {
-    color: #8a94a6;
-    font-size: var(--ui-font-size-sm);
-    line-height: 1.35;
-}
+export default {
+    name: "Footer",
+    setup(props) {
+        const state = reactive({
+            year: new Date().getFullYear(),
+            legalEnabled: false,
+        })
+        onMounted(async () => {
+            var res =  await getSettings()
+            state.legalEnabled = res.data.legal
+        })
+        return {
+            ...toRefs(state),
+        }
+    },
 
-.ui-footer__divider {
-    display: none;
-    width: 100%;
-    border-top: 1px solid var(--ui-color-border);
 }
 
-@media (max-width: 900px) {
-    .ui-footer__content {
-        min-height: unset;
-        padding: 8px 12px;
-        gap: 8px;
-    }
-
-    .ui-footer__links {
-        gap: 12px;
-    }
-
-    .ui-footer__link,
-    .ui-footer__support-btn,
-    .ui-footer__copyright {
-        font-size: var(--ui-font-size-xs);
-    }
-
-    .ui-footer__support {
-        margin-left: 0;
-    }
-
-    .ui-footer__spacer {
-        display: none;
-    }
-
-    .ui-footer__divider--mobile {
-        display: block;
-    }
-
-    .ui-footer__item--desktop {
-        display: none;
-    }
-}
+</script>
+<style scoped>
+/* Scoped styles removed */
 </style>
\ No newline at end of file
diff --git a/rootfs/web/src/components/MainNav.js b/rootfs/web/src/components/MainNav.js
deleted file mode 100644
index 9682823..0000000
--- a/rootfs/web/src/components/MainNav.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useRouter } from 'vue-router'
-
-export default {
-    name: "MainNav",
-    props: {
-        isAccessTokenActive: {
-            type: Boolean,
-            default: false
-        },
-        isAccountSettingActive: {
-            type: Boolean,
-            default: false
-        },
-    },
-    setup(props) {
-        const router = useRouter()
-        const goToAccessToken = () => {
-            router.push({ path: `/access-tokens` })
-        }
-
-        const goToAccountSetting = () => {
-            router.push({ path: `/account-setting` })
-        }
-
-        return {
-            goToAccessToken,
-            goToAccountSetting,
-        }
-    },
-
-}
diff --git a/rootfs/web/src/components/MainNav.vue b/rootfs/web/src/components/MainNav.vue
index 8ba028f..f3fc9a2 100644
--- a/rootfs/web/src/components/MainNav.vue
+++ b/rootfs/web/src/components/MainNav.vue
@@ -1,20 +1,35 @@
 <template>
-    <nav class="ui-subnav">
-        <div class="ui-subnav__inner">
-            <button @click="goToAccessToken" class="ui-subnav__item" :class="{'is-active': isAccessTokenActive}" type="button">
+    <aside class="w-full lg:w-64 flex-shrink-0 flex flex-col gap-5">
+        <div class="flex flex-col gap-0.5 mt-2">
+            <p class="text-xs font-semibold text-slate-500 mb-2 px-1">Settings</p>
+            <button @click="goToAccessToken" 
+                    class="flex items-center justify-start gap-3 px-3 py-2 transition-all w-full text-left"
+                    :class="isAccessTokenActive ? 'bg-primary-50 text-primary rounded-md font-medium' : 'text-slate-600 hover:bg-white hover:shadow-sm rounded-md'"
+                    type="button">
+                <Key :class="isAccessTokenActive ? 'w-4 h-4' : 'w-4 h-4 text-slate-400'" />
                 <span>Access Tokens</span>
             </button>
-            <button @click="goToAccountSetting" class="ui-subnav__item" :class="{'is-active': isAccountSettingActive}" type="button">
+            <button @click="goToAccountSetting" 
+                    class="flex items-center justify-start gap-3 px-3 py-2 transition-all w-full text-left"
+                    :class="isAccountSettingActive ? 'bg-primary-50 text-primary rounded-md font-medium' : 'text-slate-600 hover:bg-white hover:shadow-sm rounded-md'"
+                    type="button">
+                <Settings :class="isAccountSettingActive ? 'w-4 h-4' : 'w-4 h-4 text-slate-400'" />
                 <span>Account Setting</span>
             </button>
         </div>
-    </nav>
+    </aside>
 </template>
 
-<script>
+<script lang="ts">
 import { useRouter } from 'vue-router'
+import { Key, Settings } from 'lucide-vue-next'
 
 export default {
+    name: "MainNav",
+    components: {
+        Key,
+        Settings
+    },
     props: {
         isAccessTokenActive: {
             type: Boolean,
@@ -27,56 +42,24 @@ export default {
     },
     setup(props) {
         const router = useRouter()
-
         const goToAccessToken = () => {
-            router.push('/access-tokens')
+            router.push({ path: `/access-tokens` })
         }
 
         const goToAccountSetting = () => {
-            router.push('/account-setting')
+            router.push({ path: `/account-setting` })
         }
 
         return {
             goToAccessToken,
-            goToAccountSetting
+            goToAccountSetting,
         }
-    }
-}
-</script>
-
-<style scoped>
-.ui-subnav {
-    padding: 14px 16px 8px;
-    border-bottom: 1px solid rgba(217, 226, 239, 0.75);
-    background: linear-gradient(180deg, rgba(247, 250, 255, 0.95) 0%, rgba(255, 255, 255, 1) 100%);
-}
-
-.ui-subnav__inner {
-    display: flex;
-    gap: 8px;
-    padding: 0;
-    flex-wrap: wrap;
-}
+    },
 
-.ui-subnav__item {
-    border: none;
-    background: #edf2fa;
-    color: var(--ui-color-text-secondary);
-    padding: 9px 16px;
-    border-radius: 999px;
-    cursor: pointer;
-    font-weight: var(--ui-font-weight-semibold);
-    transition: all 0.18s ease;
 }
 
-.ui-subnav__item:hover {
-    color: var(--ui-color-primary-hover);
-    background: var(--ui-color-primary-soft);
-}
+</script>
 
-.ui-subnav__item.is-active {
-    color: #fff;
-    background: var(--ui-color-primary);
-    box-shadow: 0 8px 20px rgba(64, 158, 255, 0.34);
-}
+<style scoped>
+/* Scoped styles removed */
 </style>
diff --git a/rootfs/web/src/components/NavBar.js b/rootfs/web/src/components/NavBar.js
deleted file mode 100644
index b10471a..0000000
--- a/rootfs/web/src/components/NavBar.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import NavMenu from "./NavMenu.vue";
-import UserMenu from "./UserMenu.vue";
-
-export default {
-    name: "NavBar",
-    props: {
-        user: {
-            type: Object,
-            default: null
-        }
-    },
-    components: {
-        'nav-menu': NavMenu,
-        'user-menu': UserMenu
-    }
-}
diff --git a/rootfs/web/src/components/NavBar.vue b/rootfs/web/src/components/NavBar.vue
index 31044d8..b45c835 100755
--- a/rootfs/web/src/components/NavBar.vue
+++ b/rootfs/web/src/components/NavBar.vue
@@ -1,115 +1,50 @@
 <!--导航栏-->
 <template>
-  <header class="ui-top-area">
-    <nav class="ui-topbar">
-      <div class="ui-topbar__layer">
-        <div class="ui-topbar__brand-wrap">
-          <a class="ui-topbar__brand" href="/">
-            <el-icon size="2rem" class="ui-topbar__logo">
-              <img src="../../src/assets/icons/drycc.svg" />
-            </el-icon>
+  <header class="bg-white border-b border-slate-200 sticky top-0 z-10 shadow-sm w-full">
+    <div class="max-w-[1600px] w-full mx-auto flex items-center justify-between px-6 py-2.5 h-14">
+      <!-- Logo -->
+      <div class="flex items-center">
+          <a href="/" class="flex items-center gap-2.5 hover:opacity-80 transition-opacity">
+              <div class="flex items-center justify-center w-8 h-8 bg-primary rounded shadow-sm">
+                  <img class="w-5 h-5 object-contain filter brightness-0 invert" src="../../src/assets/icons/drycc.svg" alt="Drycc" />
+              </div>
+              <span class="text-base font-semibold text-slate-800 tracking-tight">Drycc CaaS</span>
           </a>
-        </div>
-        <div class="ui-topbar__right">
-          <div class="ui-topbar__center">
-            <nav-menu />
+      </div>
+      
+      <!-- 右侧导航与工具栏合并 -->
+      <div class="flex items-center gap-6 -mr-3">
+          <div class="hidden lg:flex items-center text-sm font-medium text-slate-600">
+              <nav-menu />
           </div>
-          <div class="ui-topbar__actions">
-            <user-menu :user="user" />
+          <div class="flex items-center text-slate-500">
+              <user-menu :user="user" />
           </div>
-        </div>
       </div>
-    </nav>
+    </div>
   </header>
-
 </template>
 
-<script>
-import NavBar from "./NavBar"
-export default NavBar
-</script>
+<script lang="ts">
+import NavMenu from "./NavMenu.vue";
+import UserMenu from "./UserMenu.vue";
 
-<style scoped>
-.ui-top-area {
-  position: sticky;
-  top: 0;
-  z-index: 40;
+export default {
+    name: "NavBar",
+    props: {
+        user: {
+            type: Object,
+            default: null
+        }
+    },
+    components: {
+        'nav-menu': NavMenu,
+        'user-menu': UserMenu
+    }
 }
 
-.ui-topbar {
-  height: 68px;
-  background: rgba(255, 255, 255, 0.95);
-  border-bottom: 1px solid rgba(217, 226, 239, 0.88);
-  backdrop-filter: blur(8px);
-  box-shadow: 0 4px 14px rgba(16, 42, 72, 0.06);
-}
-
-.ui-topbar__layer {
-  width: min(var(--ui-shell-max-width), calc(100% - (var(--ui-shell-gutter) * 2)));
-  max-width: none;
-  margin: 0 auto;
-  height: 100%;
-  padding: 0;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  box-sizing: border-box;
-}
-
-.ui-topbar__brand {
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  width: auto;
-  height: auto;
-  padding: 0;
-  border: none;
-  background: transparent;
-  text-decoration: none;
-  line-height: 0;
-}
-
-.ui-topbar__brand-wrap {
-  flex: 0 0 auto;
-}
-
-.ui-topbar__right {
-  display: inline-flex;
-  align-items: center;
-  gap: 2px;
-}
-
-.ui-topbar__center {
-  display: inline-flex;
-  align-items: center;
-}
-
-.ui-topbar__actions {
-  display: flex;
-  align-items: center;
-}
-
-.ui-topbar__logo img {
-  width: 40px;
-  height: 40px;
-  display: block;
-}
-
-@media (max-width: 900px) {
-  .ui-topbar__layer {
-    gap: 8px;
-  }
-
-  .ui-topbar {
-    height: 60px;
-  }
-
-  .ui-topbar__center {
-    justify-self: end;
-  }
+</script>
 
-  .ui-topbar__right {
-    gap: 0;
-  }
-}
+<style scoped>
+/* Scoped styles removed */
 </style>
diff --git a/rootfs/web/src/components/NavBox.js b/rootfs/web/src/components/NavBox.js
deleted file mode 100644
index 9aea2f7..0000000
--- a/rootfs/web/src/components/NavBox.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
-    name: "NavBox"
-}
diff --git a/rootfs/web/src/components/NavBox.vue b/rootfs/web/src/components/NavBox.vue
index 770035c..ad08308 100644
--- a/rootfs/web/src/components/NavBox.vue
+++ b/rootfs/web/src/components/NavBox.vue
@@ -13,9 +13,11 @@
     </div>
 </template>
 
-<script>
-import NavBox from "./NavBox"
-export default NavBox
+<script lang="ts">
+export default {
+    name: "NavBox"
+}
+
 </script>
 
 <style scoped>
diff --git a/rootfs/web/src/components/NavMenu.js b/rootfs/web/src/components/NavMenu.js
deleted file mode 100644
index 5089186..0000000
--- a/rootfs/web/src/components/NavMenu.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { markRaw } from "vue";
-import { Comment, Document, List, Notebook } from "@element-plus/icons-vue";
-
-export default {
-    name: "NavMenu",
-    data() {
-        return {
-            isMenuActived: false,
-            resourcesTriggerIcon: markRaw(List),
-            resourceItems: [
-                {
-                    key: "docs",
-                    label: "Documentation",
-                    href: "https://www.drycc.com",
-                    icon: markRaw(Document),
-                    external: true
-                },
-                {
-                    key: "community",
-                    label: "Community Support",
-                    href: "https://www.drycc.cc/community/",
-                    icon: markRaw(Comment),
-                    external: true
-                },
-                {
-                    key: "blog",
-                    label: "Drycc.cc Blog",
-                    href: "https://www.drycc.cc/blog/",
-                    icon: markRaw(Notebook),
-                    external: true
-                },
-            ]
-        }
-    }
-}
diff --git a/rootfs/web/src/components/NavMenu.vue b/rootfs/web/src/components/NavMenu.vue
index 0a52432..12ae9d8 100644
--- a/rootfs/web/src/components/NavMenu.vue
+++ b/rootfs/web/src/components/NavMenu.vue
@@ -1,12 +1,18 @@
 <template>
-    <ul id="menu-navigator" class="ui-nav-menu" :class="{'ui-nav-menu--open': isMenuActived}">
-        <li class="ui-nav-menu__item">
-            <a class="ui-nav-menu__link" href="/">
-                <el-icon class="ui-nav-menu__icon" aria-hidden="true"><Grid /></el-icon>
+    <ul id="menu-navigator" class="flex flex-wrap items-center justify-center gap-6 list-none m-0 p-0 text-sm font-medium">
+        <li class="relative">
+            <a class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group" href="/">
+                <LayoutGrid class="w-4 h-4 text-primary" aria-hidden="true" />
                 <span>Dashboard</span>
             </a>
         </li>
-        <li class="ui-nav-menu__item ui-nav-menu__item--dropdown">
+        <li class="relative hidden md:block">
+            <a class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group" href="https://www.drycc.cc/zh-cn/docs/roadmap/" target="_blank" rel="noopener noreferrer">
+                <Zap class="w-4 h-4 text-primary" aria-hidden="true" />
+                <span>What's New</span>
+            </a>
+        </li>
+        <li class="relative">
             <dropdown
                 trigger-id="menu-nav"
                 menu-id="ui-nav-menu-resources"
@@ -19,84 +25,47 @@
     </ul>
 </template>
 
-<script>
-import { Connection, Document, Grid } from "@element-plus/icons-vue"
-import NavMenu from "./NavMenu"
-import Dropdown from "./Dropdown.vue"
+<script lang="ts">
+import { markRaw } from "vue";
+import { MessageSquare, FileText, Bookmark, BookOpen, LayoutGrid, Zap } from "lucide-vue-next";
+import Dropdown from "./Dropdown.vue";
 
-NavMenu.components = {
-    ...(NavMenu.components || {}),
-    Dropdown,
-    Grid,
-    Connection,
-    Document,
+export default {
+    name: "NavMenu",
+    components: { LayoutGrid, Zap, Dropdown },
+    data() {
+        return {
+            isMenuActived: false,
+            resourcesTriggerIcon: markRaw(Bookmark),
+            resourceItems: [
+                {
+                    key: "docs",
+                    label: "Documentation",
+                    href: "https://www.drycc.cc/docs/",
+                    icon: markRaw(FileText),
+                    external: true
+                },
+                {
+                    key: "community",
+                    label: "Community Support",
+                    href: "https://www.drycc.cc/community/",
+                    icon: markRaw(MessageSquare),
+                    external: true
+                },
+                {
+                    key: "blog",
+                    label: "Drycc.cc Blog",
+                    href: "https://www.drycc.cc/blog/",
+                    icon: markRaw(BookOpen),
+                    external: true
+                },
+            ]
+        }
+    }
 }
 
-export default NavMenu
 </script>
 
 <style scoped>
-.ui-nav-menu {
-    list-style: none;
-    margin: 0;
-    padding: 0;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-}
-
-.ui-nav-menu__item {
-    position: relative;
-}
-
-.ui-nav-menu__link {
-    display: inline-flex;
-    align-items: center;
-    gap: 6px;
-    text-decoration: none;
-    color: #4e5e76;
-    font-size: var(--ui-font-size-xl);
-    font-weight: var(--ui-font-weight-semibold);
-    border: 1px solid transparent;
-    border-radius: 8px;
-    padding: 8px 10px;
-    background: transparent;
-    transition: all 0.16s ease;
-}
-
-.ui-nav-menu__icon {
-    font-size: var(--ui-font-size-2xl);
-    color: var(--ui-color-primary);
-}
-
-.ui-nav-menu__icon :deep(svg) {
-    stroke-width: 2.4;
-    stroke-linecap: round;
-    stroke-linejoin: round;
-}
-
-.ui-nav-menu__link:hover {
-    color: #394e6b;
-    background: #f5f8fd;
-    border-color: #e1e9f5;
-}
-
-.ui-nav-menu__link:hover .ui-nav-menu__icon {
-    color: var(--ui-color-primary);
-}
-
-@media (max-width: 900px) {
-    .ui-nav-menu {
-        gap: 4px;
-    }
-
-    .ui-nav-menu__link {
-        padding: 6px;
-        font-size: var(--ui-font-size-md);
-    }
-
-    .ui-nav-menu__icon {
-        font-size: var(--ui-font-size-xl);
-    }
-}
+/* Scoped styles removed */
 </style>
diff --git a/rootfs/web/src/components/UserMenu.js b/rootfs/web/src/components/UserMenu.js
deleted file mode 100644
index 1775713..0000000
--- a/rootfs/web/src/components/UserMenu.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import { computed, markRaw, onBeforeMount, reactive, toRefs, watch } from "vue";
-import { useRouter } from 'vue-router'
-import {dealUser, getUser, postLogout} from "../services/user";
-import { Key, Setting, SwitchButton, UserFilled } from "@element-plus/icons-vue";
-
-export default {
-    name: "UserMenu",
-    props: {
-        user: {
-            type: Object,
-            default: null
-        }
-    },
-    setup(props) {
-        const router = useRouter()
-        const normalizeUser = (input) => {
-            if (!input) {
-                return {
-                    username: null,
-                    email: null,
-                    first_name: null,
-                    last_name: null
-                }
-            }
-            if (input.data) {
-                return dealUser(input)
-            }
-            return {
-                username: input.username ?? null,
-                email: input.email ?? null,
-                first_name: input.first_name ?? null,
-                last_name: input.last_name ?? null
-            }
-        }
-        const state = reactive({
-            user :{
-                username: null,
-                email: null,
-                is_superuser: null
-            },
-        })
-        const logout = () => {
-            postLogout().then(res=>{
-                if (res.status == 200) {
-                    sessionStorage.clear()
-                    router.push({ path: '/'})
-                }
-            })
-            location.reload(true)
-        }
-
-        const accountTriggerIcon = markRaw(UserFilled)
-
-        const menuItems = computed(() => [
-            {
-                key: "settings",
-                label: "Settings",
-                href: "/account-setting",
-                icon: markRaw(Setting)
-            },
-            {
-                key: "access-tokens",
-                label: "Access Tokens",
-                href: "/access-tokens",
-                icon: markRaw(Key)
-            },
-            {
-                key: "sign-out",
-                label: "Sign Out",
-                actionKey: "logout",
-                icon: markRaw(SwitchButton)
-            }
-        ])
-
-        const handleMenuAction = (actionKey) => {
-            if (actionKey === "logout") {
-                logout()
-            }
-        }
-
-        watch(
-            () => props.user,
-            (newUser) => {
-                if (newUser) {
-                    state.user = normalizeUser(newUser)
-                }
-            },
-            { immediate: true }
-        )
-
-        onBeforeMount(async () => {
-            if (!props.user) {
-                const res = await getUser()
-                state.user = normalizeUser(res)
-            }
-        })
-
-        return {
-            ...toRefs(state),
-            accountTriggerIcon,
-            menuItems,
-            handleMenuAction
-        }
-    }
-}
diff --git a/rootfs/web/src/components/UserMenu.vue b/rootfs/web/src/components/UserMenu.vue
index 72a65cd..d825a00 100644
--- a/rootfs/web/src/components/UserMenu.vue
+++ b/rootfs/web/src/components/UserMenu.vue
@@ -1,29 +1,144 @@
 <!--用户菜单-->
-<template v-if="user.username != null">
+<template>
     <dropdown
+        v-if="currentUser && currentUser.username != null"
         trigger-id="menu-account"
         menu-id="ui-user-menu--account"
         trigger-label="Account"
         :trigger-icon="accountTriggerIcon"
         :items="menuItems"
         meta-label="Signed in as"
-        :meta-value="user.email || user.username"
+        :meta-value="currentUser?.email || currentUser?.username"
         :menu-width="226"
         :hide-label-on-mobile="true"
         @action="handleMenuAction"
-    />
+    >
+    </dropdown>
 </template>
 
-<script>
-import { UserFilled } from "@element-plus/icons-vue"
-import Dropdown from "./Dropdown.vue"
-import UserMenu from "./UserMenu";
+<script lang="ts">
+import { computed, markRaw, onBeforeMount, reactive, toRefs, watch } from "vue";
+import { useRouter } from 'vue-router'
+import {dealUser, getUser, postLogout} from "../services/user";
+import { Key, Settings, LogOut, User } from "lucide-vue-next";
+import Dropdown from "./Dropdown.vue";
 
-UserMenu.components = {
-    ...(UserMenu.components || {}),
-    Dropdown,
-    UserFilled,
-};
+export default {
+    name: "UserMenu",
+    components: { Dropdown },
+    props: {
+        user: {
+            type: Object,
+            default: null
+        }
+    },
+    setup(props) {
+        const router = useRouter()
+        const normalizeUser = (input) => {
+            if (!input) {
+                return {
+                    username: null,
+                    email: null,
+                    first_name: null,
+                    last_name: null
+                }
+            }
+            if (input.data) {
+                return dealUser(input)
+            }
+            return {
+                username: input.username ?? null,
+                email: input.email ?? null,
+                first_name: input.first_name ?? null,
+                last_name: input.last_name ?? null
+            }
+        }
+        const state = reactive({
+            currentUser :{
+                username: null,
+                email: null,
+                first_name: null,
+                last_name: null,
+                is_superuser: null
+            },
+        })
+        const logout = () => {
+            postLogout().then(res=>{
+                if (res.status == 200) {
+                    sessionStorage.clear()
+                    router.push({ path: '/'})
+                }
+            })
+            location.reload()
+        }
+
+        const accountTriggerIcon = markRaw(User)
+
+        const userInitials = computed(() => {
+            if (state.currentUser?.first_name) {
+                return state.currentUser.first_name.charAt(0).toUpperCase()
+            }
+            if (state.currentUser?.username) {
+                return state.currentUser.username.charAt(0).toUpperCase()
+            }
+            if (state.currentUser?.email) {
+                return state.currentUser.email.charAt(0).toUpperCase()
+            }
+            return 'A'
+        })
+
+        const menuItems = computed(() => [
+            {
+                key: "settings",
+                label: "Settings",
+                href: "/account-setting",
+                icon: markRaw(Settings)
+            },
+            {
+                key: "access-tokens",
+                label: "Access Tokens",
+                href: "/access-tokens",
+                icon: markRaw(Key)
+            },
+            {
+                key: "sign-out",
+                label: "Sign Out",
+                actionKey: "logout",
+                icon: markRaw(LogOut)
+            }
+        ])
+
+        const handleMenuAction = (actionKey) => {
+            if (actionKey === "logout") {
+                logout()
+            }
+        }
+
+        watch(
+            () => props.user,
+            (newUser) => {
+                if (newUser) {
+                    state.currentUser = normalizeUser(newUser)
+                }
+            },
+            { immediate: true }
+        )
+
+        onBeforeMount(async () => {
+            if (!props.user) {
+                const res = await getUser()
+                state.currentUser = normalizeUser(res)
+            }
+        })
+
+        return {
+            ...toRefs(state),
+            accountTriggerIcon,
+            menuItems,
+            userInitials,
+            handleMenuAction
+        }
+    }
+}
 
-export default UserMenu;
 </script>
diff --git a/rootfs/web/src/env.d.ts b/rootfs/web/src/env.d.ts
new file mode 100644
index 0000000..95ee9df
--- /dev/null
+++ b/rootfs/web/src/env.d.ts
@@ -0,0 +1,6 @@
+/// <reference types="vite/client" />
+declare module '*.vue' {
+  import { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}
diff --git a/rootfs/web/src/main.js b/rootfs/web/src/main.js
deleted file mode 100644
index c59cd43..0000000
--- a/rootfs/web/src/main.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import App from './App.vue'
-import router from './router'
-import { createApp } from 'vue'
-import { i18n, setLang, getUAgentLang } from './lang'
-import './styles/unified.css'
-import ElementPlus from "element-plus";
-import "element-plus/dist/index.css";
-import * as ElementPlusIconsVue from '@element-plus/icons-vue'
-
-/**
- * 时间对象的格式化;
- */
-Date.prototype.format = function(format) {
-    /*
-     * eg:format="yyyy-MM-dd hh:mm:ss";
-     */
-    var o = {
-        "M+" : this.getMonth() + 1, // month
-        "d+" : this.getDate(), // day
-        "h+" : this.getHours(), // hour
-        "m+" : this.getMinutes(), // minute
-        "s+" : this.getSeconds(), // second
-        "q+" : Math.floor((this.getMonth() + 3) / 3), // quarter
-        "S" : this.getMilliseconds()
-        // millisecond
-    }
-
-    if (/(y+)/.test(format)) {
-        format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
-    }
-
-    for (var k in o) {
-        if (new RegExp("(" + k + ")").test(format)) {
-            format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
-        }
-    }
-    return format;
-}
-
-// 初始化
-function init () {
-    // 语言初始化
-    const lang = getUAgentLang()
-    setLang(lang)
-}
-
-init()
-
-const app = createApp(App)
-app.use(router)
-app.use(i18n)
-app.use(ElementPlus);
-// registry global components for Element Plus icons
-for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
-  app.component(key, component)
-}
-app.mount('#app')
diff --git a/rootfs/web/src/main.ts b/rootfs/web/src/main.ts
new file mode 100644
index 0000000..be5d1f8
--- /dev/null
+++ b/rootfs/web/src/main.ts
@@ -0,0 +1,51 @@
+import App from './App.vue'
+import router from './router'
+import { createApp } from 'vue'
+import { i18n, setLang, getUAgentLang } from './lang'
+import './styles/unified.css'
+
+declare global {
+  interface Date {
+    format(format: string): string;
+  }
+}
+
+/**
+ * 时间对象的格式化;
+ */
+Date.prototype.format = function(format: string) {
+    var o: Record<string, number> = {
+        "M+" : this.getMonth() + 1,
+        "d+" : this.getDate(),
+        "h+" : this.getHours(),
+        "m+" : this.getMinutes(),
+        "s+" : this.getSeconds(),
+        "q+" : Math.floor((this.getMonth() + 3) / 3),
+        "S" : this.getMilliseconds()
+    }
+
+    if (/(y+)/.test(format)) {
+        format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
+    }
+
+    for (var k in o) {
+        if (new RegExp("(" + k + ")").test(format)) {
+            format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k].toString() : ("00" + o[k]).substr(("" + o[k]).length));
+        }
+    }
+    return format;
+}
+
+// 初始化
+function init () {
+    // 语言初始化
+    const lang = getUAgentLang()
+    setLang(lang)
+}
+
+init()
+
+const app = createApp(App)
+app.use(router)
+app.use(i18n)
+app.mount('#app')
diff --git a/rootfs/web/src/router/index.js b/rootfs/web/src/router/index.ts
similarity index 81%
rename from rootfs/web/src/router/index.js
rename to rootfs/web/src/router/index.ts
index 4ebbb19..a2a386a 100644
--- a/rootfs/web/src/router/index.js
+++ b/rootfs/web/src/router/index.ts
@@ -1,9 +1,9 @@
-import { createRouter, createWebHistory } from 'vue-router'
+import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
 import AccessTokenList from "../views/AccessTokenList.vue";
 import AccountSetting from "../views/AccountSetting.vue";
 
 
-const routes = [
+const routes: Array<RouteRecordRaw> = [
     {
         path: '/',
         redirect: '/access-tokens',
diff --git a/rootfs/web/src/styles/unified.css b/rootfs/web/src/styles/unified.css
index b55fedc..deaf7d0 100644
--- a/rootfs/web/src/styles/unified.css
+++ b/rootfs/web/src/styles/unified.css
@@ -1,230 +1,20 @@
-:root {
-    --ui-color-bg: #eff5ff;
-    --ui-color-surface: #ffffff;
-    --ui-color-text: #192539;
-    --ui-color-text-secondary: #5f6f86;
-    --ui-color-primary: #409eff;
-    --ui-color-primary-hover: #337ecc;
-    --ui-color-primary-soft: #eaf3ff;
-    --ui-color-primary-softer: #f4f9ff;
-    --ui-color-border: #d9e2ef;
-    --ui-color-muted-bg: #eef3fa;
-    --ui-color-accent: #337ecc;
-    --ui-shell-max-width: 1240px;
-    --ui-shell-gutter: 18px;
-
-    --ui-font-family-base: "Manrope", "IBM Plex Sans", "Helvetica Neue", helvetica, arial, sans-serif;
-    --ui-font-size-base: 14px;
-    --ui-font-size-xs: 12px;
-    --ui-font-size-sm: 13px;
-    --ui-font-size-md: 14px;
-    --ui-font-size-lg: 15px;
-    --ui-font-size-xl: 16px;
-    --ui-font-size-2xl: 18px;
-    --ui-font-size-3xl: 20px;
-    --ui-font-size-4xl: 24px;
-    --ui-font-size-5xl: 30px;
-    --ui-line-height-base: 1.42857;
-    --ui-line-height-tight: 1.25;
-    --ui-line-height-normal: 1.4;
-    --ui-line-height-relaxed: 1.6;
-    --ui-font-weight-regular: 400;
-    --ui-font-weight-medium: 500;
-    --ui-font-weight-semibold: 600;
-    --ui-font-weight-bold: 700;
-
-    --ui-radius-sm: 6px;
-    --ui-radius-md: 12px;
-    --ui-radius-lg: 18px;
-
-    --ui-shadow-sm: 0 8px 18px rgba(64, 158, 255, 0.16);
-    --ui-shadow-md: 0 12px 28px rgba(64, 158, 255, 0.22);
-    --ui-shadow-card: 0 14px 32px rgba(64, 158, 255, 0.14);
-
-    /* Element Plus theme mapping */
-    --el-color-primary: var(--ui-color-primary);
-    --el-color-primary-light-3: #79bbff;
-    --el-color-primary-light-5: #a0cfff;
-    --el-color-primary-light-7: #c6e2ff;
-    --el-color-primary-light-8: #d9ecff;
-    --el-color-primary-light-9: #ecf5ff;
-    --el-color-primary-dark-2: var(--ui-color-primary-hover);
-    --el-text-color-primary: var(--ui-color-text);
-    --el-text-color-regular: var(--ui-color-text-secondary);
-    --el-border-color: var(--ui-color-border);
-    --el-border-color-light: var(--ui-color-border);
-    --el-fill-color-blank: var(--ui-color-surface);
-    --el-bg-color: var(--ui-color-surface);
-    --el-mask-color: rgba(15, 23, 42, 0.42);
-}
-
-html,
-body,
-#app {
-    min-height: 100%;
-}
-
-#app {
-    display: flex;
-    flex-direction: column;
-}
-
-#app > div {
-    min-height: 100vh;
-    display: flex;
-    flex-direction: column;
+@import "tailwindcss";
+
+@theme {
+  --color-primary: #409eff;
+  --color-primary-50: #ecf5ff;
+  --color-primary-100: #d9ecff;
+  --color-primary-200: #c6e2ff;
+  --color-primary-300: #8cc5ff;
+  --color-primary-400: #66b1ff;
+  --color-primary-500: #409eff;
+  --color-primary-600: #337ecc;
+  --color-primary-700: #2966a3;
+  --color-primary-800: #1f4c7a;
+  --color-primary-900: #143352;
 }
 
 body {
-    margin: 0;
-    font-family: var(--ui-font-family-base);
-    font-size: var(--ui-font-size-base);
-    line-height: var(--ui-line-height-base);
-    color: var(--ui-color-text);
-    background-color: #eef2f8;
-    background-image:
-        radial-gradient(110% 68% at -8% 44%, rgba(181, 164, 232, 0.24) 0%, rgba(181, 164, 232, 0.14) 34%, rgba(181, 164, 232, 0) 72%),
-        radial-gradient(110% 68% at 108% 44%, rgba(164, 206, 241, 0.26) 0%, rgba(164, 206, 241, 0.14) 34%, rgba(164, 206, 241, 0) 72%),
-        radial-gradient(84% 36% at 50% -6%, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 78%),
-        linear-gradient(180deg, #f1f4fa 0%, #f4f1fa 52%, #edf3f9 100%);
-    background-repeat: no-repeat;
-}
-
-a {
-    color: var(--ui-color-primary);
-}
-
-a:hover {
-    color: var(--ui-color-primary-hover);
-}
-
-.main-panel {
-    width: min(var(--ui-shell-max-width), calc(100% - (var(--ui-shell-gutter) * 2)));
-    max-width: none;
-    margin: 18px auto 0;
-    padding: 0 0 24px;
-    box-sizing: border-box;
-    background: transparent;
-    flex: 1;
-}
-
-.main-content {
-    background: var(--ui-color-surface);
-    border: 1px solid rgba(217, 226, 239, 0.9);
-    border-radius: var(--ui-radius-lg);
-    box-shadow: var(--ui-shadow-card);
-    overflow: hidden;
-    animation: ui-fade-up 0.34s ease;
-}
-
-@keyframes ui-fade-up {
-    from {
-        opacity: 0;
-        transform: translateY(6px);
-    }
-
-    to {
-        opacity: 1;
-        transform: translateY(0);
-    }
-}
-
-.form-control,
-.el-input__wrapper {
-    border-radius: var(--ui-radius-sm);
-}
-
-.el-button--primary {
-    --el-button-bg-color: var(--ui-color-primary);
-    --el-button-border-color: var(--ui-color-primary);
-    --el-button-hover-bg-color: var(--ui-color-primary-hover);
-    --el-button-hover-border-color: var(--ui-color-primary-hover);
-}
-
-.el-button {
-    border-radius: var(--ui-radius-sm);
-    font-weight: var(--ui-font-weight-semibold);
-}
-
-.el-button--danger.is-plain {
-    --el-button-text-color: #b42318;
-    --el-button-border-color: #fda29b;
-    --el-button-bg-color: #fff5f4;
-    --el-button-hover-text-color: #ffffff;
-    --el-button-hover-border-color: #d92d20;
-    --el-button-hover-bg-color: #d92d20;
-}
-
-.el-table {
-    --el-table-header-bg-color: var(--ui-color-primary-softer);
-    --el-table-header-text-color: var(--ui-color-text);
-    --el-table-row-hover-bg-color: var(--ui-color-primary-softer);
-    border: 1px solid var(--ui-color-border);
-    border-radius: var(--ui-radius-md);
-    overflow: hidden;
-}
-
-.el-table th.el-table__cell {
-    font-weight: var(--ui-font-weight-bold);
-}
-
-.el-empty__description p {
-    color: var(--ui-color-text-secondary);
-}
-
-.el-message {
-    border-radius: var(--ui-radius-sm);
-    box-shadow: var(--ui-shadow-card);
-}
-
-a:focus-visible,
-button:focus-visible,
-input:focus-visible,
-.el-button:focus-visible,
-[role="button"]:focus-visible {
-    outline: 2px solid color-mix(in srgb, var(--ui-color-primary) 70%, #ffffff);
-    outline-offset: 2px;
-}
-
-@media (prefers-reduced-motion: reduce) {
-    *,
-    *::before,
-    *::after {
-        animation-duration: 0.01ms !important;
-        animation-iteration-count: 1 !important;
-        transition-duration: 0.01ms !important;
-        scroll-behavior: auto !important;
-    }
-}
-
-/* Keep Vant only for Toast usage and align its visual tone. */
-.van-toast {
-    border-radius: var(--ui-radius-sm);
-    background: rgba(15, 23, 42, 0.88);
-    color: #fff;
-    box-shadow: var(--ui-shadow-card);
-}
-
-.van-toast--loading {
-    padding: 12px;
-}
-
-@media (max-width: 576px) {
-    :root {
-        --ui-shell-gutter: 10px;
-    }
-
-    body {
-        font-size: var(--ui-font-size-sm);
-    }
-
-    .main-panel {
-        margin-top: 10px;
-        padding: 0 0 16px;
-    }
-
-    .main-content {
-        border-radius: var(--ui-radius-md);
-        box-shadow: var(--ui-shadow-sm);
-    }
+  background-color: #f3f4f6;
+  min-height: 100vh;
 }
diff --git a/rootfs/web/src/utils/axios.js b/rootfs/web/src/utils/axios.js
index a6ddbdb..b410a62 100755
--- a/rootfs/web/src/utils/axios.js
+++ b/rootfs/web/src/utils/axios.js
@@ -1,5 +1,5 @@
 import axios from 'axios'
-import { ElLoading, ElMessage } from 'element-plus'
+const ElMessage = { success: alert, error: alert }; const ElLoading = { service: () => ({ close: () => {} }) }
 import {getCookie} from "./array";
 
 axios.defaults.baseURL = process.env.VUE_APP_BASE_URL
diff --git a/rootfs/web/src/views/AccessTokenList.js b/rootfs/web/src/views/AccessTokenList.js
deleted file mode 100644
index 7905c11..0000000
--- a/rootfs/web/src/views/AccessTokenList.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import NavBar from "../components/NavBar.vue";
-import NavBox from "../components/NavBox.vue";
-import {onBeforeMount, reactive, toRefs} from 'vue'
-import MainNav from "../components/MainNav.vue";
-import MainFooter from "../components/MainFooter.vue";
-import AccessTokenDelete from "../components/AccessTokenDelete.vue"
-
-import {dealAccessTokenList, getAccessTokenList, deleteAccessToken} from "../services/tokens";
-
-export default {
-    name: "AccessTokenList",
-    components: {
-        'nav-bar': NavBar,
-        'nav-box': NavBox,
-        'main-nav': MainNav,
-        'main-footer': MainFooter,
-        'access-token-delete': AccessTokenDelete
-    },
-    setup() {
-        const state = reactive({
-            tokens: [],
-            token: Object,
-            isShowDelete: false,
-        })
-
-        onBeforeMount(async () => {
-            await fetchAccessTokenList()
-        })
-
-        const fetchAccessTokenList = (async () => {
-            let res = await getAccessTokenList()
-            state.tokens = res.data ? dealAccessTokenList(res) : []
-        })
-
-        const showDelete = (index) => {
-            state.isShowDelete = true
-            state.token = state.tokens.slice(index, index + 1)[0];
-        }
-
-        const closeDelete = (param) => {
-            state.isShowDelete = false
-            if (param.hasAccessTokenDeleted) {
-                fetchAccessTokenList()
-            }
-        }
-
-        return {
-            ...toRefs(state),
-            showDelete,
-            closeDelete
-        }
-    },
-}
diff --git a/rootfs/web/src/views/AccessTokenList.vue b/rootfs/web/src/views/AccessTokenList.vue
index 0f96eea..5be966b 100644
--- a/rootfs/web/src/views/AccessTokenList.vue
+++ b/rootfs/web/src/views/AccessTokenList.vue
@@ -1,199 +1,172 @@
 <!--app列表页-->
 <template>
-<div :id="'vue-content-'+Math.random().toString(36).substring(2)">
+<div class="min-h-screen bg-slate-50 flex flex-col font-sans" :id="'vue-content-'+Math.random().toString(36).substring(2)">
     <nav-bar />
-    <div class="main-panel ui-page-shell">
-        <div class="main-content">
-            <main-nav :is-access-token-active="true"/>
+    <main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
+        <main-nav :is-access-token-active="true"  />
+        <section class="flex-1 flex flex-col">
             <access-token-delete ref="accessTokenDelete" v-if="isShowDelete"
               :token="token"
               @closeDelete="closeDelete"
             />
-            <div class="ui-table-wrap">
-                <table class="ui-table">
-                    <tbody>
-                        <tr class="ui-table__head-row">
-                            <th>
-                                application
-                            </th>
-                            <th>
-                                token
-                            </th>
-                            <th>
-                                created
-                            </th>
-                            <th>
-                                expires
-                            </th>
-                            <th>
-                            </th>
-                        </tr>
-                    <template v-for="(token, index) in tokens">
-                        <tr class="ui-table__body-row">
-                            <td>{{ token.application }}</td>
-                            <td>{{ token.token }}</td>
-                            <td>{{ token.created }}</td>
-                            <td>{{ token.expires }}</td>
-                            <td class="ui-table__action-cell">
-                              <button @click="showDelete(index)" class="ui-icon-btn" title="Delete" type="button" >
-                                <el-icon class="ui-icon-btn__icon" :size="16"><Delete /></el-icon>
-                              </button>
-                            </td>
-                        </tr>
-                    </template>
-                    <tr v-if="!tokens || tokens.length === 0" class="ui-table__empty-row">
-                        <td class="ui-table__empty-cell" colspan="5">
-                            <div class="ui-table-empty-state">
-                                <div class="ui-table-empty-state__icon" aria-hidden="true"></div>
-                                <p class="ui-table-empty-state__title">No access tokens</p>
-                                <p class="ui-table-empty-state__desc">Create a token when an integration needs API access.</p>
-                            </div>
-                        </td>
-                    </tr>
-                    </tbody>
-                </table>
+            
+            <!-- Table Card -->
+            <div class="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden flex-1 flex flex-col min-h-[600px]">
+                <!-- Header -->
+                <div class="px-6 py-5 border-b border-slate-100 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
+                    <div>
+                        <h2 class="text-lg font-medium text-slate-800">Access Tokens</h2>
+                        <p class="text-sm text-slate-500 mt-1">Generate and manage API tokens to authenticate with third-party integrations.</p>
+                    </div>
+                </div>
+
+                <!-- Table Content -->
+                <div class="w-full overflow-x-auto">
+                    <table class="w-full text-left text-sm text-slate-600">
+                        <thead class="bg-slate-50/50 border-b border-slate-200 text-slate-500 text-xs tracking-wider font-medium">
+                            <tr>
+                                <th class="px-6 py-4">Application</th>
+                                <th class="px-6 py-4">Token</th>
+                                <th class="px-6 py-4">Created</th>
+                                <th class="px-6 py-4">Expires</th>
+                                <th class="px-6 py-4 text-right w-16"></th>
+                            </tr>
+                        </thead>
+                        <tbody class="divide-y divide-slate-100">
+                            <tr v-for="(token, index) in tokens" :key="index" class="hover:bg-primary-50 transition-colors">
+                                <td class="px-6 py-4 font-medium text-slate-800">{{ token.application }}</td>
+                                <td class="px-6 py-4 font-mono text-xs text-slate-500 break-all">{{ token.token }}</td>
+                                <td class="px-6 py-4 whitespace-nowrap">{{ token.created }}</td>
+                                <td class="px-6 py-4 whitespace-nowrap">{{ token.expires }}</td>
+                                <td class="px-6 py-4 text-right whitespace-nowrap">
+                                  <button @click="showDelete(index)" class="text-slate-400 hover:text-red-600 hover:bg-red-50 p-2 rounded-md transition-colors" title="Delete token">
+                                    <Trash2 class="w-4 h-4" />
+                                  </button>
+                                </td>
+                            </tr>
+                            <tr v-if="!tokens || tokens.length === 0">
+                                <td colspan="5" class="px-6 py-16">
+                                    <div class="flex flex-col items-center justify-center text-center">
+                                        <div class="w-16 h-16 bg-slate-50 border border-slate-100 rounded-full flex items-center justify-center mb-4">
+                                            <div class="w-10 h-10 border border-slate-200 bg-white rounded-full flex items-center justify-center shadow-sm">
+                                                <div class="w-4 h-[2px] bg-slate-300 rounded-full"></div>
+                                            </div>
+                                        </div>
+                                        <h3 class="text-lg font-medium text-slate-800 mb-1">No access tokens</h3>
+                                        <p class="text-sm text-slate-500 max-w-sm">You haven't generated any access tokens yet. Create one via Drycc CLI to authenticate API requests.</p>
+                                    </div>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
             </div>
-        </div>
-    </div>
+        </section>
+
+        <!-- Right Sidebar (Token Info & Help) -->
+        <aside class="hidden xl:flex flex-col w-[280px] flex-shrink-0 gap-5">
+            <!-- About Tokens -->
+            <div class="bg-white rounded border border-slate-200 overflow-hidden">
+                <div class="px-5 py-3.5 border-b border-slate-100 bg-white">
+                    <h3 class="text-[13px] font-semibold text-slate-700">Security Guidance</h3>
+                </div>
+                <div class="p-5">
+                    <p class="text-[12px] text-slate-500 leading-relaxed mb-4">
+                        Treat your tokens like passwords. Never share them or hardcode them directly into your application source code.
+                    </p>
+                   <a href="https://oauth.net/security/" target="_blank" class="text-[12px] font-medium text-primary hover:text-primary-600 flex items-center gap-1 transition-colors">
+                        Learn more about SSO
+                    </a>
+                </div>
+            </div>
+
+            <!-- Need Help? -->
+            <div class="bg-white rounded border border-slate-200 overflow-hidden">
+                <div class="px-5 py-3.5 border-b border-slate-100 bg-white">
+                    <h3 class="text-[13px] font-semibold text-slate-700">Need Help?</h3>
+                </div>
+                <div class="p-0">
+                    <ul class="divide-y divide-slate-100">
+                        <li>
+                            <a href="https://www.drycc.cc" target="_blank" rel="noopener noreferrer" class="px-5 py-3 text-[12px] text-slate-600 hover:text-slate-900 hover:bg-slate-50 flex items-center justify-between transition-colors w-full">
+                                <span>API Documentation</span>
+                                <i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
+                            </a>
+                        </li>
+                        <li>
+                            <a href="https://drycc.slack.com/" target="_blank" rel="noopener noreferrer" class="px-5 py-3 text-[12px] text-slate-600 hover:text-slate-900 hover:bg-slate-50 flex items-center justify-between transition-colors w-full">
+                                <span>Community Support</span>
+                                <i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </aside>
+    </main>
     <main-footer />
 </div>
 </template>
 
-<script>
-import AccessTokenList from  "./AccessTokenList"
-export default AccessTokenList
-</script>
-
-<style scoped>
-.ui-page-shell {
-    padding: 14px 0 22px;
-}
-
-.ui-table-wrap {
-    margin: 14px 16px 18px;
-    border: 1px solid rgba(217, 226, 239, 0.95);
-    border-radius: 14px;
-    background: #ffffff;
-    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.65);
-    overflow-x: auto;
-}
-
-.ui-table {
-    width: 100%;
-    border-collapse: collapse;
-}
-
-.ui-table th,
-.ui-table td {
-    padding: 12px 14px;
-    border-bottom: 1px solid var(--ui-color-border);
-    text-align: left;
-    color: var(--ui-color-text);
-}
-
-.ui-table th {
-    text-transform: uppercase;
-    letter-spacing: 0.06em;
-    font-size: var(--ui-font-size-xs);
-    background: var(--ui-color-primary-softer);
-    color: #47607f;
-        font-weight: var(--ui-font-weight-bold);
-}
-
-.ui-table td:nth-child(2) {
-    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
-    font-size: var(--ui-font-size-xs);
-    color: #3b4f69;
-}
-
-.ui-table__body-row:hover td {
-    background: var(--ui-color-primary-softer);
-}
-
-.ui-table__action-cell {
-    width: 52px;
-}
-
-.ui-table__empty-cell {
-    padding: 0;
-    border-bottom: none;
-}
-
-.ui-table-empty-state {
-    min-height: 128px;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    gap: 6px;
-    color: var(--ui-color-text-secondary);
-    background: transparent;
-}
-
-.ui-table-empty-state__icon {
-    width: 26px;
-    height: 26px;
-    border-radius: 999px;
-    border: 2px solid #b9cae3;
-    position: relative;
-}
-
-.ui-table-empty-state__icon::after {
-    content: "";
-    position: absolute;
-    left: 50%;
-    top: 50%;
-    width: 8px;
-    height: 2px;
-    margin-left: -4px;
-    margin-top: -1px;
-    border-radius: 2px;
-    background: #8fa8c9;
-}
-
-.ui-table-empty-state__title {
-    margin: 0;
-    font-size: var(--ui-font-size-lg);
-        font-weight: var(--ui-font-weight-semibold);
-    color: #4f6280;
-}
-
-.ui-table-empty-state__desc {
-    margin: 0;
-    font-size: var(--ui-font-size-sm);
+<script lang="ts">
+import NavBar from "../components/NavBar.vue";
+import NavBox from "../components/NavBox.vue";
+import {onBeforeMount, reactive, toRefs} from 'vue'
+import MainNav from "../components/MainNav.vue";
+import MainFooter from "../components/MainFooter.vue";
+import AccessTokenDelete from "../components/AccessTokenDelete.vue"
+import { Trash2 } from "lucide-vue-next"
+
+import {dealAccessTokenList, getAccessTokenList, deleteAccessToken} from "../services/tokens";
+
+export default {
+    name: "AccessTokenList",
+    components: {
+        'nav-bar': NavBar,
+        'nav-box': NavBox,
+        'main-nav': MainNav,
+        'main-footer': MainFooter,
+        'access-token-delete': AccessTokenDelete,
+        Trash2
+    },
+    setup() {
+        const state = reactive({
+            tokens: [],
+            token: Object,
+            isShowDelete: false,
+        })
+
+        onBeforeMount(async () => {
+            await fetchAccessTokenList()
+        })
+
+        const fetchAccessTokenList = (async () => {
+            let res = await getAccessTokenList()
+            state.tokens = res.data ? dealAccessTokenList(res) : []
+        })
+
+        const showDelete = (index) => {
+            state.isShowDelete = true
+            state.token = state.tokens.slice(index, index + 1)[0];
+        }
+
+        const closeDelete = (param) => {
+            state.isShowDelete = false
+            if (param.hasAccessTokenDeleted) {
+                fetchAccessTokenList()
+            }
+        }
+
+        return {
+            ...toRefs(state),
+            showDelete,
+            closeDelete
+        }
+    },
 }
 
-.ui-icon-btn {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 28px;
-    height: 28px;
-    border: 1px solid transparent;
-    background: transparent;
-    border-radius: var(--ui-radius-sm, 6px);
-    padding: 0;
-    cursor: pointer;
-    transition: all 0.16s ease;
-}
-
-.ui-icon-btn__icon {
-    color: #8a94a6;
-    transition: color 0.16s ease;
-}
-
-.ui-icon-btn:hover {
-    border-color: #fda29b;
-    background: #fff5f4;
-}
-
-.ui-icon-btn:hover .ui-icon-btn__icon {
-    color: #de0a0a;
-}
+</script>
 
-@media (max-width: 900px) {
-    .ui-table-wrap {
-        margin: 10px;
-    }
-}
+<style scoped>
+/* Styles removed in favor of Tailwind CSS framework classes */
 </style>
diff --git a/rootfs/web/src/views/AccountSetting.js b/rootfs/web/src/views/AccountSetting.js
deleted file mode 100644
index f7c4412..0000000
--- a/rootfs/web/src/views/AccountSetting.js
+++ /dev/null
@@ -1,237 +0,0 @@
-import NavBar from "../components/NavBar.vue";
-import NavBox from "../components/NavBox.vue";
-import {onMounted, reactive, toRefs} from 'vue'
-import MainNav from "../components/MainNav.vue";
-import MainFooter from "../components/MainFooter.vue";
-import { useRouter } from 'vue-router'
-import { dealUser, getUser, putAccount, putAccountPassword } from "../services/user";
-import { ElMessage } from "element-plus"
-import { getIdentityProviders, getLinkedIdentities, unlinkIdentity } from "../services/identities";
-
-export default {
-    name: "AccountSetting",
-    components: {
-        'nav-bar': NavBar,
-        'nav-box': NavBox,
-        'main-nav': MainNav,
-        'main-footer': MainFooter,
-    },
-    setup() {
-        const showBTN = "ui-inline-actions"
-        const hiddenBTN = "ui-inline-actions is-hidden"
-        const hiddenUpdateBTN = "ui-btn ui-btn--primary is-disabled"
-        const showUpdateBTN = "ui-btn ui-btn--primary"
-
-        var currentPassword, newPassword, confirmNewPassword
-
-        const router = useRouter()
-        const state = reactive({
-            user: {
-                username: null,
-                email: null,
-                first_name: null,
-                last_name: null
-            },
-            originUser: {
-                username: null,
-                email: null,
-                first_name: null,
-                last_name: null
-            },
-            emailBTN: hiddenBTN,
-            nameBTN: hiddenBTN,
-            firstNameBTN: hiddenBTN,
-            lastNameBTN: hiddenBTN,
-            updateBTN: hiddenUpdateBTN,
-            providers: [],
-            identities: [],
-            loading: false,
-        })
-
-        onMounted(async () => {
-            const res = await getUser()
-            const userData = dealUser(res)
-            state.user = { ...userData }
-            state.originUser = { ...userData }
-            await fetchProviders()
-            await fetchIdentities()
-        })
-
-        const fetchProviders = async () => {
-            state.loading = true
-            try {
-                const res = await getIdentityProviders()
-                state.providers = res.data?.results || []
-            } finally {
-                state.loading = false
-            }
-        }
-
-        const fetchIdentities = async () => {
-            state.loading = true
-            try {
-                const res = await getLinkedIdentities()
-                state.identities = res.data?.results || []
-            } finally {
-                state.loading = false
-            }
-        }
-
-        const handleLink = (provider) => {
-            if (!provider?.login_url) {
-                ElMessage.error("Provider login URL missing.")
-                return
-            }
-            window.location.href = provider.login_url
-        }
-
-        const handleUnlink = async (identity) => {
-            if (!identity?.id) {
-                return
-            }
-            await unlinkIdentity(identity.id)
-            ElMessage.success("Unlinked successfully.")
-            await fetchIdentities()
-        }
-
-        const updateUser = () => {
-            state.originUser = { ...state.user }
-        }
-
-        const emailInputChange = (event) => {
-            let newEmail = event.currentTarget.value
-            if (newEmail != state.originUser.email) {
-                state.emailBTN = showBTN
-            } else {
-                state.emailBTN = hiddenBTN
-            }
-            state.user.email = newEmail
-        }
-
-        const firstNameInputChange = (event) => {
-            let newFirstName = event.currentTarget.value
-            if (newFirstName != state.originUser.first_name) {
-                state.firstNameBTN = showBTN
-            } else {
-                state.firstNameBTN = hiddenBTN
-            }
-            state.user.first_name = newFirstName
-        }
-
-        const lastNameInputChange = (event) => {
-            let newLastName = event.currentTarget.value
-            if (newLastName != state.originUser.last_name) {
-                state.lastNameBTN = showBTN
-            } else {
-                state.lastNameBTN = hiddenBTN
-            }
-            state.user.last_name = newLastName
-        }
-
-        const currentPasswordChange = (event) => {
-            currentPassword = event.currentTarget.value
-            if (currentPassword && newPassword && confirmNewPassword) {
-                state.updateBTN = showUpdateBTN
-            } else {
-                state.updateBTN = hiddenUpdateBTN
-            }
-        }
-        const newPasswordChange = (event) => {
-            newPassword = event.currentTarget.value
-            if (currentPassword && newPassword && confirmNewPassword) {
-                state.updateBTN = showUpdateBTN
-            } else {
-                state.updateBTN = hiddenUpdateBTN
-            }
-        }
-        const confirmNewPasswordChange = (event) => {
-            confirmNewPassword = event.currentTarget.value
-            if (currentPassword && newPassword && confirmNewPassword) {
-                state.updateBTN = showUpdateBTN
-            } else {
-                state.updateBTN = hiddenUpdateBTN
-            }
-        }
-        const submitEmail = () => {
-            var re = /^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$/;
-            if(!re.test(state.user.email)) {
-                ElMessage.error("this email is not valid.")
-            } else {
-                putAccount({email: state.user.email}).then(res=>{
-                    if (res.status == 204) {
-                        updateUser()
-                        state.emailBTN = hiddenBTN
-                    }
-                })
-            }
-        }
-
-        const submitFirstName = () => {
-            putAccount({first_name: state.user.first_name}).then(res=>{
-                if (res.status == 204) {
-                    updateUser()
-                    state.firstNameBTN = hiddenBTN
-                }
-            })
-        }
-
-        const submitLastName = () => {
-            putAccount({last_name: state.user.last_name}).then(res=>{
-                if (res.status == 204) {
-                    updateUser()
-                    state.lastNameBTN = hiddenBTN
-                }
-            })
-        }
-
-        const submitPassowrd = () => {
-            if (currentPassword == newPassword){
-                ElMessage.error("Current Password and New Password are the same.")
-            } else if( newPassword.length < 8){
-                ElMessage.error("this New Password is not valid.")
-            } else if (newPassword != confirmNewPassword){
-                ElMessage.error("New Password Confirm and New Password are different.")
-            }else {
-                putAccountPassword({password: currentPassword, new_password: newPassword}).then(res=>{
-                    if (res.status == 204) {
-                        router.push({ path: `/` })
-                    }
-                })
-            }
-        }
-
-        const cancelEmail = () => {
-            state.emailBTN = hiddenBTN
-            state.user.email = state.originUser.email
-        }
-
-        const cancelFirstName = () => {
-            state.firstNameBTN = hiddenBTN
-            state.user.first_name = state.originUser.first_name
-        }
-
-        const cancelLastName = () => {
-            state.lastNameBTN = hiddenBTN
-            state.user.last_name = state.originUser.last_name
-        }
-
-        return {
-            ...toRefs(state),
-            emailInputChange,
-            firstNameInputChange,
-            lastNameInputChange,
-            submitEmail,
-            cancelEmail,
-            submitFirstName,
-            cancelFirstName,
-            submitLastName,
-            cancelLastName,
-            currentPasswordChange,
-            newPasswordChange,
-            confirmNewPasswordChange,
-            submitPassowrd,
-            handleLink,
-            handleUnlink
-        }
-    },
-}
diff --git a/rootfs/web/src/views/AccountSetting.vue b/rootfs/web/src/views/AccountSetting.vue
index 3f39c0a..1b02642 100644
--- a/rootfs/web/src/views/AccountSetting.vue
+++ b/rootfs/web/src/views/AccountSetting.vue
@@ -1,35 +1,42 @@
 <!--app列表页-->
 <template>
-<div :id="'vue-content-'+Math.random().toString(36).substring(2)">
+<div class="min-h-screen bg-slate-50 flex flex-col font-sans" :id="'vue-content-'+Math.random().toString(36).substring(2)">
     <nav-bar :user="user" />
-    <div class="main-panel ui-page-shell">
-        <div class="main-content">
-            <main-nav :is-account-setting-active="true"/>
-        <ul class="ui-settings-list">
-            <li class="ui-settings-card">
-                <div class="ui-settings-section">
-                    <div class="ui-settings-head">
-                        <div class="ui-settings-title" role="heading" aria-level="3">
+    <main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
+        <main-nav :is-account-setting-active="true"  />
+        <section class="flex-1 flex flex-col">
+        <div class="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden flex-1 flex flex-col min-h-[600px]">
+            <!-- Header -->
+            <div class="px-6 py-5 border-b border-slate-100 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
+                <div>
+                    <h2 class="text-lg font-medium text-slate-800">Account Settings</h2>
+                    <p class="text-sm text-slate-500 mt-1">Manage your personal information, security settings, and linked accounts.</p>
+                </div>
+            </div>
+            <div class="flex flex-col w-full divide-y divide-slate-100">
+            <div>
+                <div class="p-6 sm:p-8 flex flex-col md:flex-row gap-8">
+                    <div class="md:w-1/3 flex-shrink-0">
+                        <h3 class="text-base font-medium text-slate-800 mb-1">
                             Profile
-                        </div>
-                        <div class="ui-settings-desc">
-                            Your username is your identity on drycc and is used to log in, update account may require email confirmation.
-                        </div>
+                        </h3>
+                        <p class="text-sm text-slate-500 leading-relaxed md:pr-4">
+                            Manage your basic account details. Your username is your unique identity on the platform.
+                        </p>
                     </div>
 
-                    <div class="ui-settings-body">
-                        <div class="ui-avatar-placeholder">
-                        </div>
-                        <div class="ui-settings-content-wrap">
-                            <div class="ui-form-grid">
+                    <div class="md:w-2/3 flex-grow max-w-2xl">
+                        
+                        <div class="">
+                            <div class="space-y-5">
                                 <div>
                                     <div>
-                                        <label class="ui-field-label" for="name">Username
+                                        <label class="block text-[13px] font-medium text-slate-700 mb-1.5" for="name">Username
                                         </label>
-                                        <div class="ui-inline-field">
-                                            <input autocomplete="off" placeholder="Your name"  id="username" :value=user.username
-                                                    class="ui-input"
-                                                    disabled="disabled"
+                                        <div class="flex items-center gap-3">
+                                            <input autocomplete="off" placeholder="Your name"  id="username" :value="user.username"
+                                                    class="flex-1 px-3 py-2 text-[13px] text-slate-700 bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 transition-colors disabled:bg-slate-50 disabled:text-slate-500"
+                                                    disabled
                                                     type="text">
                                         </div>
                                     </div>
@@ -37,24 +44,17 @@
                                 <div>
                                     <div>
                                         <form role="form">
-                                            <label class="ui-field-label"
+                                            <label class="block text-[13px] font-medium text-slate-700 mb-1.5"
                                                    for="new-e-mail">Email
                                                 Address</label>
-                                            <div class="ui-inline-field">
+                                            <div class="flex items-center gap-3">
                                                 <input autocomplete="off" placeholder="Your email address" @input="emailInputChange($event)"  id="new-e-mail" :value=user.email
-                                                       class="ui-input"
+                                                       class="flex-1 px-3 py-2 text-[13px] text-slate-700 bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 transition-colors disabled:bg-slate-50 disabled:text-slate-500"
                                                        type="text">
 
-                                                <div :class=emailBTN>
-                                                    <button class="ui-btn ui-btn--primary" @click="submitEmail()"
-                                                            type="button"> Save
-                                                    </button>
-                                                    <button class="ui-btn ui-btn--secondary" @click="cancelEmail()"
-                                                            type="button"> Cancel
-                                                    </button>
-                                                </div>
+                                                
                                             </div>
-                                            <div style="display: none" class="email_error">提示信息</div>
+                                            <div style="display: none" class="text-red-500 text-[13px] mt-1">提示信息</div>
                                         </form>
                                     </div>
                                     <span class="confirmable-action"> </span>
@@ -62,22 +62,13 @@
                                 <div>
                                     <div>
                                         <form role="form">
-                                            <label class="ui-field-label" for="name">Last Name
+                                            <label class="block text-[13px] font-medium text-slate-700 mb-1.5" for="first-name">First Name
                                             </label>
-                                            <div class="ui-inline-field">
-                                                <input autocomplete="off" placeholder="Your last name" @input="lastNameInputChange($event)" id="last-name" :value=user.last_name
-                                                       class="ui-input"
+                                            <div class="flex items-center gap-3">
+                                                <input autocomplete="off" placeholder="Your first name" @input="firstNameInputChange($event)" id="first-name" :value=user.first_name
+                                                       class="flex-1 px-3 py-2 text-[13px] text-slate-700 bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 transition-colors disabled:bg-slate-50 disabled:text-slate-500"
                                                        type="text">
-                                                <div :class=lastNameBTN>
-                                                    <button
-                                                            class="ui-btn ui-btn--primary" @click="submitLastName()"
-                                                            type="button"> Save
-                                                    </button>
-                                                    <button
-                                                            class="ui-btn ui-btn--secondary" @click="cancelLastName()"
-                                                            type="button"> Cancel
-                                                    </button>
-                                                </div>
+                                                
                                             </div>
                                         </form>
                                     </div>
@@ -85,86 +76,81 @@
                                 <div>
                                     <div>
                                         <form role="form">
-                                            <label class="ui-field-label" for="name">First Name
+                                            <label class="block text-[13px] font-medium text-slate-700 mb-1.5" for="last-name">Last Name
                                             </label>
-                                            <div class="ui-inline-field">
-                                                <input autocomplete="off" placeholder="Your first name" @input="firstNameInputChange($event)" id="first-name" :value=user.first_name
-                                                       class="ui-input"
+                                            <div class="flex items-center gap-3">
+                                                <input autocomplete="off" placeholder="Your last name" @input="lastNameInputChange($event)" id="last-name" :value=user.last_name
+                                                       class="flex-1 px-3 py-2 text-[13px] text-slate-700 bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 transition-colors disabled:bg-slate-50 disabled:text-slate-500"
                                                        type="text">
-                                                <div :class=firstNameBTN>
-                                                    <button
-                                                            class="ui-btn ui-btn--primary" @click="submitFirstName()"
-                                                            type="button"> Save
-                                                    </button>
-                                                    <button
-                                                            class="ui-btn ui-btn--secondary" @click="cancelFirstName()"
-                                                            type="button"> Cancel
-                                                    </button>
-                                                </div>
+                                                
                                             </div>
                                         </form>
                                     </div>
                                 </div>
-                                <p class="ui-settings-note">
-                                Manage your avatar using <a href="https://gravatar.com" target="_blank" rel="noopener noreferrer" class="ui-inline-link">Gravatar</a>.
-                                </p>
+                                <div class="flex items-center justify-between mt-6">
+                                    <p class="text-[13px] text-slate-500">Manage your avatar using <a href="https://gravatar.com" target="_blank" rel="noopener noreferrer" class="text-primary hover:text-primary-600 hover:underline">Gravatar</a>.</p>
+                                    <button class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" :disabled="!isProfileChanged" @click="submitProfile()" type="button">
+                                        Update Profile
+                                    </button>
+                                </div>
                             </div>
                         </div>
                     </div>
                 </div>
-            </li>
-            <li class="ui-settings-card">
-                <div class="ui-settings-section">
-                    <div class="ui-settings-head">
-                        <div class="ui-settings-title" role="heading" aria-level="3">
+            </div>
+            <div>
+                <div class="p-6 sm:p-8 flex flex-col md:flex-row gap-8">
+                    <div class="md:w-1/3 flex-shrink-0">
+                        <h3 class="text-base font-medium text-slate-800 mb-1">
                             Password
-                        </div>
+                        </h3>
 
-                        <div class="ui-settings-desc">
-                            Change the password here.
-                        </div>
+                        <p class="text-sm text-slate-500 leading-relaxed md:pr-4">
+                            Ensure your account is using a secure password to stay protected.
+                        </p>
                     </div>
 
-                    <div class="ui-settings-body">
+                    <div class="md:w-2/3 flex-grow max-w-2xl">
                         <div>
-                            <div class="ui-settings-content-wrap">
-                            <div class="ui-form-grid ui-form-grid--password">
-                                <form role="form" method="POST" class="ui-password-form">
-                                    <div class="ui-form-group">
-                                        <label for="current-password">Current Password</label>
+                            <div class="">
+                            <div class="space-y-5 ui-form-grid--password">
+                                <form role="form" method="POST" class="ui-password-form space-y-5">
+                                    <div>
+                                        <label class="block text-sm font-medium text-slate-700 mb-1.5" for="current-password">Current Password</label>
                                         <input name="password" @input="currentPasswordChange($event)"
                                                placeholder="enter your current password"
                                                id="current-password"
-                                               class="ui-input"
+                                               class="w-full px-3 py-2 text-sm text-slate-700 bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 transition-colors disabled:bg-slate-50 disabled:text-slate-500"
                                                type="password">
                                     </div>
 
-                                    <div class="ui-form-group">
-                                        <label for="new-password">New
+                                    <div>
+                                        <label class="block text-sm font-medium text-slate-700 mb-1.5" for="new-password">New
                                             Password</label>
                                         <input placeholder="enter a new password" @input="newPasswordChange($event)"
                                                id="new-password"
-                                               class="ui-input"
+                                               class="w-full px-3 py-2 text-sm text-slate-700 bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 transition-colors disabled:bg-slate-50 disabled:text-slate-500"
                                                type="password">
-                                        <p class="ui-settings-note">
+                                        <p class="text-[13px] text-slate-500 mt-1">
                                             Password must be 8 or more characters.
                                         </p>
                                     </div>
 
-                                    <div class="ui-form-group">
-                                        <label for="confirm-new-password">Confirm
+                                    <div>
+                                        <label class="block text-sm font-medium text-slate-700 mb-1.5" for="confirm-new-password">Confirm
                                             New Password</label>
                                         <input placeholder="enter the password again"  @input="confirmNewPasswordChange($event)"
                                                id="confirm-new-password"
-                                               class="ui-input"
+                                               class="w-full px-3 py-2 text-sm text-slate-700 bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 transition-colors disabled:bg-slate-50 disabled:text-slate-500"
+                                               :class="{ 'border-red-300 focus:border-red-500 focus:ring-red-500/20': passwordMismatch }"
                                                type="password">
+                                        <p v-if="passwordMismatch" class="text-[13px] text-red-500 mt-1">
+                                            Passwords do not match.
+                                        </p>
                                     </div>
-
-                                    <div class="ui-form-group">
-                                        <button @click="submitPassowrd()"
-                                                :class=updateBTN
-                                                type="button">
-                                            Update Password
+                                    <div class="flex justify-end mt-6">
+                                        <button class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" :disabled="!isPasswordFormValid" @click="submitPassowrd()" type="button">
+                                            Update Passwd
                                         </button>
                                     </div>
                                 </form>
@@ -173,424 +159,377 @@
                         </div>
                     </div>
                 </div>
-            </li>
-            <li class="ui-settings-card">
-                <div class="ui-settings-section">
-                    <div class="ui-settings-head">
-                        <div class="ui-settings-title" role="heading" aria-level="3">
+            </div>
+            <div>
+                <div class="p-6 sm:p-8 flex flex-col md:flex-row gap-8">
+                    <div class="md:w-1/3 flex-shrink-0">
+                        <h3 class="text-base font-medium text-slate-800 mb-1">
                             Linked SSO
-                        </div>
-                        <div class="ui-settings-desc">
-                            Manage linked OAuth identities for your account.
-                        </div>
+                        </h3>
+                        <p class="text-sm text-slate-500 leading-relaxed md:pr-4">
+                            Manage linked OAuth identities for faster single sign-on access.
+                        </p>
                     </div>
-                    <div class="ui-settings-body">
+                    <div class="md:w-2/3 flex-grow max-w-2xl">
                         <div class="ui-identities-table-wrap">
-                            <el-table class="ui-identities-table" :data="identities" v-loading="loading" style="width: 100%">
-                                <el-table-column label="Provider">
-                                    <template #default="scope">
-                                        <el-space alignment="center" :size="8">
-                                            <span v-html="scope.row.icon"></span>
-                                            <span>{{ scope.row.display_name }}</span>
-                                        </el-space>
-                                    </template>
-                                </el-table-column>
-                                <el-table-column prop="email" label="Email" />
-                                <el-table-column label="Action" width="140">
-                                    <template #default="scope">
-                                        <el-button type="danger" plain @click="handleUnlink(scope.row)">Unlink</el-button>
-                                    </template>
-                                </el-table-column>
-                                <template #empty>
-                                    <div class="ui-table-empty-state">
-                                        <div class="ui-table-empty-state__icon" aria-hidden="true"></div>
-                                        <p class="ui-table-empty-state__title">No linked identities</p>
-                                        <p class="ui-table-empty-state__desc">Link an OAuth provider below to get started.</p>
-                                    </div>
-                                </template>
-                            </el-table>
+                            <div class="w-full border border-slate-200 rounded-md shadow-sm overflow-hidden bg-white">
+                                <ul class="divide-y divide-slate-100">
+                                    <li v-for="scope in identities" :key="scope.email" class="flex flex-col sm:flex-row sm:items-center justify-between p-4 sm:p-5 hover:bg-slate-50 transition-colors gap-4">
+                                        <div class="flex items-center gap-4">
+                                            <div class="w-10 h-10 rounded-full bg-slate-100 border border-slate-200 flex items-center justify-center flex-shrink-0">
+                                                <template v-if="getProviderIcon(scope.provider)">
+                                                    <span class="w-5 h-5 flex items-center justify-center [&>svg]:w-full [&>svg]:h-full" v-html="getProviderIcon(scope.provider)"></span>
+                                                </template>
+                                                <template v-else-if="scope.provider === 'github'">
+                                                    <i data-lucide="github" class="w-5 h-5 text-slate-700"></i>
+                                                </template>
+                                                <template v-else-if="scope.provider === 'gitlab'">
+                                                    <i data-lucide="gitlab" class="w-5 h-5 text-orange-600"></i>
+                                                </template>
+                                                <template v-else-if="scope.provider === 'gitee'">
+                                                    <i data-lucide="git-merge" class="w-5 h-5 text-red-600"></i>
+                                                </template>
+                                                <template v-else>
+                                                    <span class="font-bold text-slate-400">{{ scope.provider.charAt(0).toUpperCase() }}</span>
+                                                </template>
+                                            </div>
+                                            <div class="flex flex-col">
+                                                <span class="font-medium text-[14px] text-slate-800">
+                                                    <template v-if="scope.provider === 'github'">GitHub</template>
+                                                    <template v-else-if="scope.provider === 'gitlab'">GitLab</template>
+                                                    <template v-else-if="scope.provider === 'gitee'">Gitee</template>
+                                                    <template v-else>{{ scope.provider.charAt(0).toUpperCase() + scope.provider.slice(1) }}</template>
+                                                </span>
+                                                <span class="text-[13px] text-slate-500 mt-0.5">{{ scope.email }}</span>
+                                            </div>
+                                        </div>
+                                        <button class="text-red-500 hover:text-red-700 hover:bg-red-50 px-3 py-1.5 rounded-md text-[13px] font-medium transition-colors border border-transparent hover:border-red-200 w-full sm:w-auto" @click="handleUnlink(scope)">
+                                            Unlink Identity
+                                        </button>
+                                    </li>
+                                    <li v-if="!identities || identities.length === 0">
+                                        <div class="px-6 py-10 flex flex-col items-center justify-center text-center">
+                                            <div class="w-12 h-12 bg-slate-50 border border-slate-100 rounded-full flex items-center justify-center mb-3">
+                                                <i data-lucide="link-2" class="w-5 h-5 text-slate-400"></i>
+                                            </div>
+                                            <p class="text-[14px] font-medium text-slate-700">No linked SSO accounts</p>
+                                            <p class="text-[13px] text-slate-500 mt-1">You haven't linked any third-party accounts yet.</p>
+                                        </div>
+                                    </li>
+                                </ul>
+                            </div>
                         </div>
                     </div>
                 </div>
-            </li>
-            <li class="ui-settings-card">
-                <div class="ui-settings-section">
-                    <div class="ui-settings-head">
-                        <div class="ui-settings-title" role="heading" aria-level="3">
+            </div>
+            <div>
+                <div class="p-6 sm:p-8 flex flex-col md:flex-row gap-8">
+                    <div class="md:w-1/3 flex-shrink-0">
+                        <h3 class="text-base font-medium text-slate-800 mb-1">
                             Link New SSO
-                        </div>
-                        <div class="ui-settings-desc">
+                        </h3>
+                        <p class="text-sm text-slate-500 leading-relaxed md:pr-4">
                             Choose a provider to link a new OAuth identity.
-                        </div>
+                        </p>
                     </div>
-                    <div class="ui-settings-body">
+                    <div class="md:w-2/3 flex-grow max-w-2xl">
                         <div v-if="providers.length === 0" class="ui-left-align">
-                            <el-empty description="No providers available." :image-size="80" />
+                            
                         </div>
                         <div v-else class="ui-sso-providers-wrap">
-                            <el-space wrap class="ui-sso-providers-list">
-                                <el-button
+                            <div class="flex flex-wrap gap-3 ui-sso-providers-list">
+                                <button class="inline-flex items-center justify-center gap-2 bg-white border border-slate-200 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 text-slate-700 rounded-md py-2 px-4 shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-slate-200 text-[13px] font-medium"
                                     v-for="provider in providers"
                                     :key="provider.name"
-                                    type="primary"
-                                    plain
                                     @click.prevent="handleLink(provider)"
                                 >
-                                    <span v-html="provider.icon"></span>
-                                    <strong>Link {{ provider.name.charAt(0).toUpperCase() + provider.name.slice(1) }}</strong>
-                                </el-button>
-                            </el-space>
+                                    <span class="w-4 h-4 flex items-center justify-center [&>svg]:w-full [&>svg]:h-full" v-html="provider.icon"></span>
+                                    <span>Link {{ provider.name.charAt(0).toUpperCase() + provider.name.slice(1) }}</span>
+                                </button>
+                            </div>
                         </div>
                     </div>
                 </div>
-            </li>
-        </ul>
-      </div>
-  </div>
-  <main-footer />
+            </div>
+            </div> <!-- Close flex flex-col w-full divide-y -->
+        </div> <!-- Close bg-white rounded-lg -->
+        </section>
+
+        <!-- Right Sidebar (Profile & Help) -->
+        <aside class="hidden xl:flex flex-col w-[280px] flex-shrink-0 gap-5">
+            <!-- SSO & Security Guide -->
+            <div class="bg-white rounded border border-slate-200 overflow-hidden">
+                <div class="px-5 py-3.5 border-b border-slate-100 bg-white">
+                    <h3 class="text-[13px] font-semibold text-slate-700">Security Guidance</h3>
+                </div>
+                <div class="p-5">
+                    <p class="text-[12px] text-slate-500 leading-relaxed mb-4">
+                        Link your third-party OAuth accounts to enable seamless one-click sign-in and ensure account recovery access.
+                    </p>
+                    <a href="https://oauth.net" target="_blank" class="text-[12px] font-medium text-primary hover:text-primary-600 flex items-center gap-1 transition-colors">
+                        Learn more about SSO
+                    </a>
+                </div>
+            </div>
+            
+            <!-- Extra Documentation Link -->
+            <div class="bg-white rounded border border-slate-200 overflow-hidden">
+                <div class="px-5 py-3.5 border-b border-slate-100 bg-white">
+                    <h3 class="text-[13px] font-semibold text-slate-700">Need Help?</h3>
+                </div>
+                <div class="p-0">
+                    <ul class="divide-y divide-slate-100">
+                        <li>
+                            <a href="https://www.drycc.cc" target="_blank" rel="noopener noreferrer" class="px-5 py-3 text-[12px] text-slate-600 hover:text-slate-900 hover:bg-slate-50 flex items-center justify-between transition-colors w-full">
+                                <span>API Documentation</span>
+                                <i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
+                            </a>
+                        </li>
+                        <li>
+                            <a href="https://drycc.slack.com/" target="_blank" rel="noopener noreferrer" class="px-5 py-3 text-[12px] text-slate-600 hover:text-slate-900 hover:bg-slate-50 flex items-center justify-between transition-colors w-full">
+                                <span>Community Support</span>
+                                <i data-lucide="external-link" class="w-3.5 h-3.5 text-slate-400"></i>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </aside>
+    </main>
+    <main-footer />
 </div>
 </template>
-<style lang="css" scoped>
-.ui-page-shell {
-    padding: 14px 0 22px;
-}
-
-.ui-settings-list {
-    list-style: none;
-    margin: 0;
-    padding: 14px;
-    display: flex;
-    flex-direction: column;
-    gap: 14px;
-}
-
-.ui-settings-card {
-    border: 1px solid rgba(217, 226, 239, 0.92);
-    border-radius: 14px;
-    padding: 22px;
-    background: linear-gradient(180deg, #ffffff 0%, #fcfdff 100%);
-    box-shadow: 0 10px 22px rgba(16, 42, 72, 0.06);
-}
-
-.ui-settings-section {
-    display: flex;
-    gap: 24px;
-}
-
-.ui-settings-head {
-    flex: 0 0 230px;
-}
-
-.ui-settings-title {
-    font-size: var(--ui-font-size-5xl);
-    line-height: 1.3;
-    color: var(--ui-color-primary);
-    font-weight: var(--ui-font-weight-bold);
-    letter-spacing: -0.01em;
-}
-
-.ui-settings-desc {
-    margin-top: 12px;
-    color: var(--ui-color-text-secondary);
-    font-size: var(--ui-font-size-xl);
-    line-height: 1.35;
-    max-width: 220px;
-}
-
-.ui-settings-body {
-    flex: 1;
-}
-
-.ui-avatar-placeholder {
-    display: none;
-}
-
-.ui-form-grid {
-    max-width: 760px;
-}
-
-.ui-form-grid > div {
-    margin-bottom: 14px;
-}
-
-.ui-form-grid > div:last-of-type {
-    margin-bottom: 0;
-}
-
-.ui-settings-content-wrap {
-    width: 100%;
-    max-width: none;
-    border: 1px solid var(--ui-color-border);
-    border-radius: 14px;
-    background: #fff;
-    padding: 14px;
-    box-sizing: border-box;
-}
-
-.ui-field-label {
-    display: block;
-    margin-bottom: 8px;
-    font-weight: var(--ui-font-weight-bold);
-    color: #2a3f5e;
-    font-size: var(--ui-font-size-lg);
-}
-
-.ui-inline-field {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    flex-wrap: wrap;
-}
-
-.ui-input {
-    width: 100%;
-    max-width: 500px;
-    height: 42px;
-    border: 1px solid var(--ui-color-border);
-    border-radius: 10px;
-    padding: 0 13px;
-    color: var(--ui-color-text);
-    background: #fbfcff;
-    box-sizing: border-box;
-    transition: all 0.16s ease;
-}
-
-.ui-input:focus {
-    outline: 2px solid rgba(47, 128, 237, 0.2);
-    border-color: var(--ui-color-primary);
-    background: #fff;
-    box-shadow: 0 6px 16px rgba(47, 128, 237, 0.16);
-}
-
-.ui-inline-actions {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-}
-
-.is-hidden {
-    display: none;
-}
-
-.ui-btn {
-    border-radius: 10px;
-    border: 1px solid transparent;
-    height: 36px;
-    padding: 0 15px;
-    cursor: pointer;
-    font-weight: var(--ui-font-weight-bold);
-    transition: all 0.18s ease;
-}
-
-.ui-btn--primary {
-    background: var(--ui-color-primary);
-    border-color: var(--ui-color-primary);
-    color: #fff;
-}
-
-.ui-btn--primary:hover {
-    background: var(--ui-color-primary-hover);
-    border-color: var(--ui-color-primary-hover);
-    box-shadow: 0 8px 18px rgba(47, 128, 237, 0.28);
-}
-
-.ui-btn--secondary {
-    background: #fff;
-    border-color: var(--ui-color-border);
-    color: var(--ui-color-text);
-}
-
-.ui-btn--secondary:hover {
-    border-color: var(--ui-color-primary);
-    color: var(--ui-color-primary);
-}
-
-.is-disabled {
-    opacity: 0.6;
-    cursor: not-allowed;
-}
-
-.ui-form-group {
-    margin-bottom: 16px;
-}
-
-.ui-password-form {
-    width: 100%;
-}
-
-.ui-form-grid--password .ui-form-group {
-    display: block;
-    margin-bottom: 14px;
-}
-
-.ui-form-grid--password .ui-form-group > label {
-    display: block;
-    margin-bottom: 8px;
-    color: #2f3f56;
-    font-weight: var(--ui-font-weight-semibold);
-}
-
-.ui-form-grid--password .ui-form-group > .ui-input {
-    width: 100%;
-    max-width: 500px;
-}
-
-.ui-form-grid--password .ui-form-group > .ui-settings-note {
-    margin: 8px 0 0;
-    font-size: var(--ui-font-size-md);
-}
-
-.ui-form-grid--password .ui-form-group > .ui-btn {
-    margin-top: 4px;
-}
-
-.ui-identities-table-wrap {
-    width: 100%;
-    border: 1px solid var(--ui-color-border);
-    border-radius: 14px;
-    overflow: hidden;
-    background: #fff;
-    box-sizing: border-box;
-}
-
-:deep(.ui-identities-table) {
-    border: none;
-    border-radius: 0;
-    overflow: visible;
-}
-
-:deep(.ui-identities-table .el-table__inner-wrapper),
-:deep(.ui-identities-table .el-table__header-wrapper),
-:deep(.ui-identities-table .el-table__body-wrapper) {
-    border-radius: 0;
-}
-
-.ui-sso-providers-wrap {
-    width: 100%;
-    border: 1px solid var(--ui-color-border);
-    border-radius: 14px;
-    background: #fff;
-    min-height: 92px;
-    padding: 14px;
-    box-sizing: border-box;
-}
-
-.ui-sso-providers-list {
-    width: 100%;
-}
-
-.ui-table-empty-state {
-    min-height: 120px;
-    width: 100%;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    gap: 6px;
-    color: var(--ui-color-text-secondary);
-    background: transparent;
-}
-
-.ui-table-empty-state__icon {
-    width: 26px;
-    height: 26px;
-    border-radius: 999px;
-    border: 2px solid #b9cae3;
-    position: relative;
-}
-
-.ui-table-empty-state__icon::after {
-    content: "";
-    position: absolute;
-    left: 50%;
-    top: 50%;
-    width: 8px;
-    height: 2px;
-    margin-left: -4px;
-    margin-top: -1px;
-    border-radius: 2px;
-    background: #8fa8c9;
-}
-
-.ui-table-empty-state__title {
-    margin: 0;
-    font-size: var(--ui-font-size-lg);
-    font-weight: var(--ui-font-weight-semibold);
-    color: #4f6280;
-}
-
-.ui-table-empty-state__desc {
-    margin: 0;
-    font-size: var(--ui-font-size-sm);
-}
-
-:deep(.ui-identities-table .el-table__empty-block) {
-    width: 100%;
-    min-height: 120px;
-}
-
-:deep(.ui-identities-table .el-table__empty-text) {
-    width: 100%;
-    margin: 0;
-    line-height: normal;
+<style scoped></style>
+<script lang="ts">
+import NavBar from "../components/NavBar.vue";
+import NavBox from "../components/NavBox.vue";
+import {onMounted, reactive, toRefs, computed} from 'vue'
+import MainNav from "../components/MainNav.vue";
+import MainFooter from "../components/MainFooter.vue";
+import { useRouter } from 'vue-router'
+import { dealUser, getUser, putAccount, putAccountPassword } from "../services/user";
+const ElMessage = { success: alert, error: alert }
+import { getIdentityProviders, getLinkedIdentities, unlinkIdentity } from "../services/identities";
+
+export default {
+    name: "AccountSetting",
+    components: {
+        'nav-bar': NavBar,
+        'nav-box': NavBox,
+        'main-nav': MainNav,
+        'main-footer': MainFooter,
+    },
+    setup() {
+        const showBTN = "ui-inline-actions"
+        const hiddenBTN = "ui-inline-actions is-hidden"
+
+        const router = useRouter()
+        const state = reactive({
+            user: {
+                username: null,
+                email: null,
+                first_name: null,
+                last_name: null
+            },
+            originUser: {
+                username: null,
+                email: null,
+                first_name: null,
+                last_name: null
+            },
+            passwords: {
+                current: '',
+                new: '',
+                confirm: ''
+            },
+            emailBTN: hiddenBTN,
+            nameBTN: hiddenBTN,
+            firstNameBTN: hiddenBTN,
+            lastNameBTN: hiddenBTN,
+            providers: [],
+            identities: [],
+            loading: false,
+        })
+
+        const isProfileChanged = computed(() => {
+            return state.user.email !== state.originUser.email || 
+                   state.user.first_name !== state.originUser.first_name || 
+                   state.user.last_name !== state.originUser.last_name;
+        })
+
+        const isPasswordFormValid = computed(() => {
+            return state.passwords.current && state.passwords.new && state.passwords.confirm;
+        })
+
+        const passwordMismatch = computed(() => {
+            if (!state.passwords.new || !state.passwords.confirm) return false;
+            return state.passwords.new !== state.passwords.confirm;
+        })
+
+        onMounted(async () => {
+            const res = await getUser()
+            const userData = dealUser(res)
+            state.user = { ...userData }
+            state.originUser = { ...userData }
+            await fetchProviders()
+            await fetchIdentities()
+        })
+
+        const fetchProviders = async () => {
+            state.loading = true
+            try {
+                const res = await getIdentityProviders()
+                state.providers = res.data?.results || []
+            } finally {
+                state.loading = false
+            }
+        }
+
+        const fetchIdentities = async () => {
+            state.loading = true
+            try {
+                const res = await getLinkedIdentities()
+                state.identities = res.data?.results || []
+            } finally {
+                state.loading = false
+            }
+        }
+
+        const handleLink = (provider) => {
+            if (!provider?.login_url) {
+                ElMessage.error("Provider login URL missing.")
+                return
+            }
+            window.location.href = provider.login_url
+        }
+
+        const handleUnlink = async (identity) => {
+            if (!identity?.id) {
+                return
+            }
+            await unlinkIdentity(identity.id)
+            ElMessage.success("Unlinked successfully.")
+            await fetchIdentities()
+        }
+
+        const updateUser = () => {
+            state.originUser = { ...state.user }
+        }
+
+        const emailInputChange = (event) => {
+            let newEmail = event.currentTarget.value
+            if (newEmail != state.originUser.email) {
+                state.emailBTN = showBTN
+            } else {
+                state.emailBTN = hiddenBTN
+            }
+            state.user.email = newEmail
+        }
+
+        const firstNameInputChange = (event) => {
+            let newFirstName = event.currentTarget.value
+            if (newFirstName != state.originUser.first_name) {
+                state.firstNameBTN = showBTN
+            } else {
+                state.firstNameBTN = hiddenBTN
+            }
+            state.user.first_name = newFirstName
+        }
+
+        const lastNameInputChange = (event) => {
+            let newLastName = event.currentTarget.value
+            if (newLastName != state.originUser.last_name) {
+                state.lastNameBTN = showBTN
+            } else {
+                state.lastNameBTN = hiddenBTN
+            }
+            state.user.last_name = newLastName
+        }
+
+        const currentPasswordChange = (event) => {
+            state.passwords.current = event.currentTarget.value
+        }
+        const newPasswordChange = (event) => {
+            state.passwords.new = event.currentTarget.value
+        }
+        const confirmNewPasswordChange = (event) => {
+            state.passwords.confirm = event.currentTarget.value
+        }
+        const submitProfile = () => {
+            var re = /^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$/;
+            if(!re.test(state.user.email)) {
+                ElMessage.error("This email is not valid.")
+                return;
+            }
+            putAccount({
+              email: state.user.email,
+              first_name: state.user.first_name,
+              last_name: state.user.last_name
+            }).then(res=>{
+                if (res.status == 204) {
+                    updateUser()
+                    ElMessage.success("Profile updated successfully")
+                }
+            })
+        }
+
+        const submitPassowrd = () => {
+            if (state.passwords.current == state.passwords.new){
+                ElMessage.error("Current Password and New Password are the same.")
+            } else if( state.passwords.new.length < 8){
+                ElMessage.error("this New Password is not valid.")
+            } else if (state.passwords.new != state.passwords.confirm){
+                ElMessage.error("New Password Confirm and New Password are different.")
+            }else {
+                putAccountPassword({password: state.passwords.current, new_password: state.passwords.new}).then(res=>{
+                    if (res.status == 204) {
+                        router.push({ path: `/` })
+                    }
+                })
+            }
+        }
+
+        const cancelEmail = () => {
+            state.emailBTN = hiddenBTN
+            state.user.email = state.originUser.email
+        }
+
+        const cancelFirstName = () => {
+            state.firstNameBTN = hiddenBTN
+            state.user.first_name = state.originUser.first_name
+        }
+
+        const cancelLastName = () => {
+            state.lastNameBTN = hiddenBTN
+            state.user.last_name = state.originUser.last_name
+        }
+
+        const getProviderIcon = (providerName) => {
+            const provider = state.providers.find((p) => p.name === providerName)
+            if (provider) {
+                return provider.icon
+            }
+            return null
+        }
+
+        return {
+            getProviderIcon,
+            ...toRefs(state),
+            isProfileChanged,
+            isPasswordFormValid,
+            passwordMismatch,
+            emailInputChange,
+            firstNameInputChange,
+            lastNameInputChange,
+            submitProfile,
+            currentPasswordChange,
+            newPasswordChange,
+            confirmNewPasswordChange,
+            submitPassowrd,
+            handleLink,
+            handleUnlink
+        }
+    },
 }
 
-.ui-settings-note {
-    color: var(--ui-color-text-secondary);
-    margin: 10px 0 0;
-    font-size: var(--ui-font-size-lg);
-}
-
-.ui-inline-link {
-    color: var(--ui-color-primary);
-    text-decoration: none;
-}
-
-.ui-inline-link:hover {
-    color: var(--ui-color-primary-hover);
-}
-
-.ui-left-align {
-    text-align: left;
-}
-
-.el-empty-custom {
-    --el-empty-padding: 0px;
-}
-
-@media (max-width: 900px) {
-    .ui-settings-card {
-        padding: 16px;
-    }
-
-    .ui-settings-section {
-        flex-direction: column;
-        gap: 16px;
-    }
-
-    .ui-settings-head {
-        flex: 1 1 auto;
-    }
-
-    .ui-settings-title {
-        font-size: var(--ui-font-size-4xl);
-    }
-
-    .ui-settings-desc {
-        font-size: var(--ui-font-size-lg);
-        max-width: none;
-    }
-
-    .ui-settings-content-wrap {
-        padding: 12px;
-    }
-
-    .ui-form-grid--password .ui-form-group {
-        margin-bottom: 12px;
-    }
-}
-</style>
-<script>
-import AccountSetting from  "./AccountSetting"
-export default AccountSetting
 </script>
diff --git a/rootfs/web/tsconfig.json b/rootfs/web/tsconfig.json
new file mode 100644
index 0000000..9c3e181
--- /dev/null
+++ b/rootfs/web/tsconfig.json
@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "strict": false,
+    "jsx": "preserve",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": ["ESNext", "DOM"],
+    "skipLibCheck": true,
+    "noEmit": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/rootfs/web/tsconfig.node.json b/rootfs/web/tsconfig.node.json
new file mode 100644
index 0000000..36b98be
--- /dev/null
+++ b/rootfs/web/tsconfig.node.json
@@ -0,0 +1,16 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "target": "ESNext",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "strict": false,
+    "jsx": "preserve",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": ["ESNext", "DOM"],
+    "skipLibCheck": true
+  },
+  "include": ["vite.config.ts"]
+}
diff --git a/rootfs/web/vite.config.js b/rootfs/web/vite.config.js
deleted file mode 100644
index 2dbbb43..0000000
--- a/rootfs/web/vite.config.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { defineConfig } from "vite";
-import vue from "@vitejs/plugin-vue";
-
-VUE_APP_BASE_URL = process.env.VUE_APP_BASE_URL
-  ? process.env.VUE_APP_BASE_URL
-  : "";
-
-// https://vitejs.dev/config/
-export default defineConfig({
-  plugins: [vue()],
-  build: {
-    minify: true,
-    chunkSizeWarningLimit: 3000,
-    rollupOptions: {
-      output: {
-        manualChunks(id) {
-          if (id.includes('element-plus')) {
-            return 'element-plus';
-          }
-        },
-      },
-    },
-  },
-  server: {
-    proxy: {
-      // 代理配置
-      "/avatar": {
-        target: VUE_APP_BASE_URL,
-        changeOrigin: false,
-        xfwd: true,
-      },
-      "/orgs": {
-        target: VUE_APP_BASE_URL,
-        changeOrigin: false,
-        xfwd: true,
-      },
-      "/user": {
-        target: VUE_APP_BASE_URL,
-        changeOrigin: false,
-        xfwd: true,
-      },
-      "/auth": {
-        target: VUE_APP_BASE_URL,
-        changeOrigin: false,
-        xfwd: true,
-      },
-      "/oauth": {
-        target: VUE_APP_BASE_URL,
-        changeOrigin: false,
-        xfwd: true,
-      },
-      "/assets": {
-        target: VUE_APP_BASE_URL,
-        changeOrigin: false,
-        xfwd: true,
-      },
-      "/settings": {
-        target: VUE_APP_BASE_URL,
-        changeOrigin: false,
-        xfwd: true,
-      },
-    },
-  },
-  define: {
-    "process.env": {
-      VUE_APP_BASE_URL: VUE_APP_BASE_URL,
-    },
-  },
-});
diff --git a/rootfs/web/vite.config.ts b/rootfs/web/vite.config.ts
new file mode 100644
index 0000000..5b55b35
--- /dev/null
+++ b/rootfs/web/vite.config.ts
@@ -0,0 +1,28 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import tailwindcss from "@tailwindcss/vite";
+
+const VUE_APP_BASE_URL = process.env.VUE_APP_BASE_URL || "";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue(), tailwindcss()],
+  build: {
+    minify: true,
+    chunkSizeWarningLimit: 3000,
+  },
+  server: {
+    proxy: {
+      "/avatar": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
+      "/orgs": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
+      "/user": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
+      "/auth": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
+      "/oauth": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
+      "/assets": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
+      "/settings": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true }
+    },
+  },
+  define: {
+    "process.env.VUE_APP_BASE_URL": JSON.stringify(VUE_APP_BASE_URL),
+  },
+});

From df1ae4229cda4df1132c553ae6a50b502a094c9f Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 23 Apr 2026 10:41:52 +0800
Subject: [PATCH 29/30] feat(messages): add messages

---
 .woodpecker/manifest.yml                      |   1 +
 charts/passport/templates/_helpers.tpl        |   2 +
 .../passport-api-deployment.yaml}             |  26 +-
 .../celery/passport-celery-deployment.yaml    |  65 ++
 .../passport/templates/passport-service.yaml  |   3 +
 charts/passport/values.yaml                   |  70 +-
 rootfs/api/__init__.py                        |   6 +
 rootfs/api/admin.py                           |  16 +-
 ...thorization_grant_type_message_and_more.py |  61 ++
 rootfs/api/models.py                          | 109 +++
 rootfs/api/serializers.py                     |  24 +
 rootfs/api/settings/celery.py                 |  56 ++
 rootfs/api/settings/production.py             |  17 +-
 rootfs/api/settings/testing.py                |   9 +
 rootfs/api/signals.py                         |  15 +
 rootfs/api/tasks.py                           |  27 +
 .../notifications/email_message.html          |  99 +++
 rootfs/api/tests/test_pipelines.py            | 117 +++
 rootfs/api/tests/test_serializers.py          |  50 ++
 rootfs/api/tests/test_tasks.py                | 118 +++
 rootfs/api/tests/test_utils.py                | 105 +++
 rootfs/api/tests/test_views.py                | 674 ++++++++++++++++++
 rootfs/api/urls.py                            |  29 +-
 rootfs/api/utils.py                           |  83 ++-
 rootfs/api/views/api.py                       | 168 +++--
 rootfs/api/views/web.py                       |   4 +-
 rootfs/passport/views.py                      |  11 +-
 rootfs/requirements.txt                       |   3 +-
 rootfs/web/src/assets/account-setting.js      |   8 -
 rootfs/web/src/assets/images/avatar.png       | Bin 11762 -> 0 bytes
 .../{MainNav.vue => AccountSidebar.vue}       |  24 +-
 .../src/components/MessageRightSidebar.vue    | 119 ++++
 rootfs/web/src/components/MessageSidebar.vue  | 111 +++
 rootfs/web/src/components/NavBar.vue          | 128 +++-
 rootfs/web/src/components/NavBox.vue          |  46 --
 rootfs/web/src/components/NavMenu.vue         |  21 +-
 rootfs/web/src/components/UserMenu.vue        |  22 +-
 rootfs/web/src/lang/index.js                  |   2 +-
 rootfs/web/src/main.ts                        |   6 +-
 rootfs/web/src/router/index.ts                |  19 +-
 rootfs/web/src/services/messages.js           |  65 ++
 rootfs/web/src/store.ts                       |  31 +
 rootfs/web/src/views/AccessTokenList.vue      |  10 +-
 rootfs/web/src/views/AccountSetting.vue       |  39 +-
 rootfs/web/src/views/MessageDetail.vue        | 164 +++++
 rootfs/web/src/views/MessageList.vue          | 252 +++++++
 rootfs/web/src/views/MessagePreferences.vue   | 310 ++++++++
 rootfs/web/src/views/Overview.vue             |  98 +++
 rootfs/web/vite.config.ts                     |   1 -
 49 files changed, 3207 insertions(+), 237 deletions(-)
 rename charts/passport/templates/{passport-deployment.yaml => api/passport-api-deployment.yaml} (70%)
 create mode 100644 charts/passport/templates/celery/passport-celery-deployment.yaml
 create mode 100644 rootfs/api/migrations/0004_alter_application_authorization_grant_type_message_and_more.py
 create mode 100644 rootfs/api/settings/celery.py
 create mode 100644 rootfs/api/tasks.py
 create mode 100644 rootfs/api/templates/notifications/email_message.html
 create mode 100644 rootfs/api/tests/test_pipelines.py
 create mode 100644 rootfs/api/tests/test_serializers.py
 create mode 100644 rootfs/api/tests/test_tasks.py
 create mode 100644 rootfs/api/tests/test_utils.py
 delete mode 100644 rootfs/web/src/assets/account-setting.js
 delete mode 100644 rootfs/web/src/assets/images/avatar.png
 rename rootfs/web/src/components/{MainNav.vue => AccountSidebar.vue} (70%)
 create mode 100644 rootfs/web/src/components/MessageRightSidebar.vue
 create mode 100644 rootfs/web/src/components/MessageSidebar.vue
 delete mode 100644 rootfs/web/src/components/NavBox.vue
 create mode 100644 rootfs/web/src/services/messages.js
 create mode 100644 rootfs/web/src/store.ts
 create mode 100644 rootfs/web/src/views/MessageDetail.vue
 create mode 100644 rootfs/web/src/views/MessageList.vue
 create mode 100644 rootfs/web/src/views/MessagePreferences.vue
 create mode 100644 rootfs/web/src/views/Overview.vue

diff --git a/.woodpecker/manifest.yml b/.woodpecker/manifest.yml
index 825ea37..19fc70c 100644
--- a/.woodpecker/manifest.yml
+++ b/.woodpecker/manifest.yml
@@ -15,6 +15,7 @@ steps:
     event:
     - tag
     - push
+    - cron
 
 - name: publish-manifest
   image: bash
diff --git a/charts/passport/templates/_helpers.tpl b/charts/passport/templates/_helpers.tpl
index 834cb5a..e29a72f 100644
--- a/charts/passport/templates/_helpers.tpl
+++ b/charts/passport/templates/_helpers.tpl
@@ -16,6 +16,8 @@ rbac.authorization.k8s.io/v1
 env:
 - name: TZ
   value: {{ .Values.time_zone | default "UTC" | quote }}
+- name: "DJANGO_SETTINGS_MODULE"
+  value: "api.settings.production"
 - name: VERSION
   value: {{ .Chart.AppVersion }}
 - name: ADMIN_USERNAME
diff --git a/charts/passport/templates/passport-deployment.yaml b/charts/passport/templates/api/passport-api-deployment.yaml
similarity index 70%
rename from charts/passport/templates/passport-deployment.yaml
rename to charts/passport/templates/api/passport-api-deployment.yaml
index 9d38074..a06f714 100644
--- a/charts/passport/templates/passport-deployment.yaml
+++ b/charts/passport/templates/api/passport-api-deployment.yaml
@@ -1,13 +1,13 @@
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: drycc-passport
+  name: drycc-passport-api
   labels:
     heritage: drycc
   annotations:
     component.drycc.cc/version: {{ .Values.imageTag }}
 spec:
-  replicas: {{ .Values.replicas }}
+  replicas: {{ .Values.api.replicas }}
   strategy:
     rollingUpdate:
       maxSurge: 1
@@ -16,28 +16,30 @@ spec:
   selector:
     matchLabels:
       app: drycc-passport
+      component: drycc-passport-api
   template:
     metadata:
       labels: {{- include "common.labels.standard" . | nindent 8 }}
         app: drycc-passport
+        component: drycc-passport-api
     spec:
       affinity:
-        podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.podAffinityPreset.type "component" "" "extraMatchLabels" .Values.podAffinityPreset.extraMatchLabels "topologyKey" "" "context" $) | nindent 10 }}
-        podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.podAntiAffinityPreset.type "component" "" "extraMatchLabels" .Values.podAntiAffinityPreset.extraMatchLabels "topologyKey" "" "context" $) | nindent 10 }}
-        nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.nodeAffinityPreset.type "key" .Values.nodeAffinityPreset.key "values" .Values.nodeAffinityPreset.values ) | nindent 10 }}
+        podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.api.podAffinityPreset.type "component" "" "extraMatchLabels" .Values.api.podAffinityPreset.extraMatchLabels "topologyKey" "" "context" $) | nindent 10 }}
+        podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.api.podAntiAffinityPreset.type "component" "" "extraMatchLabels" .Values.api.podAntiAffinityPreset.extraMatchLabels "topologyKey" "" "context" $) | nindent 10 }}
+        nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.api.nodeAffinityPreset.type "key" .Values.api.nodeAffinityPreset.key "values" .Values.api.nodeAffinityPreset.values ) | nindent 10 }}
       serviceAccount: drycc-passport
       initContainers:
-      - name: drycc-passport-init
+      - name: drycc-passport-api-init
         image: {{.Values.imageRegistry}}/{{.Values.imageOrg}}/python-dev:latest
         imagePullPolicy: {{.Values.imagePullPolicy}}
         args:
-          - netcat
-          - -v
-          - -u
-          - $(DRYCC_DATABASE_URL),$(DRYCC_DATABASE_REPLICA_URL)
+        - netcat
+        - -v
+        - -u
+        - $(DRYCC_DATABASE_URL),$(DRYCC_DATABASE_REPLICA_URL),$(DRYCC_VALKEY_URL)
         {{- include "passport.envs" . | indent 8 }}
       containers:
-      - name: drycc-passport
+      - name: drycc-passport-api
         image: {{.Values.imageRegistry}}/{{.Values.imageOrg}}/passport:{{.Values.imageTag}}
         imagePullPolicy: {{.Values.imagePullPolicy}}
         {{- if .Values.diagnosticMode.enabled }}
@@ -62,7 +64,7 @@ spec:
         ports:
           - containerPort: 8000
             name: http
-        {{- with index .Values "resources" }}
+        {{- with index .Values "api" "resources" }}
         resources:
           {{- toYaml . | nindent 10 }}
         {{- end }}
diff --git a/charts/passport/templates/celery/passport-celery-deployment.yaml b/charts/passport/templates/celery/passport-celery-deployment.yaml
new file mode 100644
index 0000000..5c118b0
--- /dev/null
+++ b/charts/passport/templates/celery/passport-celery-deployment.yaml
@@ -0,0 +1,65 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: drycc-passport-celery
+  labels:
+    heritage: drycc
+  annotations:
+    component.drycc.cc/version: {{ .Values.imageTag }}
+spec:
+  replicas: {{ .Values.celery.replicas }}
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 0
+    type: RollingUpdate
+  selector:
+    matchLabels:
+      app: drycc-passport
+      component: drycc-passport-celery
+  template:
+    metadata:
+      labels: {{- include "common.labels.standard" . | nindent 8 }}
+        app: drycc-passport
+        component: drycc-passport-celery
+    spec:
+      affinity:
+        podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.celery.podAffinityPreset.type "component" "" "extraMatchLabels" .Values.celery.podAffinityPreset.extraMatchLabels "topologyKey" "" "context" $) | nindent 10 }}
+        podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.celery.podAntiAffinityPreset.type "component" "" "extraMatchLabels" .Values.celery.podAntiAffinityPreset.extraMatchLabels "topologyKey" "" "context" $) | nindent 10 }}
+        nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.celery.nodeAffinityPreset.type "key" .Values.celery.nodeAffinityPreset.key "values" .Values.celery.nodeAffinityPreset.values ) | nindent 10 }}
+      serviceAccount: drycc-passport
+      initContainers:
+      - name: drycc-passport-init
+        image: {{.Values.imageRegistry}}/{{.Values.imageOrg}}/python-dev:latest
+        imagePullPolicy: {{.Values.imagePullPolicy}}
+        args:
+          - netcat
+          - -v
+          - -u
+          - $(DRYCC_DATABASE_URL),$(DRYCC_DATABASE_REPLICA_URL),$(DRYCC_VALKEY_URL)
+        {{- include "passport.envs" . | indent 8 }}
+      containers:
+      - name: drycc-passport-celery
+        image: {{.Values.imageRegistry}}/{{.Values.imageOrg}}/passport:{{.Values.imageTag}}
+        imagePullPolicy: {{.Values.imagePullPolicy}}
+        {{- if .Values.diagnosticMode.enabled }}
+        command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 10 }}
+        args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 10 }}
+        {{- else }}
+        args:
+          - celery
+          - -A
+          - api
+          - worker
+          - -Q
+          - passport.notifications
+          - -l
+          - info
+        {{- end }}
+        {{- with index .Values "celery" "resources" }}
+        resources:
+          {{- toYaml . | nindent 10 }}
+        {{- end }}
+        {{- include "passport.envs" . | indent 8 }}
+        {{- include "passport.volumeMounts" . | indent 8 }}
+      {{- include "passport.volumes" . | indent 6 }}
diff --git a/charts/passport/templates/passport-service.yaml b/charts/passport/templates/passport-service.yaml
index 1b47a57..17119f7 100644
--- a/charts/passport/templates/passport-service.yaml
+++ b/charts/passport/templates/passport-service.yaml
@@ -7,7 +7,9 @@ metadata:
     {{- toYaml . | nindent 4 }}
     {{- end }}
   labels:
+    app: drycc-passport
     heritage: drycc
+    component: drycc-passport-api
 spec:
   ports:
     - name: http
@@ -15,3 +17,4 @@ spec:
       targetPort: 8000
   selector:
     app: drycc-passport
+    component: drycc-passport-api
diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index 22986bb..f5e7464 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -18,32 +18,52 @@ diagnosticMode:
   args:
     - infinity
 
-resources: {}
-  # limits:
-  #   cpu: 200m
-  #   memory: 50Mi
-  # requests:
-  #   cpu: 100m
-  #   memory: 30Mi
+api:
+  replicas: 1
+  resources: {}
+    # limits:
+    #   cpu: 200m
+    #   memory: 50Mi
+    # requests:
+    #   cpu: 100m
+    #   memory: 30Mi
+  nodeAffinityPreset:
+    key: "drycc.cc/node"
+    type: "soft"
+    values:
+    - "true"
+  podAffinityPreset:
+    type: ""
+    extraMatchLabels:
+      security: "drycc-security"
+  podAntiAffinityPreset:
+    type: "soft"
+    extraMatchLabels:
+      app: "drycc-passport-api"
 
-nodeAffinityPreset:
-  key: "drycc.cc/node"
-  type: "soft"
-  values:
-  - "true"
-
-podAffinityPreset:
-  type: ""
-  extraMatchLabels:
-    security: "drycc-security"
-
-podAntiAffinityPreset:
-  type: "soft"
-  extraMatchLabels:
-    app: "drycc-passport"
-
-# Set passport deployment replicas
-replicas: 1
+celery:
+  replicas: 1
+  resources: {}
+    # limits:
+    #   cpu: 200m
+    #   memory: 50Mi
+    # requests:
+    #   cpu: 100m
+    #   memory: 30Mi
+  nodeAffinityPreset:
+    key: "drycc.cc/node"
+    type: "soft"
+    values:
+    - "true"
+  podAffinityPreset:
+    type: ""
+    extraMatchLabels:
+      security: "drycc-security"
+  podAntiAffinityPreset:
+    type: "soft"
+    extraMatchLabels:
+      app: "drycc-passport"
+      component: "drycc-passport-celery"
 ## valkeyUrl is will no longer use the built-in valkey component
 valkeyUrl: ""
 ## databaseUrl and databaseReplicaUrl are will no longer use the built-in database component
diff --git a/rootfs/api/__init__.py b/rootfs/api/__init__.py
index 1a72d32..15d33ec 100644
--- a/rootfs/api/__init__.py
+++ b/rootfs/api/__init__.py
@@ -1 +1,7 @@
+"""
+The **api** Django app presents a RESTful web API for interacting with the **drycc** system.
+"""
+from .settings.celery import app as celery_app
+
 __version__ = '1.1.0'
+__all__ = ('celery_app',)
diff --git a/rootfs/api/admin.py b/rootfs/api/admin.py
index c37cf8b..bdba29a 100644
--- a/rootfs/api/admin.py
+++ b/rootfs/api/admin.py
@@ -1,7 +1,7 @@
 from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
 
-from .models import User
+from .models import User, Message, MessagePreference
 
 
 class UserAdmin(BaseUserAdmin):
@@ -13,4 +13,18 @@ class UserAdmin(BaseUserAdmin):
     )
 
 
+class MessageAdmin(admin.ModelAdmin):
+    list_display = ('id', 'user', 'category', 'title', 'severity', 'is_read', 'created_at')
+    list_filter = ('category', 'severity', 'is_read', 'created_at')
+    search_fields = ('title', 'content', 'user__username')
+    readonly_fields = ('created_at', 'updated_at')
+
+
+class MessagePreferenceAdmin(admin.ModelAdmin):
+    list_display = ('user', 'email_alerts', 'push_alerts')
+    search_fields = ('user__username',)
+
+
 admin.site.register(User, UserAdmin)
+admin.site.register(Message, MessageAdmin)
+admin.site.register(MessagePreference, MessagePreferenceAdmin)
diff --git a/rootfs/api/migrations/0004_alter_application_authorization_grant_type_message_and_more.py b/rootfs/api/migrations/0004_alter_application_authorization_grant_type_message_and_more.py
new file mode 100644
index 0000000..3e7d8bd
--- /dev/null
+++ b/rootfs/api/migrations/0004_alter_application_authorization_grant_type_message_and_more.py
@@ -0,0 +1,61 @@
+# Generated by Django 5.2.13 on 2026-04-23 03:45
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0003_application_allowed_origins_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='application',
+            name='authorization_grant_type',
+            field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('urn:ietf:params:oauth:grant-type:device_code', 'Device Code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=44),
+        ),
+        migrations.CreateModel(
+            name='Message',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('category', models.CharField(choices=[('system', 'System'), ('product', 'Product Updates'), ('security', 'Security'), ('alert', 'Alerts'), ('service', 'Service')], default='system', max_length=20, verbose_name='category')),
+                ('title', models.CharField(max_length=255, verbose_name='title')),
+                ('content', models.TextField(verbose_name='content')),
+                ('full_content', models.TextField(blank=True, verbose_name='full content')),
+                ('severity', models.CharField(choices=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('success', 'Success')], default='info', max_length=20, verbose_name='severity')),
+                ('is_read', models.BooleanField(default=False, verbose_name='is read')),
+                ('action_link', models.CharField(blank=True, max_length=500, verbose_name='action link')),
+                ('action_text', models.CharField(blank=True, max_length=255, verbose_name='action text')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
+                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to=settings.AUTH_USER_MODEL, verbose_name='user')),
+            ],
+            options={
+                'verbose_name': 'message',
+                'verbose_name_plural': 'messages',
+                'ordering': ['-created_at'],
+            },
+        ),
+        migrations.CreateModel(
+            name='MessagePreference',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('email_alerts', models.BooleanField(default=True, verbose_name='email alerts')),
+                ('push_alerts', models.BooleanField(default=False, verbose_name='push alerts')),
+                ('webhook_url', models.URLField(blank=True, max_length=500, verbose_name='webhook url')),
+                ('notify_security', models.BooleanField(default=True, verbose_name='notify security')),
+                ('notify_system', models.BooleanField(default=True, verbose_name='notify system')),
+                ('notify_product', models.BooleanField(default=False, verbose_name='notify product')),
+                ('notify_alert', models.BooleanField(default=True, verbose_name='notify alert')),
+                ('notify_service', models.BooleanField(default=True, verbose_name='notify service')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='message_preference', to=settings.AUTH_USER_MODEL, verbose_name='user')),
+            ],
+            options={
+                'verbose_name': 'message preference',
+                'verbose_name_plural': 'message preferences',
+            },
+        ),
+    ]
diff --git a/rootfs/api/models.py b/rootfs/api/models.py
index 397af2f..9adb0c7 100644
--- a/rootfs/api/models.py
+++ b/rootfs/api/models.py
@@ -16,3 +16,112 @@ def allows_grant_type(self, *grant_types):
         return self.GRANT_AUTHORIZATION_CODE in grant_types or super().allows_grant_type(
             *grant_types
         )
+
+
+class Message(models.Model):
+    CATEGORY_CHOICES = [
+        ('system', _('System')),
+        ('product', _('Product Updates')),
+        ('security', _('Security')),
+        ('alert', _('Alerts')),
+        ('service', _('Service')),
+    ]
+
+    SEVERITY_CHOICES = [
+        ('info', _('Info')),
+        ('warning', _('Warning')),
+        ('error', _('Error')),
+        ('success', _('Success')),
+    ]
+
+    user = models.ForeignKey(
+        User,
+        on_delete=models.CASCADE,
+        related_name='messages',
+        verbose_name=_('user')
+    )
+    category = models.CharField(
+        _('category'),
+        max_length=20,
+        choices=CATEGORY_CHOICES,
+        default='system'
+    )
+    title = models.CharField(_('title'), max_length=255)
+    content = models.TextField(_('content'))
+    full_content = models.TextField(_('full content'), blank=True)
+    severity = models.CharField(
+        _('severity'),
+        max_length=20,
+        choices=SEVERITY_CHOICES,
+        default='info'
+    )
+    is_read = models.BooleanField(_('is read'), default=False)
+    action_link = models.CharField(
+        _('action link'),
+        max_length=500,
+        blank=True
+    )
+    action_text = models.CharField(
+        _('action text'),
+        max_length=255,
+        blank=True
+    )
+    created_at = models.DateTimeField(_('created at'), auto_now_add=True)
+    updated_at = models.DateTimeField(_('updated at'), auto_now=True)
+
+    class Meta:
+        ordering = ['-created_at']
+        verbose_name = _('message')
+        verbose_name_plural = _('messages')
+
+    def __str__(self):
+        return f"[{self.category}] {self.title}"
+
+
+class MessagePreference(models.Model):
+    user = models.OneToOneField(
+        User,
+        on_delete=models.CASCADE,
+        related_name='message_preference',
+        verbose_name=_('user')
+    )
+    email_alerts = models.BooleanField(
+        _('email alerts'),
+        default=True
+    )
+    push_alerts = models.BooleanField(
+        _('push alerts'),
+        default=False
+    )
+    webhook_url = models.URLField(
+        _('webhook url'),
+        max_length=500,
+        blank=True
+    )
+    notify_security = models.BooleanField(
+        _('notify security'),
+        default=True
+    )
+    notify_system = models.BooleanField(
+        _('notify system'),
+        default=True
+    )
+    notify_product = models.BooleanField(
+        _('notify product'),
+        default=True
+    )
+    notify_alert = models.BooleanField(
+        _('notify alert'),
+        default=True
+    )
+    notify_service = models.BooleanField(
+        _('notify service'),
+        default=True
+    )
+
+    class Meta:
+        verbose_name = _('message preference')
+        verbose_name_plural = _('message preferences')
+
+    def __str__(self):
+        return f"{self.user.username}'s message preference"
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index 9c73ff3..956e8d6 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -8,6 +8,7 @@
 from oauth2_provider.models import Grant, AccessToken
 from rest_framework import serializers
 
+from api.models import Message, MessagePreference
 from api.utils import timestamp2datetime
 
 User = get_user_model()
@@ -74,3 +75,26 @@ class Meta:
         fields = '__all__'
         read_only_fields = ['action_time', 'user', 'content_type', 'object_id',
                             'object_repr', 'action_flag', 'change_message']
+
+
+class MessageSerializer(serializers.ModelSerializer):
+    """Serialize message model."""
+    date = serializers.DateTimeField(
+        source='created_at', format='%b %d, %Y, %H:%M', read_only=True
+    )
+
+    class Meta:
+        model = Message
+        fields = ['id', 'category', 'title', 'content', 'full_content',
+                  'severity', 'is_read', 'action_link', 'action_text', 'date']
+        read_only_fields = ['id', 'date']
+
+
+class MessagePreferenceSerializer(serializers.ModelSerializer):
+    """Serialize message preference model."""
+
+    class Meta:
+        model = MessagePreference
+        fields = ['email_alerts', 'push_alerts', 'webhook_url',
+                  'notify_security', 'notify_system', 'notify_product',
+                  'notify_alert', 'notify_service']
diff --git a/rootfs/api/settings/celery.py b/rootfs/api/settings/celery.py
new file mode 100644
index 0000000..30af003
--- /dev/null
+++ b/rootfs/api/settings/celery.py
@@ -0,0 +1,56 @@
+import os
+
+from kombu import Exchange, Queue
+from celery import Celery
+
+
+class Config(object):
+    # Celery Configuration Options
+    enable_utc = True
+    task_serializer = 'pickle'
+    accept_content = frozenset([
+        'application/data',
+        'application/json',
+        'application/text',
+        'application/x-python-serialize',
+    ])
+    task_track_started = True
+    task_time_limit = 5 * 60
+    worker_max_tasks_per_child = 200
+    worker_prefetch_multiplier = 1
+    result_expires = 24 * 60 * 60
+    cache_backend = 'django-cache'
+    task_default_queue = 'passport.notifications'
+    task_default_exchange = 'passport.priority'
+    task_default_routing_key = 'passport.priority.notifications'
+    broker_transport_options = {"queue_order_strategy": "sorted"}
+    task_create_missing_queues = True
+    task_inherit_parent_priority = True
+    broker_connection_retry_on_startup = True
+    worker_cancel_long_running_tasks_on_connection_loss = True
+
+
+app = Celery('passport')
+app.config_from_object(Config())
+app.conf.update(
+    timezone=os.environ.get('TZ', 'UTC'),
+    task_routes={
+        'api.tasks.send_notification': {
+            'queue': 'passport.notifications',
+            'exchange': 'passport.priority', 'routing_key': 'passport.priority.notifications',
+        },
+    },
+    task_queues=(
+        Queue(
+            'passport.notifications', exchange=Exchange('passport.priority', type="direct"),
+            routing_key='passport.priority.notifications',
+        ),
+    ),
+)
+DRYCC_VALKEY_URL = os.environ.get('DRYCC_VALKEY_URL', 'redis://:@127.0.0.1:6379')
+app.conf.update(
+    broker_url=DRYCC_VALKEY_URL,
+    result_backend=DRYCC_VALKEY_URL,
+    broker_transport_options={"queue_order_strategy": "sorted", "visibility_timeout": 43200},
+)
+app.autodiscover_tasks()
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index c3536dc..181efdb 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -25,13 +25,19 @@
 # https://docs.djangoproject.com/en/2.2/ref/settings/#debug-propagate-exceptions
 DEBUG_PROPAGATE_EXCEPTIONS = True
 
-# Enable Legal
-LEGAL_ENABLED = os.environ.get('LEGAL_ENABLED', 'false').lower() == "true"
 # Enable Django admin
 ADMIN_ENABLED = os.environ.get('ADMIN_ENABLED', 'false').lower() == "true"
-# Enable Registration
-# If this function is enabled, please set Django email related parameters
+
+# The following information is for configuring app settings
+# Enable Legal
+LEGAL_ENABLED = os.environ.get('LEGAL_ENABLED', 'false').lower() == "true"
+# Enable Registration, email configuration is required
 REGISTRATION_ENABLED = os.environ.get('REGISTRATION_ENABLED', 'false').lower() == "true"
+# URLs for the application
+DASHBOARD_URL = os.environ.get('DASHBOARD_URL', '/')
+# Contact support URL, which can be a mailto: link or a link to a support ticket system
+CONTACT_SUPPORT_URL = os.environ.get('CONTACT_SUPPORT_URL', 'https://community.drycc.cc/')
+
 # Silence two security messages around SSL as router takes care of them
 # https://docs.djangoproject.com/en/2.2/ref/checks/#security
 SILENCED_SYSTEM_CHECKS = [
@@ -298,9 +304,6 @@
 STATIC_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'web', 'dist', 'assets'))
 STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
 
-# Avatar URL
-AVATAR_URL = "https://cravatar.cn/avatar/"
-
 # see: https://django-oauth-toolkit.readthedocs.io/en/latest/oidc.html?highlight=oidc.key#creating-rsa-private-key  # noqa
 OIDC_ENABLED = False
 OIDC_RSA_PRIVATE_KEY = None
diff --git a/rootfs/api/settings/testing.py b/rootfs/api/settings/testing.py
index ffb1527..7f3331c 100644
--- a/rootfs/api/settings/testing.py
+++ b/rootfs/api/settings/testing.py
@@ -2,9 +2,18 @@
 import string
 import os
 
+from api.settings.celery import app
+
 from api.settings.production import *  # noqa
 from api.settings.production import DATABASES
 
+
+app.conf.update(task_always_eager=True)
+OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'oauth2_provider.AccessToken'
+OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'oauth2_provider.RefreshToken'
+OAUTH2_PROVIDER_GRANT_MODEL = 'oauth2_provider.Grant'
+OAUTH2_PROVIDER_ID_TOKEN_MODEL = 'oauth2_provider.IDToken'
+
 # A boolean that turns on/off debug mode.
 # https://docs.djangoproject.com/en/2.2/ref/settings/#debug
 DEBUG = True
diff --git a/rootfs/api/signals.py b/rootfs/api/signals.py
index e69de29..e13a018 100644
--- a/rootfs/api/signals.py
+++ b/rootfs/api/signals.py
@@ -0,0 +1,15 @@
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from django.contrib.auth import get_user_model
+
+from api.models import Message
+
+User = get_user_model()
+
+
+@receiver(post_save, sender=Message)
+def message_changed_handle(sender, instance, created, **kwargs):
+    """Queue async notification dispatch when a new message is created."""
+    if created:
+        from api.tasks import send_notification
+        send_notification.delay(instance)
diff --git a/rootfs/api/tasks.py b/rootfs/api/tasks.py
new file mode 100644
index 0000000..e52abef
--- /dev/null
+++ b/rootfs/api/tasks.py
@@ -0,0 +1,27 @@
+"""Celery tasks for Drycc Passport."""
+import logging
+
+from django.conf import settings
+from celery import shared_task
+from api.utils import send_email_notification, send_webhook_notification
+from api.models import MessagePreference
+
+logger = logging.getLogger(__name__)
+
+
+@shared_task(bind=True, max_retries=3, default_retry_delay=10)
+def send_notification(self, message) -> None:
+    use_email, use_webhook = False, False
+    try:
+        preference = message.user.message_preference
+    except MessagePreference.DoesNotExist:
+        use_email = True
+        logger.info("Send email notification for message %s without user preference", message.id)
+    else:
+        if getattr(preference, f"notify_{message.category}", True):
+            use_email = preference.email_alerts
+            use_webhook = preference.push_alerts and preference.webhook_url
+    if use_email and message.user.email and getattr(settings, 'EMAIL_HOST', ''):
+        send_email_notification(message)
+    if use_webhook:
+        send_webhook_notification(message, preference.webhook_url)
diff --git a/rootfs/api/templates/notifications/email_message.html b/rootfs/api/templates/notifications/email_message.html
new file mode 100644
index 0000000..5c1c6a7
--- /dev/null
+++ b/rootfs/api/templates/notifications/email_message.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ message.title }}</title>
+    <style>
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+            background-color: #f8fafc;
+            margin: 0;
+            padding: 0;
+            color: #334155;
+        }
+        .container {
+            max-width: 600px;
+            margin: 0 auto;
+            padding: 20px;
+        }
+        .card {
+            background-color: #ffffff;
+            border-radius: 8px;
+            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+            overflow: hidden;
+        }
+        .header {
+            padding: 24px;
+            border-bottom: 1px solid #e2e8f0;
+        }
+        .header h1 {
+            margin: 0;
+            font-size: 18px;
+            font-weight: 600;
+            color: #0f172a;
+        }
+        .badge {
+            display: inline-block;
+            padding: 4px 12px;
+            border-radius: 9999px;
+            font-size: 11px;
+            font-weight: 600;
+            text-transform: uppercase;
+            letter-spacing: 0.05em;
+            margin-top: 8px;
+        }
+        .badge-system { background-color: #dbeafe; color: #1e40af; }
+        .badge-product { background-color: #d1fae5; color: #065f46; }
+        .badge-security { background-color: #e0e7ff; color: #3730a3; }
+        .badge-alert { background-color: #fee2e2; color: #991b1b; }
+        .badge-service { background-color: #f3e8ff; color: #6b21a8; }
+        .content {
+            padding: 24px;
+            line-height: 1.6;
+            font-size: 14px;
+        }
+        .content p {
+            margin: 0 0 16px 0;
+        }
+        .footer {
+            padding: 16px 24px;
+            background-color: #f8fafc;
+            border-top: 1px solid #e2e8f0;
+            font-size: 12px;
+            color: #64748b;
+        }
+        .action-button {
+            display: inline-block;
+            padding: 10px 20px;
+            background-color: #0f172a;
+            color: #ffffff;
+            text-decoration: none;
+            border-radius: 6px;
+            font-size: 13px;
+            font-weight: 500;
+            margin-top: 16px;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="card">
+            <div class="header">
+                <h1>{{ message.title }}</h1>
+                <span class="badge badge-{{ message.category }}">{{ message.get_category_display }}</span>
+            </div>
+            <div class="content">
+                <p>{{ message.content }}</p>
+                {% if message.action_link and message.action_text %}
+                <a href="{{ message.action_link }}" class="action-button">{{ message.action_text }}</a>
+                {% endif %}
+            </div>
+            <div class="footer">
+                <p>You received this notification because you have email alerts enabled in your Drycc Passport account.</p>
+                <p>To manage your notification preferences, visit your <a href="{{ domain }}/messages/preferences">Account Settings</a>.</p>
+            </div>
+        </div>
+    </div>
+</body>
+</html>
diff --git a/rootfs/api/tests/test_pipelines.py b/rootfs/api/tests/test_pipelines.py
new file mode 100644
index 0000000..78b6a62
--- /dev/null
+++ b/rootfs/api/tests/test_pipelines.py
@@ -0,0 +1,117 @@
+from unittest.mock import Mock, patch
+
+from django.contrib.auth import get_user_model
+from django.test import RequestFactory, TestCase
+from social_django.models import UserSocialAuth
+
+from api.apps_extra.social_core.pipelines import (
+    handle_authenticated_binding, require_username_password
+)
+
+User = get_user_model()
+
+
+class PipelineTestCase(TestCase):
+    def setUp(self):
+        self.factory = RequestFactory()
+        self.user = User.objects.create_user(
+            username='pipe-user',
+            email='pipe@example.com',
+            password='password123',
+        )
+
+    def _make_backend(self, request, name='feishu'):
+        strategy = Mock()
+        strategy.request = request
+        strategy.session_set = Mock()
+        backend = Mock()
+        backend.name = name
+        backend.strategy = strategy
+        return backend
+
+    @patch(
+        'api.apps_extra.social_core.pipelines.get_oauth_callback',
+        return_value='/oauth/callback?status=linked'
+    )
+    def test_handle_authenticated_binding_creates_identity(self, _mock_callback):
+        request = self.factory.get('/oauth/callback')
+        request.user = self.user
+        backend = self._make_backend(request)
+
+        response = handle_authenticated_binding(
+            backend, 'uid-1', {}, {'email': 'pipe@example.com'}
+        )
+
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue(UserSocialAuth.objects.filter(
+            user=self.user, provider='feishu', uid='uid-1'
+        ).exists())
+
+    @patch(
+        'api.apps_extra.social_core.pipelines.get_oauth_callback',
+        return_value='/oauth/callback?status=conflict'
+    )
+    def test_handle_authenticated_binding_conflict(self, _mock_callback):
+        other = User.objects.create_user(
+            username='other', email='other@example.com', password='password123'
+        )
+        UserSocialAuth.objects.create(
+            user=other, provider='feishu', uid='uid-1', extra_data={}
+        )
+        request = self.factory.get('/oauth/callback')
+        request.user = self.user
+        backend = self._make_backend(request)
+
+        response = handle_authenticated_binding(
+            backend, 'uid-1', {}, {'email': 'pipe@example.com'}
+        )
+
+        self.assertEqual(response.status_code, 302)
+
+    @patch(
+        'api.apps_extra.social_core.pipelines.get_oauth_callback',
+        return_value='/oauth/callback?status=already_linked'
+    )
+    def test_handle_authenticated_binding_already_linked(self, _mock_callback):
+        UserSocialAuth.objects.create(
+            user=self.user, provider='feishu', uid='uid-1', extra_data={}
+        )
+        request = self.factory.get('/oauth/callback')
+        request.user = self.user
+        backend = self._make_backend(request)
+
+        response = handle_authenticated_binding(
+            backend, 'uid-1', {}, {'email': 'pipe@example.com'}
+        )
+
+        self.assertEqual(response.status_code, 302)
+
+    @patch(
+        'api.apps_extra.social_core.pipelines.get_oauth_callback',
+        return_value='/oauth/callback?status=pending'
+    )
+    def test_require_username_password_sets_pending_session(self, _mock_callback):
+        request = self.factory.get('/oauth/callback')
+        request.user = Mock(is_authenticated=False)
+        backend = self._make_backend(request)
+
+        response = require_username_password(
+            backend, 'uid-1', {'email': 'pipe@example.com'},
+            {'email': 'pipe@example.com'}
+        )
+
+        self.assertEqual(response.status_code, 302)
+        backend.strategy.session_set.assert_called_once()
+
+    @patch(
+        'api.apps_extra.social_core.pipelines.get_oauth_callback',
+        return_value='/oauth/callback?status=missing_email'
+    )
+    def test_require_username_password_requires_email(self, _mock_callback):
+        request = self.factory.get('/oauth/callback')
+        request.user = Mock(is_authenticated=False)
+        backend = self._make_backend(request)
+
+        response = require_username_password(backend, 'uid-1', {}, {})
+
+        self.assertEqual(response.status_code, 302)
diff --git a/rootfs/api/tests/test_serializers.py b/rootfs/api/tests/test_serializers.py
new file mode 100644
index 0000000..a25ddf8
--- /dev/null
+++ b/rootfs/api/tests/test_serializers.py
@@ -0,0 +1,50 @@
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+
+from api.models import Message, MessagePreference
+from api.serializers import ListSerializer, MessagePreferenceSerializer, MessageSerializer
+
+User = get_user_model()
+
+
+class SerializerTestCase(TestCase):
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='serializer-user',
+            email='serializer@example.com',
+            password='password123',
+        )
+        self.message = Message.objects.create(
+            user=self.user,
+            category='system',
+            title='Serializer message',
+            content='Serializer content',
+            full_content='Serializer full content',
+            severity='info',
+        )
+        self.preference = MessagePreference.objects.create(
+            user=self.user,
+            email_alerts=True,
+            push_alerts=False,
+            webhook_url='',
+            notify_security=True,
+            notify_system=True,
+            notify_product=True,
+            notify_alert=True,
+            notify_service=True,
+        )
+
+    def test_list_serializer_validate_section(self):
+        result = ListSerializer.validate_section('1,2')
+        self.assertEqual(len(result), 2)
+
+    def test_message_serializer_contains_expected_fields(self):
+        data = MessageSerializer(self.message).data
+        self.assertEqual(data['category'], 'system')
+        self.assertEqual(data['title'], 'Serializer message')
+        self.assertIn('date', data)
+
+    def test_message_preference_serializer_contains_singular_alert_field(self):
+        data = MessagePreferenceSerializer(self.preference).data
+        self.assertIn('notify_alert', data)
+        self.assertTrue(data['notify_alert'])
diff --git a/rootfs/api/tests/test_tasks.py b/rootfs/api/tests/test_tasks.py
new file mode 100644
index 0000000..87fffb6
--- /dev/null
+++ b/rootfs/api/tests/test_tasks.py
@@ -0,0 +1,118 @@
+from unittest.mock import patch
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase, override_settings
+
+from api.models import Message, MessagePreference
+from api.tasks import send_notification
+
+User = get_user_model()
+
+
+@override_settings(EMAIL_HOST='smtp.example.com')
+class NotificationTaskTestCase(TestCase):
+    def setUp(self):
+        delay_patcher = patch('api.tasks.send_notification.delay')
+        self.addCleanup(delay_patcher.stop)
+        self.mock_delay = delay_patcher.start()
+
+        self.user = User.objects.create_user(
+            username='task-user',
+            email='task@example.com',
+            password='password123',
+        )
+        self.preference = MessagePreference.objects.create(
+            user=self.user,
+            email_alerts=True,
+            push_alerts=True,
+            webhook_url='https://example.com/webhook',
+            notify_security=True,
+            notify_system=True,
+            notify_product=True,
+            notify_alert=True,
+            notify_service=True,
+        )
+        self.message = Message.objects.create(
+            user=self.user,
+            category='security',
+            title='Security event',
+            content='A security event occurred',
+            full_content='A security event occurred in detail',
+            severity='warning',
+        )
+
+    @patch('api.tasks.send_email_notification')
+    @patch('api.tasks.send_webhook_notification')
+    def test_send_notification_uses_enabled_channels(self, mock_webhook, mock_email):
+        send_notification.run(self.message)
+
+        mock_email.assert_called_once()
+        mock_webhook.assert_called_once_with(
+            mock_email.call_args[0][0], 'https://example.com/webhook'
+        )
+
+    @patch('api.tasks.send_email_notification')
+    @patch('api.tasks.send_webhook_notification')
+    def test_send_notification_respects_category_preference(self, mock_webhook, mock_email):
+        self.preference.notify_security = False
+        self.preference.save(update_fields=['notify_security'])
+
+        send_notification.run(self.message)
+
+        mock_email.assert_not_called()
+        mock_webhook.assert_not_called()
+
+    @patch('api.tasks.send_email_notification')
+    @patch('api.tasks.send_webhook_notification')
+    def test_send_notification_uses_default_true_for_unknown_category(
+        self, mock_webhook, mock_email
+    ):
+        self.message.category = 'unknown'
+        self.message.save(update_fields=['category'])
+
+        send_notification.run(self.message)
+
+        mock_email.assert_called_once()
+        mock_webhook.assert_called_once_with(
+            mock_email.call_args[0][0], 'https://example.com/webhook'
+        )
+
+    @patch('api.tasks.send_email_notification')
+    @patch('api.tasks.send_webhook_notification')
+    @patch('api.tasks.logger')
+    def test_send_notification_without_preference_sends_email_only(
+        self, mock_logger, mock_webhook, mock_email
+    ):
+        self.preference.delete()
+        # Refresh to clear cached related object
+        self.message.refresh_from_db()
+        self.message.user = User.objects.get(pk=self.user.pk)
+
+        send_notification.run(self.message)
+
+        mock_email.assert_called_once()
+        mock_webhook.assert_not_called()
+        mock_logger.info.assert_called_once_with(
+            'Send email notification for message %s without user preference',
+            self.message.id,
+        )
+
+    @patch('api.tasks.send_webhook_notification', side_effect=RuntimeError('boom'))
+    def test_send_notification_propagates_dispatch_error(self, _mock_webhook):
+        with self.assertRaises(RuntimeError):
+            send_notification.run(self.message)
+
+    @patch('api.tasks.send_email_notification')
+    @patch('api.tasks.send_webhook_notification')
+    def test_send_notification_supports_alert_category_with_singular_field(
+        self, mock_webhook, mock_email
+    ):
+        self.message.category = 'alert'
+        self.message.save(update_fields=['category'])
+        self.preference.notify_alert = False
+        self.preference.save(update_fields=['notify_alert'])
+
+        send_notification.run(self.message)
+
+        mock_email.assert_not_called()
+        mock_webhook.assert_not_called()
diff --git a/rootfs/api/tests/test_utils.py b/rootfs/api/tests/test_utils.py
new file mode 100644
index 0000000..fd9201e
--- /dev/null
+++ b/rootfs/api/tests/test_utils.py
@@ -0,0 +1,105 @@
+from unittest.mock import Mock, patch
+
+from django.contrib.auth import get_user_model
+from django.test import RequestFactory, TestCase, override_settings
+from social_django.models import UserSocialAuth
+from rest_framework.exceptions import ValidationError
+
+from api.models import Message
+from api.utils import (
+    get_local_host,
+    get_oauth_callback,
+    get_user_socials,
+    send_activation_email,
+    send_email_notification,
+    send_webhook_notification,
+    timestamp2datetime,
+    validate_reserved_names,
+)
+
+User = get_user_model()
+
+
+class UtilsTestCase(TestCase):
+    def setUp(self):
+        self.factory = RequestFactory()
+        self.user = User.objects.create_user(
+            username='util-user',
+            email='util@example.com',
+            password='password123',
+        )
+        self.message = Message.objects.create(
+            user=self.user,
+            category='security',
+            title='Utility message',
+            content='Utility content',
+            full_content='Utility content full',
+            severity='warning',
+        )
+
+    def test_timestamp2datetime(self):
+        dt = timestamp2datetime(0)
+        self.assertEqual(dt.year, 1970)
+
+    def test_get_local_host(self):
+        request = self.factory.get('/user/info', secure=True, HTTP_HOST='example.com')
+        self.assertEqual(get_local_host(request), 'https://example.com')
+
+    @override_settings(SOCIAL_AUTH_LOGIN_CALLBACK_URL='/oauth/callback')
+    def test_get_oauth_callback(self):
+        request = self.factory.get('/user/login', secure=True, HTTP_HOST='example.com')
+        callback = get_oauth_callback(request, 'linked', 'feishu')
+        self.assertEqual(
+            callback,
+            'https://example.com/oauth/callback?status=linked&provider=feishu'
+        )
+
+    @override_settings(RESERVED_NAME_PATTERNS=[r'^admin$', r'^root$'])
+    def test_validate_reserved_names_rejects_reserved(self):
+        with self.assertRaises(ValidationError):
+            validate_reserved_names('admin')
+
+    @override_settings(RESERVED_NAME_PATTERNS=[r'^admin$', r'^root$'])
+    def test_validate_reserved_names_accepts_normal_name(self):
+        validate_reserved_names('normal-user')
+
+    @override_settings(EMAIL_HOST='smtp.example.com')
+    @patch('api.utils.render_to_string', return_value='rendered body')
+    def test_send_activation_email(self, mock_render):
+        request = self.factory.get('/user/registration', secure=True, HTTP_HOST='example.com')
+        with patch.object(self.user, 'email_user') as mock_email_user:
+            send_activation_email(request, self.user)
+
+        mock_render.assert_called_once()
+        mock_email_user.assert_called_once()
+
+    @override_settings(EMAIL_HOST='smtp.example.com')
+    @patch('api.utils.render_to_string', return_value='<b>Hello</b>')
+    def test_send_email_notification(self, mock_render):
+        with patch.object(self.user, 'email_user') as mock_email_user:
+            send_email_notification(self.message)
+
+        mock_render.assert_called_once()
+        mock_email_user.assert_called_once()
+
+    @patch('api.utils.requests.post')
+    def test_send_webhook_notification(self, mock_post):
+        mock_post.return_value.raise_for_status = Mock()
+
+        send_webhook_notification(self.message, 'https://example.com/webhook')
+
+        mock_post.assert_called_once()
+
+    @patch('api.apps_extra.social_core.backends.__all__', new=[])
+    def test_get_user_socials_without_backend_metadata(self):
+        UserSocialAuth.objects.create(
+            user=self.user,
+            provider='feishu',
+            uid='uid-1',
+            extra_data={'email': 'social@example.com'},
+        )
+
+        results = get_user_socials(self.user)
+
+        self.assertEqual(len(results), 1)
+        self.assertEqual(results[0]['email'], 'social@example.com')
diff --git a/rootfs/api/tests/test_views.py b/rootfs/api/tests/test_views.py
index 91fe633..bd65486 100644
--- a/rootfs/api/tests/test_views.py
+++ b/rootfs/api/tests/test_views.py
@@ -1,17 +1,22 @@
 """
 Test cases for API views
 """
+from unittest.mock import patch
+
 from django.test import TestCase, RequestFactory
 from django.contrib.auth import get_user_model
 from django.urls import reverse
 from rest_framework.test import APITestCase
 from rest_framework import status
 from oauth2_provider.models import AccessToken
+from social_django.models import UserSocialAuth
+from django.core.cache import cache
 
 from api.views import (
     ReadinessCheckView, LivenessCheckView, ActivateAccount
 )
 from api.forms import RegistrationForm
+from api.models import Message, MessagePreference
 
 User = get_user_model()
 
@@ -62,6 +67,51 @@ def test_registration_form_valid(self):
         form = RegistrationForm(data=form_data)
         self.assertTrue(form.is_valid())
 
+    @patch('api.views.web.settings.REGISTRATION_ENABLED', False)
+    def test_registration_get_disabled(self):
+        response = self.client.get(reverse('registration'))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(
+            response,
+            'The registration function of drycc passport is not enabled.',
+            status_code=200,
+        )
+
+    @patch('api.views.web.settings.REGISTRATION_ENABLED', True)
+    @patch('api.views.web.settings.EMAIL_HOST', '')
+    def test_registration_post_creates_active_user_when_email_disabled(self):
+        response = self.client.post(reverse('registration'), {
+            'username': 'newuser',
+            'email': 'new@example.com',
+            'password1': 'testpassword123',
+            'password2': 'testpassword123',
+            'h-captcha-response': 'PASSED',
+        })
+
+        self.assertEqual(response.status_code, 302)
+        user = User.objects.get(username='newuser')
+        self.assertTrue(user.is_active)
+
+    @patch('api.views.web.settings.REGISTRATION_ENABLED', True)
+    @patch('api.views.web.settings.EMAIL_HOST', 'smtp.example.com')
+    @patch('api.views.web.send_activation_email')
+    def test_registration_post_sends_activation_email_when_email_enabled(
+        self, mock_send_activation_email
+    ):
+        response = self.client.post(reverse('registration'), {
+            'username': 'inactiveuser',
+            'email': 'inactive@example.com',
+            'password1': 'testpassword123',
+            'password2': 'testpassword123',
+            'h-captcha-response': 'PASSED',
+        })
+
+        self.assertEqual(response.status_code, 302)
+        user = User.objects.get(username='inactiveuser')
+        self.assertFalse(user.is_active)
+        mock_send_activation_email.assert_called_once()
+
 
 class TestActivateAccount(TestCase):
     """Test account activation view"""
@@ -90,6 +140,26 @@ def test_activate_account_invalid_token(self):
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.url, '/user/activate/fail/')
 
+    def test_activate_account_valid_token(self):
+        from django.utils.http import urlsafe_base64_encode
+        from django.utils.encoding import force_bytes
+        from django.contrib.messages.storage.fallback import FallbackStorage
+        from api.utils import token_generator
+
+        uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+        token = token_generator.make_token(self.user)
+        request = self.factory.get(f'/activate/{uid}/{token}/')
+        request.session = self.client.session
+        messages = FallbackStorage(request)
+        setattr(request, '_messages', messages)
+
+        response = ActivateAccount.as_view()(request, uidb64=uid, token=token)
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response.url, '/user/activate/done/')
+        self.user.refresh_from_db()
+        self.assertTrue(self.user.is_active)
+
 
 class TestUserDetailView(APITestCase):
     """Test user detail viewset"""
@@ -108,6 +178,24 @@ def test_get_user_detail(self):
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.data['username'], 'testuser')
 
+    @patch('api.views.api.settings.EMAIL_HOST', 'smtp.example.com')
+    @patch('api.views.api.render_to_string', return_value='mail-body')
+    def test_update_user_detail_sends_confirmation_email_and_caches_data(self, _mock_render):
+        url = '/user/info'
+        response = self.client.put(url, {'first_name': 'Updated'})
+
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        self.user.refresh_from_db()
+        self.assertEqual(self.user.first_name, '')
+        self.assertEqual(cache.get(f'user:serializer:{self.user.pk}'), {'first_name': 'Updated'})
+
+    def test_update_user_detail_fails_validation(self):
+        url = '/user/info'
+        response = self.client.put(url, {'email': 'invalid-email'})
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertIn('email', response.data)
+
 
 class TestUserTokensView(APITestCase):
     """Test user tokens viewset"""
@@ -161,3 +249,589 @@ def test_update_password_valid(self):
 
         self.user.refresh_from_db()
         self.assertTrue(self.user.check_password('newpassword123'))
+
+
+class TestIdentityProviderView(APITestCase):
+
+    @patch('api.views.api.settings.SOCIAL_AUTH_FEISHU_KEY', 'key')
+    @patch('api.views.api.settings.SOCIAL_AUTH_FEISHU_SECRET', 'secret')
+    @patch('api.views.api.settings.SOCIAL_AUTH_GOOGLE_KEY', '')
+    @patch('api.views.api.settings.SOCIAL_AUTH_GOOGLE_SECRET', '')
+    def test_only_configured_identity_providers_are_returned(self):
+        response = self.client.get(reverse('user_identity_providers'))
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertGreaterEqual(response.data['count'], 1)
+        provider_names = [item['name'] for item in response.data['results']]
+        self.assertIn('feishu', provider_names)
+        self.assertNotIn('google', provider_names)
+
+
+class TestUserIdentityView(APITestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='identity-user',
+            email='identity@example.com',
+            password='testpass123'
+        )
+        self.other_user = User.objects.create_user(
+            username='other-user',
+            email='other@example.com',
+            password='testpass123'
+        )
+        self.identity = UserSocialAuth.objects.create(
+            user=self.user,
+            provider='feishu',
+            uid='uid-1',
+            extra_data={'email': 'identity@example.com'}
+        )
+        self.other_identity = UserSocialAuth.objects.create(
+            user=self.other_user,
+            provider='github',
+            uid='uid-2',
+            extra_data={'email': 'other@example.com'}
+        )
+        self.client.force_authenticate(user=self.user)
+
+    def test_list_user_identities(self):
+        response = self.client.get(reverse('user_identities-list'))
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['provider'], 'feishu')
+
+    def test_delete_own_identity(self):
+        response = self.client.delete(reverse('user_identities-detail', args=[self.identity.id]))
+
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        self.assertFalse(UserSocialAuth.objects.filter(id=self.identity.id).exists())
+
+    def test_cannot_delete_other_user_identity(self):
+        response = self.client.delete(
+            reverse('user_identities-detail', args=[self.other_identity.id])
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+
+class TestOAuthPendingAndCreateView(APITestCase):
+
+    def setUp(self):
+        self.client_handler = self.client.handler
+
+    def test_oauth_pending_without_session_data(self):
+        response = self.client.get(reverse('user_oauth_pending'))
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_oauth_pending_with_session_data(self):
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'email': 'pending@example.com',
+            'uid': 'pending-uid',
+            'extra_data': {'email': 'pending@example.com'},
+        }
+        session.save()
+
+        response = self.client.get(reverse('user_oauth_pending'))
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['provider'], 'feishu')
+        self.assertEqual(response.data['email'], 'pending@example.com')
+
+    def test_oauth_create_user_success(self):
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'email': 'new@example.com',
+            'uid': 'pending-uid',
+            'extra_data': {'email': 'new@example.com'},
+        }
+        session.save()
+
+        response = self.client.post(reverse('user_oauth_create'), {
+            'username': 'oauth-user',
+            'password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertTrue(
+            User.objects.filter(username='oauth-user', email='new@example.com').exists()
+        )
+        self.assertTrue(
+            UserSocialAuth.objects.filter(provider='feishu', uid='pending-uid').exists()
+        )
+
+    def test_oauth_create_user_requires_pending(self):
+        response = self.client.post(reverse('user_oauth_create'), {
+            'username': 'oauth-user',
+            'password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_oauth_create_user_requires_username(self):
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'email': 'new@example.com',
+            'uid': 'pending-uid',
+            'extra_data': {'email': 'new@example.com'},
+        }
+        session.save()
+
+        response = self.client.post(reverse('user_oauth_create'), {
+            'password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_oauth_create_user_rejects_existing_email(self):
+        User.objects.create_user(
+            username='exists', email='new@example.com', password='password123'
+        )
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'email': 'new@example.com',
+            'uid': 'pending-uid',
+            'extra_data': {'email': 'new@example.com'},
+        }
+        session.save()
+
+        response = self.client.post(reverse('user_oauth_create'), {
+            'username': 'oauth-user',
+            'password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_oauth_create_user_rejects_existing_identity(self):
+        existing_user = User.objects.create_user(
+            username='exists', email='exists@example.com', password='password123'
+        )
+        UserSocialAuth.objects.create(
+            user=existing_user,
+            provider='feishu',
+            uid='pending-uid',
+            extra_data={'email': 'exists@example.com'}
+        )
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'email': 'new@example.com',
+            'uid': 'pending-uid',
+            'extra_data': {'email': 'new@example.com'},
+        }
+        session.save()
+
+        response = self.client.post(reverse('user_oauth_create'), {
+            'username': 'oauth-user',
+            'password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class TestUserMessageViews(APITestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='message-user',
+            email='message@example.com',
+            password='testpass123'
+        )
+        self.other_user = User.objects.create_user(
+            username='other-message-user',
+            email='other-message@example.com',
+            password='testpass123'
+        )
+        self.client.force_authenticate(user=self.user)
+        self.message1 = Message.objects.create(
+            user=self.user,
+            category='system',
+            title='System message',
+            content='System content',
+            severity='info'
+        )
+        self.message2 = Message.objects.create(
+            user=self.user,
+            category='security',
+            title='Security message',
+            content='Security content',
+            severity='warning'
+        )
+        self.other_message = Message.objects.create(
+            user=self.other_user,
+            category='system',
+            title='Other message',
+            content='Other content',
+            severity='info'
+        )
+
+    def test_list_messages(self):
+        response = self.client.get(reverse('user_messages-list'))
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 2)
+        self.assertIn('results', response.data)
+
+    def test_filter_messages_by_category(self):
+        response = self.client.get(reverse('user_messages-list'), {'category': 'security'})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['category'], 'security')
+
+    def test_list_messages_supports_limit_offset_pagination(self):
+        response = self.client.get(reverse('user_messages-list'), {'limit': 1, 'offset': 0})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(len(response.data['results']), 1)
+        self.assertEqual(response.data['count'], 2)
+
+    def test_create_message_for_current_user(self):
+        response = self.client.post(reverse('user_messages-list'), {
+            'category': 'alert',
+            'title': 'Created from API',
+            'content': 'API created content',
+            'full_content': 'API created full content',
+            'severity': 'warning',
+            'is_read': False,
+            'action_link': '/messages',
+            'action_text': 'Open',
+        })
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        created = Message.objects.get(id=response.data['id'])
+        self.assertEqual(created.user, self.user)
+        self.assertEqual(created.category, 'alert')
+
+    def test_mark_all_messages_as_read(self):
+        response = self.client.put(reverse('user_messages-mark-all-read'))
+
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        self.assertFalse(Message.objects.filter(user=self.user, is_read=False).exists())
+
+    def test_get_message_detail_marks_message_as_read(self):
+        response = self.client.get(reverse('user_messages-detail', args=[self.message1.id]))
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.message1.refresh_from_db()
+        self.assertTrue(self.message1.is_read)
+
+    def test_cannot_get_other_users_message(self):
+        response = self.client.get(reverse('user_messages-detail', args=[self.other_message.id]))
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_filter_messages_by_is_read_false(self):
+        # message1 defaults to is_read=False; mark message2 as read
+        self.message2.is_read = True
+        self.message2.save()
+        response = self.client.get(reverse('user_messages-list'), {'is_read': 'false'})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+        self.assertFalse(response.data['results'][0]['is_read'])
+
+    def test_filter_messages_by_is_read_true(self):
+        # mark message1 as read while message2 remains unread
+        self.message1.is_read = True
+        self.message1.save()
+        response = self.client.get(reverse('user_messages-list'), {'is_read': 'true'})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+        self.assertTrue(response.data['results'][0]['is_read'])
+
+    def test_filter_messages_by_category_and_is_read(self):
+        # message1: category=system is_read=False；message2: category=security is_read=True
+        self.message2.is_read = True
+        self.message2.save()
+        response = self.client.get(
+            reverse('user_messages-list'),
+            {'category': 'system', 'is_read': 'false'}
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['category'], 'system')
+        self.assertFalse(response.data['results'][0]['is_read'])
+
+    def test_filter_messages_without_is_read_returns_all(self):
+        # returns all messages when is_read is not provided
+        self.message1.is_read = True
+        self.message1.save()
+        response = self.client.get(reverse('user_messages-list'))
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 2)
+
+    def test_search_by_title(self):
+        response = self.client.get(reverse('user_messages-list'), {'search': 'System'})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+
+    def test_search_by_content(self):
+        response = self.client.get(reverse('user_messages-list'), {'search': 'Security content'})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['id'], self.message2.id)
+
+    def test_search_case_insensitive(self):
+        response = self.client.get(reverse('user_messages-list'), {'search': 'SYSTEM MESSAGE'})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+
+    def test_search_no_match_returns_empty(self):
+        response = self.client.get(reverse('user_messages-list'), {'search': 'zzznomatch'})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 0)
+
+    def test_search_with_category_applies_and(self):
+        # search matches title+content, but category filter should restrict results to system
+        response = self.client.get(
+            reverse('user_messages-list'),
+            {'search': 'content', 'category': 'system'}
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 1)
+        self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+
+    def test_search_empty_string_returns_all(self):
+        response = self.client.get(reverse('user_messages-list'), {'search': ''})
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['count'], 2)
+
+
+class TestUserMessagePreferenceView(APITestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='preference-user',
+            email='preference@example.com',
+            password='testpass123'
+        )
+        self.client.force_authenticate(user=self.user)
+
+    def test_get_creates_preference_if_missing(self):
+        response = self.client.get(reverse('user_message_preferences'))
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertTrue(MessagePreference.objects.filter(user=self.user).exists())
+
+    def test_put_updates_preference(self):
+        response = self.client.put(reverse('user_message_preferences'), {
+            'email_alerts': False,
+            'notify_alert': False,
+        })
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        preference = MessagePreference.objects.get(user=self.user)
+        self.assertFalse(preference.email_alerts)
+        self.assertFalse(preference.notify_alert)
+
+
+class TestUserLoginIdentityLinking(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='login-user',
+            email='login@example.com',
+            password='password123'
+        )
+
+    def test_login_links_pending_identity(self):
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'uid': 'uid-100',
+            'extra_data': {'email': 'login@example.com'},
+        }
+        session.save()
+
+        response = self.client.post(reverse('user_login') + '?identity_linking=1', {
+            'username': 'login-user',
+            'password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, 302)
+        self.assertIn('status=linked', response.url)
+        self.assertTrue(UserSocialAuth.objects.filter(
+            user=self.user, provider='feishu', uid='uid-100'
+        ).exists())
+
+    def test_login_identity_linking_conflict_redirects(self):
+        other_user = User.objects.create_user(
+            username='other-login-user',
+            email='other-login@example.com',
+            password='password123'
+        )
+        UserSocialAuth.objects.create(
+            user=other_user,
+            provider='feishu',
+            uid='uid-100',
+            extra_data={'email': 'other-login@example.com'}
+        )
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'uid': 'uid-100',
+            'extra_data': {'email': 'login@example.com'},
+        }
+        session.save()
+
+        response = self.client.post(reverse('user_login') + '?identity_linking=1', {
+            'username': 'login-user',
+            'password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, 302)
+        self.assertIn('status=conflict', response.url)
+
+    @patch('api.views.web.settings.EMAIL_HOST', 'smtp.example.com')
+    @patch('api.views.web.settings.REGISTRATION_ENABLED', True)
+    def test_login_page_context_shows_identity_linking_error(self):
+        response = self.client.post(reverse('user_login') + '?identity_linking=1', {
+            'username': 'login-user',
+            'password': 'wrong-password',
+        })
+
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Identity linking login failed. Please try again.')
+
+    @patch('api.views.web.settings.EMAIL_HOST', '')
+    @patch('api.views.web.settings.REGISTRATION_ENABLED', False)
+    def test_login_page_context_flags_follow_settings(self):
+        response = self.client.get(reverse('user_login') + '?identity_linking=1')
+
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Identity Linking')
+
+
+class TestOAuthCallbackTemplateView(TestCase):
+
+    def test_get_without_pending_renders_error_state(self):
+        response = self.client.get(reverse('oauth_callback') + '?status=pending')
+
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'OAuth flow error. Please try again.')
+
+    @patch('api.views.api.OAuthCreateUserView._create_oauth_user')
+    def test_post_password_mismatch_renders_error(self, _mock_create):
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'email': 'pending@example.com',
+        }
+        session.save()
+
+        response = self.client.post(reverse('oauth_callback'), {
+            'username': 'oauth-user',
+            'password': 'password123',
+            'confirm_password': 'different123',
+        })
+
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Password confirmation does not match')
+
+    @patch('api.views.api.OAuthCreateUserView._create_oauth_user')
+    def test_post_success_redirects_account_setting(self, mock_create):
+        session = self.client.session
+        session['oauth_pending'] = {
+            'provider': 'feishu',
+            'email': 'pending@example.com',
+        }
+        session.save()
+
+        response = self.client.post(reverse('oauth_callback'), {
+            'username': 'oauth-user',
+            'password': 'password123',
+            'confirm_password': 'password123',
+        })
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response.url, '/account-setting')
+        mock_create.assert_called_once()
+
+
+class TestUpdateAccountView(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user(
+            username='update-user',
+            email='update@example.com',
+            password='password123'
+        )
+
+    def test_update_account_applies_cached_serializer_data(self):
+        from django.utils.http import urlsafe_base64_encode
+        from django.utils.encoding import force_bytes
+        from api.utils import token_generator
+
+        cache.set(f'user:serializer:{self.user.pk}', {'first_name': 'Cached'}, 1800)
+        uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+        token = token_generator.make_token(self.user)
+
+        response = self.client.get(reverse('user_update_account', args=[uid, token]))
+
+        self.assertEqual(response.status_code, 200)
+        self.user.refresh_from_db()
+        self.assertEqual(self.user.first_name, 'Cached')
+        self.assertIsNone(cache.get(f'user:serializer:{self.user.pk}'))
+
+    def test_update_account_invalid_token_renders_fail(self):
+        from django.utils.http import urlsafe_base64_encode
+        from django.utils.encoding import force_bytes
+
+        uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+        response = self.client.get(reverse('user_update_account', args=[uid, 'invalid-token']))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'update fail', status_code=200)
+
+    def test_update_account_without_cached_data_renders_fail(self):
+        from django.utils.http import urlsafe_base64_encode
+        from django.utils.encoding import force_bytes
+        from api.utils import token_generator
+
+        uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+        token = token_generator.make_token(self.user)
+        response = self.client.get(reverse('user_update_account', args=[uid, token]))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'update fail', status_code=200)
+
+
+class TestPasswordResetViews(TestCase):
+
+    def test_password_reset_form_page_loads(self):
+        response = self.client.get(reverse('user_password_reset'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_password_reset_done_page_loads(self):
+        response = self.client.get(reverse('user_password_reset_done'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_password_reset_complete_page_loads(self):
+        response = self.client.get(reverse('user_password_reset_complete'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_password_reset_confirm_invalid_token_page_loads(self):
+        response = self.client.get(
+            reverse('user_password_reset_confirm', args=['invalid', 'invalid-token'])
+        )
+        self.assertEqual(response.status_code, 200)
diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py
index a5e8a68..b2fbd58 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -2,21 +2,30 @@
 from rest_framework.routers import SimpleRouter
 
 from api.views import web, api
-from passport.views import SettingsViewSet
+from passport.views import AppSettingsViewSet
 
-router = SimpleRouter(trailing_slash=False)
+
+class OptionalSlashRouter(SimpleRouter):
+    """Router that accepts both trailing-slash and no-trailing-slash URLs."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.trailing_slash = '/?'
+
+
+router = OptionalSlashRouter()
+router.register(r'user/messages', api.UserMessageViewSet, basename='user_messages')
+router.register(r'user/identities', api.UserIdentityViewSet, basename='user_identities')
 
 urlpatterns = [
     re_path(r'^', include(router.urls)),
     # Settings URL (moved from main urls.py)
-    re_path(r'^settings/?$', SettingsViewSet.as_view({'get': 'retrieve'})),
+    re_path(r'^settings/?$', AppSettingsViewSet.as_view({'get': 'retrieve'})),
     # User URLs (add user prefix)
     re_path(r'^user/info/?$',
             api.UserDetailView.as_view({'get': 'retrieve', 'put': 'update'})),
     re_path(r'^user/update/(?P<uidb64>.+)/(?P<token>.+)/?$',
             web.UpdateAccount.as_view(), name='user_update_account'),
-    re_path(r'^user/avatar/(?P<username>[-_\w]+)/?$',
-            api.UserAvatarViewSet.as_view({'get': 'avatar'})),
     re_path(r'^user/registration/?$', web.RegistrationView.as_view(), name='registration'),
     re_path(r'^user/activate/(?P<uidb64>.+)/(?P<token>.+)/?$',
             web.ActivateAccount.as_view(), name='user_activate_account'),
@@ -50,16 +59,16 @@
             name='user_grants'),
     re_path(r'^user/email/?$', api.UserEmailView.as_view({'get': 'retrieve'})),
     re_path(r'^user/password/?$',
-            api.UserAccountPasswordView.as_view({'put': 'update'}),
+            api.UserAccountPasswordView.as_view(),
             name='user_account_update_password'),
     re_path(r'^user/identity-providers/?$',
             api.IdentityProviderView.as_view(), name='user_identity_providers'),
-    re_path(r'^user/identities/?$',
-            api.UserIdentityView.as_view(), name='user_identities'),
-    re_path(r'^user/identities/(?P<identity_id>\d+)/?$',
-            api.UserIdentityView.as_view(), name='user_identity_detail'),
     re_path(r'^user/oauth/pending/?$',
             api.OAuthPendingView.as_view(), name='user_oauth_pending'),
     re_path(r'^user/oauth/create/?$',
             api.OAuthCreateUserView.as_view(), name='user_oauth_create'),
+    re_path(r'^user/message-preferences/?$',
+            api.UserMessagePreferenceViewSet.as_view(
+                {'get': 'retrieve', 'put': 'update', 'patch': 'partial_update'}),
+            name='user_message_preferences'),
 ]
diff --git a/rootfs/api/utils.py b/rootfs/api/utils.py
index 26b2a98..5eeefeb 100644
--- a/rootfs/api/utils.py
+++ b/rootfs/api/utils.py
@@ -5,9 +5,12 @@
 import logging
 import datetime
 
-from urllib.parse import urlencode
+import requests
 from django.conf import settings
 
+from urllib.parse import urlencode
+
+from django.utils.html import strip_tags
 from django.utils.encoding import force_bytes
 from django.template.loader import render_to_string
 from django.utils.http import urlsafe_base64_encode
@@ -92,6 +95,18 @@ def validate_reserved_names(value):
 
 
 def send_activation_email(request, user):
+    if not user.email:
+        logger.warning(
+            "Activation email skipped for user %s due to missing email address",
+            user.username
+        )
+        return
+    if not getattr(settings, 'EMAIL_HOST', ''):
+        logger.warning(
+            "Activation email skipped for user %s due to missing SMTP configuration",
+            user.username
+        )
+        return
     domain = get_local_host(request)
     mail_subject = 'Activate your account.'
     message = render_to_string(
@@ -104,6 +119,72 @@ def send_activation_email(request, user):
     user.email_user(mail_subject, message, fail_silently=True)
 
 
+def send_email_notification(message) -> None:
+    """Send an email notification for a message.
+
+    Silently skips when SMTP / DEFAULT_FROM_EMAIL is not configured, or
+    when the target user has no email address. This avoids triggering
+    endless Celery retries in environments without a configured mail
+    server (e.g. local development).
+    """
+    if message.user.email is None:
+        logger.info(
+            "Email notification skipped for message %s due to missing user email",
+            message.id
+        )
+        return
+    if not getattr(settings, 'EMAIL_HOST', ''):
+        logger.info(
+            "Email notification skipped for message %s due to missing SMTP configuration",
+            message.id
+        )
+        return
+    mail_subject = f"[{message.get_category_display()}] {message.title}"
+    html_message = render_to_string(
+        'notifications/email_message.html',
+        {
+            'message': message,
+            'user': message.user,
+            'domain': getattr(settings, 'DASHBOARD_URL', '/'),
+        }
+    )
+    plain_message = strip_tags(html_message)
+    message.user.email_user(
+        subject=mail_subject,
+        message=plain_message,
+        html_message=html_message,
+        fail_silently=False,
+    )
+    logger.info("Email notification sent to %s for message %s", message.user.email, message.id)
+
+
+def send_webhook_notification(message, webhook_url) -> None:
+    """Send a webhook notification for a message."""
+    if not webhook_url:
+        return
+    payload = {
+        'id': str(message.id),
+        'category': message.category,
+        'title': message.title,
+        'content': message.content,
+        'severity': message.severity,
+        'created_at': message.created_at.isoformat() if message.created_at else None,
+        'user': {
+            'id': message.user.id,
+            'username': message.user.username,
+            'email': message.user.email,
+        },
+    }
+    response = requests.post(
+        webhook_url,
+        json=payload,
+        headers={'Content-Type': 'application/json'},
+        timeout=10,
+    )
+    response.raise_for_status()
+    logger.info("Webhook notification sent for message %s to %s", message.id, webhook_url)
+
+
 if __name__ == "__main__":
     import doctest
 
diff --git a/rootfs/api/views/api.py b/rootfs/api/views/api.py
index dd84dca..055dba0 100644
--- a/rootfs/api/views/api.py
+++ b/rootfs/api/views/api.py
@@ -1,16 +1,12 @@
-import hashlib
-
 from django.conf import settings
 from django.core.cache import cache
-from django.utils.decorators import method_decorator
-from django.views.decorators.cache import cache_page
 from django.contrib import auth
 from django.contrib.auth import get_user_model
 from django.db import IntegrityError
 from django.db.models import Q
 from django.shortcuts import get_object_or_404
 from django.template.loader import render_to_string
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse
 from django.utils.encoding import force_bytes
 from django.utils.http import urlsafe_base64_encode
 from django.urls import reverse_lazy
@@ -21,13 +17,16 @@
 from rest_framework.permissions import AllowAny, IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.views import APIView
-from rest_framework.viewsets import ModelViewSet
+from rest_framework.viewsets import GenericViewSet
+from rest_framework.decorators import action
+from rest_framework.mixins import ListModelMixin, DestroyModelMixin
 
 from oauth2_provider.models import AccessToken
 from social_django.models import UserSocialAuth
 
 from api import serializers
 from api.exceptions import ServiceUnavailable, DryccException
+from api.models import Message, MessagePreference
 from api.utils import get_local_host, get_user_socials, token_generator
 
 
@@ -83,14 +82,19 @@ def get(self, request, *args, **kwargs):
         return Response({'count': len(results), 'results': results})
 
 
-class UserIdentityView(APIView):
+class UserIdentityViewSet(NormalUserViewSet):
+    serializer_class = serializers.UserEmailSerializer
+    http_method_names = ['get', 'delete']
 
-    def get(self, request, *args, **kwargs):
+    def get_queryset(self):
+        return UserSocialAuth.objects.filter(user=self.request.user).order_by('id')
+
+    def list(self, request, *args, **kwargs):
         results = get_user_socials(request.user)
         return Response({'count': len(results), 'results': results})
 
-    def delete(self, request, identity_id, *args, **kwargs):
-        identity = get_object_or_404(UserSocialAuth, id=identity_id, user=request.user)
+    def destroy(self, request, *args, **kwargs):
+        identity = self.get_object()
         identity.delete()
         return Response(status=status.HTTP_204_NO_CONTENT)
 
@@ -117,6 +121,41 @@ def get(self, request, *args, **kwargs):
         })
 
 
+class UserMessageViewSet(NormalUserViewSet):
+    serializer_class = serializers.MessageSerializer
+
+    def get_queryset(self):
+        queryset = Message.objects.filter(user=self.request.user)
+        category = self.request.query_params.get('category')
+        if category and category != 'all':
+            queryset = queryset.filter(category=category)
+        is_read = self.request.query_params.get('is_read')
+        if is_read is not None:
+            queryset = queryset.filter(is_read=is_read.lower() not in ('false', '0', 'no'))
+        search = self.request.query_params.get('search')
+        if search:
+            queryset = queryset.filter(
+                Q(title__icontains=search) | Q(content__icontains=search)
+            )
+        return queryset.order_by('-created_at')
+
+    def perform_create(self, serializer):
+        serializer.save(user=self.request.user)
+
+    def retrieve(self, request, *args, **kwargs):
+        message = self.get_object()
+        if not message.is_read:
+            message.is_read = True
+            message.save(update_fields=['is_read'])
+        serializer = self.get_serializer(message)
+        return Response(serializer.data)
+
+    @action(detail=False, methods=['put'], url_path='mark-all-read')
+    def mark_all_read(self, request, *args, **kwargs):
+        self.get_queryset().filter(is_read=False).update(is_read=True)
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
 class OAuthCreateUserView(APIView):
     permission_classes = (AllowAny, )
 
@@ -188,41 +227,29 @@ def update(self, request, *args, **kwargs):
             instance=request.user,
             partial=True,
         )
-        if user_serializer.is_valid():
-            if settings.EMAIL_HOST:
-                user = self.get_object()
-                mail_subject = 'Update your account.'
-                uid = urlsafe_base64_encode(force_bytes(user.pk))
-                token = token_generator.make_token(user)
-                message = render_to_string(
-                    self.email_template_name,
-                    {
-                        'uid': uid,
-                        'user': user,
-                        'token': token,
-                        'domain': get_local_host(request),
-                    },
-                )
-                cache_key = "user:serializer:%s" % user.pk
-                cache.set(cache_key, request.data, 60 * 30)
-                user.email_user(mail_subject, message, fail_silently=True)
-            else:
-                user_serializer.save()
+        user_serializer.is_valid(raise_exception=True)
+        if getattr(settings, 'EMAIL_HOST', ''):
+            user = self.get_object()
+            mail_subject = 'Update your account.'
+            uid = urlsafe_base64_encode(force_bytes(user.pk))
+            token = token_generator.make_token(user)
+            message = render_to_string(
+                self.email_template_name,
+                {
+                    'uid': uid,
+                    'user': user,
+                    'token': token,
+                    'domain': get_local_host(request),
+                },
+            )
+            cache_key = "user:serializer:%s" % user.pk
+            cache.set(cache_key, request.data, 60 * 30)
+            user.email_user(mail_subject, message, fail_silently=True)
+        else:
+            user_serializer.save()
         return Response(status=status.HTTP_204_NO_CONTENT)
 
 
-class UserAvatarViewSet(NormalUserViewSet):
-
-    @method_decorator(cache_page(60 * 5))
-    def avatar(self, request, *args, **kwargs):
-        user = User.objects.filter(username=kwargs["username"]).first()
-        size = request.GET.get("s", "80")
-        md5 = hashlib.md5()
-        if user:
-            md5.update(user.email.encode("utf8"))
-        return HttpResponseRedirect(settings.AVATAR_URL + md5.hexdigest() + "?s=" + size)
-
-
 class UserEmailView(NormalUserViewSet):
     serializer_class = serializers.UserEmailSerializer
     required_scopes = ['openid']
@@ -231,35 +258,19 @@ def get_object(self):
         return self.request.user
 
 
-class ListViewSet(ModelViewSet):
-
-    def get_queryset(self, *args, **kwargs):
-        serializer = self.serializer_class(data=self.request.query_params)
-        serializer.is_valid(raise_exception=True)
-
-        serializerlist = serializers.ListSerializer(data=self.request.query_params)
-        serializerlist.is_valid(raise_exception=True)
-        q = Q(user=self.request.user)
-        if serializerlist.validated_data.get('section'):
-            q &= Q(created__range=serializerlist.validated_data.get('section'))
-
-        return self.model.objects.filter(
-            q, **serializer.validated_data
-        ).order_by(self.order_by)[0:100]
-
-
-class UserTokensView(ListViewSet):
-    model = AccessToken
+class UserTokensView(ListModelMixin, DestroyModelMixin, GenericViewSet):
     serializer_class = serializers.UserTokensSerializer
-    order_by = '-created'
+    permission_classes = [IsAuthenticated]
 
-    def destroy(self, request, *args, **kwargs):
-        token = get_object_or_404(self.model, id=self.kwargs['pk'], user=request.user)
-        token.delete()
-        return Response(status=status.HTTP_204_NO_CONTENT)
+    def get_queryset(self):
+        return AccessToken.objects.filter(user=self.request.user).order_by('-created')
+
+    def get_object(self):
+        return get_object_or_404(self.get_queryset(), id=self.kwargs['pk'])
 
 
-class UserAccountPasswordView(ListViewSet):
+class UserAccountPasswordView(APIView):
+    permission_classes = [IsAuthenticated]
 
     def update(self, request, *args, **kwargs):
         if settings.LDAP_ENDPOINT:
@@ -276,3 +287,26 @@ def update(self, request, *args, **kwargs):
         request.user.save()
         auth.logout(request)
         return HttpResponse(status=204)
+
+    def put(self, request, *args, **kwargs):
+        return self.update(request, *args, **kwargs)
+
+
+class UserMessagePreferenceViewSet(NormalUserViewSet):
+    serializer_class = serializers.MessagePreferenceSerializer
+    http_method_names = ['get', 'put', 'patch']
+
+    def get_object(self):
+        preference, _ = MessagePreference.objects.get_or_create(user=self.request.user)
+        return preference
+
+    def retrieve(self, request, *args, **kwargs):
+        serializer = self.get_serializer(self.get_object())
+        return Response(serializer.data)
+
+    def update(self, request, *args, **kwargs):
+        partial = kwargs.pop('partial', False)
+        serializer = self.get_serializer(self.get_object(), data=request.data, partial=partial)
+        serializer.is_valid(raise_exception=True)
+        serializer.save()
+        return Response(serializer.data)
diff --git a/rootfs/api/views/web.py b/rootfs/api/views/web.py
index 952f29c..b3104f8 100644
--- a/rootfs/api/views/web.py
+++ b/rootfs/api/views/web.py
@@ -38,7 +38,7 @@ def post(self, request, *args, **kwargs):
         self.object = None
         if form.is_valid():
             user = form.save(commit=False)
-            if settings.EMAIL_HOST:
+            if getattr(settings, 'EMAIL_HOST', ''):
                 user.is_active = False
                 user.save()
                 send_activation_email(request, user)
@@ -109,7 +109,7 @@ def get_context_data(self, **kwargs):
         context.update({
             'identity_linking': identity_linking,
             'registration_enabled': settings.REGISTRATION_ENABLED,
-            'password_reset_enabled': True if settings.EMAIL_HOST else False,
+            'password_reset_enabled': True if getattr(settings, 'EMAIL_HOST', '') else False,
             'identity_linking_error': (
                 _('Identity linking login failed. Please try again.')
                 if identity_linking and form is not None and form.errors else None
diff --git a/rootfs/passport/views.py b/rootfs/passport/views.py
index 42f9493..ede1bf9 100644
--- a/rootfs/passport/views.py
+++ b/rootfs/passport/views.py
@@ -4,12 +4,17 @@
 from rest_framework.response import Response
 
 
-class SettingsViewSet(GenericViewSet):
+class AppSettingsViewSet(GenericViewSet):
 
     permission_classes = (AllowAny, )
 
     def retrieve(self, request, *args, **kwargs):
         return Response(data={
-            "legal": settings.LEGAL_ENABLED,
-            "registration_enabled": settings.REGISTRATION_ENABLED,
+            "legal": getattr(settings, 'LEGAL_ENABLED', False),
+            "dashboard_url": getattr(settings, 'DASHBOARD_URL', '/'),
+            "contact_support_url": getattr(
+                settings, 'CONTACT_SUPPORT_URL',
+                'https://community.drycc.cc/'
+            ),
+            "registration_enabled": getattr(settings, 'REGISTRATION_ENABLED', False),
         })
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index 65ca2c5..c4c1733 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -10,7 +10,8 @@ asgiref==3.11.1
 psycopg[binary]==3.3.3
 requests==2.33.0
 requests-toolbelt==1.0.0
-redis==7.4.0
+redis==6.3.0
+celery[redis]==5.6.3
 hiredis==3.3.1
 dj-database-url==3.1.2
 django-oauth-toolkit==3.2.0
diff --git a/rootfs/web/src/assets/account-setting.js b/rootfs/web/src/assets/account-setting.js
deleted file mode 100644
index 7dc6e22..0000000
--- a/rootfs/web/src/assets/account-setting.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const fs = require('fs');
-const content = fs.readFileSync('/home/duanhongyi/Sources/passport/rootfs/web/src/views/AccountSetting.vue', 'utf8');
-
-const regex = /<style lang="css" scoped>[\s\S]*<\/style>/g;
-const newContent = content.replace(regex, '<style scoped>\n/* Removed legacy styles */\n</style>');
-
-// We also need to rewrite the template part to use tailwind classes.
-
diff --git a/rootfs/web/src/assets/images/avatar.png b/rootfs/web/src/assets/images/avatar.png
deleted file mode 100644
index a09db42c34002ca1b12353058833182e2cf1e28f..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/rootfs/web/src/components/MainNav.vue b/rootfs/web/src/components/AccountSidebar.vue
similarity index 70%
rename from rootfs/web/src/components/MainNav.vue
rename to rootfs/web/src/components/AccountSidebar.vue
index f3fc9a2..9d13f00 100644
--- a/rootfs/web/src/components/MainNav.vue
+++ b/rootfs/web/src/components/AccountSidebar.vue
@@ -1,5 +1,14 @@
 <template>
     <aside class="w-full lg:w-64 flex-shrink-0 flex flex-col gap-5">
+        <!-- Back to Dashboard -->
+        <div>
+            <a :href="globalState.dashboardUrl" class="w-full bg-white border border-slate-200 rounded-md px-3 py-2 flex items-center justify-center shadow-sm hover:border-primary transition-colors focus:outline-none group">
+                <span class="flex items-center gap-2 text-sm text-slate-600 group-hover:text-primary transition-colors font-medium">
+                    <ArrowLeft class="w-4 h-4" /> Back to Dashboard
+                </span>
+            </a>
+        </div>
+
         <div class="flex flex-col gap-0.5 mt-2">
             <p class="text-xs font-semibold text-slate-500 mb-2 px-1">Settings</p>
             <button @click="goToAccessToken" 
@@ -22,13 +31,16 @@
 
 <script lang="ts">
 import { useRouter } from 'vue-router'
-import { Key, Settings } from 'lucide-vue-next'
+import { Key, Settings, ArrowLeft } from 'lucide-vue-next'
+import { globalState, fetchGlobalSettings } from '../store'
+import { onMounted } from 'vue'
 
 export default {
-    name: "MainNav",
+    name: "AccountSidebar",
     components: {
         Key,
-        Settings
+        Settings,
+        ArrowLeft
     },
     props: {
         isAccessTokenActive: {
@@ -42,6 +54,11 @@ export default {
     },
     setup(props) {
         const router = useRouter()
+        
+        onMounted(async () => {
+            await fetchGlobalSettings()
+        })
+
         const goToAccessToken = () => {
             router.push({ path: `/access-tokens` })
         }
@@ -51,6 +68,7 @@ export default {
         }
 
         return {
+            globalState,
             goToAccessToken,
             goToAccountSetting,
         }
diff --git a/rootfs/web/src/components/MessageRightSidebar.vue b/rootfs/web/src/components/MessageRightSidebar.vue
new file mode 100644
index 0000000..2d2a6d8
--- /dev/null
+++ b/rootfs/web/src/components/MessageRightSidebar.vue
@@ -0,0 +1,119 @@
+<script setup lang="ts">
+import { Settings, CheckCircle2, LifeBuoy, BookOpen, Activity, ArrowUpRight, ArrowRight } from 'lucide-vue-next'
+import { globalState, fetchGlobalSettings, invalidateMessages } from '../store'
+import { onMounted } from 'vue'
+import { markAllAsRead } from '../services/messages'
+
+const emit = defineEmits(['mark-all-read'])
+
+onMounted(async () => {
+    await fetchGlobalSettings()
+})
+
+const handleMarkAllAsRead = async () => {
+    try {
+        await markAllAsRead()
+        invalidateMessages()
+        emit('mark-all-read')
+    } catch (e) {
+        console.error('Failed to mark all as read', e)
+    }
+}
+</script>
+
+<template>
+  <aside class="w-full lg:w-72 flex-shrink-0 space-y-5">
+    
+    <!-- Settings & Actions Card -->
+    <div class="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden relative overflow-y-hidden group">
+      <!-- Decorative hover flare -->
+      <div class="absolute -right-20 -top-20 w-40 h-40 bg-primary/5 rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
+
+      <div class="px-5 py-4 border-b border-slate-100 bg-slate-50/50 flex flex-col gap-1 relative z-10">
+        <h3 class="text-[13px] font-semibold text-slate-800 flex items-center gap-2 uppercase tracking-wide">
+          <Settings class="w-3.5 h-3.5 text-primary" />
+          Actions
+        </h3>
+      </div>
+      <div class="p-3 relative z-10">
+        <button @click="handleMarkAllAsRead" class="w-full flex items-center justify-between px-3 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition-all group/item focus:outline-none">
+          <span class="flex items-center gap-2.5">
+            <CheckCircle2 class="w-4 h-4 text-emerald-400 group-hover/item:text-emerald-500 transition-colors" />
+            Mark all as read
+          </span>
+          <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 opacity-0 group-hover/item:opacity-100 shadow-[0_0_4px_rgba(16,185,129,0.5)] transition-all"></span>
+        </button>
+        <router-link to="/messages/preferences" class="w-full flex items-center justify-between px-3 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition-all group/item mt-1 focus:outline-none">
+          <span class="flex items-center gap-2.5">
+            <Settings class="w-4 h-4 text-primary/60 group-hover/item:text-primary transition-colors" />
+            Notification Preferences
+          </span>
+          <ArrowRight class="w-3.5 h-3.5 text-slate-300 group-hover/item:text-primary -translate-x-1 group-hover/item:translate-x-0 opacity-0 group-hover/item:opacity-100 transition-all" />
+        </router-link>
+      </div>
+    </div>
+
+    <!-- System Status Card -->
+    <div class="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden opacity-95 hover:opacity-100 transition-opacity">
+      <div class="px-5 py-4 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
+        <h3 class="text-[13px] font-semibold text-slate-800 flex items-center gap-2 uppercase tracking-wide">
+          <BookOpen class="w-3.5 h-3.5 text-blue-500" />
+          Pro Tips
+        </h3>
+      </div>
+      <div class="p-5">
+        <div class="space-y-4">
+          <div class="flex gap-3 items-start group/tip cursor-default">
+            <div class="w-6 h-6 rounded-md bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0 mt-0.5 group-hover/tip:bg-blue-100 transition-colors">
+              <span class="text-[11px] font-bold">1</span>
+            </div>
+            <div>
+              <h4 class="text-[13px] font-semibold text-slate-700 mb-0.5">Filter by Severity</h4>
+              <p class="text-xs text-slate-500 leading-relaxed">Use the left sidebar to quickly isolate critical alerts or system updates.</p>
+            </div>
+          </div>
+          
+          <div class="flex gap-3 items-start group/tip cursor-default">
+            <div class="w-6 h-6 rounded-md bg-indigo-50 text-indigo-600 flex items-center justify-center flex-shrink-0 mt-0.5 group-hover/tip:bg-indigo-100 transition-colors">
+              <span class="text-[11px] font-bold">2</span>
+            </div>
+            <div>
+              <h4 class="text-[13px] font-semibold text-slate-700 mb-0.5">Automate Workflows</h4>
+              <p class="text-xs text-slate-500 leading-relaxed">Configure Webhooks in preferences to forward messages directly to Slack or Teams.</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Support / Resources Card -->
+    <div class="bg-gradient-to-b from-slate-800 to-slate-900 rounded-lg shadow-[0_4px_20px_-4px_rgba(15,23,42,0.3)] border border-slate-700 overflow-hidden text-white relative group">
+      <!-- Decorative animated background dots -->
+      <div class="absolute -right-6 top-8 w-24 h-24 bg-primary/30 rounded-full blur-3xl group-hover:bg-primary/40 transition-colors pointer-events-none"></div>
+      <div class="absolute -left-10 -bottom-10 w-20 h-20 bg-blue-500/20 rounded-full blur-2xl group-hover:bg-blue-500/30 transition-colors pointer-events-none"></div>
+      
+      <div class="p-6 relative z-10">
+        <h3 class="text-sm font-semibold mb-2.5 flex items-center gap-2">
+          <div class="p-1.5 bg-white/10 rounded-md">
+            <LifeBuoy class="w-4 h-4 text-blue-300" />
+          </div>
+          Need Assistance?
+        </h3>
+        <p class="text-[13px] text-slate-300 leading-relaxed mb-5 font-medium opacity-90">
+          Our support team is here to help you 24/7 with enterprise-grade SLA and troubleshooting.
+        </p>
+        <div class="space-y-2.5">
+          <a :href="globalState.contactSupportUrl" class="flex items-center justify-center gap-2 w-full py-2 px-4 bg-white hover:bg-slate-50 text-slate-900 text-[13px] font-semibold rounded-lg transition-all focus:outline-none shadow-sm hover:shadow active:scale-[0.98]">
+            <LifeBuoy class="w-4 h-4 shrink-0" />
+            Contact Support
+          </a>
+          <a href="https://www.drycc.cc/zh-cn/docs/" target="_blank" class="flex items-center justify-center gap-2 w-full py-2 px-4 bg-white/5 hover:bg-white/10 border border-white/10 text-[13px] font-semibold text-slate-200 hover:text-white rounded-lg transition-colors focus:outline-none">
+            <BookOpen class="w-4 h-4 shrink-0 opacity-70" />
+            Read Documentation
+          </a>
+        </div>
+      </div>
+    </div>
+
+  </aside>
+</template>
\ No newline at end of file
diff --git a/rootfs/web/src/components/MessageSidebar.vue b/rootfs/web/src/components/MessageSidebar.vue
new file mode 100644
index 0000000..126a21f
--- /dev/null
+++ b/rootfs/web/src/components/MessageSidebar.vue
@@ -0,0 +1,111 @@
+<script setup lang="ts">
+import { computed, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { Bell, ShieldAlert, Cpu, Activity, Info, AlertTriangle, Inbox, ArrowLeft, MailOpen } from 'lucide-vue-next'
+import { globalState, fetchGlobalSettings } from '../store'
+
+const route = useRoute()
+const router = useRouter()
+
+onMounted(async () => {
+    await fetchGlobalSettings()
+})
+
+const isActive = (categoryId: string) => {
+    if (route.path !== '/messages') return false
+    const queryCat = route.query.category || 'all'
+    return queryCat === categoryId
+}
+
+const navigateToCategory = (categoryId: string) => {
+    if (categoryId === 'all') {
+        router.push({ path: '/messages' })
+    } else {
+        router.push({ path: '/messages', query: { category: categoryId } })
+    }
+}
+</script>
+
+<template>
+  <aside class="w-full lg:w-64 flex-shrink-0 flex flex-col gap-5">
+      
+      <!-- Back to Dashboard / Apps (since it replaces AppSidebar) -->
+      <div>
+          <a :href="globalState.dashboardUrl" class="w-full bg-white border border-slate-200 rounded-md px-3 py-2 flex items-center justify-center shadow-sm hover:border-primary transition-colors focus:outline-none group">
+              <span class="flex items-center gap-2 text-sm text-slate-600 group-hover:text-primary transition-colors font-medium">
+                  <ArrowLeft class="w-4 h-4" /> Back to Dashboard
+              </span>
+          </a>
+      </div>
+
+      <div class="flex flex-col gap-0.5 mt-2">
+          <p class="text-xs font-semibold text-slate-500 mb-2 px-1">Message Types</p>
+          
+          <button @click="navigateToCategory('all')" :class="[
+              'w-full flex items-center gap-3 px-3 py-2 rounded-md font-medium transition-all text-left focus:outline-none',
+              isActive('all') 
+                  ? 'bg-primary-50 text-primary' 
+                  : 'text-slate-600 hover:bg-white hover:shadow-sm'
+          ]">
+              <Inbox :class="['w-4 h-4', isActive('all') ? 'text-primary' : 'text-slate-400']" /> All Messages
+          </button>
+          
+          <button @click="navigateToCategory('unread')" :class="[
+              'w-full flex items-center gap-3 px-3 py-2 rounded-md font-medium transition-all text-left focus:outline-none',
+              isActive('unread') 
+                  ? 'bg-primary-50 text-primary' 
+                  : 'text-slate-600 hover:bg-white hover:shadow-sm'
+          ]">
+              <MailOpen :class="['w-4 h-4', isActive('unread') ? 'text-primary' : 'text-slate-400']" /> Unread
+          </button>
+
+          <div class="h-px bg-slate-200/80 my-2"></div>
+
+          <button @click="navigateToCategory('system')" :class="[
+              'w-full flex items-center gap-3 px-3 py-2 rounded-md font-medium transition-all text-left focus:outline-none',
+              isActive('system') 
+                  ? 'bg-primary-50 text-primary' 
+                  : 'text-slate-600 hover:bg-white hover:shadow-sm'
+          ]">
+              <Info :class="['w-4 h-4', isActive('system') ? 'text-primary' : 'text-slate-400']" /> System
+          </button>
+
+          <button @click="navigateToCategory('product')" :class="[
+              'w-full flex items-center gap-3 px-3 py-2 rounded-md font-medium transition-all text-left focus:outline-none',
+              isActive('product') 
+                  ? 'bg-primary-50 text-primary' 
+                  : 'text-slate-600 hover:bg-white hover:shadow-sm'
+          ]">
+              <Cpu :class="['w-4 h-4', isActive('product') ? 'text-primary' : 'text-slate-400']" /> Product Updates
+          </button>
+
+          <button @click="navigateToCategory('security')" :class="[
+              'w-full flex items-center gap-3 px-3 py-2 rounded-md font-medium transition-all text-left focus:outline-none',
+              isActive('security') 
+                  ? 'bg-primary-50 text-primary' 
+                  : 'text-slate-600 hover:bg-white hover:shadow-sm'
+          ]">
+              <ShieldAlert :class="['w-4 h-4', isActive('security') ? 'text-primary' : 'text-slate-400']" /> Security
+          </button>
+
+          <button @click="navigateToCategory('alert')" :class="[
+              'w-full flex items-center gap-3 px-3 py-2 rounded-md font-medium transition-all text-left focus:outline-none',
+              isActive('alert') 
+                  ? 'bg-primary-50 text-primary' 
+                  : 'text-slate-600 hover:bg-white hover:shadow-sm'
+          ]">
+              <AlertTriangle :class="['w-4 h-4', isActive('alert') ? 'text-primary' : 'text-slate-400']" /> Alerts
+          </button>
+
+          <button @click="navigateToCategory('service')" :class="[
+              'w-full flex items-center gap-3 px-3 py-2 rounded-md font-medium transition-all text-left focus:outline-none',
+              isActive('service') 
+                  ? 'bg-primary-50 text-primary' 
+                  : 'text-slate-600 hover:bg-white hover:shadow-sm'
+          ]">
+              <Activity :class="['w-4 h-4', isActive('service') ? 'text-primary' : 'text-slate-400']" /> Service
+          </button>
+
+      </div>
+  </aside>
+</template>
\ No newline at end of file
diff --git a/rootfs/web/src/components/NavBar.vue b/rootfs/web/src/components/NavBar.vue
index b45c835..c5f3317 100755
--- a/rootfs/web/src/components/NavBar.vue
+++ b/rootfs/web/src/components/NavBar.vue
@@ -1,23 +1,68 @@
-<!--导航栏-->
+<!-- Navigation bar -->
 <template>
-  <header class="bg-white border-b border-slate-200 sticky top-0 z-10 shadow-sm w-full">
+  <header class="bg-white border-b border-slate-200 sticky top-0 z-40 shadow-sm w-full">
     <div class="max-w-[1600px] w-full mx-auto flex items-center justify-between px-6 py-2.5 h-14">
       <!-- Logo -->
       <div class="flex items-center">
-          <a href="/" class="flex items-center gap-2.5 hover:opacity-80 transition-opacity">
+          <router-link to="/" class="flex items-center gap-2.5 hover:opacity-80 transition-opacity">
               <div class="flex items-center justify-center w-8 h-8 bg-primary rounded shadow-sm">
                   <img class="w-5 h-5 object-contain filter brightness-0 invert" src="../../src/assets/icons/drycc.svg" alt="Drycc" />
               </div>
               <span class="text-base font-semibold text-slate-800 tracking-tight">Drycc CaaS</span>
-          </a>
+          </router-link>
       </div>
       
-      <!-- 右侧导航与工具栏合并 -->
-      <div class="flex items-center gap-6 -mr-3">
+      <!-- Navigation and tools on the right -->
+      <div class="flex items-center gap-3">
           <div class="hidden lg:flex items-center text-sm font-medium text-slate-600">
               <nav-menu />
           </div>
-          <div class="flex items-center text-slate-500">
+          
+          <!-- Divider -->
+          <div class="w-px h-4 bg-slate-200 hidden lg:block mx-0.5"></div>
+
+          <div class="flex items-center gap-1.5 text-slate-500">
+              <!-- Notifications / Inbox -->
+              <div class="relative group">
+                  <button @click="$router.push('/messages')" class="relative flex items-center justify-center w-8 h-8 text-slate-500 hover:text-slate-700 bg-transparent hover:bg-slate-50 rounded-lg transition-colors focus:outline-none" type="button" aria-label="Notifications">
+                      <Inbox class="w-[18px] h-[18px] stroke-[2]" aria-hidden="true" />
+                      <!-- New message badge: positioned at the top-right corner -->
+                      <span v-if="unreadCount > 0" class="absolute top-[4px] right-[4px] flex h-2 w-2">
+                          <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
+                          <span class="relative inline-flex rounded-full h-[7px] w-[7px] bg-rose-500 ring-[1.5px] ring-white"></span>
+                      </span>
+                  </button>
+
+                  <!-- Dropdown message panel (shown on hover) -->
+                  <div class="absolute top-full right-0 mt-2 w-80 bg-white border border-slate-200 shadow-[0_10px_40px_-10px_rgba(0,0,0,0.1)] rounded-xl z-50 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top translate-y-2 group-hover:translate-y-0 cursor-default">
+                      <div class="px-4 py-3 border-b border-slate-100 flex items-center justify-between bg-slate-50/50 rounded-t-xl">
+                          <h3 class="text-xs font-semibold text-slate-800 uppercase tracking-wider">Inbox</h3>
+                          <button @click="handleMarkAllRead" class="text-[11px] text-primary hover:text-primary-700 font-medium transition-colors">Mark all read</button>
+                      </div>
+                      <div class="max-h-[320px] overflow-y-auto w-full">
+                          <div v-if="loading" class="px-4 py-6 text-center text-xs text-slate-400">Loading...</div>
+                          <div v-else-if="recentMessages.length === 0" class="px-4 py-6 text-center text-xs text-slate-400">No messages</div>
+                          <router-link
+                              v-for="msg in recentMessages"
+                              :key="msg.id"
+                              :to="`/messages/${msg.id}`"
+                              class="block px-4 py-3 hover:bg-slate-50 border-b border-slate-50 last:border-0 transition-colors relative group/item"
+                          >
+                              <span v-if="!msg.isRead" class="absolute left-2.5 top-4 w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_4px_rgba(16,185,129,0.5)]"></span>
+                              <div class="pl-3">
+                                  <p class="text-xs font-semibold mb-1 group-hover/item:text-primary transition-colors" :class="msg.isRead ? 'text-slate-600' : 'text-slate-800'">{{ msg.title }}</p>
+                                  <p class="text-[11px] text-slate-500 leading-snug line-clamp-2 mb-1.5">{{ msg.content }}</p>
+                                  <p class="text-[10px] text-slate-400 font-medium">{{ formatTime(msg.date) }}</p>
+                              </div>
+                          </router-link>
+                      </div>
+                      <div class="px-4 py-2 border-t border-slate-100 text-center rounded-b-xl bg-slate-50/50">
+                          <router-link to="/messages" class="text-[11px] text-slate-500 hover:text-slate-800 font-semibold transition-colors">View all activity</router-link>
+                      </div>
+                  </div>
+              </div>
+
+              <!-- Account -->
               <user-menu :user="user" />
           </div>
       </div>
@@ -25,24 +70,69 @@
   </header>
 </template>
 
-<script lang="ts">
+<script setup lang="ts">
+import { ref, computed, onMounted, watch } from 'vue'
+import { Inbox } from "lucide-vue-next";
 import NavMenu from "./NavMenu.vue";
 import UserMenu from "./UserMenu.vue";
+import { getMessages, dealMessages, markAllAsRead } from '../services/messages'
+import { globalState, invalidateMessages } from '../store'
+
+defineProps<{
+    user?: object
+}>()
+
+const recentMessages = ref<any[]>([])
+const loading = ref(false)
+
+const unreadCount = computed(() => recentMessages.value.filter(m => !m.isRead).length)
 
-export default {
-    name: "NavBar",
-    props: {
-        user: {
-            type: Object,
-            default: null
-        }
-    },
-    components: {
-        'nav-menu': NavMenu,
-        'user-menu': UserMenu
+const fetchRecentMessages = async () => {
+    loading.value = true
+    try {
+        const res = await getMessages('all', 5, 0)
+        const payload = dealMessages(res)
+        recentMessages.value = payload.results
+    } catch (e) {
+        console.error('Failed to fetch recent messages', e)
+    } finally {
+        loading.value = false
     }
 }
 
+const handleMarkAllRead = async () => {
+    try {
+        await markAllAsRead()
+        recentMessages.value.forEach(m => m.isRead = true)
+        invalidateMessages()
+    } catch (e) {
+        console.error('Failed to mark all as read', e)
+    }
+}
+
+onMounted(() => {
+    fetchRecentMessages()
+})
+
+// Refresh when other components signal that message read-state changed.
+watch(() => globalState.messagesVersion, () => {
+    fetchRecentMessages()
+})
+
+const formatTime = (dateStr: string) => {
+    if (!dateStr) return ''
+    const date = new Date(dateStr)
+    const now = new Date()
+    const diff = now.getTime() - date.getTime()
+    const minutes = Math.floor(diff / 60000)
+    const hours = Math.floor(diff / 3600000)
+    const days = Math.floor(diff / 86400000)
+    if (minutes < 1) return 'Just now'
+    if (minutes < 60) return `${minutes}m ago`
+    if (hours < 24) return `${hours}h ago`
+    if (days < 7) return `${days}d ago`
+    return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
+}
 </script>
 
 <style scoped>
diff --git a/rootfs/web/src/components/NavBox.vue b/rootfs/web/src/components/NavBox.vue
deleted file mode 100644
index ad08308..0000000
--- a/rootfs/web/src/components/NavBox.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<!--子导航插槽组件-->
-<template>
-    <div class="ui-nav-box">
-        <div class="ui-nav-box__inner">
-            <div class="ui-nav-box__cluster">
-                <nav class="ui-nav-box__trail">
-                    <slot name="nav-cluster"></slot>
-                </nav>
-            </div>
-            <slot name="create-new-app"></slot>
-        </div>
-        <div class="ui-nav-box__inner"></div>
-    </div>
-</template>
-
-<script lang="ts">
-export default {
-    name: "NavBox"
-}
-
-</script>
-
-<style scoped>
-.ui-nav-box {
-    padding: 8px 0;
-}
-
-.ui-nav-box__inner {
-    display: flex;
-    align-items: center;
-    flex-wrap: wrap;
-    gap: 8px;
-}
-
-.ui-nav-box__cluster {
-    flex: 1;
-}
-
-.ui-nav-box__trail {
-    display: flex;
-    align-items: center;
-    color: var(--ui-color-text);
-    font-size: var(--ui-font-size-2xl);
-    line-height: 1.4;
-}
-</style>
diff --git a/rootfs/web/src/components/NavMenu.vue b/rootfs/web/src/components/NavMenu.vue
index 12ae9d8..2442d6a 100644
--- a/rootfs/web/src/components/NavMenu.vue
+++ b/rootfs/web/src/components/NavMenu.vue
@@ -1,14 +1,14 @@
 <template>
-    <ul id="menu-navigator" class="flex flex-wrap items-center justify-center gap-6 list-none m-0 p-0 text-sm font-medium">
+    <ul id="menu-navigator" class="flex flex-wrap items-center justify-center gap-1 list-none m-0 p-0 text-sm font-medium">
         <li class="relative">
-            <a class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group" href="/">
+            <a class="flex items-center gap-1.5 px-2.5 py-1.5 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group" :href="globalState.dashboardUrl">
                 <LayoutGrid class="w-4 h-4 text-primary" aria-hidden="true" />
                 <span>Dashboard</span>
             </a>
         </li>
         <li class="relative hidden md:block">
-            <a class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group" href="https://www.drycc.cc/zh-cn/docs/roadmap/" target="_blank" rel="noopener noreferrer">
-                <Zap class="w-4 h-4 text-primary" aria-hidden="true" />
+            <a class="flex items-center gap-1.5 px-2.5 py-1.5 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group" href="https://www.drycc.cc/zh-cn/docs/roadmap/" target="_blank" rel="noopener noreferrer">
+                <Zap class="w-4 h-4 text-primary opacity-80" aria-hidden="true" />
                 <span>What's New</span>
             </a>
         </li>
@@ -20,7 +20,14 @@
                 :trigger-icon="resourcesTriggerIcon"
                 :items="resourceItems"
                 :menu-width="226"
-            />
+            >
+                <template #trigger>
+                    <button class="flex items-center gap-1.5 px-2.5 py-1.5 text-sm font-medium text-slate-700 bg-white border border-transparent rounded-lg hover:bg-slate-50 transition-all focus:outline-none group" type="button">
+                        <component class="w-4 h-4 text-primary opacity-80" aria-hidden="true" :is="resourcesTriggerIcon" />
+                        <span>Resources</span>
+                    </button>
+                </template>
+            </dropdown>
         </li>
     </ul>
 </template>
@@ -29,10 +36,14 @@
 import { markRaw } from "vue";
 import { MessageSquare, FileText, Bookmark, BookOpen, LayoutGrid, Zap } from "lucide-vue-next";
 import Dropdown from "./Dropdown.vue";
+import { globalState } from "../store";
 
 export default {
     name: "NavMenu",
     components: { LayoutGrid, Zap, Dropdown },
+    setup() {
+        return { globalState }
+    },
     data() {
         return {
             isMenuActived: false,
diff --git a/rootfs/web/src/components/UserMenu.vue b/rootfs/web/src/components/UserMenu.vue
index d825a00..95bfc36 100644
--- a/rootfs/web/src/components/UserMenu.vue
+++ b/rootfs/web/src/components/UserMenu.vue
@@ -1,4 +1,4 @@
-<!--用户菜单-->
+<!-- User menu -->
 <template>
     <dropdown
         v-if="currentUser && currentUser.username != null"
@@ -13,6 +13,11 @@
         :hide-label-on-mobile="true"
         @action="handleMenuAction"
     >
+        <template #trigger>
+            <button class="relative flex items-center justify-center w-8 h-8 rounded-lg bg-blue-100 text-blue-600 hover:bg-blue-200 transition-colors focus:outline-none" type="button" aria-label="Account profile">
+                <span class="text-sm font-bold">{{ userInitials }}</span>
+            </button>
+        </template>
     </dropdown>
 </template>
 
@@ -62,14 +67,17 @@ export default {
                 is_superuser: null
             },
         })
-        const logout = () => {
-            postLogout().then(res=>{
-                if (res.status == 200) {
+        const logout = async () => {
+            try {
+                const res = await postLogout()
+                if (res.status === 200) {
                     sessionStorage.clear()
-                    router.push({ path: '/'})
+                    await router.push({ path: '/' })
+                    location.reload()
                 }
-            })
-            location.reload()
+            } catch (error) {
+                console.error('Logout failed:', error)
+            }
         }
 
         const accountTriggerIcon = markRaw(User)
diff --git a/rootfs/web/src/lang/index.js b/rootfs/web/src/lang/index.js
index eb2bda9..0796d08 100644
--- a/rootfs/web/src/lang/index.js
+++ b/rootfs/web/src/lang/index.js
@@ -31,7 +31,7 @@ export const getLang = () => {
   return i18n && i18n.global.locale.value
 }
 
-// 获取UA语言类型
+// Get the UA language type
 export const getUAgentLang = () => {
   const UA = window.navigator.userAgent
   const regx = new RegExp(`LANG/(${ENUM_LANG.enUS}|${ENUM_LANG.zhHK}|${ENUM_LANG.zhCN})`, 'g')
diff --git a/rootfs/web/src/main.ts b/rootfs/web/src/main.ts
index be5d1f8..a4d7815 100644
--- a/rootfs/web/src/main.ts
+++ b/rootfs/web/src/main.ts
@@ -11,7 +11,7 @@ declare global {
 }
 
 /**
- * 时间对象的格式化;
+ * Date object formatting;
  */
 Date.prototype.format = function(format: string) {
     var o: Record<string, number> = {
@@ -36,9 +36,9 @@ Date.prototype.format = function(format: string) {
     return format;
 }
 
-// 初始化
+// Initialization
 function init () {
-    // 语言初始化
+    // Language initialization
     const lang = getUAgentLang()
     setLang(lang)
 }
diff --git a/rootfs/web/src/router/index.ts b/rootfs/web/src/router/index.ts
index a2a386a..2995668 100644
--- a/rootfs/web/src/router/index.ts
+++ b/rootfs/web/src/router/index.ts
@@ -6,7 +6,8 @@ import AccountSetting from "../views/AccountSetting.vue";
 const routes: Array<RouteRecordRaw> = [
     {
         path: '/',
-        redirect: '/access-tokens',
+        name: 'Overview',
+        component: () => import('../views/Overview.vue')
     },
     {
         path: '/access-tokens',
@@ -18,6 +19,22 @@ const routes: Array<RouteRecordRaw> = [
         name: 'AccountSetting',
         component: AccountSetting
     },
+    {
+        path: '/messages',
+        name: 'messages',
+        component: () => import('../views/MessageList.vue')
+    },
+    {
+        path: '/messages/preferences',
+        name: 'message-preferences',
+        component: () => import('../views/MessagePreferences.vue')
+    },
+    {
+        path: '/messages/:id',
+        name: 'message-detail',
+        component: () => import('../views/MessageDetail.vue'),
+        props: true
+    },
 ]
 
 const router = createRouter({
diff --git a/rootfs/web/src/services/messages.js b/rootfs/web/src/services/messages.js
new file mode 100644
index 0000000..4db0665
--- /dev/null
+++ b/rootfs/web/src/services/messages.js
@@ -0,0 +1,65 @@
+import axios from "../utils/axios";
+
+export function getMessages(category, limit = 30, offset = 0, isRead = undefined, search = undefined) {
+    const params = { limit, offset };
+    if (category && category !== 'all') {
+        params.category = category;
+    }
+    if (isRead !== undefined) {
+        params.is_read = isRead;
+    }
+    if (search) {
+        params.search = search;
+    }
+    return axios.get(`/user/messages/`, { params })
+}
+
+export function dealMessages(obj) {
+    return {
+        count: obj.data.count,
+        next: obj.data.next,
+        previous: obj.data.previous,
+        results: obj.data.results.map(item => {
+            return {
+                id: item.id,
+                category: item.category,
+                title: item.title,
+                content: item.content,
+                date: item.date,
+                isRead: item.is_read,
+                severity: item.severity
+            }
+        })
+    }
+}
+
+export function getMessageDetail(id) {
+    return axios.get(`/user/messages/` + id)
+}
+
+export function dealMessageDetail(obj) {
+    const item = obj.data;
+    return {
+        id: item.id,
+        category: item.category,
+        title: item.title,
+        fullContent: item.full_content || item.content,
+        date: item.date,
+        isRead: item.is_read,
+        severity: item.severity,
+        actionLink: item.action_link,
+        actionText: item.action_text
+    }
+}
+
+export function markAllAsRead() {
+    return axios.put(`/user/messages/mark-all-read/`)
+}
+
+export function getMessagePreferences() {
+    return axios.get(`/user/message-preferences/`)
+}
+
+export function updateMessagePreferences(data) {
+    return axios.put(`/user/message-preferences/`, data)
+}
diff --git a/rootfs/web/src/store.ts b/rootfs/web/src/store.ts
new file mode 100644
index 0000000..40e5905
--- /dev/null
+++ b/rootfs/web/src/store.ts
@@ -0,0 +1,31 @@
+import { reactive } from 'vue'
+import { getSettings } from './services/settings'
+
+export const globalState = reactive({
+    dashboardUrl: '/',
+    contactSupportUrl: 'https://community.drycc.cc/',
+    settingsLoaded: false,
+    // Bumped whenever the user's message read-state changes (e.g. mark-all-read,
+    // mark single message as read). Components that display messages should
+    // watch this value and refresh their local state.
+    messagesVersion: 0,
+})
+
+/** Notify all message-aware components to refresh. */
+export const invalidateMessages = () => {
+    globalState.messagesVersion += 1
+}
+
+export const fetchGlobalSettings = async () => {
+    if (globalState.settingsLoaded) return;
+    try {
+        const res = await getSettings()
+        if (res.data) {
+            if (res.data.dashboard_url) globalState.dashboardUrl = res.data.dashboard_url
+            if (res.data.contact_support_url) globalState.contactSupportUrl = res.data.contact_support_url
+        }
+        globalState.settingsLoaded = true
+    } catch (e) {
+        console.error("Failed to fetch settings", e)
+    }
+}
diff --git a/rootfs/web/src/views/AccessTokenList.vue b/rootfs/web/src/views/AccessTokenList.vue
index 5be966b..0d2d46d 100644
--- a/rootfs/web/src/views/AccessTokenList.vue
+++ b/rootfs/web/src/views/AccessTokenList.vue
@@ -1,9 +1,9 @@
-<!--app列表页-->
+<!-- App list page -->
 <template>
 <div class="min-h-screen bg-slate-50 flex flex-col font-sans" :id="'vue-content-'+Math.random().toString(36).substring(2)">
     <nav-bar />
     <main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
-        <main-nav :is-access-token-active="true"  />
+        <account-sidebar :is-access-token-active="true"  />
         <section class="flex-1 flex flex-col">
             <access-token-delete ref="accessTokenDelete" v-if="isShowDelete"
               :token="token"
@@ -110,9 +110,8 @@
 
 <script lang="ts">
 import NavBar from "../components/NavBar.vue";
-import NavBox from "../components/NavBox.vue";
 import {onBeforeMount, reactive, toRefs} from 'vue'
-import MainNav from "../components/MainNav.vue";
+import AccountSidebar from "../components/AccountSidebar.vue";
 import MainFooter from "../components/MainFooter.vue";
 import AccessTokenDelete from "../components/AccessTokenDelete.vue"
 import { Trash2 } from "lucide-vue-next"
@@ -123,8 +122,7 @@ export default {
     name: "AccessTokenList",
     components: {
         'nav-bar': NavBar,
-        'nav-box': NavBox,
-        'main-nav': MainNav,
+        'account-sidebar': AccountSidebar,
         'main-footer': MainFooter,
         'access-token-delete': AccessTokenDelete,
         Trash2
diff --git a/rootfs/web/src/views/AccountSetting.vue b/rootfs/web/src/views/AccountSetting.vue
index 1b02642..2c579f1 100644
--- a/rootfs/web/src/views/AccountSetting.vue
+++ b/rootfs/web/src/views/AccountSetting.vue
@@ -1,9 +1,9 @@
-<!--app列表页-->
+<!-- App list page -->
 <template>
 <div class="min-h-screen bg-slate-50 flex flex-col font-sans" :id="'vue-content-'+Math.random().toString(36).substring(2)">
     <nav-bar :user="user" />
     <main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
-        <main-nav :is-account-setting-active="true"  />
+        <account-sidebar :is-account-setting-active="true"  />
         <section class="flex-1 flex flex-col">
         <div class="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden flex-1 flex flex-col min-h-[600px]">
             <!-- Header -->
@@ -87,8 +87,33 @@
                                         </form>
                                     </div>
                                 </div>
-                                <div class="flex items-center justify-between mt-6">
-                                    <p class="text-[13px] text-slate-500">Manage your avatar using <a href="https://gravatar.com" target="_blank" rel="noopener noreferrer" class="text-primary hover:text-primary-600 hover:underline">Gravatar</a>.</p>
+
+                                <!-- Email Confirmation Notice -->
+                                <transition
+                                    enter-active-class="transition duration-300 ease-out"
+                                    enter-from-class="transform -translate-y-2 opacity-0"
+                                    enter-to-class="transform translate-y-0 opacity-100"
+                                    leave-active-class="transition duration-200 ease-in"
+                                    leave-from-class="transform translate-y-0 opacity-100"
+                                    leave-to-class="transform -translate-y-2 opacity-0"
+                                >
+                                    <div v-if="isProfileChanged" class="flex items-start gap-3 p-3.5 bg-primary-50/50 border border-primary-100/60 rounded-lg">
+                                        <div class="mt-0.5 flex-shrink-0 bg-white p-1.5 rounded-md shadow-sm border border-primary-100/50">
+                                            <svg class="w-4 h-4 text-primary-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                                                <rect width="20" height="16" x="2" y="4" rx="2"></rect>
+                                                <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
+                                            </svg>
+                                        </div>
+                                        <div>
+                                            <h4 class="text-[13px] font-medium text-primary-900">Email Confirmation Required</h4>
+                                            <p class="text-[12.5px] text-primary-700/80 mt-0.5 leading-relaxed">
+                                                To secure your account, a verification link will be sent to your email upon updating.
+                                            </p>
+                                        </div>
+                                    </div>
+                                </transition>
+
+                                <div class="flex items-center justify-end mt-6">
                                     <button class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" :disabled="!isProfileChanged" @click="submitProfile()" type="button">
                                         Update Profile
                                     </button>
@@ -302,9 +327,8 @@
 <style scoped></style>
 <script lang="ts">
 import NavBar from "../components/NavBar.vue";
-import NavBox from "../components/NavBox.vue";
 import {onMounted, reactive, toRefs, computed} from 'vue'
-import MainNav from "../components/MainNav.vue";
+import AccountSidebar from "../components/AccountSidebar.vue";
 import MainFooter from "../components/MainFooter.vue";
 import { useRouter } from 'vue-router'
 import { dealUser, getUser, putAccount, putAccountPassword } from "../services/user";
@@ -315,8 +339,7 @@ export default {
     name: "AccountSetting",
     components: {
         'nav-bar': NavBar,
-        'nav-box': NavBox,
-        'main-nav': MainNav,
+        'account-sidebar': AccountSidebar,
         'main-footer': MainFooter,
     },
     setup() {
diff --git a/rootfs/web/src/views/MessageDetail.vue b/rootfs/web/src/views/MessageDetail.vue
new file mode 100644
index 0000000..88d51e5
--- /dev/null
+++ b/rootfs/web/src/views/MessageDetail.vue
@@ -0,0 +1,164 @@
+<script setup lang="ts">
+import { ref, onMounted, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import NavBar from '../components/NavBar.vue'
+import MessageSidebar from '../components/MessageSidebar.vue'
+import { ArrowLeft, Bell, ShieldAlert, Cpu, Activity, Info, AlertTriangle, AlertCircle, Clock, ChevronRight } from 'lucide-vue-next'
+import MessageRightSidebar from '../components/MessageRightSidebar.vue'
+import { getMessageDetail, dealMessageDetail } from '../services/messages'
+
+const route = useRoute()
+const router = useRouter()
+const messageId = route.params.id as string
+
+const message = ref<any>(null)
+const loading = ref(false)
+
+onMounted(async () => {
+    loading.value = true
+    try {
+        const res = await getMessageDetail(messageId)
+        message.value = dealMessageDetail(res)
+    } catch (e) {
+        console.error('Failed to fetch message detail', e)
+        message.value = {
+            id: messageId,
+            category: 'system',
+            title: 'Message Details',
+            fullContent: 'Failed to load message content.',
+            date: '',
+            severity: 'info'
+        }
+    } finally {
+        loading.value = false
+    }
+})
+
+// Custom parser: no external prose or brittle global CSS
+const parsedContent = computed(() => {
+    if (!message.value) return '';
+    let text = message.value.fullContent;
+    
+    // Parse basic HTML tag markup
+    text = text.replace(/<b>(.*?)<\/b>/g, '<strong class="font-semibold text-slate-800">$1</strong>');
+    text = text.replace(/<code>(.*?)<\/code>/g, '<code class="bg-rose-50 text-rose-600 px-1.5 py-0.5 rounded border border-rose-100/60 text-[0.85em] font-mono leading-none">$1</code>');
+    
+    const lines = text.split('\n');
+    let html = '';
+    let inList = false;
+    
+    lines.forEach((line) => {
+        const trimmed = line.trim();
+        if (trimmed.startsWith('•') || trimmed.startsWith('-')) {
+            if (!inList) {
+                // Start unordered list with proper vertical spacing
+                html += '<ul class="my-5 space-y-3.5">';
+                inList = true;
+            }
+            const liText = trimmed.substring(1).trim();
+            // Customize bullet styling and alignment
+            html += `<li class="flex items-start"><span class="mr-3 mt-2 inline-flex w-1.5 h-1.5 rounded-full bg-slate-300 flex-shrink-0"></span><span class="flex-1 leading-relaxed text-slate-600">${liText}</span></li>`;
+        } else {
+            if (inList) {
+                html += '</ul>';
+                inList = false;
+            }
+            if (trimmed === '') {
+                // Convert blank line to vertical spacing
+                html += '<div class="h-6"></div>'; 
+            } else {
+                // Regular paragraph
+                html += `<div class="mb-1 leading-relaxed text-slate-600">${trimmed}</div>`;
+            }
+        }
+    });
+    
+    if (inList) html += '</ul>';
+    return html;
+})
+
+const getCategoryIndicator = (category: string, severity: string) => {
+    const map: Record<string, any> = {
+        'system': { icon: Info, bg: 'bg-blue-50', text: 'text-blue-600', ring: 'ring-blue-50/50', badgeBg: 'bg-blue-50', badgeBorder: 'border-blue-100/60' },
+        'security': { icon: ShieldAlert, bg: 'bg-indigo-50', text: 'text-indigo-600', ring: 'ring-indigo-50/50', badgeBg: 'bg-indigo-50', badgeBorder: 'border-indigo-100/60' },
+        'alert': { icon: AlertTriangle, bg: 'bg-rose-50', text: 'text-rose-600', ring: 'ring-rose-50/50', badgeBg: 'bg-rose-50', badgeBorder: 'border-rose-100/60' },
+        'product': { icon: Cpu, bg: 'bg-emerald-50', text: 'text-emerald-600', ring: 'ring-emerald-50/50', badgeBg: 'bg-emerald-50', badgeBorder: 'border-emerald-100/60' },
+        'service': { icon: Activity, bg: 'bg-purple-50', text: 'text-purple-600', ring: 'ring-purple-50/50', badgeBg: 'bg-purple-50', badgeBorder: 'border-purple-100/60' }
+    }
+    if (severity === 'error') return { icon: AlertCircle, bg: 'bg-rose-50', text: 'text-rose-600', ring: 'ring-rose-50/50', badgeBg: 'bg-rose-50', badgeBorder: 'border-rose-100/60' }
+    if (severity === 'warning') return { icon: ShieldAlert, bg: 'bg-orange-50', text: 'text-orange-600', ring: 'ring-orange-50/50', badgeBg: 'bg-orange-50', badgeBorder: 'border-orange-100/60' } 
+    return map[category] || { icon: Bell, bg: 'bg-slate-100', text: 'text-slate-600', ring: 'ring-slate-50', badgeBg: 'bg-slate-50', badgeBorder: 'border-slate-200' }
+}
+</script>
+
+<template>
+<div class="min-h-screen bg-slate-50 flex flex-col font-sans">
+    <NavBar />
+    <main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
+        <MessageSidebar />
+  <!-- Main content area -->
+  <section class="flex-1 flex flex-col w-full min-w-0">
+      <div v-if="message" class="bg-white rounded-lg shadow-sm border border-slate-200 min-h-[600px] flex flex-col flex-1 overflow-hidden">
+          
+          <!-- Navbar: minimal mode -->
+          <div class="px-6 py-4 flex items-center border-b border-slate-100 bg-slate-50/40">
+              <button @click="router.push('/messages')" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 transition-colors focus:outline-none">
+                  <ArrowLeft class="w-4 h-4 mr-2 text-slate-400" />
+                  Back to Inbox
+              </button>
+          </div>
+
+          <!-- Header section: strong visual hierarchy -->
+          <div class="px-8 md:px-12 pt-10 pb-8 border-b border-slate-100/80">
+              <div class="flex items-start gap-4 md:gap-5">
+                  <!-- Category icon container -->
+                  <div class="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 ring-4 mt-0.5" 
+                       :class="[getCategoryIndicator(message.category, message.severity).bg, getCategoryIndicator(message.category, message.severity).ring]">
+                      <component :is="getCategoryIndicator(message.category, message.severity).icon" class="w-6 h-6" 
+                                 :class="getCategoryIndicator(message.category, message.severity).text" />
+                  </div>
+                  
+                  <!-- Text details area -->
+                  <div class="flex-1 w-full min-w-0">
+                      <h1 class="text-xl md:text-[22px] font-bold text-slate-900 tracking-tight leading-tight mb-3">
+                          {{ message.title }}
+                      </h1>
+                      
+                      <div class="flex flex-wrap items-center gap-3 text-[11px] font-medium text-slate-500">
+                          <span class="inline-flex items-center px-2 py-0.5 rounded border uppercase tracking-wider font-semibold"
+                                :class="[getCategoryIndicator(message.category, message.severity).badgeBg, getCategoryIndicator(message.category, message.severity).badgeBorder, getCategoryIndicator(message.category, message.severity).text]">
+                              {{ message.category }}
+                          </span>
+                          <span class="flex items-center gap-1.5 opacity-80">
+                              <Clock class="w-3 h-3" />
+                              {{ message.date }}
+                          </span>
+                      </div>
+                  </div>
+              </div>
+          </div>
+
+          <!-- Content body -->
+          <div class="px-8 md:px-12 py-10 flex-1">
+              <div class="max-w-3xl">
+                  <!-- Message body (HTML rendered after parsing) -->
+                  <div class="text-[15px]" v-html="parsedContent"></div>
+
+                  <!-- Action button (flows naturally after body) -->
+                  <div v-if="message.actionLink" class="mt-12">
+                      <button class="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-primary hover:bg-primary-600 text-white text-sm font-semibold rounded-lg shadow-sm transition-all focus:outline-none">
+                          {{ message.actionText }}
+                          <ChevronRight class="w-4 h-4 opacity-80" />
+                      </button>
+                  </div>
+              </div>
+          </div>
+          
+      </div>
+  </section>
+
+  <!-- Right sidebar -->
+  <MessageRightSidebar />
+    </main>
+</div>
+</template>
diff --git a/rootfs/web/src/views/MessageList.vue b/rootfs/web/src/views/MessageList.vue
new file mode 100644
index 0000000..4848c6c
--- /dev/null
+++ b/rootfs/web/src/views/MessageList.vue
@@ -0,0 +1,252 @@
+<script setup lang="ts">
+import { ref, computed, onMounted, watch } from 'vue'
+import NavBar from '../components/NavBar.vue'
+import MessageSidebar from '../components/MessageSidebar.vue'
+import { useRoute } from 'vue-router'
+import { Bell, ShieldAlert, Cpu, Activity, Info, AlertTriangle, ChevronRight, Search, MailOpen, X } from 'lucide-vue-next'
+import MessageRightSidebar from '../components/MessageRightSidebar.vue'
+import { getMessages, dealMessages, markAllAsRead as apiMarkAllAsRead } from '../services/messages'
+import { globalState, invalidateMessages } from '../store'
+
+const route = useRoute()
+
+// Define message categories
+const tabs = [
+  { id: 'all', label: 'All Messages' },
+  { id: 'unread', label: 'Unread', icon: MailOpen },
+  { id: 'system', label: 'System', icon: Info },
+  { id: 'product', label: 'Product Updates', icon: Cpu },
+  { id: 'security', label: 'Security', icon: ShieldAlert },
+  { id: 'alert', label: 'Alerts', icon: AlertTriangle },
+  { id: 'service', label: 'Service', icon: Activity }
+]
+
+const messages = ref<any[]>([])
+const loading = ref(false)
+const totalCount = ref(0)
+const limit = ref(30)
+const offset = ref(0)
+const searchQuery = ref('')
+
+const currentCategory = computed(() => {
+    return (route.query.category as string) || 'all'
+})
+
+const filteredMessages = computed(() => messages.value)
+
+const hasPrevious = computed(() => offset.value > 0)
+const hasNext = computed(() => offset.value + messages.value.length < totalCount.value)
+
+const fetchMessages = async () => {
+    loading.value = true
+    try {
+        const category = currentCategory.value === 'all' || currentCategory.value === 'unread'
+            ? undefined
+            : currentCategory.value
+        const isRead = currentCategory.value === 'unread' ? false : undefined
+        const res = await getMessages(category, limit.value, offset.value, isRead, searchQuery.value || undefined)
+        const payload = dealMessages(res)
+        messages.value = payload.results
+        totalCount.value = payload.count
+    } catch (e) {
+        console.error('Failed to fetch messages', e)
+    } finally {
+        loading.value = false
+    }
+}
+
+watch(() => route.query.category, () => {
+    offset.value = 0
+    searchQuery.value = ''
+    fetchMessages()
+})
+
+const handleSearch = () => {
+    offset.value = 0
+    fetchMessages()
+}
+
+const clearSearch = () => {
+    searchQuery.value = ''
+    offset.value = 0
+    fetchMessages()
+}
+
+onMounted(() => {
+    fetchMessages()
+})
+
+const handleMarkAllAsRead = async () => {
+    try {
+        await apiMarkAllAsRead()
+        messages.value.forEach(m => m.isRead = true)
+        invalidateMessages()
+    } catch (e) {
+        console.error('Failed to mark all as read', e)
+    }
+}
+
+// Refresh when other components signal that message read-state changed.
+watch(() => globalState.messagesVersion, () => {
+    fetchMessages()
+})
+
+const goPreviousPage = async () => {
+    if (!hasPrevious.value) return
+    offset.value = Math.max(0, offset.value - limit.value)
+    await fetchMessages()
+}
+
+const goNextPage = async () => {
+    if (!hasNext.value) return
+    offset.value += limit.value
+    await fetchMessages()
+}
+
+// Helper: get category icon and color
+const getCategoryIndicator = (category: string, severity: string) => {
+    const map: Record<string, any> = {
+        'system': { icon: Info, bg: 'bg-blue-50', text: 'text-blue-500' },
+        'security': { icon: ShieldAlert, bg: 'bg-indigo-50', text: 'text-indigo-500' },
+        'alert': { icon: AlertTriangle, bg: 'bg-rose-50', text: 'text-rose-500' },
+        'product': { icon: Cpu, bg: 'bg-emerald-50', text: 'text-emerald-500' },
+        'service': { icon: Activity, bg: 'bg-purple-50', text: 'text-purple-500' }
+    }
+    
+    // Use warning color directly for severe errors or warnings
+    if (severity === 'error') {
+        return { icon: AlertTriangle, bg: 'bg-rose-50', text: 'text-rose-500' }
+    }
+    if (severity === 'warning') {
+        return { icon: ShieldAlert, bg: 'bg-amber-50', text: 'text-amber-500' }
+    }
+    
+    return map[category] || { icon: Bell, bg: 'bg-slate-100', text: 'text-slate-500' }
+}
+
+const getCategoryName = (category: string) => {
+    const found = tabs.find(t => t.id === category)
+    return found ? found.label : category
+}
+</script>
+
+<template>
+<div class="min-h-screen bg-slate-50 flex flex-col font-sans">
+    <NavBar />
+    <main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
+        <MessageSidebar />
+  <!-- Main content: message list -->
+  <section class="flex-1 flex flex-col w-full min-w-0">
+      <div class="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden flex flex-col flex-1 h-full min-h-[600px]">
+          
+          <!-- Header: title and description -->
+          <div class="px-8 pt-8 pb-6 border-b border-slate-100 flex flex-col md:flex-row md:items-end justify-between gap-4 bg-white">
+              <div>
+                  <h1 class="text-2xl font-bold text-slate-800 tracking-tight flex items-center gap-3 mb-1">
+                      Inbox
+                      <span v-if="messages.filter(m => !m.isRead).length > 0" class="px-2 py-0.5 bg-primary/10 text-primary text-xs font-bold rounded-full border border-primary/20">
+                          {{ messages.filter(m => !m.isRead).length }} New
+                      </span>
+                      <span v-if="loading" class="text-xs text-slate-400 font-normal">Loading...</span>
+                  </h1>
+                  <p class="text-[13px] text-slate-500">Manage all your notifications and system alerts.</p>
+              </div>
+              
+              <!-- Right side: search input -->
+              <div class="relative w-full md:w-64">
+                  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
+                      <Search class="h-4 w-4 text-slate-400" />
+                  </div>
+                  <input 
+                      v-model="searchQuery"
+                      type="text" 
+                      class="block w-full pl-9 pr-8 py-1.5 border border-slate-200 rounded-lg leading-5 bg-slate-50 hover:bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary sm:text-sm transition-all shadow-sm"
+                      placeholder="Search messages..."
+                      @keyup.enter="handleSearch"
+                  />
+                  <button v-if="searchQuery" @click="clearSearch" class="absolute inset-y-0 right-0 pr-2.5 flex items-center text-slate-400 hover:text-slate-600">
+                      <X class="h-3.5 w-3.5" />
+                  </button>
+              </div>
+          </div>
+
+          <!-- Message list -->
+          <div class="flex-1 flex flex-col overflow-y-auto">
+              <div v-if="filteredMessages.length === 0" class="flex-1 flex flex-col items-center justify-center p-12 text-center text-slate-500">
+                  <div class="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4 border border-slate-100">
+                      <Bell class="w-6 h-6 text-slate-400 opacity-50" />
+                  </div>
+                  <p class="text-base font-medium text-slate-700">No messages found</p>
+                  <p class="text-sm mt-1">You're all caught up in this category.</p>
+              </div>
+
+              <!-- List content -->
+                <div v-else class="divide-y divide-slate-100/60">
+                    <router-link 
+                        v-for="msg in filteredMessages" 
+                        :key="msg.id" 
+                        :to="`/messages/${msg.id}`"
+                        class="group block transition-colors relative overflow-hidden bg-white hover:bg-slate-50/70 p-6"
+                    >
+                          <div class="flex gap-4 md:gap-5">
+                              <!-- Category icon container -->
+                              <div class="flex-shrink-0 self-start w-11 h-11 rounded-full flex items-center justify-center border-2 border-white shadow-[0_2px_6px_rgba(0,0,0,0.04)]" :class="getCategoryIndicator(msg.category, msg.severity).bg">
+                                <component :is="getCategoryIndicator(msg.category, msg.severity).icon" class="w-5 h-5" :class="getCategoryIndicator(msg.category, msg.severity).text" />
+                            </div>
+
+                            <!-- Main content area -->
+                            <div class="flex-1 min-w-0 pr-8">
+                                <div class="flex flex-col md:flex-row md:items-center gap-1.5 md:gap-3 mb-1.5">
+                                    <h3 class="text-[15px] truncate transition-colors tracking-tight" :class="msg.isRead ? 'text-slate-500 font-medium' : 'text-slate-900 font-bold'">
+                                        {{ msg.title }}
+                                    </h3>
+                                    <span class="hidden md:inline-block px-2 py-0.5 rounded-md bg-slate-100 text-slate-500 text-[10px] font-semibold uppercase tracking-widest border border-slate-200/50">
+                                        {{ getCategoryName(msg.category) }}
+                                    </span>
+                                </div>
+                                <p class="text-sm leading-relaxed line-clamp-2 md:pr-10 transition-colors" :class="msg.isRead ? 'text-slate-400 font-normal' : 'text-slate-500 font-medium'">
+                                    {{ msg.content }}
+                                </p>
+                                <div class="mt-3 flex items-center gap-3 text-[11px] font-medium transition-colors" :class="msg.isRead ? 'text-slate-400/80' : 'text-slate-400'">
+                                      <span>{{ msg.date }}</span>
+                                      <div class="w-1 h-1 rounded-full bg-slate-300 md:hidden"></div>
+                                      <span class="md:hidden text-primary">{{ getCategoryName(msg.category) }}</span>
+                                  </div>
+                              </div>
+
+                              <!-- Detail arrow (shown on hover) -->
+                              <div class="absolute right-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-200">
+                                  <ChevronRight class="w-5 h-5 text-slate-400" />
+                              </div>
+                          </div>
+                      </router-link>
+              </div>
+          </div>
+
+          <!-- Bottom action bar -->
+          <div class="p-4 border-t border-slate-100 bg-slate-50/50 flex justify-between items-center text-xs text-slate-500 mt-auto">
+              <div class="flex items-center gap-4">
+                  <span>Showing {{ filteredMessages.length }} of {{ totalCount }} messages</span>
+                  <button class="disabled:text-slate-300" :disabled="!hasPrevious" @click="goPreviousPage">Previous</button>
+                  <button class="disabled:text-slate-300" :disabled="!hasNext" @click="goNextPage">Next</button>
+              </div>
+              <router-link to="/messages/preferences" class="font-medium text-primary hover:text-primary-700 transition-colors">Message Preferences</router-link>
+          </div>
+      </div>
+  </section>
+
+  <!-- Right sidebar -->
+  <MessageRightSidebar @mark-all-read="fetchMessages" />
+    </main>
+</div>
+</template>
+
+<style scoped>
+.hide-scrollbar::-webkit-scrollbar {
+  display: none;
+}
+.hide-scrollbar {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+</style>
diff --git a/rootfs/web/src/views/MessagePreferences.vue b/rootfs/web/src/views/MessagePreferences.vue
new file mode 100644
index 0000000..a5b905a
--- /dev/null
+++ b/rootfs/web/src/views/MessagePreferences.vue
@@ -0,0 +1,310 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import NavBar from '../components/NavBar.vue'
+import MessageSidebar from '../components/MessageSidebar.vue'
+import { ArrowLeft, BellRing, Mail, ShieldCheck, Check, Webhook, Link, Info, Cpu, AlertTriangle, Activity } from 'lucide-vue-next'
+import MessageRightSidebar from '../components/MessageRightSidebar.vue'
+import { getMessagePreferences, updateMessagePreferences } from '../services/messages'
+
+const router = useRouter()
+
+const settings = ref({
+    emailAlerts: true,
+    pushAlerts: false,
+    webhookUrl: '',
+
+    notifySecurity: true,
+    notifySystem: true,
+    notifyProduct: true,
+    notifyAlerts: true,
+    notifyService: true,
+})
+
+const isSaving = ref(false)
+const saved = ref(false)
+const loading = ref(false)
+
+const fetchPreferences = async () => {
+    loading.value = true
+    try {
+        const res = await getMessagePreferences()
+        const data = res.data
+        settings.value = {
+            emailAlerts: data.email_alerts,
+            pushAlerts: data.push_alerts,
+            webhookUrl: data.webhook_url || '',
+            notifySecurity: data.notify_security,
+            notifySystem: data.notify_system,
+            notifyProduct: data.notify_product,
+            notifyAlerts: data.notify_alert,
+            notifyService: data.notify_service,
+        }
+    } catch (e) {
+        console.error('Failed to fetch preferences', e)
+    } finally {
+        loading.value = false
+    }
+}
+
+onMounted(() => {
+    fetchPreferences()
+})
+
+const savePreferences = async () => {
+    isSaving.value = true
+    saved.value = false
+    try {
+        await updateMessagePreferences({
+            email_alerts: settings.value.emailAlerts,
+            push_alerts: settings.value.pushAlerts,
+            webhook_url: settings.value.webhookUrl,
+            notify_security: settings.value.notifySecurity,
+            notify_system: settings.value.notifySystem,
+            notify_product: settings.value.notifyProduct,
+            notify_alert: settings.value.notifyAlerts,
+            notify_service: settings.value.notifyService,
+        })
+        saved.value = true
+        setTimeout(() => saved.value = false, 3000)
+    } catch (e) {
+        console.error('Failed to save preferences', e)
+        alert('Failed to save preferences')
+    } finally {
+        isSaving.value = false
+    }
+}
+</script>
+
+<template>
+<div class="min-h-screen bg-slate-50 flex flex-col font-sans">
+    <NavBar />
+    <main class="max-w-[1600px] w-full mx-auto p-6 flex flex-col lg:flex-row gap-6 flex-1">
+        <MessageSidebar />
+  <!-- Main content -->
+  <section class="flex-1 flex flex-col w-full min-w-0">
+      <div class="bg-white rounded-lg shadow-sm border border-slate-200 min-h-[600px] flex flex-col flex-1 overflow-hidden">
+          
+          <!-- Navigation bar -->
+          <div class="px-6 py-4 flex items-center justify-between border-b border-slate-100 bg-slate-50/40">
+              <button @click="router.push('/messages')" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-800 transition-colors focus:outline-none">
+                  <ArrowLeft class="w-4 h-4 mr-2 text-slate-400" />
+                  Back to Inbox
+              </button>
+          </div>
+
+          <!-- Header section -->
+          <div class="px-8 md:px-12 pt-10 pb-8">
+              <div class="flex items-start gap-4 md:gap-5 mb-2">
+                  <div class="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 bg-primary/10 ring-4 ring-primary/5 mt-0.5">
+                      <BellRing class="w-6 h-6 text-primary" />
+                  </div>
+                  <div>
+                      <h1 class="text-xl md:text-[22px] font-bold text-slate-900 tracking-tight leading-tight mb-2">
+                          Notification Preferences
+                      </h1>
+                      <p class="text-sm text-slate-500 leading-relaxed max-w-2xl">
+                          Control how and when you want to be notified about activity in your workspace.
+                      </p>
+                  </div>
+              </div>
+          </div>
+
+          <!-- Settings section -->
+          <div class="px-8 md:px-12 pb-12 flex-1">
+              
+              <!-- Delivery channels -->
+              <h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider mb-4 border-b border-slate-100 pb-2">Delivery Channels</h3>
+              
+              <div class="space-y-6 mb-10">
+                  <!-- Email notifications -->
+                  <div class="flex items-center justify-between">
+                      <div class="flex items-center gap-3">
+                          <div class="w-10 h-10 rounded-lg bg-slate-50 border border-slate-100 flex items-center justify-center text-slate-500">
+                              <Mail class="w-5 h-5" />
+                          </div>
+                          <div>
+                              <p class="text-[15px] font-semibold text-slate-800">Email Notifications</p>
+                              <p class="text-xs text-slate-500 mt-0.5">Receive alerts via your primary account email.</p>
+                          </div>
+                      </div>
+                      <label class="relative inline-flex items-center cursor-pointer">
+                          <input type="checkbox" class="sr-only peer" v-model="settings.emailAlerts" />
+                          <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
+                      </label>
+                  </div>
+
+                  <!-- Webhook / push notifications -->
+                  <div class="flex flex-col">
+                      <div class="flex items-center justify-between">
+                          <div class="flex items-center gap-3">
+                              <div class="w-10 h-10 rounded-lg bg-slate-50 border border-slate-100 flex items-center justify-center text-slate-500">
+                                  <Webhook class="w-5 h-5" />
+                              </div>
+                              <div>
+                                  <p class="text-[15px] font-semibold text-slate-800">Webhook / Push Notifications</p>
+                                  <p class="text-xs text-slate-500 mt-0.5">Forward real-time alerts to an external HTTP entrypoint.</p>
+                              </div>
+                          </div>
+                          <label class="relative inline-flex items-center cursor-pointer">
+                              <input type="checkbox" class="sr-only peer" v-model="settings.pushAlerts" />
+                              <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
+                          </label>
+                      </div>
+                      
+                      <!-- Webhook URL Configure -->
+                      <transition
+                          enter-active-class="transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
+                          enter-from-class="opacity-0 max-h-0 -translate-y-2 pointer-events-none"
+                          enter-to-class="opacity-100 max-h-[140px] translate-y-0 pointer-events-auto"
+                          leave-active-class="transition-all duration-200 ease-[cubic-bezier(0.4,0,0.2,1)]"
+                          leave-from-class="opacity-100 max-h-[140px] translate-y-0 pointer-events-auto"
+                          leave-to-class="opacity-0 max-h-0 -translate-y-2 pointer-events-none"
+                      >
+                          <div v-show="settings.pushAlerts" class="mt-4 pl-[52px]">
+                              <div class="flex flex-col gap-1.5">
+                                  <label class="text-[13px] font-semibold text-slate-700">Payload URL</label>
+                                  <div class="relative group/input">
+                                      <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
+                                          <Link class="h-4 w-4 text-slate-400 group-focus-within/input:text-primary transition-colors" />
+                                      </div>
+                                      <input 
+                                          type="url" 
+                                          v-model="settings.webhookUrl"
+                                          placeholder="https://yourapp.example.com/webhook"
+                                          class="block w-full max-w-lg pl-[36px] pr-3 py-2 border border-slate-200 rounded-lg text-sm text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-[3px] focus:ring-primary/10 focus:border-primary transition-all bg-white shadow-sm"
+                                      />
+                                  </div>
+                                  <p class="text-[12px] text-slate-500 mt-0.5">
+                                      We will send a POST request with a JSON payload to this endpoint.
+                                  </p>
+                              </div>
+                          </div>
+                      </transition>
+                  </div>
+              </div>
+
+              <!-- Notification type settings -->
+              <h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider mb-4 border-b border-slate-100 pb-2">Notification Types</h3>
+              
+              <div class="space-y-6 mb-8">
+                  <!-- Security Alerts -->
+                  <div class="flex items-center justify-between">
+                      <div class="flex items-center gap-3">
+                          <div class="w-10 h-10 rounded-lg bg-indigo-50 border border-indigo-100 flex items-center justify-center text-indigo-500">
+                              <ShieldCheck class="w-5 h-5" />
+                          </div>
+                          <div>
+                              <p class="text-[15px] font-semibold text-slate-800">Security Alerts</p>
+                              <p class="text-xs text-slate-500 mt-0.5">Login attempts, password resets, and critical account events.</p>
+                          </div>
+                      </div>
+                      <label class="relative inline-flex items-center cursor-pointer">
+                          <input type="checkbox" class="sr-only peer" v-model="settings.notifySecurity" />
+                          <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
+                      </label>
+                  </div>
+
+                  <!-- System Messages -->
+                  <div class="flex items-center justify-between">
+                      <div class="flex items-center gap-3">
+                          <div class="w-10 h-10 rounded-lg bg-blue-50 border border-blue-100 flex items-center justify-center text-blue-500">
+                              <Info class="w-5 h-5" />
+                          </div>
+                          <div>
+                              <p class="text-[15px] font-semibold text-slate-800">System Messages</p>
+                              <p class="text-xs text-slate-500 mt-0.5">Workspace updates, billing, and general system settings.</p>
+                          </div>
+                      </div>
+                      <label class="relative inline-flex items-center cursor-pointer">
+                          <input type="checkbox" class="sr-only peer" v-model="settings.notifySystem" />
+                          <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
+                      </label>
+                  </div>
+
+                  <!-- Product Updates -->
+                  <div class="flex items-center justify-between">
+                      <div class="flex items-center gap-3">
+                          <div class="w-10 h-10 rounded-lg bg-emerald-50 border border-emerald-100 flex items-center justify-center text-emerald-600">
+                              <Cpu class="w-5 h-5" />
+                          </div>
+                          <div>
+                              <p class="text-[15px] font-semibold text-slate-800">Product Updates</p>
+                              <p class="text-xs text-slate-500 mt-0.5">New features, changelogs, and major platform announcements.</p>
+                          </div>
+                      </div>
+                      <label class="relative inline-flex items-center cursor-pointer">
+                          <input type="checkbox" class="sr-only peer" v-model="settings.notifyProduct" />
+                          <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
+                      </label>
+                  </div>
+
+                  <!-- Alerts -->
+                  <div class="flex items-center justify-between">
+                      <div class="flex items-center gap-3">
+                          <div class="w-10 h-10 rounded-lg bg-amber-50 border border-amber-100 flex items-center justify-center text-amber-500">
+                              <AlertTriangle class="w-5 h-5" />
+                          </div>
+                          <div>
+                              <p class="text-[15px] font-semibold text-slate-800">Alerts & Warnings</p>
+                              <p class="text-xs text-slate-500 mt-0.5">Usage thresholds, quota limits, and critical warnings.</p>
+                          </div>
+                      </div>
+                      <label class="relative inline-flex items-center cursor-pointer">
+                          <input type="checkbox" class="sr-only peer" v-model="settings.notifyAlerts" />
+                          <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
+                      </label>
+                  </div>
+
+                  <!-- Service Status -->
+                  <div class="flex items-center justify-between">
+                      <div class="flex items-center gap-3">
+                          <div class="w-10 h-10 rounded-lg bg-rose-50 border border-rose-100 flex items-center justify-center text-rose-500">
+                              <Activity class="w-5 h-5" />
+                          </div>
+                          <div>
+                              <p class="text-[15px] font-semibold text-slate-800">Service Status</p>
+                              <p class="text-xs text-slate-500 mt-0.5">Application crashes, deployment failures, and infrastructure changes.</p>
+                          </div>
+                      </div>
+                      <label class="relative inline-flex items-center cursor-pointer">
+                          <input type="checkbox" class="sr-only peer" v-model="settings.notifyService" />
+                          <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
+                      </label>
+                  </div>
+              </div>
+
+              <!-- Save and submit section -->
+              <div class="mt-12 pt-6 border-t border-slate-100 flex items-center gap-4">
+                  <button 
+                      @click="savePreferences" 
+                      :disabled="isSaving"
+                      class="inline-flex flex-shrink-0 items-center justify-center w-36 py-2.5 bg-primary hover:bg-primary-600 disabled:opacity-70 disabled:cursor-not-allowed text-white text-sm font-semibold rounded-lg shadow-sm transition-all focus:outline-none"
+                  >
+                      <span v-if="isSaving" class="flex items-center gap-2">
+                        <svg class="animate-spin -ml-1 mr-1 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                          <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                          <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                        </svg>
+                        Saving...
+                      </span>
+                      <span v-else>Save Changes</span>
+                  </button>
+
+                  <transition enter-active-class="transition duration-200 ease-out" enter-from-class="opacity-0 translate-y-1" enter-to-class="opacity-100 translate-y-0" leave-active-class="transition duration-150 ease-in" leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 translate-y-1">
+                      <div v-if="saved" class="flex items-center gap-1.5 text-sm font-medium text-emerald-600 bg-emerald-50 px-3 py-1.5 rounded-md">
+                          <Check class="w-4 h-4" />
+                          Preferences saved!
+                      </div>
+                  </transition>
+              </div>
+
+          </div>
+      </div>
+  </section>
+
+  <!-- Right sidebar -->
+  <MessageRightSidebar />
+    </main>
+</div>
+</template>
diff --git a/rootfs/web/src/views/Overview.vue b/rootfs/web/src/views/Overview.vue
new file mode 100644
index 0000000..058bf24
--- /dev/null
+++ b/rootfs/web/src/views/Overview.vue
@@ -0,0 +1,98 @@
+<script setup lang="ts">
+import { onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import NavBar from '../components/NavBar.vue'
+import MainFooter from '../components/MainFooter.vue'
+import { Settings, Key, Bell, ArrowLeft, ShieldCheck } from 'lucide-vue-next'
+import { globalState, fetchGlobalSettings } from '../store'
+
+const router = useRouter()
+
+onMounted(async () => {
+    await fetchGlobalSettings()
+})
+</script>
+
+<template>
+<div class="min-h-screen bg-slate-50 flex flex-col font-sans selection:bg-primary/10">
+    <NavBar />
+    
+    <main class="max-w-[1200px] w-full mx-auto p-6 md:p-10 flex-1 flex flex-col transition-all duration-300">
+        <!-- Hero Section -->
+        <div class="flex flex-col items-center text-center mt-10 md:mt-16 mb-16 animate-fade-in-up">
+            <div class="w-20 h-20 bg-gradient-to-tr from-primary to-primary-600 rounded-2xl shadow-lg shadow-primary/20 flex items-center justify-center mb-8 relative">
+                <div class="absolute inset-0 bg-white/20 rounded-2xl blur-sm opacity-50"></div>
+                <ShieldCheck class="w-10 h-10 text-white relative z-10" stroke-width="1.5" />
+            </div>
+            <h1 class="text-4xl md:text-5xl font-extrabold text-slate-800 tracking-tight mb-4">
+                Identity & Security
+            </h1>
+            <p class="text-lg text-slate-500 max-w-2xl leading-relaxed">
+                Manage your personal identity, security preferences, API access tokens, and stay updated with crucial system notifications.
+            </p>
+        </div>
+
+        <!-- Modules Grid -->
+        <div class="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto w-full mb-12">
+            
+            <!-- Cards -->
+            <div @click="router.push('/account-setting')" class="group relative bg-white rounded-2xl shadow-sm border border-slate-200 p-8 hover:shadow-xl hover:-translate-y-1.5 hover:border-primary/30 transition-all duration-300 cursor-pointer overflow-hidden">
+                <div class="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-blue-50 to-transparent rounded-bl-full -mr-8 -mt-8 opacity-50 group-hover:opacity-100 transition-opacity"></div>
+                <div class="w-14 h-14 bg-blue-50/80 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300 relative z-10 text-blue-500 shadow-sm border border-blue-100/50">
+                    <Settings class="w-7 h-7" stroke-width="1.5" />
+                </div>
+                <h2 class="text-xl font-bold text-slate-800 mb-3 relative z-10 group-hover:text-blue-600 transition-colors">Account Setting</h2>
+                <p class="text-sm text-slate-500 leading-relaxed relative z-10">Update your basic profile, modify login credentials, and review linked identity providers.</p>
+            </div>
+
+            <div @click="router.push('/access-tokens')" class="group relative bg-white rounded-2xl shadow-sm border border-slate-200 p-8 hover:shadow-xl hover:-translate-y-1.5 hover:border-primary/30 transition-all duration-300 cursor-pointer overflow-hidden">
+                <div class="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-indigo-50 to-transparent rounded-bl-full -mr-8 -mt-8 opacity-50 group-hover:opacity-100 transition-opacity"></div>
+                <div class="w-14 h-14 bg-indigo-50/80 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300 relative z-10 text-indigo-500 shadow-sm border border-indigo-100/50">
+                    <Key class="w-7 h-7" stroke-width="1.5" />
+                </div>
+                <h2 class="text-xl font-bold text-slate-800 mb-3 relative z-10 group-hover:text-indigo-600 transition-colors">Access Tokens</h2>
+                <p class="text-sm text-slate-500 leading-relaxed relative z-10">Securely generate and manage API keys for your automated workflows and CLI authentications.</p>
+            </div>
+
+            <div @click="router.push('/messages')" class="group relative bg-white rounded-2xl shadow-sm border border-slate-200 p-8 hover:shadow-xl hover:-translate-y-1.5 hover:border-primary/30 transition-all duration-300 cursor-pointer overflow-hidden">
+                <div class="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-rose-50 to-transparent rounded-bl-full -mr-8 -mt-8 opacity-50 group-hover:opacity-100 transition-opacity"></div>
+                <div class="w-14 h-14 bg-rose-50/80 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300 relative z-10 text-rose-500 shadow-sm border border-rose-100/50">
+                    <Bell class="w-7 h-7" stroke-width="1.5" />
+                    <!-- Unread marker -->
+                    <span class="absolute top-[14px] right-[14px] w-2.5 h-2.5 bg-rose-500 border-2 border-rose-50 rounded-full animate-pulse group-hover:bg-rose-400"></span>
+                </div>
+                <h2 class="text-xl font-bold text-slate-800 mb-3 relative z-10 group-hover:text-rose-600 transition-colors">Message Center</h2>
+                <p class="text-sm text-slate-500 leading-relaxed relative z-10">Read important system alerts, review security notifications, and track platform updates.</p>
+            </div>
+        </div>
+
+        <!-- Back to Dashboard Link -->
+        <div class="mt-auto flex justify-center pb-8 w-full">
+            <a :href="globalState.dashboardUrl" class="group flex items-center gap-2.5 text-slate-500 hover:text-primary font-semibold transition-all border border-slate-200 bg-white px-8 py-3.5 rounded-full hover:shadow-md hover:border-primary/30">
+                <ArrowLeft class="w-[18px] h-[18px] group-hover:-translate-x-1.5 transition-transform" /> 
+                Enter Workspace Dashboard
+            </a>
+        </div>
+
+    </main>
+
+    <MainFooter class="mt-auto" />
+</div>
+</template>
+
+<style scoped>
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translate3d(0, 20px, 0);
+  }
+  to {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+}
+
+.animate-fade-in-up {
+  animation: fadeInUp 0.6s ease-out forwards;
+}
+</style>
diff --git a/rootfs/web/vite.config.ts b/rootfs/web/vite.config.ts
index 5b55b35..8b2a5de 100644
--- a/rootfs/web/vite.config.ts
+++ b/rootfs/web/vite.config.ts
@@ -13,7 +13,6 @@ export default defineConfig({
   },
   server: {
     proxy: {
-      "/avatar": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
       "/orgs": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
       "/user": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },
       "/auth": { target: VUE_APP_BASE_URL, changeOrigin: false, xfwd: true },

From 2107f08e8bbead8125506d7497700bc3db00678b Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Sat, 9 May 2026 15:53:51 +0800
Subject: [PATCH 30/30] feat(oauth): use oauth to unify service-to-service
 authentication.

---
 .gitignore                                    |   1 +
 .../templates/passport-job-upgrade.yaml       |   2 -
 .../templates/passport-secret-creds.yaml      |   3 +-
 charts/passport/values.yaml                   |  24 ++-
 rootfs/api/admin.py                           |  17 ++-
 .../commands/create_oauth2_application.py     |  89 ++++++++---
 ...ation_allowed_scopes_and_internal_grant.py |  35 +++++
 rootfs/api/models.py                          |  16 +-
 rootfs/api/oauth2_validators.py               |   7 +
 rootfs/api/permissions.py                     |  32 ++++
 rootfs/api/serializers.py                     |  18 +++
 rootfs/api/settings/production.py             |   8 +
 .../tests/test_create_oauth2_application.py   | 139 ++++++++++++++++++
 rootfs/api/tests/test_views.py                |  44 +++++-
 rootfs/api/urls.py                            |   1 +
 rootfs/api/views/api.py                       |  36 +++--
 rootfs/requirements.txt                       |   2 +-
 rootfs/web/package-lock.json                  |  14 +-
 rootfs/web/package.json                       |   2 +-
 rootfs/web/src/views/MessageDetail.vue        |   4 +-
 20 files changed, 432 insertions(+), 62 deletions(-)
 create mode 100644 rootfs/api/migrations/0005_application_allowed_scopes_and_internal_grant.py
 create mode 100644 rootfs/api/permissions.py
 create mode 100644 rootfs/api/tests/test_create_oauth2_application.py

diff --git a/.gitignore b/.gitignore
index 124efaf..035dd09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@ htmlcov/
 venv/
 
 .idea
+.sisyphus
 env/
 rootfs/web/yarn-error.log
 rootfs/node_modules/
diff --git a/charts/passport/templates/passport-job-upgrade.yaml b/charts/passport/templates/passport-job-upgrade.yaml
index ec06d3b..b15a2e0 100644
--- a/charts/passport/templates/passport-job-upgrade.yaml
+++ b/charts/passport/templates/passport-job-upgrade.yaml
@@ -4,8 +4,6 @@ metadata:
   name: drycc-passport-job-upgrade
   annotations:
     component.drycc.cc/version: {{ .Values.imageTag }}
-    helm.sh/hook: post-install,post-upgrade,post-rollback
-    helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
 spec:
   template:
     spec:
diff --git a/charts/passport/templates/passport-secret-creds.yaml b/charts/passport/templates/passport-secret-creds.yaml
index 1304712..a3b0618 100644
--- a/charts/passport/templates/passport-secret-creds.yaml
+++ b/charts/passport/templates/passport-secret-creds.yaml
@@ -17,9 +17,8 @@ data:
   django-secret-key: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" "django-secret-key" "defaultValue" (randAlphaNum 64) "context" $)) }}
   oidc-rsa-private-key: {{genPrivateKey "rsa" | b64enc}}
   {{- range $item := .Values.initApplications }}
-  {{- if ($item.prefix) }}
   {{- $name := ($item.name | replace " " "-" | lower) }}
   drycc-passport-{{$name}}-key: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" (printf "drycc-passport-%s-key" $name) "defaultValue" ($item.key | default (randAlphaNum 40)) "context" $)) }}
   drycc-passport-{{$name}}-secret: {{ (include "common.secrets.lookup" (dict "secret" "passport-creds" "key" (printf "drycc-passport-%s-secret" $name) "defaultValue" ($item.secret | default (randAlphaNum 64)) "context" $)) }}
-  {{- end }}
+  drycc-passport-{{$name}}-scopes: {{ ($item.allowed_scopes | default "") | b64enc | quote }}
   {{- end }}
diff --git a/charts/passport/values.yaml b/charts/passport/values.yaml
index f5e7464..0da7257 100644
--- a/charts/passport/values.yaml
+++ b/charts/passport/values.yaml
@@ -118,14 +118,34 @@ initApplications:
   key: ""
   secret: ""
   prefix: "drycc"
-  grant_type: "password"
+  grant_type: "internal"
+  client_type: "confidential"
+  allowed_scopes: "openid profile email manager:workspace manager:usage passport:message"
   redirect_uri: "/v2/complete/drycc/"
 - name: "grafana"
   key: ""
   secret: ""
   prefix: "drycc-grafana"
-  grant_type: "authorization-code"
+  grant_type: "internal"
+  client_type: "confidential"
+  allowed_scopes: "openid profile email controller:alerts controller:metrics controller:logs"
   redirect_uri: "/oauth2/callback"
+- name: "builder"
+  key: ""
+  secret: ""
+  prefix: ""
+  grant_type: "client-credentials"
+  client_type: "confidential"
+  allowed_scopes: "controller:hook"
+  redirect_uri: ""
+- name: "manager"
+  key: ""
+  secret: ""
+  prefix: ""
+  grant_type: "client-credentials"
+  client_type: "confidential"
+  allowed_scopes: "controller:blocklist"
+  redirect_uri: ""
 
 # Service
 service:
diff --git a/rootfs/api/admin.py b/rootfs/api/admin.py
index bdba29a..e6328f0 100644
--- a/rootfs/api/admin.py
+++ b/rootfs/api/admin.py
@@ -1,7 +1,22 @@
 from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
+from oauth2_provider.admin import ApplicationAdmin as BaseApplicationAdmin
 
-from .models import User, Message, MessagePreference
+from .models import User, Message, MessagePreference, Application
+
+
+try:
+    admin.site.unregister(Application)
+except admin.sites.NotRegistered:
+    pass
+
+
+@admin.register(Application)
+class ApplicationAdmin(BaseApplicationAdmin):
+    list_display = (
+        "id", "name", "user", "client_type",
+        "authorization_grant_type", "allowed_scopes",
+    )
 
 
 class UserAdmin(BaseUserAdmin):
diff --git a/rootfs/api/management/commands/create_oauth2_application.py b/rootfs/api/management/commands/create_oauth2_application.py
index 14f59d8..bd01a6c 100644
--- a/rootfs/api/management/commands/create_oauth2_application.py
+++ b/rootfs/api/management/commands/create_oauth2_application.py
@@ -4,6 +4,7 @@
 import string
 import pathlib
 from django.contrib.auth import get_user_model
+from django.contrib.auth.hashers import check_password
 from django.core.management.base import BaseCommand
 from oauth2_provider.models import get_application_model
 
@@ -14,7 +15,26 @@
 
 
 class Command(BaseCommand):
-    """Management command for create Oauth2 application"""
+    """Management command for create Oauth2 application.
+
+    Credential resolution order for ``client_id`` / ``client_secret``:
+
+    1. Explicit value from the init-applications JSON file (highest priority,
+       lets operators pin credentials via ``--set initApplications[...]``).
+    2. Mounted Kubernetes secret file at
+       ``/var/run/secrets/drycc/passport/drycc-passport-<name>-<key|secret>``.
+       This is the source of truth for credentials that the chart's
+       ``passport-creds`` Secret already exposes to every consumer (controller,
+       grafana, builder, manager, ...). Reading it here keeps the DB and the
+       Secret consistent for *every* application, including m2m apps that
+       have no public sub-domain (``prefix == ""``).
+    3. Newly generated random string (only when neither of the above is
+       available, e.g. local dev without the volume mount).
+
+    Note: ``prefix`` only controls how ``redirect_uri`` is composed. It is
+    intentionally NOT used to gate credential discovery -- m2m applications
+    legitimately have an empty prefix.
+    """
 
     def add_arguments(self, parser):
         super(Command, self).add_arguments(parser)
@@ -28,32 +48,55 @@ def handle(self, *args, **options):
         user = User.objects.filter(is_superuser=True).first()
         for item in json.loads(pathlib.Path(base_path).read_text()):
             name = item["name"]
-            _, updated = Application.objects.update_or_create(
-                name=name.lower(),
-                defaults={
-                    'client_id': self._get_creds(item, "key", 40),
-                    'client_secret': self._get_creds(item, "secret", 60),
-                    'user': user,
-                    'redirect_uris': self._get_redirect_uri(item),
-                    'authorization_grant_type': item['grant_type'],
-                    'client_type': 'public',
-                    'algorithm': 'RS256'
-                }
+            client_id = self._get_creds(item, "key", 40)
+            client_secret = self._get_creds(item, "secret", 60)
+            defaults = {
+                'client_id': client_id,
+                'user': user,
+                'redirect_uris': self._get_redirect_uri(item),
+                'authorization_grant_type': item.get('grant_type', 'public'),
+                'client_type': item.get('client_type', 'public'),
+                'allowed_scopes': item.get('allowed_scopes', ''),
+                'algorithm': 'RS256',
+            }
+            existing = Application.objects.filter(name=name.lower()).first()
+            secret_unchanged = (
+                existing is not None
+                and check_password(client_secret, existing.client_secret)
+            )
+            if not secret_unchanged:
+                defaults['client_secret'] = client_secret
+            _, created = Application.objects.update_or_create(
+                name=name.lower(), defaults=defaults,
             )
-            if updated:
-                self.stdout.write('Drycc % app created' % name)
+            if created:
+                self.stdout.write('Drycc %s app created' % name)
             else:
-                self.stdout.write('Drycc % app updated' % name)
+                self.stdout.write('Drycc %s app updated' % name)
 
     def _get_creds(self, item, suffix, size):
-        name, secret, prefix = item["name"], item[suffix], item["prefix"]
-        if not secret:
-            default_secret_path = os.path.join(
-                secrets_path, "drycc-passport-%s-%s" % (name, suffix))
-            if prefix and os.path.exists(default_secret_path):
-                secret = pathlib.Path(default_secret_path).read_text()
-            else:
-                secret = ''.join([random.choice(string.ascii_letters) for _ in range(size)])
+        name = item["name"]
+        secret = item.get(suffix)
+        if secret:
+            self.stdout.write(
+                '[%s/%s] credential source: init-config' % (name, suffix))
+            return secret
+
+        default_secret_path = os.path.join(
+            secrets_path, "drycc-passport-%s-%s" % (name, suffix))
+        if os.path.exists(default_secret_path):
+            # ``.strip()`` defends against trailing newlines that some
+            # tooling adds when materialising secrets onto disk.
+            secret = pathlib.Path(default_secret_path).read_text().strip()
+            self.stdout.write(
+                '[%s/%s] credential source: mounted-file (%s)'
+                % (name, suffix, default_secret_path))
+            return secret
+
+        secret = ''.join(random.choice(string.ascii_letters) for _ in range(size))
+        self.stdout.write(
+            '[%s/%s] credential source: generated-random '
+            '(no init value, no mounted file)' % (name, suffix))
         return secret
 
     def _get_redirect_uri(self, item):
diff --git a/rootfs/api/migrations/0005_application_allowed_scopes_and_internal_grant.py b/rootfs/api/migrations/0005_application_allowed_scopes_and_internal_grant.py
new file mode 100644
index 0000000..a5be209
--- /dev/null
+++ b/rootfs/api/migrations/0005_application_allowed_scopes_and_internal_grant.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.2.13 on 2026-05-09 12:00
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+def forward_data_migration(apps, schema_editor):
+    Application = apps.get_model('api', 'Application')
+    oauth2_settings = getattr(settings, 'OAUTH2_PROVIDER', {})
+    default_provider_scopes_list = oauth2_settings.get("DEFAULT_SCOPES", ['openid', 'email', 'profile'])
+    default_provider_scopes = " ".join(default_provider_scopes_list)
+
+    for app in Application.objects.all():
+        if not app.allowed_scopes:
+            app.allowed_scopes = default_provider_scopes
+        
+        if app.name.lower() in ("controller", "grafana"):
+            app.authorization_grant_type = "internal"
+            
+        app.save()
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0004_alter_application_authorization_grant_type_message_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='application',
+            name='allowed_scopes',
+            field=models.TextField(blank=True, default=''),
+        ),
+        migrations.RunPython(forward_data_migration, reverse_code=migrations.RunPython.noop),
+    ]
\ No newline at end of file
diff --git a/rootfs/api/models.py b/rootfs/api/models.py
index 9adb0c7..567844b 100644
--- a/rootfs/api/models.py
+++ b/rootfs/api/models.py
@@ -11,11 +11,21 @@ class User(AbstractUser):
 
 
 class Application(AbstractApplication):
+    GRANT_INTERNAL = "internal"
+    GRANT_TYPES = AbstractApplication.GRANT_TYPES + (
+        (GRANT_INTERNAL, _("Internal")),
+    )
+    allowed_scopes = models.TextField(blank=True, default="")
 
     def allows_grant_type(self, *grant_types):
-        return self.GRANT_AUTHORIZATION_CODE in grant_types or super().allows_grant_type(
-            *grant_types
-        )
+        if self.authorization_grant_type == self.GRANT_INTERNAL:
+            return True
+        return super().allows_grant_type(*grant_types)
+
+    def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
+        if self.authorization_grant_type == self.GRANT_INTERNAL:
+            return True
+        return super().validate_grant_type(client_id, grant_type, client, request, *args, **kwargs)
 
 
 class Message(models.Model):
diff --git a/rootfs/api/oauth2_validators.py b/rootfs/api/oauth2_validators.py
index b91e976..d968276 100644
--- a/rootfs/api/oauth2_validators.py
+++ b/rootfs/api/oauth2_validators.py
@@ -3,6 +3,13 @@
 
 class CustomOAuth2Validator(OAuth2Validator):
 
+    def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
+        if client.allowed_scopes:
+            allowed = set(client.allowed_scopes.split())
+            if not set(scopes).issubset(allowed):
+                return False
+        return super().validate_scopes(client_id, scopes, client, request, *args, **kwargs)
+
     oidc_claim_scope = OAuth2Validator.oidc_claim_scope
     oidc_claim_scope.update({
         "id": "profile",
diff --git a/rootfs/api/permissions.py b/rootfs/api/permissions.py
new file mode 100644
index 0000000..cb8ad2e
--- /dev/null
+++ b/rootfs/api/permissions.py
@@ -0,0 +1,32 @@
+from django.utils import timezone
+from rest_framework import permissions
+
+from oauth2_provider.models import AccessToken
+
+
+class HasOAuthScope(permissions.BasePermission):
+    """
+    Object-level permission to allow only requests with specific OAuth scopes.
+    The required scopes are defined on the view as `required_oauth_scopes = ['scope1', 'scope2']`
+    """
+    def has_permission(self, request, view):
+        required_oauth_scopes = getattr(view, 'required_oauth_scopes', [])
+        if not required_oauth_scopes:
+            return True
+
+        auth_header = request.META.get('HTTP_AUTHORIZATION', '')
+        parts = auth_header.split()
+        if len(parts) == 2 and parts[0].lower() == 'bearer':
+            token_string = parts[1]
+        else:
+            return False
+
+        scopes = self.get_token_scopes(token_string)
+        return set(required_oauth_scopes).issubset(scopes)
+
+    def get_token_scopes(self, token_string):
+        try:
+            access_token = AccessToken.objects.get(token=token_string, expires__gt=timezone.now())
+            return set(access_token.scope.split())
+        except AccessToken.DoesNotExist:
+            return set()
diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py
index 956e8d6..4ba083f 100644
--- a/rootfs/api/serializers.py
+++ b/rootfs/api/serializers.py
@@ -98,3 +98,21 @@ class Meta:
         fields = ['email_alerts', 'push_alerts', 'webhook_url',
                   'notify_security', 'notify_system', 'notify_product',
                   'notify_alert', 'notify_service']
+
+
+class ServiceMessageSerializer(MessageSerializer):
+    username = serializers.CharField(write_only=True)
+
+    class Meta(MessageSerializer.Meta):
+        fields = MessageSerializer.Meta.fields + ['username']
+
+    def create(self, validated_data):
+        from django.contrib.auth import get_user_model
+        User = get_user_model()
+        username = validated_data.pop('username')
+        try:
+            user = User.objects.get(username=username)
+        except User.DoesNotExist:
+            raise serializers.ValidationError({"username": "User not found."})
+        validated_data['user'] = user
+        return super().create(validated_data)
diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py
index 181efdb..40418ec 100644
--- a/rootfs/api/settings/production.py
+++ b/rootfs/api/settings/production.py
@@ -329,6 +329,14 @@
         "email": "Email",
         "profile": "Profile",
         "openid": "OpenID Connect scope",
+        "controller:hook": "Controller Hook",
+        "controller:blocklist": "Controller Blocklist",
+        "controller:logs": "Controller Logs",
+        "controller:alerts": "Controller Alerts",
+        "controller:metrics": "Controller Metrics",
+        "manager:workspace": "Manager Workspace",
+        "manager:usage": "Manager Usage",
+        "passport:message": "Passport Message",
     },
     "DEFAULT_SCOPES": ['openid', 'email', 'profile'],
     "DEFAULT_CODE_CHALLENGE_METHOD": 'S256',
diff --git a/rootfs/api/tests/test_create_oauth2_application.py b/rootfs/api/tests/test_create_oauth2_application.py
new file mode 100644
index 0000000..cead11f
--- /dev/null
+++ b/rootfs/api/tests/test_create_oauth2_application.py
@@ -0,0 +1,139 @@
+import json
+import os
+import tempfile
+from io import StringIO
+from unittest import mock
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.hashers import check_password
+from django.core.management import call_command
+from django.test import TestCase
+from oauth2_provider.models import get_application_model
+
+from api.management.commands import create_oauth2_application as cmd_module
+
+User = get_user_model()
+Application = get_application_model()
+
+
+class CreateOauth2ApplicationTestCase(TestCase):
+    def setUp(self):
+        self.admin = User.objects.create_superuser(
+            username='create-oauth2-admin',
+            email='create-oauth2-admin@example.com',
+            password='password123',
+        )
+        self.tmpdir = tempfile.mkdtemp()
+        self.secrets_dir = tempfile.mkdtemp()
+        self._secrets_patch = mock.patch.object(
+            cmd_module, 'secrets_path', self.secrets_dir,
+        )
+        self._secrets_patch.start()
+        self.addCleanup(self._secrets_patch.stop)
+
+    def _write_init_file(self, items):
+        path = os.path.join(self.tmpdir, 'init-applications.json')
+        with open(path, 'w') as f:
+            json.dump(items, f)
+        return path
+
+    def _write_secret_file(self, name, suffix, value):
+        path = os.path.join(
+            self.secrets_dir, 'drycc-passport-%s-%s' % (name, suffix))
+        with open(path, 'w') as f:
+            f.write(value)
+        return path
+
+    def _run(self, items):
+        path = self._write_init_file(items)
+        out = StringIO()
+        call_command('create_oauth2_application', '--path', path, stdout=out)
+        return out.getvalue()
+
+    def _m2m_app(self, **overrides):
+        item = {
+            'name': 'builder',
+            'key': '',
+            'secret': '',
+            'prefix': '',
+            'grant_type': 'client-credentials',
+            'client_type': 'confidential',
+            'allowed_scopes': 'controller:hook',
+            'redirect_uri': '',
+        }
+        item.update(overrides)
+        return item
+
+    def test_m2m_reads_credentials_from_mounted_secret_files(self):
+        self._write_secret_file('builder', 'key', 'mounted-key-value')
+        self._write_secret_file('builder', 'secret', 'mounted-secret-value\n')
+
+        self._run([self._m2m_app()])
+
+        app = Application.objects.get(name='builder')
+        self.assertEqual(app.client_id, 'mounted-key-value')
+        self.assertTrue(check_password('mounted-secret-value', app.client_secret))
+
+    def test_subdomain_app_reads_credentials_from_mounted_secret_files(self):
+        self._write_secret_file('grafana', 'key', 'gf-key')
+        self._write_secret_file('grafana', 'secret', 'gf-secret')
+
+        self._run([{
+            'name': 'grafana',
+            'key': '',
+            'secret': '',
+            'prefix': 'drycc-grafana',
+            'grant_type': 'internal',
+            'client_type': 'confidential',
+            'allowed_scopes': 'openid profile email',
+            'redirect_uri': '/oauth2/callback',
+        }])
+
+        app = Application.objects.get(name='grafana')
+        self.assertEqual(app.client_id, 'gf-key')
+        self.assertTrue(check_password('gf-secret', app.client_secret))
+
+    def test_falls_back_to_random_when_no_mount_and_no_init_value(self):
+        self._run([self._m2m_app()])
+
+        app = Application.objects.get(name='builder')
+        self.assertEqual(len(app.client_id), 40)
+        self.assertNotEqual(app.client_id, '')
+        self.assertNotEqual(app.client_secret, '')
+
+    def test_init_config_value_takes_precedence_over_mount(self):
+        self._write_secret_file('builder', 'key', 'mount-key')
+        self._write_secret_file('builder', 'secret', 'mount-secret')
+
+        self._run([self._m2m_app(key='cfg-key', secret='cfg-secret')])
+
+        app = Application.objects.get(name='builder')
+        self.assertEqual(app.client_id, 'cfg-key')
+        self.assertTrue(check_password('cfg-secret', app.client_secret))
+
+    def test_idempotent_when_mount_unchanged(self):
+        self._write_secret_file('builder', 'key', 'stable-key')
+        self._write_secret_file('builder', 'secret', 'stable-secret')
+
+        self._run([self._m2m_app()])
+        first = Application.objects.get(name='builder')
+        first_id = first.client_id
+        first_hashed_secret = first.client_secret
+
+        self._run([self._m2m_app()])
+        second = Application.objects.get(name='builder')
+
+        self.assertEqual(second.client_id, first_id)
+        self.assertEqual(second.client_secret, first_hashed_secret)
+        self.assertTrue(check_password('stable-secret', second.client_secret))
+        self.assertEqual(Application.objects.filter(name='builder').count(), 1)
+
+    def test_logs_credential_source(self):
+        self._write_secret_file('builder', 'key', 'k')
+        self._write_secret_file('builder', 'secret', 's')
+
+        out = self._run([self._m2m_app()])
+
+        self.assertIn('[builder/key] credential source: mounted-file', out)
+        self.assertIn('[builder/secret] credential source: mounted-file', out)
+        self.assertIn('Drycc builder app created', out)
diff --git a/rootfs/api/tests/test_views.py b/rootfs/api/tests/test_views.py
index bd65486..52017a4 100644
--- a/rootfs/api/tests/test_views.py
+++ b/rootfs/api/tests/test_views.py
@@ -492,8 +492,40 @@ def test_list_messages_supports_limit_offset_pagination(self):
         self.assertEqual(len(response.data['results']), 1)
         self.assertEqual(response.data['count'], 2)
 
-    def test_create_message_for_current_user(self):
-        response = self.client.post(reverse('user_messages-list'), {
+    def test_service_message_create(self):
+        from api.models import Application
+        from urllib.parse import urlencode
+
+        # Setup application
+        Application.objects.create(
+            name='test_app',
+            client_type='confidential',
+            client_id='my_test_client_id',
+            client_secret='my_test_client_secret',
+            authorization_grant_type='client-credentials',
+            user=self.user
+        )
+
+        # 1. Fetch access token via client-credentials flow
+        data = urlencode({
+            'grant_type': 'client_credentials',
+            'client_id': 'my_test_client_id',
+            'client_secret': 'my_test_client_secret',
+            'scope': 'passport:message'
+        })
+
+        token_response = self.client.post(
+            '/oauth/token/',
+            data=data,
+            content_type='application/x-www-form-urlencoded'
+        )
+
+        self.assertEqual(token_response.status_code, 200, token_response.json())
+        access_token = token_response.json()['access_token']
+
+        # 2. Use the access token to create a message
+        response = self.client.post('/messages/', {
+            'username': self.user.username,
             'category': 'alert',
             'title': 'Created from API',
             'content': 'API created content',
@@ -502,12 +534,10 @@ def test_create_message_for_current_user(self):
             'is_read': False,
             'action_link': '/messages',
             'action_text': 'Open',
-        })
+        }, HTTP_AUTHORIZATION=f'Bearer {access_token}')
 
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        created = Message.objects.get(id=response.data['id'])
-        self.assertEqual(created.user, self.user)
-        self.assertEqual(created.category, 'alert')
+        self.assertEqual(response.status_code, 201)
+        self.assertTrue(Message.objects.filter(user=self.user, title='Created from API').exists())
 
     def test_mark_all_messages_as_read(self):
         response = self.client.put(reverse('user_messages-mark-all-read'))
diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py
index b2fbd58..5d34651 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -15,6 +15,7 @@ def __init__(self, *args, **kwargs):
 
 router = OptionalSlashRouter()
 router.register(r'user/messages', api.UserMessageViewSet, basename='user_messages')
+router.register(r'messages', api.ServiceMessageViewSet, basename='service_messages')
 router.register(r'user/identities', api.UserIdentityViewSet, basename='user_identities')
 
 urlpatterns = [
diff --git a/rootfs/api/views/api.py b/rootfs/api/views/api.py
index 055dba0..477e397 100644
--- a/rootfs/api/views/api.py
+++ b/rootfs/api/views/api.py
@@ -2,7 +2,7 @@
 from django.core.cache import cache
 from django.contrib import auth
 from django.contrib.auth import get_user_model
-from django.db import IntegrityError
+from django.db import IntegrityError, connection, Error
 from django.db.models import Q
 from django.shortcuts import get_object_or_404
 from django.template.loader import render_to_string
@@ -24,6 +24,8 @@
 from oauth2_provider.models import AccessToken
 from social_django.models import UserSocialAuth
 
+from api.permissions import HasOAuthScope
+
 from api import serializers
 from api.exceptions import ServiceUnavailable, DryccException
 from api.models import Message, MessagePreference
@@ -38,18 +40,23 @@ class NormalUserViewSet(viewsets.ModelViewSet):
 
 
 class ReadinessCheckView(View):
-    """Simple readiness check view to determine DB connection / query."""
+    """Simple readiness check view to determine DB connection and Migrations."""
+    migrations_completed = False
 
     def get(self, request):
         try:
-            import django.db
-            with django.db.connection.cursor() as c:
+            with connection.cursor() as c:
                 c.execute("SELECT 0")
-        except django.db.Error as e:
-            raise ServiceUnavailable("Database health check failed") from e
-
+            if not ReadinessCheckView.migrations_completed:
+                from django.db.migrations.executor import MigrationExecutor
+                executor = MigrationExecutor(connection)
+                targets = executor.loader.graph.leaf_nodes()
+                if executor.migration_plan(targets):
+                    raise ServiceUnavailable("Migrations are not yet applied")
+                ReadinessCheckView.migrations_completed = True
+        except Error as e:
+            raise ServiceUnavailable(f"Database health check failed: {e}") from e
         return HttpResponse("OK")
-
     head = get
 
 
@@ -122,6 +129,7 @@ def get(self, request, *args, **kwargs):
 
 
 class UserMessageViewSet(NormalUserViewSet):
+    http_method_names = ['get', 'put', 'delete', 'head', 'options']
     serializer_class = serializers.MessageSerializer
 
     def get_queryset(self):
@@ -139,9 +147,6 @@ def get_queryset(self):
             )
         return queryset.order_by('-created_at')
 
-    def perform_create(self, serializer):
-        serializer.save(user=self.request.user)
-
     def retrieve(self, request, *args, **kwargs):
         message = self.get_object()
         if not message.is_read:
@@ -310,3 +315,12 @@ def update(self, request, *args, **kwargs):
         serializer.is_valid(raise_exception=True)
         serializer.save()
         return Response(serializer.data)
+
+
+class ServiceMessageViewSet(viewsets.ModelViewSet):
+    serializer_class = serializers.ServiceMessageSerializer
+    permission_classes = [HasOAuthScope]
+    required_oauth_scopes = ['passport:message']
+
+    def get_queryset(self):
+        return Message.objects.all().order_by('-created_at')
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index c4c1733..2b6f940 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,5 +1,5 @@
 # Drycc passport requirements
-django==5.2.13
+django==5.2.14
 pytz==2026.1
 django-auth-ldap==5.3.0
 django-cors-headers==4.9.0
diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index 69100bd..41980a8 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -9,7 +9,7 @@
       "version": "0.0.0",
       "license": "Apache-2.0",
       "dependencies": {
-        "axios": "^1.15.0",
+        "axios": "^1.15.2",
         "vue": "^3.5.30",
         "vue-i18n": "^11.3.0",
         "vue-router": "^5.0.4",
@@ -1075,9 +1075,9 @@
       "license": "MIT"
     },
     "node_modules/axios": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
-      "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
+      "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
       "license": "MIT",
       "dependencies": {
         "follow-redirects": "^1.15.11",
@@ -1918,9 +1918,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.5.8",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
-      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+      "version": "8.5.14",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+      "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
       "funding": [
         {
           "type": "opencollective",
diff --git a/rootfs/web/package.json b/rootfs/web/package.json
index cddffca..82a5d5e 100644
--- a/rootfs/web/package.json
+++ b/rootfs/web/package.json
@@ -8,7 +8,7 @@
     "serve": "vite preview"
   },
   "dependencies": {
-    "axios": "^1.15.0",
+    "axios": "^1.15.2",
     "vue": "^3.5.30",
     "vue-i18n": "^11.3.0",
     "vue-router": "^5.0.4",
diff --git a/rootfs/web/src/views/MessageDetail.vue b/rootfs/web/src/views/MessageDetail.vue
index 88d51e5..74a91ef 100644
--- a/rootfs/web/src/views/MessageDetail.vue
+++ b/rootfs/web/src/views/MessageDetail.vue
@@ -146,10 +146,10 @@ const getCategoryIndicator = (category: string, severity: string) => {
 
                   <!-- Action button (flows naturally after body) -->
                   <div v-if="message.actionLink" class="mt-12">
-                      <button class="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-primary hover:bg-primary-600 text-white text-sm font-semibold rounded-lg shadow-sm transition-all focus:outline-none">
+                      <a :href="message.actionLink" target="_blank" class="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-primary hover:bg-primary-600 text-white text-sm font-semibold rounded-lg shadow-sm transition-all focus:outline-none">
                           {{ message.actionText }}
                           <ChevronRight class="w-4 h-4 opacity-80" />
-                      </button>
+                      </a>
                   </div>
               </div>
           </div>
