From f43188d4f65a411ccb2fdf8e57e24095a47e709f Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 11:18:46 +0800 Subject: [PATCH 01/22] chore(repo): remove python files --- .cruft.json | 15 - .flake8 | 24 - .github/workflows/codeql.yml | 34 -- .github/workflows/docker-publish.yml | 52 -- .github/workflows/pylint.yml | 29 -- .gitignore | 7 - .pre-commit-config.yaml | 29 -- Dockerfile | 16 - Makefile | 10 - apps/__init__.py | 0 apps/account/__init__.py | 0 apps/account/admin.py | 45 -- apps/account/apps.py | 12 - apps/account/constants.py | 41 -- apps/account/exceptions.py | 53 --- apps/account/migrations/0001_initial.py | 94 ---- ..._wechat_id_user_wechat_open_id_and_more.py | 27 -- apps/account/migrations/0003_user_is_staff.py | 18 - ...vatar_alter_user_email_address_and_more.py | 38 -- apps/account/migrations/0005_usertoken.py | 41 -- .../0006_alter_usertoken_index_together.py | 17 - apps/account/migrations/0007_fake_email.py | 27 -- apps/account/migrations/__init__.py | 0 apps/account/models.py | 219 --------- apps/account/rates.py | 24 - apps/account/serializers.py | 171 ------- apps/account/signals.py | 32 -- apps/account/urls.py | 9 - apps/account/utils.py | 18 - apps/account/views.py | 357 -------------- apps/application/__init__.py | 0 apps/application/admin.py | 10 - apps/application/apps.py | 8 - apps/application/constants.py | 1 - apps/application/migrations/0001_initial.py | 77 --- .../migrations/0002_application_app_desc.py | 18 - .../migrations/0003_application_is_hidden.py | 18 - .../0004_alter_application_app_secret.py | 18 - apps/application/migrations/__init__.py | 0 apps/application/models.py | 122 ----- apps/application/serializers.py | 27 -- apps/application/urls.py | 8 - apps/application/views.py | 36 -- apps/cel/__init__.py | 21 - apps/cel/constants.py | 1 - apps/cel/management/__init__.py | 0 apps/cel/management/commands/__init__.py | 0 apps/cel/management/commands/celery.py | 22 - apps/cel/tasks/__init__.py | 7 - apps/cel/tasks/debug.py | 11 - apps/cel/tasks/notice.py | 13 - apps/cel/utils.py | 128 ----- apps/home/__init__.py | 0 apps/home/admin.py | 9 - apps/home/apps.py | 15 - apps/home/constants.py | 34 -- apps/home/exceptions.py | 13 - apps/home/migrations/0001_initial.py | 38 -- apps/home/migrations/__init__.py | 0 apps/home/models.py | 29 -- apps/home/serializers.py | 25 - apps/home/urls.py | 10 - apps/home/utils.py | 14 - apps/home/views.py | 88 ---- apps/notice/__init__.py | 0 apps/notice/admin.py | 15 - apps/notice/apps.py | 8 - apps/notice/constants.py | 8 - apps/notice/migrations/0001_initial.py | 62 --- ...lter_noticelog_send_at_alter_robot_name.py | 23 - apps/notice/migrations/__init__.py | 0 apps/notice/models.py | 37 -- apps/notice/rates.py | 12 - apps/notice/serializers.py | 71 --- apps/notice/urls.py | 8 - apps/notice/utils/__init__.py | 11 - apps/notice/utils/base.py | 96 ---- apps/notice/utils/mail.py | 36 -- apps/notice/utils/msg.py | 32 -- apps/notice/utils/robot.py | 42 -- apps/notice/views.py | 91 ---- apps/tcloud/__init__.py | 0 apps/tcloud/admin.py | 21 - apps/tcloud/apps.py | 8 - apps/tcloud/constants.py | 11 - apps/tcloud/migrations/0001_initial.py | 50 -- .../0002_alter_auditcallback_creation_time.py | 18 - apps/tcloud/migrations/__init__.py | 0 apps/tcloud/models.py | 62 --- apps/tcloud/parsers.py | 41 -- apps/tcloud/serializers.py | 8 - apps/tcloud/urls.py | 10 - apps/tcloud/views.py | 56 --- bin/run.sh | 7 - core/__init__.py | 0 core/auth.py | 40 -- core/constants.py | 8 - core/exceptions.py | 13 - core/utils.py | 7 - entry/__init__.py | 0 entry/asgi.py | 13 - entry/settings.py | 264 ----------- entry/urls.py | 37 -- env.example | 17 - locale/zh_Hans/LC_MESSAGES/django.po | 443 ------------------ manage.py | 17 - requirements.txt | 27 -- scripts/messages.sh | 7 - scripts/pre-commit.sh | 2 - scripts/pylint.sh | 2 - 110 files changed, 4021 deletions(-) delete mode 100644 .cruft.json delete mode 100644 .flake8 delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/docker-publish.yml delete mode 100644 .github/workflows/pylint.yml delete mode 100644 .pre-commit-config.yaml delete mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 apps/__init__.py delete mode 100644 apps/account/__init__.py delete mode 100644 apps/account/admin.py delete mode 100644 apps/account/apps.py delete mode 100644 apps/account/constants.py delete mode 100644 apps/account/exceptions.py delete mode 100644 apps/account/migrations/0001_initial.py delete mode 100644 apps/account/migrations/0002_remove_user_wechat_id_user_wechat_open_id_and_more.py delete mode 100644 apps/account/migrations/0003_user_is_staff.py delete mode 100644 apps/account/migrations/0004_user_avatar_alter_user_email_address_and_more.py delete mode 100644 apps/account/migrations/0005_usertoken.py delete mode 100644 apps/account/migrations/0006_alter_usertoken_index_together.py delete mode 100644 apps/account/migrations/0007_fake_email.py delete mode 100644 apps/account/migrations/__init__.py delete mode 100644 apps/account/models.py delete mode 100644 apps/account/rates.py delete mode 100644 apps/account/serializers.py delete mode 100644 apps/account/signals.py delete mode 100644 apps/account/urls.py delete mode 100644 apps/account/utils.py delete mode 100644 apps/account/views.py delete mode 100644 apps/application/__init__.py delete mode 100644 apps/application/admin.py delete mode 100644 apps/application/apps.py delete mode 100644 apps/application/constants.py delete mode 100644 apps/application/migrations/0001_initial.py delete mode 100644 apps/application/migrations/0002_application_app_desc.py delete mode 100644 apps/application/migrations/0003_application_is_hidden.py delete mode 100644 apps/application/migrations/0004_alter_application_app_secret.py delete mode 100644 apps/application/migrations/__init__.py delete mode 100644 apps/application/models.py delete mode 100644 apps/application/serializers.py delete mode 100644 apps/application/urls.py delete mode 100644 apps/application/views.py delete mode 100644 apps/cel/__init__.py delete mode 100644 apps/cel/constants.py delete mode 100644 apps/cel/management/__init__.py delete mode 100644 apps/cel/management/commands/__init__.py delete mode 100644 apps/cel/management/commands/celery.py delete mode 100644 apps/cel/tasks/__init__.py delete mode 100644 apps/cel/tasks/debug.py delete mode 100644 apps/cel/tasks/notice.py delete mode 100644 apps/cel/utils.py delete mode 100644 apps/home/__init__.py delete mode 100644 apps/home/admin.py delete mode 100644 apps/home/apps.py delete mode 100644 apps/home/constants.py delete mode 100644 apps/home/exceptions.py delete mode 100644 apps/home/migrations/0001_initial.py delete mode 100644 apps/home/migrations/__init__.py delete mode 100644 apps/home/models.py delete mode 100644 apps/home/serializers.py delete mode 100644 apps/home/urls.py delete mode 100644 apps/home/utils.py delete mode 100644 apps/home/views.py delete mode 100644 apps/notice/__init__.py delete mode 100644 apps/notice/admin.py delete mode 100644 apps/notice/apps.py delete mode 100644 apps/notice/constants.py delete mode 100644 apps/notice/migrations/0001_initial.py delete mode 100644 apps/notice/migrations/0002_alter_noticelog_send_at_alter_robot_name.py delete mode 100644 apps/notice/migrations/__init__.py delete mode 100644 apps/notice/models.py delete mode 100644 apps/notice/rates.py delete mode 100644 apps/notice/serializers.py delete mode 100644 apps/notice/urls.py delete mode 100644 apps/notice/utils/__init__.py delete mode 100644 apps/notice/utils/base.py delete mode 100644 apps/notice/utils/mail.py delete mode 100644 apps/notice/utils/msg.py delete mode 100644 apps/notice/utils/robot.py delete mode 100644 apps/notice/views.py delete mode 100644 apps/tcloud/__init__.py delete mode 100644 apps/tcloud/admin.py delete mode 100644 apps/tcloud/apps.py delete mode 100644 apps/tcloud/constants.py delete mode 100644 apps/tcloud/migrations/0001_initial.py delete mode 100644 apps/tcloud/migrations/0002_alter_auditcallback_creation_time.py delete mode 100644 apps/tcloud/migrations/__init__.py delete mode 100644 apps/tcloud/models.py delete mode 100644 apps/tcloud/parsers.py delete mode 100644 apps/tcloud/serializers.py delete mode 100644 apps/tcloud/urls.py delete mode 100644 apps/tcloud/views.py delete mode 100755 bin/run.sh delete mode 100644 core/__init__.py delete mode 100644 core/auth.py delete mode 100644 core/constants.py delete mode 100644 core/exceptions.py delete mode 100644 core/utils.py delete mode 100644 entry/__init__.py delete mode 100644 entry/asgi.py delete mode 100644 entry/settings.py delete mode 100644 entry/urls.py delete mode 100644 env.example delete mode 100644 locale/zh_Hans/LC_MESSAGES/django.po delete mode 100755 manage.py delete mode 100644 requirements.txt delete mode 100755 scripts/messages.sh delete mode 100755 scripts/pre-commit.sh delete mode 100755 scripts/pylint.sh diff --git a/.cruft.json b/.cruft.json deleted file mode 100644 index 9342982..0000000 --- a/.cruft.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "template": "https://github.com/OVINC-CN/DevTemplateDjango.git", - "commit": "394459732ddce4619ad7a43cb96b5e3694f97c6a", - "checkout": "main", - "context": { - "cookiecutter": { - "project_name": "union-api", - "_copy_without_render": [ - ".github" - ], - "_template": "https://github.com/OVINC-CN/DevTemplateDjango.git" - } - }, - "directory": null -} diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 16e391b..0000000 --- a/.flake8 +++ /dev/null @@ -1,24 +0,0 @@ -[flake8] -exclude = - .git, - __pycache__, - *migrations*, - *.pyc, - -max-line-length = 120 -max-complexity = 25 -format=pylint -show_source = True -statistics = True -count = True -select = C,E,F,W,B,B950 -ignore = - F405, - F9, - E203, - E231, - W503, - W291, - C901, - -per-file-ignores = __init__.py:F401 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index e05d708..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - analyze: - name: Analyze - runs-on: 'ubuntu-latest' - timeout-minutes: 360 - permissions: - actions: read - contents: read - security-events: write - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index cd4ef9a..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Docker - -on: - push: - tags: [ '*' ] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install cosign - uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 - with: - cosign-release: 'v2.2.4' - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.USER_NAME }} - password: ${{ secrets.USER_TOKEN }} - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Sign the published Docker image - env: - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} - run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml deleted file mode 100644 index b2ffeab..0000000 --- a/.github/workflows/pylint.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Pylint - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - pylint: - name: Pylint - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.12"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - pip install -r requirements.txt - - name: Analysing the code with pylint - run: | - make pylint diff --git a/.gitignore b/.gitignore index 01dbe79..397b6bd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,6 @@ env .env -# Trasnlation -*.mo - -# Python -*.pyc -*.db - # Log *.log .report diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index a85569a..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,29 +0,0 @@ -default_stages: [commit] -repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: check-merge-conflict - - repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - language_version: python3.12 - args: - - "--line-length=120" - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: [ "--profile", "black" ] - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 - hooks: - - id: flake8 - args: - - "--config" - - ".flake8" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9242984..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM python:3.12.7-slim - -WORKDIR /usr/src/app - -COPY requirements.txt /usr/src/app/ -RUN pip3 install -U pip && pip3 install -r requirements.txt - -COPY . /usr/src/app - -RUN apt-get update && apt-get install -y gettext - -RUN cp env.example .env \ - && python3 manage.py compilemessages -l zh_Hans \ - && rm -rf .env - -RUN mkdir -p /usr/src/app/celery-logs /usr/src/app/logs /usr/src/app/daphne-logs diff --git a/Makefile b/Makefile deleted file mode 100644 index 6b0a957..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -pylint: - scripts/pylint.sh - -pre-commit: - scripts/pre-commit.sh - -lint: pre-commit pylint - -messages: - scripts/messages.sh diff --git a/apps/__init__.py b/apps/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/__init__.py b/apps/account/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/admin.py b/apps/account/admin.py deleted file mode 100644 index 4b5a3bf..0000000 --- a/apps/account/admin.py +++ /dev/null @@ -1,45 +0,0 @@ -from django.contrib import admin -from django.contrib.admin import SimpleListFilter -from django.utils import timezone -from django.utils.translation import gettext_lazy -from ovinc_client.core.utils import strtobool - -from apps.account.models import User, UserToken - - -@admin.register(User) -class UserAdmin(admin.ModelAdmin): - list_display = ["username", "nick_name", "user_type", "date_joined", "last_login"] - list_filter = ["user_type"] - search_fields = ["username", "nick_name"] - - -class ExpiredFilter(SimpleListFilter): - title = gettext_lazy("Is Expired") - parameter_name = "is_expired" - - def lookups(self, request, model_admin): - return ( - (True, gettext_lazy("Yes")), - (False, gettext_lazy("No")), - ) - - def queryset(self, request, queryset): - if self.value(): - is_expired = strtobool(self.value()) - if is_expired: - return queryset.filter(expired_at__lt=timezone.now()) - return queryset.filter(expired_at__gte=timezone.now()) - return queryset - - -@admin.register(UserToken) -class UserTokenAdmin(admin.ModelAdmin): - list_display = ["id", "user", "session_key", "login_ip", "user_agent", "expired_at", "is_expired"] - list_filter = ["user__username", ExpiredFilter] - search_fields = ["session_key", "login_ip"] - ordering = ["-id"] - - @admin.display(description=gettext_lazy("Is Expired"), boolean=True) - def is_expired(self, token: UserToken) -> bool: - return token.expired_at < timezone.now() diff --git a/apps/account/apps.py b/apps/account/apps.py deleted file mode 100644 index 1fee4f2..0000000 --- a/apps/account/apps.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy - - -class HomeConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.account" - verbose_name = gettext_lazy("User Account") - - def ready(self): - # pylint: disable=C0415,W0611 - import apps.account.signals # noqa diff --git a/apps/account/constants.py b/apps/account/constants.py deleted file mode 100644 index f5b00d4..0000000 --- a/apps/account/constants.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.utils.translation import gettext_lazy -from ovinc_client.core.models import TextChoices - -USERNAME_REGEX = r"[a-zA-Z]{1}[a-zA-Z0-9-_]{3,}" - -PHONE_VERIFY_CODE_LENGTH = 6 -PHONE_VERIFY_CODE_TIMEOUT = 60 * 60 # second -PHONE_VERIFY_CODE_KEY = "phone_verify_code:{phone_number}" - -LOGIN_CODE_KEY = "login_code:{code}" - -WECHAT_LOGIN_STATE_KEY = "wechat_login_state:{state}" -WECHAT_USER_INFO_KEY = "wechat_user_info:{code}" - - -class UserTypeChoices(TextChoices): - """ - User Type Choices - """ - - PERSONAL = "personal", gettext_lazy("Personal") - PLATFORM = "platform", gettext_lazy("Platform") - - -class WeChatAuthType(TextChoices): - """ - WeChat Auth Type - """ - - CODE = "authorization_code", gettext_lazy("Code") - - -class PhoneNumberAreas(TextChoices): - """ - Phone Number Areas - """ - - CHINA = "+86", gettext_lazy("Mainland China (+86)") - HONG_KONG = "+852", gettext_lazy("Hong Kong, China (+852)") - MACAO = "+853", gettext_lazy("Macao, China (+853)") - TAIWAN = "+886", gettext_lazy("Taiwan, China (+886)") diff --git a/apps/account/exceptions.py b/apps/account/exceptions.py deleted file mode 100644 index 3cafd0f..0000000 --- a/apps/account/exceptions.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.utils.translation import gettext_lazy -from rest_framework import status -from rest_framework.exceptions import APIException - - -class SignInParamNotExist(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Sign In Params Not Exist") - - -class UserNotExist(APIException): - status_code = status.HTTP_404_NOT_FOUND - default_detail = gettext_lazy("User Not Exist") - - -class WrongSignInParam(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Wrong Username or Password") - - -class TokenNotExist(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Token Not Exist") - - -class WrongToken(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Wrong Token") - - -class StateInvalid(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("State Invalid") - - -class WeChatLoginFailed(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("WeChat Login Failed") - - -class OriginPasswordInvalid(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Origin Password Invalid") - - -class PhoneVerifyCodeInvalid(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Phone Verify Code Invalid") - - -class RegistryLocked(APIException): - status_code = status.HTTP_503_SERVICE_UNAVAILABLE - default_detail = gettext_lazy("Registry Disabled") diff --git a/apps/account/migrations/0001_initial.py b/apps/account/migrations/0001_initial.py deleted file mode 100644 index 81022fa..0000000 --- a/apps/account/migrations/0001_initial.py +++ /dev/null @@ -1,94 +0,0 @@ -# pylint: disable=R0801,C0103,C0301 -# Generated by Django 4.2 on 2023-04-15 15:09 - -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models - -import apps.account.models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), - ] - - operations = [ - migrations.CreateModel( - name="User", - fields=[ - ("password", models.CharField(max_length=128, verbose_name="password")), - ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")), - ( - "is_superuser", - models.BooleanField( - default=False, - help_text="Designates that this user has all permissions without explicitly assigning them.", - verbose_name="superuser status", - ), - ), - ("is_deleted", models.BooleanField(db_index=True, default=False, verbose_name="Soft Delete")), - ( - "username", - models.CharField( - error_messages={"unique": "already in use"}, - max_length=32, - primary_key=True, - serialize=False, - validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], - verbose_name="username", - ), - ), - ("nick_name", models.CharField(blank=True, max_length=32, null=True, verbose_name="Nick Name")), - ( - "user_type", - models.CharField( - choices=[("personal", "Personal"), ("platform", "Platform")], - default="personal", - max_length=32, - verbose_name="User Type", - ), - ), - ("date_joined", models.DateTimeField(auto_now_add=True, verbose_name="Date Joined")), - ("phone_number", models.CharField(blank=True, max_length=32, null=True, verbose_name="Phone Number")), - ( - "email_address", - models.EmailField(blank=True, max_length=254, null=True, verbose_name="Email Address"), - ), - ("wechat_id", models.CharField(blank=True, max_length=255, null=True, verbose_name="Wechat ID")), - ( - "groups", - models.ManyToManyField( - blank=True, - help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", - related_name="user_set", - related_query_name="user", - to="auth.group", - verbose_name="groups", - ), - ), - ( - "user_permissions", - models.ManyToManyField( - blank=True, - help_text="Specific permissions for this user.", - related_name="user_set", - related_query_name="user", - to="auth.permission", - verbose_name="user permissions", - ), - ), - ], - options={ - "verbose_name": "User", - "verbose_name_plural": "User", - "ordering": ["username"], - }, - managers=[ - ("objects", apps.account.models.UserManager()), - ("_objects", django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/apps/account/migrations/0002_remove_user_wechat_id_user_wechat_open_id_and_more.py b/apps/account/migrations/0002_remove_user_wechat_id_user_wechat_open_id_and_more.py deleted file mode 100644 index 71539e8..0000000 --- a/apps/account/migrations/0002_remove_user_wechat_id_user_wechat_open_id_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2 on 2023-04-21 09:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("account", "0001_initial"), - ] - - operations = [ - migrations.RemoveField( - model_name="user", - name="wechat_id", - ), - migrations.AddField( - model_name="user", - name="wechat_open_id", - field=models.CharField(blank=True, max_length=255, null=True, verbose_name="Wechat Open ID"), - ), - migrations.AddField( - model_name="user", - name="wechat_union_id", - field=models.CharField(blank=True, max_length=255, null=True, verbose_name="Wechat Union ID"), - ), - ] diff --git a/apps/account/migrations/0003_user_is_staff.py b/apps/account/migrations/0003_user_is_staff.py deleted file mode 100644 index 01ce65c..0000000 --- a/apps/account/migrations/0003_user_is_staff.py +++ /dev/null @@ -1,18 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2.3 on 2023-09-11 12:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("account", "0002_remove_user_wechat_id_user_wechat_open_id_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="user", - name="is_staff", - field=models.BooleanField(default=False, verbose_name="Is Staff"), - ), - ] diff --git a/apps/account/migrations/0004_user_avatar_alter_user_email_address_and_more.py b/apps/account/migrations/0004_user_avatar_alter_user_email_address_and_more.py deleted file mode 100644 index 6b70147..0000000 --- a/apps/account/migrations/0004_user_avatar_alter_user_email_address_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2.3 on 2023-09-15 11:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("account", "0003_user_is_staff"), - ] - - operations = [ - migrations.AddField( - model_name="user", - name="avatar", - field=models.TextField(blank=True, null=True, verbose_name="Avatar"), - ), - migrations.AlterField( - model_name="user", - name="email_address", - field=models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name="Email Address"), - ), - migrations.AlterField( - model_name="user", - name="phone_number", - field=models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name="Phone Number"), - ), - migrations.AlterField( - model_name="user", - name="wechat_open_id", - field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name="Wechat Open ID"), - ), - migrations.AlterField( - model_name="user", - name="wechat_union_id", - field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name="Wechat Union ID"), - ), - ] diff --git a/apps/account/migrations/0005_usertoken.py b/apps/account/migrations/0005_usertoken.py deleted file mode 100644 index 14fffd3..0000000 --- a/apps/account/migrations/0005_usertoken.py +++ /dev/null @@ -1,41 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2.13 on 2024-07-26 07:46 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("account", "0004_user_avatar_alter_user_email_address_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="UserToken", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False, verbose_name="ID")), - ("session_key", models.CharField(db_index=True, max_length=255, verbose_name="Session Key")), - ("cache_key", models.CharField(db_index=True, max_length=255, verbose_name="Cache Key")), - ("login_ip", models.CharField(db_index=True, max_length=64, verbose_name="Login IP")), - ("user_agent", models.TextField(blank=True, null=True, verbose_name="User Agent")), - ("expired_at", models.DateTimeField(db_index=True, verbose_name="Expired at")), - ( - "user", - models.ForeignKey( - db_constraint=False, - on_delete=django.db.models.deletion.PROTECT, - to=settings.AUTH_USER_MODEL, - verbose_name="User", - ), - ), - ], - options={ - "verbose_name": "User Token", - "verbose_name_plural": "User Token", - "ordering": ["-id"], - "index_together": {("user", "expired_at")}, - }, - ), - ] diff --git a/apps/account/migrations/0006_alter_usertoken_index_together.py b/apps/account/migrations/0006_alter_usertoken_index_together.py deleted file mode 100644 index ae65e33..0000000 --- a/apps/account/migrations/0006_alter_usertoken_index_together.py +++ /dev/null @@ -1,17 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2.13 on 2024-07-26 09:10 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("account", "0005_usertoken"), - ] - - operations = [ - migrations.AlterIndexTogether( - name="usertoken", - index_together={("user", "session_key", "expired_at"), ("user", "expired_at")}, - ), - ] diff --git a/apps/account/migrations/0007_fake_email.py b/apps/account/migrations/0007_fake_email.py deleted file mode 100644 index 347fb7a..0000000 --- a/apps/account/migrations/0007_fake_email.py +++ /dev/null @@ -1,27 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2.13 on 2024-07-26 09:10 -from urllib.parse import urlparse - -from django.conf import settings -from django.db import migrations -from django.db.models import F, Value -from django.db.models.functions import Concat - - -def fake_email(*args, **kwargs): - from apps.account.models import User # pylint: disable=C0415 - - url = urlparse(settings.BACKEND_URL) - User.objects.filter(email_address__isnull=True).update( - email_address=Concat(F("username"), Value(f"@{url.hostname}")) - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("account", "0006_alter_usertoken_index_together"), - ] - - operations = [ - migrations.RunPython(fake_email), - ] diff --git a/apps/account/migrations/__init__.py b/apps/account/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/account/models.py b/apps/account/models.py deleted file mode 100644 index 54da7f4..0000000 --- a/apps/account/models.py +++ /dev/null @@ -1,219 +0,0 @@ -import abc -import hashlib -from typing import Union - -from django.conf import settings -from django.contrib.auth.base_user import AbstractBaseUser -from django.contrib.auth.hashers import make_password -from django.contrib.auth.models import AbstractUser, AnonymousUser, PermissionsMixin -from django.contrib.auth.models import UserManager as _UserManager -from django.core.cache import cache -from django.db import models -from django.utils import timezone -from django.utils.translation import gettext, gettext_lazy -from django_redis.client import DefaultClient -from ovinc_client.core.constants import ( - MAX_CHAR_LENGTH, - MEDIUM_CHAR_LENGTH, - SHORT_CHAR_LENGTH, -) -from ovinc_client.core.models import SoftDeletedManager, SoftDeletedModel -from ovinc_client.core.utils import num_code, uniq_id - -from apps.account.constants import ( - LOGIN_CODE_KEY, - PHONE_VERIFY_CODE_KEY, - PHONE_VERIFY_CODE_LENGTH, - PHONE_VERIFY_CODE_TIMEOUT, - PhoneNumberAreas, - UserTypeChoices, -) -from apps.cel.tasks import send_notice -from apps.notice.constants import NoticeWayChoices - -cache: DefaultClient - - -class UserManager(SoftDeletedManager, _UserManager): - """ - User Manager - """ - - # pylint: disable=W0237 - def create_user(self, username, nick_name=None, password=None, **extra_fields): - if not username: - raise ValueError(gettext("Username Cannot be Empty")) - user = self.model(username=username, nick_name=nick_name, **extra_fields) - user.password = make_password(password) - user.save(using=self._db) - return user - - # pylint: disable=W0237 - def create_superuser(self, username, nick_name=None, password=None, **extra_fields): - extra_fields["is_superuser"] = True - self.create_user(username, nick_name, password, **extra_fields) - - -class User(SoftDeletedModel, AbstractBaseUser, PermissionsMixin): - """ - User - """ - - username = models.CharField( - gettext_lazy("username"), - max_length=SHORT_CHAR_LENGTH, - primary_key=True, - validators=[AbstractUser.username_validator], - error_messages={"unique": gettext_lazy("already in use")}, - ) - nick_name = models.CharField(gettext_lazy("Nick Name"), max_length=SHORT_CHAR_LENGTH, blank=True, null=True) - user_type = models.CharField( - gettext_lazy("User Type"), - choices=UserTypeChoices.choices, - max_length=SHORT_CHAR_LENGTH, - default=UserTypeChoices.PERSONAL.value, - ) - date_joined = models.DateTimeField(gettext_lazy("Date Joined"), auto_now_add=True) - phone_number = models.CharField( - gettext_lazy("Phone Number"), max_length=SHORT_CHAR_LENGTH, null=True, blank=True, unique=True - ) - email_address = models.EmailField(gettext_lazy("Email Address"), null=True, blank=True, unique=True) - wechat_open_id = models.CharField( - gettext_lazy("Wechat Open ID"), max_length=MAX_CHAR_LENGTH, null=True, blank=True, unique=True - ) - wechat_union_id = models.CharField( - gettext_lazy("Wechat Union ID"), max_length=MAX_CHAR_LENGTH, null=True, blank=True, unique=True - ) - avatar = models.TextField(gettext_lazy("Avatar"), null=True, blank=True) - is_staff = models.BooleanField(gettext_lazy("Is Staff"), default=False) - - USERNAME_FIELD = "username" - objects = UserManager() - _objects = _UserManager() - - class Meta: - verbose_name = gettext_lazy("User") - verbose_name_plural = verbose_name - ordering = ["username"] - - def generate_oauth_code(self) -> str: - """ - Generate OAuth User Code - """ - - code = uniq_id() - cache_key = LOGIN_CODE_KEY.format(code=code) - cache.set(cache_key, self.username, timeout=settings.OAUTH_CODE_TIMEOUT) - return code - - @classmethod - def check_oauth_code(cls, code: str) -> (bool, Union[models.Model, None]): - """ - Check OAuth User Code - """ - - cache_key = LOGIN_CODE_KEY.format(code=code) - username = cache.get(cache_key) - cache.delete(cache_key) - try: - return True, cls.objects.get(username=username) - except cls.DoesNotExist: # pylint: disable=E1101 - return False, None - - def reset_password(self, password: str, is_raw: bool = False) -> None: - """ - Reset User Password - """ - - if is_raw: - # pylint: disable=E1101 - password = hashlib.sha256(f"{password}{self.username.lower()}".encode()).hexdigest() - - self.set_password(password) - self.save(update_fields=["password"]) - - @classmethod - def send_phone_verify_code(cls, area: str, phone_number: str) -> None: - """ - Send Phone Verification Code - """ - - phone_number = f"{area}{phone_number}" - key = PHONE_VERIFY_CODE_KEY.format(phone_number=phone_number) - code = num_code(PHONE_VERIFY_CODE_LENGTH) - cache.set(key=key, value=code, timeout=PHONE_VERIFY_CODE_TIMEOUT) - send_notice.delay( - notice_type=NoticeWayChoices.MSG, - usernames=[], - receivers=[phone_number], - content={"tid": cls.get_verify_code_tid(area), "params": [code, str(PHONE_VERIFY_CODE_TIMEOUT // 60)]}, - ) - - @classmethod - def get_verify_code_tid(cls, area: str) -> str: - if area == PhoneNumberAreas.CHINA: - return settings.NOTICE_SMS_ID_VERIFY_CODE - return settings.NOTICE_SMS_ID_VERIFY_CODE_GLOBAL - - @classmethod - def check_phone_verify_code(cls, area: str, phone_number: str, code: str) -> bool: - """ - Verify Phone Number - """ - - phone_number = f"{area}{phone_number}" - key = PHONE_VERIFY_CODE_KEY.format(phone_number=phone_number) - val = cache.get(key=key) - result = str(val) == str(code) - if result: - cache.delete(key=key) - return result - - @classmethod - def load_user_by_union_id(cls, union_id: str) -> "User": - return cls.objects.filter(wechat_union_id=union_id).first() - - def logout_all(self) -> None: - """ - Logout all token - """ - - # pylint: disable=E1101 - tokens = UserToken.objects.filter(user=self, expired_at__gte=timezone.now()) - for token in tokens: - cache.delete(token.cache_key) - tokens.update(expired_at=timezone.now()) - - -class CustomAnonymousUser(AnonymousUser, abc.ABC): - """ - Anonymous User - """ - - nick_name = "AnonymousUser" - user_type = UserTypeChoices.PLATFORM.value - - -class UserToken(models.Model): - """ - User Token - """ - - id = models.BigAutoField(gettext_lazy("ID"), primary_key=True) - user = models.ForeignKey( - verbose_name=gettext_lazy("User"), to="User", on_delete=models.PROTECT, db_constraint=False - ) - session_key = models.CharField(gettext_lazy("Session Key"), max_length=MAX_CHAR_LENGTH, db_index=True) - cache_key = models.CharField(gettext_lazy("Cache Key"), max_length=MAX_CHAR_LENGTH, db_index=True) - login_ip = models.CharField(gettext_lazy("Login IP"), max_length=MEDIUM_CHAR_LENGTH, db_index=True) - user_agent = models.TextField(gettext_lazy("User Agent"), null=True, blank=True) - expired_at = models.DateTimeField(gettext_lazy("Expired at"), db_index=True) - - class Meta: - verbose_name = gettext_lazy("User Token") - verbose_name_plural = verbose_name - ordering = ["-id"] - index_together = [ - ["user", "expired_at"], - ["user", "session_key", "expired_at"], - ] diff --git a/apps/account/rates.py b/apps/account/rates.py deleted file mode 100644 index b0c4b36..0000000 --- a/apps/account/rates.py +++ /dev/null @@ -1,24 +0,0 @@ -from ovinc_client.core.utils import get_ip -from rest_framework.throttling import SimpleRateThrottle - - -class SMSRateThrottle(SimpleRateThrottle): - """ - SMS Throttle - """ - - scope = "phone_verify_code" - - def get_cache_key(self, request, view) -> str: - return f'rate:{self.scope}:{request.data.get("phone_area")}{request.data.get("phone_number")}' - - -class IPRateThrottle(SimpleRateThrottle): - """ - IP Throttle - """ - - scope = "ip" - - def get_cache_key(self, request, view) -> str: - return f"rate:{self.scope}:{get_ip(request)}" diff --git a/apps/account/serializers.py b/apps/account/serializers.py deleted file mode 100644 index d89e326..0000000 --- a/apps/account/serializers.py +++ /dev/null @@ -1,171 +0,0 @@ -import re - -from django.contrib.auth import get_user_model -from django.utils.translation import gettext, gettext_lazy -from ovinc_client.tcaptcha.exceptions import TCaptchaInvalid -from ovinc_client.tcaptcha.utils import TCaptchaVerify -from rest_framework import serializers - -from apps.account.constants import ( - PHONE_VERIFY_CODE_LENGTH, - USERNAME_REGEX, - PhoneNumberAreas, -) -from apps.account.exceptions import PhoneVerifyCodeInvalid -from apps.account.models import User -from apps.home.constants import BuildInKeys -from apps.home.models import MetaConfig - -USER_MODEL: User = get_user_model() - - -class UserInfoSerializer(serializers.ModelSerializer): - """ - User Info - """ - - class Meta: - model = USER_MODEL - fields = ["username", "nick_name", "last_login", "user_type"] - - -class SignInSerializer(serializers.Serializer): - """ - Sign in - """ - - username = serializers.CharField(label=gettext_lazy("Username")) - password = serializers.CharField(label=gettext_lazy("Password")) - is_oauth = serializers.BooleanField(label=gettext_lazy("Is OAuth"), default=False) - wechat_code = serializers.CharField(label=gettext_lazy("WeChat Code"), required=False) - tcaptcha = serializers.JSONField(label=gettext_lazy("Tencent Captcha"), required=False, default=dict) - - def validate(self, attrs: dict) -> dict: - data = super().validate(attrs) - if not TCaptchaVerify(user_ip=self.context.get("user_ip", ""), **data.get("tcaptcha", {})).verify(): - raise TCaptchaInvalid() - return data - - -class UserRegistrySerializer(serializers.ModelSerializer): - """ - User Registry - """ - - username = serializers.RegexField(label=gettext_lazy("Username"), regex=USERNAME_REGEX) - is_oauth = serializers.BooleanField(label=gettext_lazy("Is OAuth"), default=False) - wechat_code = serializers.CharField(label=gettext_lazy("WeChat Code"), required=False) - phone_area = serializers.ChoiceField(label=gettext_lazy("Phone Area"), choices=PhoneNumberAreas.choices) - phone_number = serializers.CharField(label=gettext_lazy("Phone Number")) - phone_verify = serializers.CharField( - label=gettext_lazy("Phone Verify Code"), - min_length=PHONE_VERIFY_CODE_LENGTH, - max_length=PHONE_VERIFY_CODE_LENGTH, - ) - tcaptcha = serializers.JSONField(label=gettext_lazy("Tencent Captcha"), required=False, default=dict) - - class Meta: - model = USER_MODEL - fields = [ - "username", - "nick_name", - "password", - "is_oauth", - "wechat_code", - "phone_area", - "phone_number", - "phone_verify", - "tcaptcha", - ] - - def validate(self, attrs: dict) -> dict: - data = super().validate(attrs) - if not TCaptchaVerify(user_ip=self.context.get("user_ip", ""), **data.get("tcaptcha", {})).verify(): - raise TCaptchaInvalid() - if not USER_MODEL.check_phone_verify_code( - area=data["phone_area"], phone_number=data["phone_number"], code=data["phone_verify"] - ): - raise PhoneVerifyCodeInvalid() - return data - - def validate_username(self, username: str) -> str: - username_extra_regex = MetaConfig.objects.filter(key=BuildInKeys.USERNAME_EXTRA_REGEX[0]).first() - if not username_extra_regex: - return username - if re.compile(username_extra_regex.val).match(username): - return username - raise serializers.ValidationError(gettext("Username Invalid")) - - def validate_phone_number(self, phone_number: str) -> str: - if USER_MODEL.objects.filter(phone_number=phone_number).exists(): - raise serializers.ValidationError(gettext("Phone Number Already Exists")) - return phone_number - - -class VerifyCodeRequestSerializer(serializers.Serializer): - """ - Verify Code - """ - - code = serializers.CharField(label=gettext_lazy("Code")) - - -class WeChatLoginReqSerializer(serializers.Serializer): - """ - WeChat Login - """ - - code = serializers.CharField(label=gettext_lazy("Code")) - state = serializers.CharField(label=gettext_lazy("State")) - is_oauth = serializers.BooleanField(label=gettext_lazy("Is OAuth"), default=False) - - -class ResetPasswordRequestSerializer(serializers.Serializer): - """ - Reset Password - """ - - username = serializers.RegexField(label=gettext_lazy("Username"), regex=USERNAME_REGEX) - password = serializers.CharField(label=gettext_lazy("Password"), max_length=128) - phone_area = serializers.ChoiceField(label=gettext_lazy("Phone Area"), choices=PhoneNumberAreas.choices) - phone_number = serializers.CharField(label=gettext_lazy("Phone Number")) - phone_verify = serializers.CharField( - label=gettext_lazy("Phone Verify Code"), - min_length=PHONE_VERIFY_CODE_LENGTH, - max_length=PHONE_VERIFY_CODE_LENGTH, - ) - tcaptcha = serializers.JSONField(label=gettext_lazy("Tencent Captcha"), required=False, default=dict) - - def validate(self, attrs: dict) -> dict: - data = super().validate(attrs) - if not TCaptchaVerify(user_ip=self.context.get("user_ip", ""), **data.get("tcaptcha", {})).verify(): - raise TCaptchaInvalid() - if not USER_MODEL.check_phone_verify_code( - area=data["phone_area"], phone_number=data["phone_number"], code=data["phone_verify"] - ): - raise PhoneVerifyCodeInvalid() - return data - - -class SendVerifyCodeRequestSerializer(serializers.Serializer): - """ - Verify Code - """ - - phone_area = serializers.ChoiceField(label=gettext_lazy("Phone Area"), choices=PhoneNumberAreas.choices) - phone_number = serializers.CharField(label=gettext_lazy("Phone Number")) - tcaptcha = serializers.JSONField(label=gettext_lazy("Tencent Captcha"), required=False, default=dict) - - def validate(self, attrs: dict) -> dict: - data = super().validate(attrs) - if not TCaptchaVerify(user_ip=self.context.get("user_ip", ""), **data.get("tcaptcha", {})).verify(): - raise TCaptchaInvalid() - return data - - -class OIDCLoginRequestSerializer(serializers.Serializer): - """ - OIDC Login - """ - - next = serializers.CharField(label=gettext_lazy("Next")) diff --git a/apps/account/signals.py b/apps/account/signals.py deleted file mode 100644 index 151a10e..0000000 --- a/apps/account/signals.py +++ /dev/null @@ -1,32 +0,0 @@ -import datetime - -from django.conf import settings -from django.contrib.auth.signals import user_logged_in, user_logged_out -from django.dispatch import receiver -from django.utils import timezone -from ovinc_client.core.utils import get_ip - -from apps.account.models import UserToken - - -@receiver(user_logged_in) -def user_logged_in_handler(sender, request, user, **kwargs): - # pylint: disable=E1101 - UserToken.objects.create( - user=user, - session_key=request.session.session_key, - cache_key=request.session.cache_key, - login_ip=get_ip(request), - user_agent=request.headers.get("User-Agent"), - expired_at=timezone.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE), - ) - - -@receiver(user_logged_out) -def user_logged_out_handler(sender, request, user, **kwargs): - # pylint: disable=E1101 - ( - UserToken.objects.filter(user=user) - .filter(session_key=request.session.session_key) - .filter(expired_at__gte=timezone.now()) - ).update(expired_at=timezone.now()) diff --git a/apps/account/urls.py b/apps/account/urls.py deleted file mode 100644 index c59d1af..0000000 --- a/apps/account/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework.routers import DefaultRouter - -from apps.account.views import UserInfoViewSet, UserSignViewSet - -router = DefaultRouter() -router.register("", UserSignViewSet) -router.register("user_info", UserInfoViewSet, basename="user_info") - -urlpatterns = router.urls diff --git a/apps/account/utils.py b/apps/account/utils.py deleted file mode 100644 index f2714a4..0000000 --- a/apps/account/utils.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework.settings import api_settings - - -def userinfo(claims, user): - claims.update( - { - "name": user.username, - "nickname": user.nick_name, - "preferred_username": user.username, - "updated_at": user.last_login.strftime(api_settings.DATETIME_FORMAT), - "email": user.email_address, - } - ) - return claims - - -def default_sub_generator(user): - return f"{user.username}" diff --git a/apps/account/views.py b/apps/account/views.py deleted file mode 100644 index b5c62b7..0000000 --- a/apps/account/views.py +++ /dev/null @@ -1,357 +0,0 @@ -import datetime -import json -from json import JSONDecodeError -from urllib.parse import quote, urlparse - -import httpx -from django.conf import settings -from django.contrib import auth -from django.contrib.auth import get_user_model -from django.core.cache import cache -from django.http import HttpResponseRedirect -from ovinc_client.core.auth import SessionAuthenticate -from ovinc_client.core.logger import logger -from ovinc_client.core.utils import get_ip, uniq_id -from ovinc_client.core.viewsets import MainViewSet -from rest_framework.decorators import action -from rest_framework.response import Response - -from apps.account.constants import ( - WECHAT_LOGIN_STATE_KEY, - WECHAT_USER_INFO_KEY, - PhoneNumberAreas, - WeChatAuthType, -) -from apps.account.exceptions import ( - RegistryLocked, - StateInvalid, - UserNotExist, - WeChatLoginFailed, - WrongSignInParam, - WrongToken, -) -from apps.account.models import User -from apps.account.rates import IPRateThrottle, SMSRateThrottle -from apps.account.serializers import ( - OIDCLoginRequestSerializer, - ResetPasswordRequestSerializer, - SendVerifyCodeRequestSerializer, - SignInSerializer, - UserInfoSerializer, - UserRegistrySerializer, - VerifyCodeRequestSerializer, - WeChatLoginReqSerializer, -) -from apps.home.constants import BuildInKeys -from apps.home.models import MetaConfig -from core.auth import ApplicationAuthenticate -from core.utils import is_wechat - -USER_MODEL: User = get_user_model() - - -class UserInfoViewSet(MainViewSet): - """ - User Info - """ - - queryset = USER_MODEL.get_queryset() - serializer_class = UserInfoSerializer - - def list(self, request, *args, **kwargs): - """ - Get User Info - """ - - serializer = self.serializer_class(instance=request.user) - return Response(serializer.data) - - -class UserSignViewSet(MainViewSet): - """ - User Login and Logout - """ - - queryset = USER_MODEL.get_queryset() - - @action(methods=["POST"], detail=False, authentication_classes=[SessionAuthenticate]) - def sign_in(self, request, *args, **kwargs): - """ - Sign in - """ - - # Validate Request Data - request_serializer = SignInSerializer(data=request.data, context={"user_ip": get_ip(request)}) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # login - user: User = auth.authenticate(request, **request_data) - if not user: - raise WrongSignInParam() - - # bind wechat - if request_data.get("wechat_code"): - self.update_user_by_wechat(user, request_data["wechat_code"]) - - # auth session - auth.login(request, user) - - # oauth - if request_data["is_oauth"]: - return Response({"code": user.generate_oauth_code()}) - - return Response() - - @action(methods=["GET"], detail=False) - def sign_out(self, request, *args, **kwargs): - """ - Sign out - """ - - auth.logout(request) - return Response() - - @action(methods=["POST"], detail=False, authentication_classes=[SessionAuthenticate]) - def sign_up(self, request, *args, **kwargs): - """ - sign up - """ - - # check registry locked - registry_lock_config: MetaConfig | None = MetaConfig.objects.filter(key=BuildInKeys.REGISTRY_LOCKED[0]).first() - if registry_lock_config and registry_lock_config.val: - raise RegistryLocked() - - # validate request - request_serializer = UserRegistrySerializer(data=request.data, context={"user_ip": get_ip(request)}) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # save - user = USER_MODEL.objects.create_user( - last_login=datetime.datetime.now(), - username=request_data["username"], - password=request_data["password"], - nick_name=request_data["nick_name"], - phone_number=request_data["phone_number"], - email_address=f"{request_data["username"]}@{urlparse(settings.BACKEND_URL).hostname}", - ) - - # bind wechat - if request_data.get("wechat_code"): - self.update_user_by_wechat(user, request_data["wechat_code"]) - - # login session - auth.login(request, user) - - # oauth - if request_data["is_oauth"]: - return Response({"code": user.generate_oauth_code()}) - - # response - return Response() - - @action( - methods=["POST"], - detail=False, - authentication_classes=[SessionAuthenticate], - throttle_classes=[SMSRateThrottle, IPRateThrottle], - ) - def phone_verify_code(self, request, *args, **kwargs): - """ - send verify code - """ - - # validate request - request_serializer = SendVerifyCodeRequestSerializer(data=request.data, context={"user_ip": get_ip(request)}) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # send - USER_MODEL.send_phone_verify_code(area=request_data["phone_area"], phone_number=request_data["phone_number"]) - - # response - return Response() - - @action(methods=["GET"], detail=False) - def oauth_code(self, request, *args, **kwargs): - """ - oauth code - """ - - return Response({"code": request.user.generate_oauth_code()}) - - @action(methods=["POST"], detail=False, authentication_classes=[ApplicationAuthenticate]) - def verify_code(self, request, *args, **kwargs): - """ - verify oauth code - """ - - # validate request - request_serializer = VerifyCodeRequestSerializer(data=request.data) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # load user - is_success, user = USER_MODEL.check_oauth_code(request_data["code"]) - if is_success: - return Response(UserInfoSerializer(instance=user).data) - raise WrongToken() - - @action(methods=["GET"], detail=False, authentication_classes=[SessionAuthenticate]) - def wechat_config(self, request, *args, **kwargs): - """ - WeChat Config - """ - - state = uniq_id() - cache_key = WECHAT_LOGIN_STATE_KEY.format(state=state) - cache.set(cache_key, True, timeout=settings.WECHAT_SCOPE_TIMEOUT) - - is_wechat_ua = is_wechat(request) - return Response( - { - "app_id": settings.MP_WECHAT_APP_ID if is_wechat_ua else settings.WECHAT_APP_ID, - "state": state, - "is_wechat": is_wechat_ua, - } - ) - - # pylint: disable=R0914 - @action(methods=["POST"], detail=False, authentication_classes=[SessionAuthenticate]) - def wechat_login(self, request, *args, **kwargs): - """ - WeChat Login - """ - - # validate request - request_serializer = WeChatLoginReqSerializer(data=request.data) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # check state - cache_key = WECHAT_LOGIN_STATE_KEY.format(state=request_data["state"]) - if not cache.get(cache_key): - raise StateInvalid() - cache.delete(cache_key) - - # load app key app secret - if is_wechat(request): - app_id = settings.MP_WECHAT_APP_ID - app_key = settings.MP_WECHAT_APP_KEY - else: - app_id = settings.WECHAT_APP_ID - app_key = settings.WECHAT_APP_KEY - - # load access token - url = ( - f"{settings.WECHAT_OAUTH_API}/access_token" - f"?appid={app_id}" - f"&secret={app_key}" - f"&code={request_data['code']}" - f"&grant_type={WeChatAuthType.CODE}" - ) - client = httpx.Client() - try: - resp = client.get(url) - access_info = resp.json() - except Exception as err: - logger.exception("[CallWeChatAPIFailed] %s %s", url, err) - raise WeChatLoginFailed() from err - finally: - client.close() - - if "openid" not in access_info: - logger.error("[WeChatLoginFailed] %s", access_info) - raise WeChatLoginFailed() - - # get user info - url = ( - f"{settings.WECHAT_USER_INFO_API}?access_token={access_info['access_token']}&openid={access_info['openid']}" - ) - client = httpx.Client() - try: - resp = client.get(url) - user_info = resp.json() - except Exception as err: - logger.exception("[CallWeChatAPIFailed] %s", url, err) - raise WeChatLoginFailed() from err - finally: - client.close() - - code = uniq_id() - cache_key = WECHAT_USER_INFO_KEY.format(code=code) - cache.set(cache_key, json.dumps(user_info, ensure_ascii=False), timeout=settings.WECHAT_SCOPE_TIMEOUT) - - # load user - user: User = USER_MODEL.load_user_by_union_id(union_id=user_info["unionid"]) - if user: - self.update_user_by_wechat(user, code) - auth.login(request, user) - return Response({"code": user.generate_oauth_code() if request_data["is_oauth"] else ""}) - - # need registry - return Response({"wechat_code": code}) - - def update_user_by_wechat(self, user: User, wechat_code: str) -> None: - """ - Update User Info By WeChat - """ - - cache_key = WECHAT_USER_INFO_KEY.format(code=wechat_code) - try: - user_info = json.loads(cache.get(cache_key, default="{}")) - except JSONDecodeError: - return - - cache.delete(cache_key) - - user.wechat_union_id = user_info["unionid"] - user.wechat_open_id = user_info["openid"] - user.avatar = user_info["headimgurl"] - user.save(update_fields=["wechat_union_id", "wechat_open_id", "avatar"]) - - @action(methods=["POST"], detail=False, authentication_classes=[SessionAuthenticate]) - def reset_password(self, request, *args, **kwargs) -> Response: - """ - Reset Password - """ - - # validate request - request_serializer = ResetPasswordRequestSerializer(data=request.data, context={"user_ip": get_ip(request)}) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # load user - try: - user: User = USER_MODEL.objects.get( - username=request_data["username"], phone_number=request_data["phone_number"] - ) - except USER_MODEL.DoesNotExist as err: - raise UserNotExist() from err - - # set new password - user.reset_password(request_data["password"]) - - return Response() - - @action(methods=["GET"], detail=False, authentication_classes=[SessionAuthenticate]) - def phone_areas(self, request, *args, **kwargs) -> Response: - """ - Phone Number Areas - """ - - return Response(data=[{"value": value, "label": str(label)} for value, label in PhoneNumberAreas.choices]) - - @action(methods=["GET"], detail=False, authentication_classes=[SessionAuthenticate]) - def oidc_login(self, request, *args, **kwargs) -> HttpResponseRedirect: - """ - OIDC Login, Redirect to Login Page - """ - - req_slz = OIDCLoginRequestSerializer(data=request.query_params) - req_slz.is_valid(raise_exception=True) - req_data = req_slz.validated_data - next_url = quote(settings.BACKEND_URL + req_data["next"]) - return HttpResponseRedirect(redirect_to=f"{settings.FRONTEND_URL}/login/?next={next_url}") diff --git a/apps/application/__init__.py b/apps/application/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/application/admin.py b/apps/application/admin.py deleted file mode 100644 index 4d0d127..0000000 --- a/apps/application/admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from apps.application.models import Application - - -@admin.register(Application) -class ApplicationAdmin(admin.ModelAdmin): - list_display = ["app_code", "app_name", "is_deleted"] - list_filter = ["is_deleted"] - search_fields = ["app_code", "app_name"] diff --git a/apps/application/apps.py b/apps/application/apps.py deleted file mode 100644 index 7d96bc4..0000000 --- a/apps/application/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy - - -class HomeConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.application" - verbose_name = gettext_lazy("Application") diff --git a/apps/application/constants.py b/apps/application/constants.py deleted file mode 100644 index f4fa11c..0000000 --- a/apps/application/constants.py +++ /dev/null @@ -1 +0,0 @@ -APP_CODE_REGEXP = r"^[a-zA-Z0-9-_]+$" diff --git a/apps/application/migrations/0001_initial.py b/apps/application/migrations/0001_initial.py deleted file mode 100644 index 5a18be8..0000000 --- a/apps/application/migrations/0001_initial.py +++ /dev/null @@ -1,77 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2 on 2023-04-15 07:51 - -import django.db.models.deletion -import ovinc_client.core.models -import ovinc_client.core.utils -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="Application", - fields=[ - ("is_deleted", models.BooleanField(db_index=True, default=False, verbose_name="Soft Delete")), - ("app_name", models.CharField(max_length=32, verbose_name="App Name")), - ( - "app_code", - models.CharField(max_length=32, primary_key=True, serialize=False, verbose_name="App Code"), - ), - ("app_secret", models.TextField(blank=True, null=True, verbose_name="App Secret (Encoded)")), - ("app_url", models.URLField(blank=True, null=True, verbose_name="App Url")), - ("app_logo", models.URLField(blank=True, null=True, verbose_name="App Logo")), - ], - options={ - "verbose_name": "Application", - "verbose_name_plural": "Application", - "ordering": ["app_code"], - }, - ), - migrations.CreateModel( - name="ApplicationManager", - fields=[ - ( - "id", - ovinc_client.core.models.UniqIDField( - default=ovinc_client.core.utils.uniq_id_without_time, - max_length=32, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "application", - ovinc_client.core.models.ForeignKey( - db_constraint=False, - on_delete=django.db.models.deletion.CASCADE, - to="application.application", - verbose_name="Application", - ), - ), - ( - "manager", - ovinc_client.core.models.ForeignKey( - db_constraint=False, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - verbose_name="Manager", - ), - ), - ], - options={ - "verbose_name": "Application Manager", - "verbose_name_plural": "Application Manager", - "ordering": ["id"], - "unique_together": {("application", "manager")}, - }, - ), - ] diff --git a/apps/application/migrations/0002_application_app_desc.py b/apps/application/migrations/0002_application_app_desc.py deleted file mode 100644 index a81512c..0000000 --- a/apps/application/migrations/0002_application_app_desc.py +++ /dev/null @@ -1,18 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2 on 2023-04-24 09:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("application", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="application", - name="app_desc", - field=models.TextField(blank=True, null=True, verbose_name="App Desc"), - ), - ] diff --git a/apps/application/migrations/0003_application_is_hidden.py b/apps/application/migrations/0003_application_is_hidden.py deleted file mode 100644 index 1d8afd5..0000000 --- a/apps/application/migrations/0003_application_is_hidden.py +++ /dev/null @@ -1,18 +0,0 @@ -# pylint: disable=C0103 -# Generated by Django 4.2.16 on 2024-11-20 12:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("application", "0002_application_app_desc"), - ] - - operations = [ - migrations.AddField( - model_name="application", - name="is_hidden", - field=models.BooleanField(default=False, verbose_name="Is Hidden"), - ), - ] diff --git a/apps/application/migrations/0004_alter_application_app_secret.py b/apps/application/migrations/0004_alter_application_app_secret.py deleted file mode 100644 index f941c48..0000000 --- a/apps/application/migrations/0004_alter_application_app_secret.py +++ /dev/null @@ -1,18 +0,0 @@ -# pylint: disable=C0103 -# Generated by Django 4.2.17 on 2024-12-18 07:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("application", "0003_application_is_hidden"), - ] - - operations = [ - migrations.AlterField( - model_name="application", - name="app_secret", - field=models.CharField(blank=True, max_length=255, null=True, verbose_name="App Secret"), - ), - ] diff --git a/apps/application/migrations/__init__.py b/apps/application/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/application/models.py b/apps/application/models.py deleted file mode 100644 index 2d3442c..0000000 --- a/apps/application/models.py +++ /dev/null @@ -1,122 +0,0 @@ -import hashlib -import secrets -import time -from typing import List - -from django.conf import settings -from django.core.cache import cache -from django.db import models, transaction -from django.utils.translation import gettext_lazy -from django_redis.client import DefaultClient -from ovinc_client.core.constants import MAX_CHAR_LENGTH, SHORT_CHAR_LENGTH -from ovinc_client.core.models import ( - BaseModel, - ForeignKey, - SoftDeletedManager, - SoftDeletedModel, - UniqIDField, -) - -from apps.account.models import User -from core.constants import APP_AUTH_NONCE_CACHE_KEY - -cache: DefaultClient - - -class ApplicationObjects(SoftDeletedManager): - """ - Application Manager Objects - """ - - @transaction.atomic() - def create(self, app_name: str, app_code: str, app_secret: str, managers: List[User], **kwargs) -> "Application": - """ - Create Application - """ - - # create application - application = super().create(app_name=app_name, app_code=app_code, **kwargs) - - # set secret - application.set_secret(app_secret) - - # set managers - app_managers = [ApplicationManager(application=application, manager=manager) for manager in managers] - ApplicationManager.objects.bulk_create(app_managers) - - return application - - -class Application(SoftDeletedModel): - """ - Application - """ - - app_name = models.CharField(gettext_lazy("App Name"), max_length=SHORT_CHAR_LENGTH) - app_code = models.CharField(gettext_lazy("App Code"), max_length=SHORT_CHAR_LENGTH, primary_key=True) - app_secret = models.CharField(gettext_lazy("App Secret"), max_length=MAX_CHAR_LENGTH, blank=True, null=True) - app_url = models.URLField(gettext_lazy("App Url"), null=True, blank=True) - app_logo = models.URLField(gettext_lazy("App Logo"), null=True, blank=True) - app_desc = models.TextField(gettext_lazy("App Desc"), null=True, blank=True) - is_hidden = models.BooleanField(gettext_lazy("Is Hidden"), default=False) - - objects = ApplicationObjects() - - class Meta: - verbose_name = gettext_lazy("Application") - verbose_name_plural = verbose_name - ordering = ["app_code"] - - def check_sign(self, signature: str, timestamp: str, nonce: str) -> bool: - """ - check signature - """ - - # check timestamp - if not timestamp.isdigit() or int(timestamp) + settings.APP_AUTH_SIGN_EXPIRE < time.time(): - return False - - # check nonce deplicate - if not cache.add( - key=APP_AUTH_NONCE_CACHE_KEY.format(appid=self.app_code, nonce=nonce), - value=signature, - timeout=settings.APP_AUTH_SIGN_EXPIRE, - ): - return False - - # check signature - raw_content = f"{timestamp}-{nonce}-{self.app_secret}" - expect_signature = hashlib.sha256(raw_content.encode()).hexdigest() - return secrets.compare_digest(signature, expect_signature) - - def set_secret(self, raw_secret: str) -> None: - """ - set secret - """ - - self.app_secret = raw_secret - self.save(update_fields=["app_secret"]) - - -class ApplicationManager(BaseModel): - """ - Application Manager - """ - - id = UniqIDField(gettext_lazy("ID")) - application = ForeignKey(gettext_lazy("Application"), to="application.Application", on_delete=models.CASCADE) - manager = ForeignKey(gettext_lazy("Manager"), to="account.User", on_delete=models.CASCADE) - - class Meta: - verbose_name = gettext_lazy("Application Manager") - verbose_name_plural = verbose_name - unique_together = ["application", "manager"] - ordering = ["id"] - - @classmethod - def load_managers(cls, applications: List[Application]) -> List[User]: - return ApplicationManager.objects.filter(application__in=applications).select_related("manager") - - @classmethod - def load_manage_apps(cls, manager): - return ApplicationManager.objects.filter(manager=manager).values("application") diff --git a/apps/application/serializers.py b/apps/application/serializers.py deleted file mode 100644 index cd25c49..0000000 --- a/apps/application/serializers.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.contrib.auth import get_user_model -from rest_framework.serializers import ModelSerializer - -from apps.account.models import User -from apps.application.models import Application - -USER_MODEL: User = get_user_model() - - -class ApplicationSerializer(ModelSerializer): - """ - Application Info - """ - - class Meta: - model = Application - fields = ["app_name", "app_code"] - - -class ApplicationListSerializer(ModelSerializer): - """ - Application List - """ - - class Meta: - model = Application - fields = ["app_name", "app_code", "app_logo", "app_url", "app_desc"] diff --git a/apps/application/urls.py b/apps/application/urls.py deleted file mode 100644 index 699802c..0000000 --- a/apps/application/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework.routers import DefaultRouter - -from apps.application.views import ApplicationViewSet - -router = DefaultRouter() -router.register("", ApplicationViewSet) - -urlpatterns = router.urls diff --git a/apps/application/views.py b/apps/application/views.py deleted file mode 100644 index e0fcc13..0000000 --- a/apps/application/views.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.contrib.auth import get_user_model -from ovinc_client.core.auth import SessionAuthenticate -from ovinc_client.core.viewsets import MainViewSet -from rest_framework.decorators import action -from rest_framework.response import Response - -from apps.account.models import User -from apps.application.models import Application -from apps.application.serializers import ( - ApplicationListSerializer, - ApplicationSerializer, -) - -USER_MODEL: User = get_user_model() - - -class ApplicationViewSet(MainViewSet): - """ - Application - """ - - queryset = Application.get_queryset() - serializer_class = ApplicationSerializer - - @action(methods=["GET"], detail=False, authentication_classes=[SessionAuthenticate]) - def all(self, request, *args, **kwargs): - """ - list all applications - """ - - # filter - queryset = Application.get_queryset().filter(is_hidden=False) - - # response - serializer = ApplicationListSerializer(queryset, many=True) - return Response(serializer.data) diff --git a/apps/cel/__init__.py b/apps/cel/__init__.py deleted file mode 100644 index df97b04..0000000 --- a/apps/cel/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -import os - -from celery import Celery -from celery.schedules import crontab -from django.conf import settings - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "entry.settings") -os.environ.setdefault("C_FORCE_ROOT", "True") - -app = Celery("main", broker=settings.BROKER_URL) -app.config_from_object("django.conf:settings", namespace="CELERY") -app.autodiscover_tasks() - -# Schedule Tasks -app.conf.beat_schedule = { - "celery_debug": { - "task": "apps.cel.tasks.debug.celery_debug", - "schedule": crontab(minute="*"), - "args": (), - }, -} diff --git a/apps/cel/constants.py b/apps/cel/constants.py deleted file mode 100644 index b39074d..0000000 --- a/apps/cel/constants.py +++ /dev/null @@ -1 +0,0 @@ -CELERY_RETRY_SLEEP = 1 diff --git a/apps/cel/management/__init__.py b/apps/cel/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/cel/management/commands/__init__.py b/apps/cel/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/cel/management/commands/celery.py b/apps/cel/management/commands/celery.py deleted file mode 100644 index 0669959..0000000 --- a/apps/cel/management/commands/celery.py +++ /dev/null @@ -1,22 +0,0 @@ -import sys - -from django.core.management import BaseCommand - -from apps.cel import app - - -class Command(BaseCommand): - """ - Celery Command - """ - - def handle(self, *args, **options): - argv = sys.argv[2:] - app.start(argv) - - def add_arguments(self, parser): - parser.add_argument("worker", nargs="*") - parser.add_argument("beat", nargs="*") - parser.add_argument("-l") - parser.add_argument("-f") - parser.add_argument("-c") diff --git a/apps/cel/tasks/__init__.py b/apps/cel/tasks/__init__.py deleted file mode 100644 index db8bd97..0000000 --- a/apps/cel/tasks/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from apps.cel.tasks.debug import celery_debug -from apps.cel.tasks.notice import send_notice - -__all__ = [ - "celery_debug", - "send_notice", -] diff --git a/apps/cel/tasks/debug.py b/apps/cel/tasks/debug.py deleted file mode 100644 index 611af16..0000000 --- a/apps/cel/tasks/debug.py +++ /dev/null @@ -1,11 +0,0 @@ -from ovinc_client.core.logger import celery_logger - -from apps.cel import app -from apps.cel.utils import task_lock - - -@app.task(bind=True) -@task_lock() -def celery_debug(self): - celery_logger.info(f"[CeleryDebug] Start {self.request.id}") - celery_logger.info(f"[CeleryDebug] End {self.request.id}") diff --git a/apps/cel/tasks/notice.py b/apps/cel/tasks/notice.py deleted file mode 100644 index 454ed06..0000000 --- a/apps/cel/tasks/notice.py +++ /dev/null @@ -1,13 +0,0 @@ -from ovinc_client.core.logger import celery_logger - -from apps.cel import app - - -@app.task(bind=True) -def send_notice(self, notice_type: str, **kwargs): - # pylint: disable=C0415,R0401 - from apps.notice.utils import NoticeBase - - celery_logger.info(f"[SendNotice] Start {self.request.id}") - NoticeBase.get_instance(notice_type, **kwargs).send() - celery_logger.info(f"[SendNotice] End {self.request.id}") diff --git a/apps/cel/utils.py b/apps/cel/utils.py deleted file mode 100644 index 70c03d6..0000000 --- a/apps/cel/utils.py +++ /dev/null @@ -1,128 +0,0 @@ -import time -from functools import wraps - -from django.core.cache import cache -from ovinc_client.core.logger import logger - -from apps.cel.constants import CELERY_RETRY_SLEEP - - -class LockKey: - """ - Lock Key - """ - - _key_prefix = "task-lock" - timeout = 24 * 60 * 60 - - def __init__(self, func: callable, *args, **kwargs): - self.func = func - self.args = args - self.kwargs = kwargs - - @property - def _key(self): - """ - cache key - """ - - return self.func.__name__ - - @property - def key(self): - """ - cache key - """ - - return f"{self._key_prefix}:{self._key}" - - -class TaskLock: - def __init__(self, func: callable, lock_key: LockKey.__class__ = LockKey, retry: bool = False, *args, **kwargs): - self.func = func - self.lock_key = lock_key(func, *args, **kwargs) - self.retry = retry - self.args = args - self.kwargs = kwargs - - # pylint: disable=R1710 - def __call__(self): - """ - run task or skip - """ - - # check status - if not self.set_status(): - logger.warning("[TaskLockExecuteTask] Duplicate => %s", self.func) - if self.retry: - self.retry_task() - return - - # run task - try: - logger.debug( - "[TaskLockExecuteTask] Task => %s; LockKey => %s; Args => %s; Kwargs => %s", - self.func, - self.lock_key, - self.args, - self.kwargs, - ) - return self.func(*self.args, **self.kwargs) - except Exception as err: - raise err - # clean status - finally: - self.clean_status() - - def retry_task(self): - result = self.args[0].delay(*self.args[1:], **self.kwargs) - time.sleep(CELERY_RETRY_SLEEP) - logger.info("[TaskLockExecuteTask] Retry => %s; Result => %s", self.func, result) - - @property - def cache_key(self) -> str: - """ - lock key and timeout - """ - - return self.lock_key.key - - @property - def cache_timeout(self) -> int: - """ - lick timeout - """ - - return self.lock_key.timeout - - def set_status(self) -> bool: - """ - set running - """ - - return cache.set(self.cache_key, True, timeout=self.cache_timeout, nx=True) - - def clean_status(self) -> None: - """ - set running finished - """ - - cache.delete(self.cache_key) - - -def task_lock(func: callable = None, lock_key: LockKey.__class__ = LockKey, retry: bool = False): - """ - wait task finished - """ - - @wraps(func) - def wrapper(task: callable): - @wraps(task) - def warp_func(*args, **kwargs): - TaskLock(task, lock_key, retry, *args, **kwargs)() - - return warp_func - - if callable(func): - return wrapper(func) - return wrapper diff --git a/apps/home/__init__.py b/apps/home/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/home/admin.py b/apps/home/admin.py deleted file mode 100644 index b1c3fbc..0000000 --- a/apps/home/admin.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib import admin - -from apps.home.models import MetaConfig - - -@admin.register(MetaConfig) -class MetaConfigAdmin(admin.ModelAdmin): - list_display = ["key", "is_public", "val"] - ordering = ["key"] diff --git a/apps/home/apps.py b/apps/home/apps.py deleted file mode 100644 index 1d2b9d1..0000000 --- a/apps/home/apps.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.apps import AppConfig -from django.db.models.signals import post_migrate -from django.utils.translation import gettext_lazy - - -class HomeConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.home" - verbose_name = gettext_lazy("Home Module") - - def ready(self): - # pylint: disable=C0415 - from apps.home.utils import registry_build_in_config_key - - post_migrate.connect(registry_build_in_config_key, self) diff --git a/apps/home/constants.py b/apps/home/constants.py deleted file mode 100644 index c33d282..0000000 --- a/apps/home/constants.py +++ /dev/null @@ -1,34 +0,0 @@ -from dataclasses import dataclass -from typing import Tuple - -# pylint: disable=C0103 -CONFIG_DEFAULT_T = Tuple[str, str] - - -# pylint: disable=C0103,R0902 -@dataclass -class BuildInKeys: - """ - Build in config keys - """ - - LOGO_URL: CONFIG_DEFAULT_T = "logo_url", "/favicon.ico" - WEBSITE_TITLE: CONFIG_DEFAULT_T = "website_title", "" - BRAND_NAME: CONFIG_DEFAULT_T = "brand_name", "OVINC" - BRAND_TITLE: CONFIG_DEFAULT_T = "brand_title", "OVINC CN" - BRAND_DESC: CONFIG_DEFAULT_T = "brand_desc", "" - BRAND_VISION: CONFIG_DEFAULT_T = "brand_vision", "" - CONTACT_PICTURE_URL: CONFIG_DEFAULT_T = "contact_picture_url", "" - CONTACT_MAIL: CONFIG_DEFAULT_T = "contact_email", "" - CONTACT_PHONE: CONFIG_DEFAULT_T = "contact_phone", "" - CONTACT_PLACE: CONFIG_DEFAULT_T = "contact_place", "" - COPYRIGHT: CONFIG_DEFAULT_T = "copyright", "OVINC CN" - MIIT_FILLING_CODE: CONFIG_DEFAULT_T = "miit_filling_code", "" - MIIT_FILLING_URL: CONFIG_DEFAULT_T = "miit_filling_url", "" - GONGAN_FILLING_ID: CONFIG_DEFAULT_T = "gongan_filling_id", "" - GONGAN_FILLING_URL: CONFIG_DEFAULT_T = "gongan_filling_url", "" - USER_AGREEMENT: CONFIG_DEFAULT_T = "user_agreement", "" - PRIVACY_AGREEMENT: CONFIG_DEFAULT_T = "privacy_agreement", "" - BACKGROUND_IMAGE: CONFIG_DEFAULT_T = "background_image", "" - USERNAME_EXTRA_REGEX: CONFIG_DEFAULT_T = "username_extra_regex", "" - REGISTRY_LOCKED: CONFIG_DEFAULT_T = "registry_locked", "" diff --git a/apps/home/exceptions.py b/apps/home/exceptions.py deleted file mode 100644 index e4eb8e2..0000000 --- a/apps/home/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.utils.translation import gettext_lazy -from rest_framework import status -from rest_framework.exceptions import APIException - - -class LanguageCodeInvalid(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Language Code Invalid") - - -class SearchIPFailed(APIException): - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - default_detail = gettext_lazy("Search IP Failed") diff --git a/apps/home/migrations/0001_initial.py b/apps/home/migrations/0001_initial.py deleted file mode 100644 index 9ac4e94..0000000 --- a/apps/home/migrations/0001_initial.py +++ /dev/null @@ -1,38 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2.13 on 2024-07-10 09:44 - -import ovinc_client.core.models -import ovinc_client.core.utils -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="MetaConfig", - fields=[ - ( - "id", - ovinc_client.core.models.UniqIDField( - default=ovinc_client.core.utils.uniq_id_without_time, - max_length=32, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("key", models.CharField(max_length=255, unique=True, verbose_name="Config Key")), - ("val", models.TextField(blank=True, null=True, verbose_name="Config Value")), - ("is_public", models.BooleanField(db_index=True, default=True, verbose_name="Is Public")), - ], - options={ - "verbose_name": "Meta Config", - "verbose_name_plural": "Meta Configs", - "ordering": ["key"], - }, - ), - ] diff --git a/apps/home/migrations/__init__.py b/apps/home/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/home/models.py b/apps/home/models.py deleted file mode 100644 index 60ac1a7..0000000 --- a/apps/home/models.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Dict - -from django.db import models -from django.db.models import Q -from django.utils.translation import gettext_lazy -from ovinc_client.core.models import BaseModel, UniqIDField - - -class MetaConfig(BaseModel): - """ - Meta Config - """ - - id = UniqIDField(gettext_lazy("ID"), primary_key=True) - key = models.CharField(gettext_lazy("Config Key"), unique=True, max_length=255) - val = models.TextField(gettext_lazy("Config Value"), null=True, blank=True) - is_public = models.BooleanField(gettext_lazy("Is Public"), default=True, db_index=True) - - class Meta: - verbose_name = gettext_lazy("Meta Config") - verbose_name_plural = gettext_lazy("Meta Configs") - ordering = ["key"] - - def __str__(self): - return str(self.key) - - @classmethod - def as_map(cls, condition: Q) -> Dict[str, str]: - return {config.key: config.val for config in cls.objects.filter(condition)} diff --git a/apps/home/serializers.py b/apps/home/serializers.py deleted file mode 100644 index a068346..0000000 --- a/apps/home/serializers.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.utils.translation import check_for_language, gettext_lazy -from rest_framework import serializers - -from apps.home.exceptions import LanguageCodeInvalid - - -class I18nRequestSerializer(serializers.Serializer): - """ - I18n - """ - - language = serializers.CharField(label=gettext_lazy("Language Code")) - - def validate_language(self, language: str) -> str: - if check_for_language(language): - return language - raise LanguageCodeInvalid() - - -class MetaConfigQuerySerializer(serializers.Serializer): - """ - MetaConfig - """ - - key = serializers.CharField(label=gettext_lazy("Config Key"), required=False) diff --git a/apps/home/urls.py b/apps/home/urls.py deleted file mode 100644 index 510dada..0000000 --- a/apps/home/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from rest_framework.routers import DefaultRouter - -from apps.home.views import HomeView, I18nViewSet, MetaConfigViewSet - -router = DefaultRouter() -router.register("", HomeView) -router.register("i18n", I18nViewSet, basename="i18n") -router.register("meta", MetaConfigViewSet) - -urlpatterns = router.urls diff --git a/apps/home/utils.py b/apps/home/utils.py deleted file mode 100644 index 0ecd49b..0000000 --- a/apps/home/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -from dataclasses import fields - -from apps.home.constants import BuildInKeys -from apps.home.models import MetaConfig - - -def registry_build_in_config_key(*args, **kwargs): - """ - Registry Build In Config Key To DB - """ - - for field in fields(BuildInKeys): - key, val = getattr(BuildInKeys, field.name) - MetaConfig.objects.get_or_create(key=key, defaults={"val": val}) diff --git a/apps/home/views.py b/apps/home/views.py deleted file mode 100644 index 9f786d0..0000000 --- a/apps/home/views.py +++ /dev/null @@ -1,88 +0,0 @@ -from django.conf import settings -from django.conf.global_settings import LANGUAGE_COOKIE_NAME -from django.contrib.auth import get_user_model -from django.db.models import Q -from ovinc_client.core.auth import SessionAuthenticate -from ovinc_client.core.viewsets import MainViewSet -from rest_framework.response import Response - -from apps.account.models import User -from apps.home.models import MetaConfig -from apps.home.serializers import I18nRequestSerializer, MetaConfigQuerySerializer - -USER_MODEL: User = get_user_model() - - -class HomeView(MainViewSet): - """ - Home View - """ - - queryset = USER_MODEL.get_queryset() - authentication_classes = [SessionAuthenticate] - - def list(self, request, *args, **kwargs): - msg = f"[{request.method}] Connect Success" - return Response( - { - "resp": msg, - "user": request.user.username, - } - ) - - -class I18nViewSet(MainViewSet): - """ - International - """ - - authentication_classes = [SessionAuthenticate] - - def create(self, request, *args, **kwargs): - """ - Change Language - """ - - # Varify Request Data - request_serializer = I18nRequestSerializer(data=request.data) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # Change Lang - lang_code = request_data["language"] - response = Response() - response.set_cookie( - LANGUAGE_COOKIE_NAME, - lang_code, - max_age=settings.SESSION_COOKIE_AGE, - domain=settings.SESSION_COOKIE_DOMAIN, - path=settings.SESSION_COOKIE_PATH, - secure=settings.SESSION_COOKIE_SECURE or None, - httponly=settings.SESSION_COOKIE_HTTPONLY or None, - samesite=settings.SESSION_COOKIE_SAMESITE, - ) - return response - - -class MetaConfigViewSet(MainViewSet): - """ - Meta Config - """ - - queryset = MetaConfig.get_queryset() - authentication_classes = [SessionAuthenticate] - - def list(self, request, *args, **kwargs): - """ - Meta Config - """ - - request_serializer = MetaConfigQuerySerializer(data=request.query_params) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - condition = Q(is_public=True) - if request_data: - condition &= Q(**request_data) - - return Response(MetaConfig.as_map(condition)) diff --git a/apps/notice/__init__.py b/apps/notice/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/notice/admin.py b/apps/notice/admin.py deleted file mode 100644 index 19d04a5..0000000 --- a/apps/notice/admin.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.contrib import admin - -from apps.notice.models import NoticeLog, Robot - - -@admin.register(Robot) -class RobotAdmin(admin.ModelAdmin): - list_display = ["id", "name", "webhook"] - ordering = ["name"] - - -@admin.register(NoticeLog) -class NoticeLogAdmin(admin.ModelAdmin): - list_display = ["id", "receivers", "content", "result", "send_at"] - ordering = ["-send_at"] diff --git a/apps/notice/apps.py b/apps/notice/apps.py deleted file mode 100644 index 0b254f0..0000000 --- a/apps/notice/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy - - -class NoticeConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.notice" - verbose_name = gettext_lazy("Notice") diff --git a/apps/notice/constants.py b/apps/notice/constants.py deleted file mode 100644 index 0c56e25..0000000 --- a/apps/notice/constants.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.utils.translation import gettext_lazy -from ovinc_client.core.models import TextChoices - - -class NoticeWayChoices(TextChoices): - MSG = "msg", gettext_lazy("msg") - MAIL = "mail", gettext_lazy("mail") - ROBOT = "robot", gettext_lazy("robot") diff --git a/apps/notice/migrations/0001_initial.py b/apps/notice/migrations/0001_initial.py deleted file mode 100644 index beb4f56..0000000 --- a/apps/notice/migrations/0001_initial.py +++ /dev/null @@ -1,62 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2 on 2023-04-15 08:59 - -import ovinc_client.core.models -import ovinc_client.core.utils -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="NoticeLog", - fields=[ - ( - "id", - ovinc_client.core.models.UniqIDField( - default=ovinc_client.core.utils.uniq_id_without_time, - max_length=32, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("receivers", models.JSONField(verbose_name="Receivers")), - ("content", models.JSONField(verbose_name="Content")), - ("extra_params", models.JSONField(default=dict, null=True, verbose_name="Extra Params")), - ("result", models.TextField(blank=True, null=True, verbose_name="Result")), - ("send_at", models.DateTimeField(auto_now_add=True, verbose_name="Send At")), - ], - options={ - "verbose_name": "Notice Log", - "verbose_name_plural": "Notice Log", - "ordering": ["-send_at"], - }, - ), - migrations.CreateModel( - name="Robot", - fields=[ - ( - "id", - ovinc_client.core.models.UniqIDField( - default=ovinc_client.core.utils.uniq_id_without_time, - max_length=32, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("webhook", models.CharField(max_length=255, verbose_name="Webhook")), - ("name", models.CharField(max_length=32, verbose_name="Name")), - ], - options={ - "verbose_name": "Robot", - "verbose_name_plural": "Robot", - "ordering": ["id"], - }, - ), - ] diff --git a/apps/notice/migrations/0002_alter_noticelog_send_at_alter_robot_name.py b/apps/notice/migrations/0002_alter_noticelog_send_at_alter_robot_name.py deleted file mode 100644 index 364223f..0000000 --- a/apps/notice/migrations/0002_alter_noticelog_send_at_alter_robot_name.py +++ /dev/null @@ -1,23 +0,0 @@ -# pylint: disable=R0801,C0103 -# Generated by Django 4.2.11 on 2024-05-08 03:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("notice", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="noticelog", - name="send_at", - field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="Send At"), - ), - migrations.AlterField( - model_name="robot", - name="name", - field=models.CharField(db_index=True, max_length=32, verbose_name="Name"), - ), - ] diff --git a/apps/notice/migrations/__init__.py b/apps/notice/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/notice/models.py b/apps/notice/models.py deleted file mode 100644 index 9ae339d..0000000 --- a/apps/notice/models.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy -from ovinc_client.core.constants import MAX_CHAR_LENGTH, SHORT_CHAR_LENGTH -from ovinc_client.core.models import BaseModel, UniqIDField - - -class NoticeLog(BaseModel): - """ - Notice Log - """ - - id = UniqIDField(gettext_lazy("ID")) - receivers = models.JSONField(gettext_lazy("Receivers")) - content = models.JSONField(gettext_lazy("Content")) - extra_params = models.JSONField(gettext_lazy("Extra Params"), default=dict, null=True) - result = models.TextField(gettext_lazy("Result"), blank=True, null=True) - send_at = models.DateTimeField(gettext_lazy("Send At"), auto_now_add=True, db_index=True) - - class Meta: - verbose_name = gettext_lazy("Notice Log") - verbose_name_plural = verbose_name - ordering = ["-send_at"] - - -class Robot(BaseModel): - """ - Robot of Wecom or Feishu - """ - - id = UniqIDField(gettext_lazy("ID")) - webhook = models.CharField(gettext_lazy("Webhook"), max_length=MAX_CHAR_LENGTH) - name = models.CharField(gettext_lazy("Name"), max_length=SHORT_CHAR_LENGTH, db_index=True) - - class Meta: - verbose_name = gettext_lazy("Robot") - verbose_name_plural = verbose_name - ordering = ["id"] diff --git a/apps/notice/rates.py b/apps/notice/rates.py deleted file mode 100644 index 2c1986d..0000000 --- a/apps/notice/rates.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework.throttling import SimpleRateThrottle - - -class AppRateThrottle(SimpleRateThrottle): - """ - App Throttle - """ - - scope = "app" - - def get_cache_key(self, request, view): - return f'rate:{self.scope}:{getattr(request.user, "app_code", "")}' diff --git a/apps/notice/serializers.py b/apps/notice/serializers.py deleted file mode 100644 index 191ecc6..0000000 --- a/apps/notice/serializers.py +++ /dev/null @@ -1,71 +0,0 @@ -from django.conf import settings -from django.utils.translation import gettext_lazy -from rest_framework import serializers - -from apps.notice.models import Robot - - -class NoticeRequestSerializer(serializers.Serializer): - """ - Notice Base - """ - - usernames = serializers.ListField( - label=gettext_lazy("Username List"), child=serializers.CharField(label=gettext_lazy("Username")) - ) - content = serializers.JSONField(label=gettext_lazy("Content")) - - -class MailContentSerializer(serializers.Serializer): - """ - Mail Content - """ - - title = serializers.CharField(label=gettext_lazy("Title")) - content = serializers.CharField(label=gettext_lazy("Content")) - - -class MailRequestSerialzier(NoticeRequestSerializer): - """ - Mail - """ - - content = MailContentSerializer(label=gettext_lazy("Content")) - - -class SmsContentSerializer(serializers.Serializer): - """ - SMS Content - """ - - tid = serializers.CharField(label=gettext_lazy("Template ID")) - params = serializers.ListSerializer(label=gettext_lazy("Params"), child=serializers.CharField()) - - -class SmsRequestSerializer(NoticeRequestSerializer): - """ - SMS - """ - - content = SmsContentSerializer(label=gettext_lazy("Content")) - - -class RegistryRobotSerializer(serializers.ModelSerializer): - """ - registry robot - """ - - class Meta: - model = Robot - fields = "__all__" - - -class RobotRequestSerializer(serializers.Serializer): - """ - Robot - """ - - robots = serializers.ListField( - label=gettext_lazy("Robot ID"), child=serializers.CharField(label=gettext_lazy("Robot ID")) - ) - content = serializers.JSONField(label=gettext_lazy("Content"), help_text=settings.NOTICE_ROBOT_CONTENT_HELP) diff --git a/apps/notice/urls.py b/apps/notice/urls.py deleted file mode 100644 index 8b05e17..0000000 --- a/apps/notice/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework.routers import DefaultRouter - -from apps.notice.views import NoticeViewSet - -router = DefaultRouter() -router.register("", NoticeViewSet) - -urlpatterns = router.urls diff --git a/apps/notice/utils/__init__.py b/apps/notice/utils/__init__.py deleted file mode 100644 index 99e1900..0000000 --- a/apps/notice/utils/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from apps.notice.utils.base import NoticeBase -from apps.notice.utils.mail import MailHandler -from apps.notice.utils.msg import MsgHandler -from apps.notice.utils.robot import RobotHandler - -__all__ = [ - "NoticeBase", - "MailHandler", - "MsgHandler", - "RobotHandler", -] diff --git a/apps/notice/utils/base.py b/apps/notice/utils/base.py deleted file mode 100644 index 828421a..0000000 --- a/apps/notice/utils/base.py +++ /dev/null @@ -1,96 +0,0 @@ -import abc -import traceback -from typing import List, Union - -from django.contrib.auth import get_user_model -from django.utils.module_loading import import_string -from django.utils.translation import gettext -from ovinc_client.core.logger import logger - -from apps.account.models import User -from apps.cel.tasks import send_notice -from apps.notice.constants import NoticeWayChoices -from apps.notice.models import NoticeLog - -USER_MODEL: User = get_user_model() - - -class NoticeBase: - """ - Notice Base - """ - - # pylint: disable=R1710 - @classmethod - def send_notice(cls, notice_type: str, is_async: bool = True, **kwargs) -> None: - if is_async: - send_notice.delay(notice_type, **kwargs) - return None - send_notice(notice_type, **kwargs) - - @classmethod - def get_instance(cls, notice_type: str, **kwargs) -> "NoticeBase": - instance_map = {_value: f"apps.notice.utils.{_value.capitalize()}Handler" for _value in NoticeWayChoices.values} - try: - notice_type_class = import_string(instance_map[notice_type]) - return notice_type_class(**kwargs) - except KeyError as err: - raise KeyError(gettext("Notice Type not Exists => %s") % notice_type) from err - - def __init__(self, usernames: List[str], content: Union[dict, str], **kwargs) -> None: - self.kwargs = kwargs - self.receivers = self._load_receivers(usernames) - self.content = self._build_content(content) - - @property - @abc.abstractmethod - def property_key(self) -> str: - raise NotImplementedError - - def send(self) -> None: - """ - send notice - """ - - logger.info("[%s SendNotice] Content => %s", self.__class__.__name__, self.content) - try: - result = self._send() - logger.info("[%s SendNoticeSuccess] Result => %s", self.__class__.__name__, result) - except Exception as err: # pylint: disable=W0718 - msg = traceback.format_exc() - logger.error("[%s SendNoticeFailed] Err => %s; Detail => %s", self.__class__.__name__, err, msg) - result = {"err": str(err)} - NoticeLog.objects.create( - receivers=self.receivers, content=self.content, extra_params=self.kwargs, result=str(result) - ) - return result - - @abc.abstractmethod - def _send(self) -> None: - """ - send notice - """ - - raise NotImplementedError - - def _load_receivers(self, usernames: List[str]) -> List[str]: - """ - trans username to receiver - """ - - logger.info("[%s LoadReceivers] Usernames => %s", self.__class__.__name__, usernames) - # load user - users = list(User.objects.filter(username__in=usernames)) - receivers = list(getattr(u, self.property_key) for u in users if getattr(u, self.property_key, None)) - if not receivers and "receivers" in self.kwargs: - receivers = self.kwargs["receivers"] - logger.info("[%s LoadReceivers] Receivers => %s", self.__class__.__name__, receivers) - # return - return receivers - - def _build_content(self, content: Union[dict, str]) -> any: - """ - build content for notice - """ - - return content diff --git a/apps/notice/utils/mail.py b/apps/notice/utils/mail.py deleted file mode 100644 index 05abb13..0000000 --- a/apps/notice/utils/mail.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import List, Union - -from django.conf import settings -from django.core.mail import send_mail -from django.core.mail.backends.smtp import EmailBackend - -from apps.notice.utils.base import NoticeBase - - -class MailHandler(NoticeBase): - """ - Send Mail - """ - - def __init__(self, usernames: List[str], content: Union[dict, str], **kwargs) -> None: - super().__init__(usernames=usernames, content=content, **kwargs) - self.host = settings.NOTICE_MAIL_HOST - self.port = settings.NOTICE_MAIL_PORT - self.username = settings.NOTICE_MAIL_USERNAME - self.password = settings.NOTICE_MAIL_PASSWORD - - @property - def property_key(self) -> str: - return "email_address" - - def _send(self) -> None: - connection = EmailBackend( - host=self.host, port=self.port, username=self.username, password=self.password, use_ssl=True - ) - send_mail( - subject=self.content["title"], - message=self.content["content"], - from_email=self.username, - recipient_list=self.receivers, - connection=connection, - ) diff --git a/apps/notice/utils/msg.py b/apps/notice/utils/msg.py deleted file mode 100644 index 465aad7..0000000 --- a/apps/notice/utils/msg.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import List, Union - -from django.conf import settings -from tencentcloud.common import credential -from tencentcloud.sms.v20210111 import models, sms_client - -from apps.notice.utils.base import NoticeBase - - -class MsgHandler(NoticeBase): - """ - Send SMS - """ - - def __init__(self, usernames: List[str], content: Union[dict, str], **kwargs): - super().__init__(usernames, content, **kwargs) - self._cred = credential.Credential(settings.NOTICE_MSG_TCLOUD_ID, settings.NOTICE_MSG_TCLOUD_KEY) - self._client = sms_client.SmsClient(self._cred, settings.NOTICE_MSG_TCLUD_REGION) - - @property - def property_key(self): - return "phone_number" - - def _send(self) -> None: - req = models.SendSmsRequest() - req.SmsSdkAppId = settings.NOTICE_MSG_TCLOUD_APP - req.SignName = settings.NOTICE_MSG_TCLOUD_SIGN - req.TemplateId = self.content["tid"] - req.TemplateParamSet = self.content["params"] - req.PhoneNumberSet = self.receivers - resp = self._client.SendSms(req) - return resp.to_json_string(indent=2) diff --git a/apps/notice/utils/robot.py b/apps/notice/utils/robot.py deleted file mode 100644 index 9be49b0..0000000 --- a/apps/notice/utils/robot.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import List, Union - -import httpx -from ovinc_client.core.logger import logger - -from apps.notice.models import Robot -from apps.notice.utils.base import NoticeBase - - -class RobotHandler(NoticeBase): - """ - Robot - """ - - property_key = None - - # pylint: disable=W0246 - def __init__(self, robots: List[str], content: Union[dict, str], **kwargs) -> None: - super().__init__(robots, content, **kwargs) - - def _send(self) -> list: - result = [] - with httpx.Client() as client: - for robot in self.receivers: - resp = client.post(robot, json=self.content) - result.append(resp.json()) - return result - - # pylint: disable=W0237 - def _load_receivers(self, robot_ids: List[str]) -> List[str]: - """ - robot id - """ - - logger.info("[%s LoadReceivers] Robots => %s", self.__class__.__name__, robot_ids) - robots = Robot.objects.filter(id__in=robot_ids) - logger.info( - "[%s LoadReceivers] Receivers => %s", - self.__class__.__name__, - [f"{r.id}:{r.webhook}" for r in robots], - ) - return [r.webhook for r in robots] diff --git a/apps/notice/views.py b/apps/notice/views.py deleted file mode 100644 index 5990a11..0000000 --- a/apps/notice/views.py +++ /dev/null @@ -1,91 +0,0 @@ -from django.shortcuts import get_object_or_404 -from ovinc_client.core.viewsets import MainViewSet -from rest_framework.decorators import action -from rest_framework.response import Response - -from apps.notice.constants import NoticeWayChoices -from apps.notice.models import NoticeLog, Robot -from apps.notice.rates import AppRateThrottle -from apps.notice.serializers import ( - MailRequestSerialzier, - RegistryRobotSerializer, - RobotRequestSerializer, - SmsRequestSerializer, -) -from apps.notice.utils import NoticeBase -from core.auth import ApplicationAuthenticate - - -class NoticeViewSet(MainViewSet): - """ - Notice - """ - - queryset = NoticeLog.get_queryset() - authentication_classes = [ApplicationAuthenticate] - throttle_classes = [AppRateThrottle] - - @action(methods=["POST"], detail=False) - def mail(self, request, *args, **kwargs): - """ - mail - """ - - # validate request - request_serializer = MailRequestSerialzier(data=request.data) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # send - NoticeBase.send_notice(NoticeWayChoices.MAIL, **request_data) - return Response() - - @action(methods=["POST"], detail=False) - def sms(self, request, *args, **kwargs): - """ - sms - """ - - # validate request - request_serializer = SmsRequestSerializer(data=request.data) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # send - NoticeBase.send_notice(NoticeWayChoices.MSG, **request_data) - return Response() - - @action(methods=["POST"], detail=False) - def robot(self, request, *args, **kwargs): - """ - robot of Wecom or Feishu - """ - - # validate request - request_serializer = RobotRequestSerializer(data=request.data) - request_serializer.is_valid(raise_exception=True) - request_data = request_serializer.validated_data - - # send - NoticeBase.send_notice(NoticeWayChoices.ROBOT, **request_data) - return Response() - - @action(methods=["POST"], detail=False) - def registry_robot(self, request, *args, **kwargs): - """ - registry robot - """ - - # get instance - instance = None - if request.data.get("id"): - instance = get_object_or_404(Robot, pk=request.data.pop("id")) - - # validate request - request_serializer = RegistryRobotSerializer(instance=instance, data=request.data, partial=bool(instance)) - request_serializer.is_valid(raise_exception=True) - - # save - request_serializer.save() - - return Response(request_serializer.data) diff --git a/apps/tcloud/__init__.py b/apps/tcloud/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tcloud/admin.py b/apps/tcloud/admin.py deleted file mode 100644 index 163a8f1..0000000 --- a/apps/tcloud/admin.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.contrib import admin - -from apps.tcloud.models import AuditCallback - - -@admin.register(AuditCallback) -class AuditCallbackAdmin(admin.ModelAdmin): - list_display = [ - "id", - "bucket_region", - "bucket_id", - "event_name", - "audit_id", - "is_sensitive", - "creation_time", - "callback_time", - "updated_time", - "is_handled", - ] - list_filter = ["bucket_region", "bucket_id", "event_name", "is_sensitive", "is_handled"] - list_editable = ["is_handled"] diff --git a/apps/tcloud/apps.py b/apps/tcloud/apps.py deleted file mode 100644 index ca3d266..0000000 --- a/apps/tcloud/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy - - -class TCloudConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.tcloud" - verbose_name = gettext_lazy("TCloud") diff --git a/apps/tcloud/constants.py b/apps/tcloud/constants.py deleted file mode 100644 index e104c95..0000000 --- a/apps/tcloud/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.utils.translation import gettext_lazy -from ovinc_client.core.models import TextChoices - - -class CICallbackEventName(TextChoices): - REVIEW_IMAGE = "ReviewImage", gettext_lazy("Review Image") - REVIEW_DOCUMENT = "ReviewDocument", gettext_lazy("Review Document") - REVIEW_VIDEO = "ReviewVideo", gettext_lazy("Review Video") - REVIEW_AUDIO = "ReviewAudio", gettext_lazy("Review Audio") - REVIEW_TEXT = "ReviewText", gettext_lazy("Review Text") - REVIEW_HTML = "ReviewHtml", gettext_lazy("Review HTML") diff --git a/apps/tcloud/migrations/0001_initial.py b/apps/tcloud/migrations/0001_initial.py deleted file mode 100644 index 3cfd79a..0000000 --- a/apps/tcloud/migrations/0001_initial.py +++ /dev/null @@ -1,50 +0,0 @@ -# pylint: disable=C0301,C0103 -# Generated by Django 4.2.16 on 2024-11-29 12:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="AuditCallback", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False, verbose_name="ID")), - ("bucket_region", models.CharField(max_length=32, verbose_name="Bucket Region")), - ("bucket_id", models.CharField(db_index=True, max_length=255, verbose_name="Bucket ID")), - ( - "event_name", - models.CharField( - choices=[ - ("ReviewImage", "Review Image"), - ("ReviewDocument", "Review Document"), - ("ReviewVideo", "Review Video"), - ("ReviewAudio", "Review Audio"), - ("ReviewText", "Review Text"), - ("ReviewHtml", "Review HTML"), - ], - db_index=True, - max_length=32, - verbose_name="Event Name", - ), - ), - ("audit_id", models.CharField(db_index=True, max_length=255, verbose_name="Audit ID")), - ("is_sensitive", models.BooleanField(db_index=True, verbose_name="Result")), - ("detail", models.JSONField(verbose_name="Detail")), - ("creation_time", models.DateTimeField(db_index=True, verbose_name="Audit Time")), - ("callback_time", models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="Callback Time")), - ("updated_time", models.DateTimeField(auto_now=True, db_index=True, verbose_name="Updated Time")), - ("is_handled", models.BooleanField(db_index=True, default=False, verbose_name="Handled")), - ], - options={ - "verbose_name": "Audit Callback", - "verbose_name_plural": "Audit Callback", - "ordering": ["-id"], - "unique_together": {("bucket_region", "bucket_id", "event_name", "audit_id")}, - }, - ), - ] diff --git a/apps/tcloud/migrations/0002_alter_auditcallback_creation_time.py b/apps/tcloud/migrations/0002_alter_auditcallback_creation_time.py deleted file mode 100644 index c98d36e..0000000 --- a/apps/tcloud/migrations/0002_alter_auditcallback_creation_time.py +++ /dev/null @@ -1,18 +0,0 @@ -# pylint: disable=C0301,C0103 -# Generated by Django 4.2.16 on 2024-11-29 13:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("tcloud", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="auditcallback", - name="creation_time", - field=models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name="Audit Time"), - ), - ] diff --git a/apps/tcloud/migrations/__init__.py b/apps/tcloud/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tcloud/models.py b/apps/tcloud/models.py deleted file mode 100644 index 08ea0a5..0000000 --- a/apps/tcloud/models.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.contrib.auth import get_user_model -from django.db import models -from django.utils.translation import gettext_lazy -from ovinc_client.core.constants import MAX_CHAR_LENGTH, SHORT_CHAR_LENGTH -from ovinc_client.core.models import BaseModel - -from apps.tcloud.constants import CICallbackEventName -from apps.tcloud.parsers import TCICallbackParser, TCIDocumentCallbackParser - -USER_MODEL = get_user_model() - - -class AuditCallback(BaseModel): - """ - Audit Callback - """ - - id = models.BigAutoField(verbose_name=gettext_lazy("ID"), primary_key=True) - bucket_region = models.CharField(verbose_name=gettext_lazy("Bucket Region"), max_length=SHORT_CHAR_LENGTH) - bucket_id = models.CharField(verbose_name=gettext_lazy("Bucket ID"), max_length=MAX_CHAR_LENGTH, db_index=True) - event_name = models.CharField( - verbose_name=gettext_lazy("Event Name"), - max_length=SHORT_CHAR_LENGTH, - db_index=True, - choices=CICallbackEventName.choices, - ) - audit_id = models.CharField(verbose_name=gettext_lazy("Audit ID"), max_length=MAX_CHAR_LENGTH, db_index=True) - is_sensitive = models.BooleanField(verbose_name=gettext_lazy("Is Sensitive"), db_index=True) - detail = models.JSONField(verbose_name=gettext_lazy("Detail")) - creation_time = models.CharField( - verbose_name=gettext_lazy("Audit Time"), max_length=SHORT_CHAR_LENGTH, db_index=True, null=True, blank=True - ) - callback_time = models.DateTimeField(verbose_name=gettext_lazy("Callback Time"), db_index=True, auto_now_add=True) - updated_time = models.DateTimeField(verbose_name=gettext_lazy("Updated Time"), db_index=True, auto_now=True) - is_handled = models.BooleanField(verbose_name=gettext_lazy("Handled"), db_index=True, default=False) - - class Meta: - verbose_name = gettext_lazy("Audit Callback") - verbose_name_plural = verbose_name - ordering = ["-id"] - unique_together = [["bucket_region", "bucket_id", "event_name", "audit_id"]] - - def __str__(self): - return f"{self.bucket_region}:{self.bucket_id}:{self.event_name}:{self.audit_id}" - - @classmethod - def add_callback(cls, data: dict) -> "AuditCallback": - event_name = data["EventName"] - match event_name: - case CICallbackEventName.REVIEW_DOCUMENT: - parser = TCIDocumentCallbackParser(data) - case _: - parser = TCICallbackParser(data) - return cls.objects.create( - bucket_region=parser.bucket_region, - bucket_id=parser.bucket_id, - event_name=event_name, - audit_id=parser.audit_id, - is_sensitive=parser.is_sensitive, - detail=parser.detail, - creation_time=parser.creation_time, - ) diff --git a/apps/tcloud/parsers.py b/apps/tcloud/parsers.py deleted file mode 100644 index eba89cc..0000000 --- a/apps/tcloud/parsers.py +++ /dev/null @@ -1,41 +0,0 @@ -class TCICallbackParser: - """ - callback parser - """ - - def __init__(self, data: dict): - self.data = data - - @property - def is_sensitive(self) -> bool: - return self.data["JobsDetail"]["Result"] != 0 - - @property - def audit_id(self) -> str: - return self.data["JobsDetail"]["JobId"] - - @property - def detail(self) -> dict: - return self.data - - @property - def bucket_id(self) -> str: - return self.data["JobsDetail"]["BucketId"] - - @property - def bucket_region(self) -> str: - return self.data["JobsDetail"]["Region"] - - @property - def creation_time(self) -> str: - return self.data["JobsDetail"]["CreationTime"] - - -class TCIDocumentCallbackParser(TCICallbackParser): - """ - document callback parser - """ - - @property - def is_sensitive(self) -> bool: - return self.data["JobsDetail"]["Suggestion"] != 0 diff --git a/apps/tcloud/serializers.py b/apps/tcloud/serializers.py deleted file mode 100644 index f85614b..0000000 --- a/apps/tcloud/serializers.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework import serializers - -from apps.tcloud.constants import CICallbackEventName - - -class TCICallbackSerializer(serializers.Serializer): - EventName = serializers.ChoiceField(choices=CICallbackEventName.choices) - JobsDetail = serializers.JSONField() diff --git a/apps/tcloud/urls.py b/apps/tcloud/urls.py deleted file mode 100644 index 085b288..0000000 --- a/apps/tcloud/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf import settings -from django.urls import include, path -from rest_framework.routers import SimpleRouter - -from apps.tcloud.views import AuditCallbackViewSet - -router = SimpleRouter() -router.register("", AuditCallbackViewSet, basename="audit_callback") - -urlpatterns = [path(f"ci/callback/{settings.TCI_AUDIT_CALLBACK_PREFIX}", include(router.urls))] diff --git a/apps/tcloud/views.py b/apps/tcloud/views.py deleted file mode 100644 index 693a166..0000000 --- a/apps/tcloud/views.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.conf import settings -from django.utils.translation import gettext -from ovinc_client.core.auth import SessionAuthenticate -from ovinc_client.core.viewsets import MainViewSet -from rest_framework.request import Request -from rest_framework.response import Response - -from apps.cel.tasks import send_notice -from apps.notice.constants import NoticeWayChoices -from apps.tcloud.models import AuditCallback -from apps.tcloud.serializers import TCICallbackSerializer - - -class AuditCallbackViewSet(MainViewSet): - """ - callback view set - """ - - authentication_classes = [SessionAuthenticate] - - def create(self, request: Request, *args, **kwargs) -> Response: - """ - callback - """ - # validate - req_slz = TCICallbackSerializer(data=request.data) - req_slz.is_valid(raise_exception=True) - # save - callback = AuditCallback.add_callback(req_slz.validated_data) - if callback.is_sensitive: - send_notice.delay( - notice_type=NoticeWayChoices.ROBOT, - robots=[settings.NOTICE_AUDIT_SENSITIVE_ROBOT], - content={ - "msgtype": "markdown", - "markdown": { - "content": gettext( - 'Sensitive Object Notification\n' - "Bucket Region: %(region)s\n" - "Bucket ID: %(bucket)s\n" - "Event Name: %(event)s\n" - "Audit ID: %(audit_id)s\n" - "Creation Time: %(creation_time)s\n" - ) - % { - "region": callback.bucket_region, - "bucket": callback.bucket_id, - "event": callback.get_event_name_display(), - "audit_id": callback.audit_id, - "creation_time": callback.creation_time, - } - }, - }, - ) - # response - return Response() diff --git a/bin/run.sh b/bin/run.sh deleted file mode 100755 index 0080b8d..0000000 --- a/bin/run.sh +++ /dev/null @@ -1,7 +0,0 @@ -#! /bin/sh - -python manage.py collectstatic --noinput -python manage.py migrate --noinput -nohup python manage.py celery worker -c ${WORKER_COUNT:-1} -l INFO >/dev/stdout 2>&1 & -nohup python manage.py celery beat -l INFO >/dev/stdout 2>&1 & -gunicorn --bind "[::]:8020" -w ${WEB_PROCESSES:-1} --threads ${WEB_THREADS:-10} -k uvicorn_worker.UvicornWorker --proxy-protocol --proxy-allow-from "*" --forwarded-allow-ips "*" entry.asgi:application diff --git a/core/__init__.py b/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/auth.py b/core/auth.py deleted file mode 100644 index d793a2d..0000000 --- a/core/auth.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.contrib.auth import get_user_model -from django.utils.translation import gettext -from rest_framework.authentication import BaseAuthentication -from rest_framework.request import Request - -from apps.application.models import Application -from core.constants import ( - APP_AUTH_HEADER_APPID_KEY, - APP_AUTH_HEADER_APPID_NONCE, - APP_AUTH_HEADER_APPID_SIGN, - APP_AUTH_HEADER_APPID_TIMESTAMP, -) -from core.exceptions import AppAuthFailed - -USER_MODEL = get_user_model() - - -class ApplicationAuthenticate(BaseAuthentication): - """ - Application Authenticate - """ - - # pylint: disable=W0236 - def authenticate(self, request: Request) -> (Application, None): - # load params - app_code = request.headers.get(APP_AUTH_HEADER_APPID_KEY) - signature = request.headers.get(APP_AUTH_HEADER_APPID_SIGN) - timestamp = request.headers.get(APP_AUTH_HEADER_APPID_TIMESTAMP) - nonce = request.headers.get(APP_AUTH_HEADER_APPID_NONCE) - if not app_code or not signature or not timestamp or not nonce: - raise AppAuthFailed(gettext("App Auth Headers Not Exist")) - # varify app - try: - app = Application.objects.get(pk=app_code) - except Application.DoesNotExist as err: # pylint: disable=E1101 - raise AppAuthFailed(gettext("App Not Exist")) from err - # verify secret - if app.check_sign(signature=signature, timestamp=timestamp, nonce=nonce): - return app, None - raise AppAuthFailed(gettext("Signature Invalid")) diff --git a/core/constants.py b/core/constants.py deleted file mode 100644 index fe8aac7..0000000 --- a/core/constants.py +++ /dev/null @@ -1,8 +0,0 @@ -APP_AUTH_HEADER_APPID_KEY = "ovinc-appid" -APP_AUTH_HEADER_APPID_SIGN = "ovinc-sign" -APP_AUTH_HEADER_APPID_TIMESTAMP = "ovinc-timestamp" -APP_AUTH_HEADER_APPID_NONCE = "ovinc-nonce" - -APP_AUTH_NONCE_CACHE_KEY = "ovinc-app-auth:nonce:{appid}:{nonce}" - -WECHAT_USER_AGENT_KEY = "micromessenger" diff --git a/core/exceptions.py b/core/exceptions.py deleted file mode 100644 index 6a69872..0000000 --- a/core/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.utils.translation import gettext_lazy -from rest_framework import status -from rest_framework.exceptions import APIException - - -class AuthTokenInvalid(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = gettext_lazy("Auth Token Invalid") - - -class AppAuthFailed(APIException): - status_code = status.HTTP_403_FORBIDDEN - default_detail = gettext_lazy("App Auth Failed") diff --git a/core/utils.py b/core/utils.py deleted file mode 100644 index 69810f2..0000000 --- a/core/utils.py +++ /dev/null @@ -1,7 +0,0 @@ -from rest_framework.request import Request - -from core.constants import WECHAT_USER_AGENT_KEY - - -def is_wechat(request: Request) -> bool: - return str(request.headers.get("User-Agent") or "").lower().find(WECHAT_USER_AGENT_KEY) != -1 diff --git a/entry/__init__.py b/entry/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/entry/asgi.py b/entry/asgi.py deleted file mode 100644 index 8d74061..0000000 --- a/entry/asgi.py +++ /dev/null @@ -1,13 +0,0 @@ -import os - -from channels.routing import ProtocolTypeRouter -from django.core.asgi import get_asgi_application -from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "entry.settings") - -application = ProtocolTypeRouter( - { - "http": OpenTelemetryMiddleware(get_asgi_application()), - } -) diff --git a/entry/settings.py b/entry/settings.py deleted file mode 100644 index a8c1fde..0000000 --- a/entry/settings.py +++ /dev/null @@ -1,264 +0,0 @@ -import os -from pathlib import Path - -import pymysql -from environ.environ import Env -from ovinc_client.core.logger import get_logging_config_dict -from ovinc_client.core.utils import getenv_or_raise, strtobool - -pymysql.install_as_MySQLdb() - -# Base Dir -BASE_DIR = Path(__file__).resolve().parent.parent - -# Env -env = Env() -env.read_env(os.path.join(BASE_DIR, ".env")) - -# DEBUG -DEBUG = strtobool(os.getenv("DEBUG", "False")) - -# APP_CODE & SECRET -APP_CODE = getenv_or_raise("APP_CODE") -APP_SECRET = getenv_or_raise("APP_SECRET") -SECRET_KEY = getenv_or_raise("APP_SECRET") - -# Hosts -BACKEND_URL = getenv_or_raise("BACKEND_URL") -ALLOWED_HOSTS = getenv_or_raise("ALLOWED_HOSTS").split(",") -CORS_ALLOW_CREDENTIALS = strtobool(os.getenv("CORS_ALLOW_CREDENTIALS", "True")) -CORS_ORIGIN_WHITELIST = getenv_or_raise("CORS_ORIGIN_WHITELIST").split(",") -CSRF_TRUSTED_ORIGINS = CORS_ORIGIN_WHITELIST -FRONTEND_URL = getenv_or_raise("FRONTEND_URL") - -# APPs -INSTALLED_APPS = [ - "daphne", - "simpleui", - "corsheaders", - "django.contrib.auth", - "django.contrib.admin", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "rest_framework", - "sslserver", - "oidc_provider", - "apps.account", - "apps.application", - "apps.cel", - "apps.home", - "apps.notice", - "apps.tcloud", - "ovinc_client.tcaptcha", - "ovinc_client.trace", -] - -# MIDDLEWARE -MIDDLEWARE = [ - "corsheaders.middleware.CorsMiddleware", - "ovinc_client.core.middlewares.CSRFExemptMiddleware", - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.middleware.common.CommonMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django.middleware.locale.LocaleMiddleware", - "ovinc_client.core.middlewares.SQLDebugMiddleware", -] -if not DEBUG: - MIDDLEWARE += ["ovinc_client.core.middlewares.UnHandleExceptionMiddleware"] - -# Urls -ROOT_URLCONF = "entry.urls" - -# TEMPLATES -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -# DB and Cache -DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", - "NAME": getenv_or_raise("DB_NAME"), - "USER": getenv_or_raise("DB_USER"), - "PASSWORD": getenv_or_raise("DB_PASSWORD"), - "HOST": getenv_or_raise("DB_HOST"), - "PORT": int(getenv_or_raise("DB_PORT")), - "CONN_MAX_AGE": int(os.getenv("DB_CONN_MAX_AGE", str(60 * 60))), - "OPTIONS": {"charset": "utf8mb4"}, - } -} -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -REDIS_HOST = getenv_or_raise("REDIS_HOST") -REDIS_PORT = int(getenv_or_raise("REDIS_PORT")) -REDIS_PASSWORD = getenv_or_raise("REDIS_PASSWORD") -REDIS_DB = int(getenv_or_raise("REDIS_DB")) -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}", - } -} - -# ASGI -ASGI_APPLICATION = "entry.asgi.application" -CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [ - f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}", - ], - }, - }, -} - -# Auth -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - -# International -LANGUAGE_CODE = os.getenv("DEFAULT_LANGUAGE", "zh-hans") -TIME_ZONE = os.getenv("DEFAULT_TIME_ZONE", "Asia/Shanghai") -USE_I18N = True -USE_L10N = True -USE_TZ = True -LANGUAGES = (("zh-hans", "中文简体"), ("en", "English")) -LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),) - -# Static -STATIC_URL = "/static/" -STATIC_ROOT = os.path.join(BASE_DIR, "static") -STATICFILES_DIRS = [os.path.join(BASE_DIR, "staticfiles")] - -# Session -SESSION_COOKIE_NAME = os.getenv("SESSION_COOKIE_NAME", f"{'dev-' if DEBUG else ''}{APP_CODE}-sessionid") -SESSION_ENGINE = "django.contrib.sessions.backends.cache" -SESSION_CACHE_ALIAS = "default" -SESSION_COOKIE_AGE = int(os.getenv("SESSION_COOKIE_AGE", str(60 * 60 * 24 * 7))) -SESSION_COOKIE_DOMAIN = os.getenv("SESSION_COOKIE_DOMAIN") - -# Log -LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") -LOG_FORMAT = os.getenv("LOG_FORMAT", "json") -LOGGING = get_logging_config_dict(log_level=LOG_LEVEL, log_format=LOG_FORMAT) - -# rest_framework -REST_FRAMEWORK = { - "DEFAULT_RENDERER_CLASSES": ["ovinc_client.core.renderers.APIRenderer"], - "DEFAULT_PAGINATION_CLASS": "ovinc_client.core.paginations.NumPagination", - "DATETIME_FORMAT": "%Y-%m-%dT%H:%M%z", - "DEFAULT_THROTTLE_RATES": { - "phone_verify_code": "1/m", - "ip": "60/m", - "app": "20/s", - }, - "EXCEPTION_HANDLER": "ovinc_client.core.exceptions.exception_handler", - "UNAUTHENTICATED_USER": "apps.account.models.CustomAnonymousUser", - "DEFAULT_AUTHENTICATION_CLASSES": ["ovinc_client.core.auth.LoginRequiredAuthenticate"], -} - -# User -AUTH_USER_MODEL = "account.User" - -# App -APP_AUTH_SIGN_EXPIRE = int(os.getenv("APP_AUTH_SIGN_EXPIRE", "60")) - -# Celery -CELERY_TIMEZONE = TIME_ZONE -CELERY_ENABLE_UTC = True -CELERY_TASK_TRACK_STARTED = True -CELERY_TASK_TIME_LIMIT = 30 * 60 -CELERY_ACCEPT_CONTENT = ["pickle", "json"] -CELERY_WORKER_HIJACK_ROOT_LOGGER = False -BROKER_URL = f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}" - -# APM -ENABLE_TRACE = strtobool(os.getenv("ENABLE_TRACE", "False")) -SERVICE_NAME = os.getenv("SERVICE_NAME", APP_CODE) -OTLP_HOST = os.getenv("OTLP_HOST", "http://127.0.0.1:4317") -OTLP_TOKEN = os.getenv("OTLP_TOKEN", "") - -# RUM -RUM_ID = os.getenv("RUM_ID", "") -RUM_HOST = os.getenv("RUM_HOST", "https://rumt-zh.com") - -# QCloud -QCLOUD_SECRET_ID = os.getenv("QCLOUD_SECRET_ID") -QCLOUD_SECRET_KEY = os.getenv("QCLOUD_SECRET_KEY") - -# Notice -NOTICE_MAIL_HOST = os.getenv("NOTICE_MAIL_HOST") -NOTICE_MAIL_PORT = int(os.getenv("NOTICE_MAIL_PORT", "465")) -NOTICE_MAIL_USERNAME = os.getenv("NOTICE_MAIL_USERNAME") -NOTICE_MAIL_PASSWORD = os.getenv("NOTICE_MAIL_PASSWORD") -NOTICE_MSG_TCLOUD_ID = os.getenv("NOTICE_MSG_TCLOUD_ID", QCLOUD_SECRET_ID) -NOTICE_MSG_TCLOUD_KEY = os.getenv("NOTICE_MSG_TCLOUD_KEY", QCLOUD_SECRET_KEY) -NOTICE_MSG_TCLUD_REGION = os.getenv("NOTICE_MSG_TCLUD_REGION", "ap-guangzhou") -NOTICE_MSG_TCLOUD_APP = os.getenv("NOTICE_MSG_TCLOUD_APP") -NOTICE_MSG_TCLOUD_SIGN = os.getenv("NOTICE_MSG_TCLOUD_SIGN") -NOTICE_ROBOT_CONTENT_HELP = os.getenv( - "NOTICE_ROBOT_CONTENT_HELP", "https://developer.work.weixin.qq.com/document/path/91770" -) -NOTICE_SMS_ID_VERIFY_CODE = os.getenv("NOTICE_SMS_ID_VERIFY_CODE", "") -NOTICE_SMS_ID_VERIFY_CODE_GLOBAL = os.getenv("NOTICE_SMS_ID_VERIFY_CODE_GLOBAL", "") -NOTICE_AUDIT_SENSITIVE_ROBOT = os.getenv("NOTICE_AUDIT_SENSITIVE_ROBOT", "") - -# TCI Callback -TCI_AUDIT_CALLBACK_PREFIX = os.getenv("TCI_AUDIT_CALLBACK_PREFIX", "") - -# OAuth -OAUTH_CODE_TIMEOUT = int(os.getenv("OAUTH_CODE_TIMEOUT", str(60 * 5))) - -# WeChat -WECHAT_APP_ID = os.getenv("WECHAT_APP_ID") -WECHAT_APP_KEY = os.getenv("WECHAT_APP_KEY") -MP_WECHAT_APP_ID = os.getenv("MP_WECHAT_APP_ID") -MP_WECHAT_APP_KEY = os.getenv("MP_WECHAT_APP_KEY") -WECHAT_SCOPE_TIMEOUT = int(os.getenv("WECHAT_SCOPE_TIMEOUT", str(60 * 30))) -WECHAT_OAUTH_API = os.getenv("WECHAT_OAUTH_API", "https://api.weixin.qq.com/sns/oauth2") -WECHAT_USER_INFO_API = os.getenv("WECHAT_OAUTH_API", "https://api.weixin.qq.com/sns/userinfo") - -# Captcha -CAPTCHA_TCLOUD_ID = os.getenv("CAPTCHA_TCLOUD_ID", QCLOUD_SECRET_ID) -CAPTCHA_TCLOUD_KEY = os.getenv("CAPTCHA_TCLOUD_KEY", QCLOUD_SECRET_KEY) -CAPTCHA_ENABLED = strtobool(os.getenv("CAPTCHA_ENABLED", "False")) -CAPTCHA_APP_ID = int(os.getenv("CAPTCHA_APP_ID", "0")) -CAPTCHA_APP_SECRET = os.getenv("CAPTCHA_APP_SECRET", "") -CAPTCHA_APP_INFO_TIMEOUT = int(os.getenv("CAPTCHA_APP_INFO_TIMEOUT", str(60 * 10))) - -# OIDC -OIDC_USERINFO = "apps.account.utils.userinfo" -OIDC_IDTOKEN_SUB_GENERATOR = "apps.account.utils.default_sub_generator" -OIDC_IDTOKEN_INCLUDE_CLAIMS = strtobool(os.getenv("OIDC_IDTOKEN_INCLUDE_CLAIMS", "True")) -OIDC_LOGIN_URL = "/account/oidc_login/" -OIDC_CODE_EXPIRE = int(os.getenv("OIDC_CODE_EXPIRE", "600")) -OIDC_IDTOKEN_EXPIRE = int(os.getenv("OIDC_IDTOKEN_EXPIRE", "600")) diff --git a/entry/urls.py b/entry/urls.py deleted file mode 100644 index 63eec1e..0000000 --- a/entry/urls.py +++ /dev/null @@ -1,37 +0,0 @@ -from urllib.parse import quote - -from django.conf import settings -from django.contrib import admin -from django.contrib.staticfiles.views import serve -from django.urls import include, path, re_path -from django.views.generic import RedirectView -from ovinc_client.core import exceptions - - -# pylint: disable=W0621 -def serve_static(request, path, insecure=True, **kwargs): - return serve(request, path, insecure=True, **kwargs) - - -ADMIN_PAGE_URL = f"{settings.BACKEND_URL}/admin/" -ADMIN_PAGE_LOGIN_URL = f"{settings.FRONTEND_URL}/login/?next={quote(ADMIN_PAGE_URL)}" - -urlpatterns = [ - path("favicon.ico", RedirectView.as_view(url=f"{settings.FRONTEND_URL}/favicon.ico")), - re_path(r"^static/(?P.*)$", serve_static, name="static"), - path("admin/login/", RedirectView.as_view(url=ADMIN_PAGE_LOGIN_URL.replace("%", "%%"))), - path("admin/", admin.site.urls), - path("openid/", include("oidc_provider.urls", namespace="oidc_provider")), - path("", include("apps.home.urls")), - path("account/", include("apps.account.urls")), - path("application/", include("apps.application.urls")), - path("notice/", include("apps.notice.urls")), - path("tcloud/", include("apps.tcloud.urls")), - path("trace/", include("ovinc_client.trace.urls")), - path("tcaptcha/", include("ovinc_client.tcaptcha.urls")), -] - -handler400 = exceptions.bad_request -handler403 = exceptions.permission_denied -handler404 = exceptions.page_not_found -handler500 = exceptions.server_error diff --git a/env.example b/env.example deleted file mode 100644 index 8bc01f6..0000000 --- a/env.example +++ /dev/null @@ -1,17 +0,0 @@ -APP_CODE=union-api -APP_SECRET=union-api -BACKEND_URL=https://dev-api.example.com -ALLOWED_HOSTS=dev-api.example.com -FRONTEND_URL=https://dev.example.com -CORS_ORIGIN_WHITELIST=https://dev.example.com -DB_NAME=union-api -DB_USER=union-api -DB_PASSWORD= -DB_HOST=127.0.0.1 -DB_PORT=3306 -REDIS_HOST=127.0.0.1 -REDIS_PORT=6379 -REDIS_PASSWORD= -REDIS_DB=0 -OVINC_API_DOMAIN=https://api.example.com -OVINC_WEB_URL=https://www.example.com diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po deleted file mode 100644 index f81f5d0..0000000 --- a/locale/zh_Hans/LC_MESSAGES/django.po +++ /dev/null @@ -1,443 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-14 13:29+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -msgid "Is Expired" -msgstr "是否过期" - -msgid "Yes" -msgstr "是" - -msgid "No" -msgstr "否" - -msgid "User Account" -msgstr "用户账户" - -msgid "Personal" -msgstr "个人" - -msgid "Platform" -msgstr "平台" - -msgid "Code" -msgstr "Code" - -msgid "Mainland China (+86)" -msgstr "中国 (+86)" - -msgid "Hong Kong, China (+852)" -msgstr "中国香港 (+852)" - -msgid "Macao, China (+853)" -msgstr "中国澳门 (+853)" - -msgid "Taiwan, China (+886)" -msgstr "中国台湾 (+886)" - -msgid "Sign In Params Not Exist" -msgstr "登录参数缺失" - -msgid "User Not Exist" -msgstr "用户不存在" - -msgid "Wrong Username or Password" -msgstr "用户名或密码错误" - -msgid "Token Not Exist" -msgstr "Token 不存在" - -msgid "Wrong Token" -msgstr "Token 错误" - -msgid "State Invalid" -msgstr "状态异常" - -msgid "WeChat Login Failed" -msgstr "微信登录失败" - -msgid "Origin Password Invalid" -msgstr "原密码不正确" - -msgid "Phone Verify Code Invalid" -msgstr "电话验证码错误" - -msgid "Registry Disabled" -msgstr "未开放注册" - -msgid "Username Cannot be Empty" -msgstr "用户名不能为空" - -msgid "username" -msgstr "用户名" - -msgid "already in use" -msgstr "已被占用" - -msgid "Nick Name" -msgstr "用户昵称" - -msgid "User Type" -msgstr "用户类型" - -msgid "Date Joined" -msgstr "注册时间" - -msgid "Phone Number" -msgstr "联系电话" - -msgid "Email Address" -msgstr "邮件地址" - -msgid "Wechat Open ID" -msgstr "微信 Open ID" - -msgid "Wechat Union ID" -msgstr "微信 Union ID" - -msgid "Avatar" -msgstr "头像" - -msgid "Is Staff" -msgstr "是否管理员" - -msgid "User" -msgstr "用户" - -msgid "ID" -msgstr "ID" - -msgid "Session Key" -msgstr "会话键" - -msgid "Cache Key" -msgstr "缓存键" - -msgid "Login IP" -msgstr "" - -#, fuzzy -#| msgid "User Account" -msgid "User Agent" -msgstr "用户账户" - -msgid "Expired at" -msgstr "过期时间" - -msgid "User Token" -msgstr "用户令牌" - -msgid "Username" -msgstr "用户名" - -msgid "Password" -msgstr "密码" - -msgid "Is OAuth" -msgstr "是否 OAuth" - -msgid "WeChat Code" -msgstr "微信 Code" - -msgid "Tencent Captcha" -msgstr "腾讯云验证码" - -msgid "Phone Area" -msgstr "电话归属国家" - -msgid "Phone Verify Code" -msgstr "电话验证码" - -msgid "Username Invalid" -msgstr "用户名不符合校验规则" - -msgid "Phone Number Already Exists" -msgstr "电话已被使用" - -msgid "State" -msgstr "状态" - -msgid "Application" -msgstr "应用" - -msgid "App Name" -msgstr "应用名称" - -msgid "App Code" -msgstr "应用ID" - -msgid "App Secret" -msgstr "应用密码" - -msgid "App Url" -msgstr "应用访问地址" - -msgid "App Logo" -msgstr "应用图标" - -msgid "App Desc" -msgstr "应用描述" - -msgid "Is Hidden" -msgstr "是否隐藏" - -msgid "Manager" -msgstr "管理员" - -msgid "Application Manager" -msgstr "应用管理员" - -msgid "Home Module" -msgstr "首页" - -msgid "Language Code Invalid" -msgstr "语言ID非法" - -msgid "Search IP Failed" -msgstr "检索 IP 失败" - -msgid "Config Key" -msgstr "配置键" - -msgid "Config Value" -msgstr "配置值" - -msgid "Is Public" -msgstr "是否公开" - -msgid "Meta Config" -msgstr "基础配置" - -msgid "Meta Configs" -msgstr "基础配置" - -msgid "Language Code" -msgstr "语言ID" - -msgid "Notice" -msgstr "通知" - -msgid "msg" -msgstr "短信" - -msgid "mail" -msgstr "邮件" - -msgid "robot" -msgstr "机器人" - -msgid "Receivers" -msgstr "接收人" - -msgid "Content" -msgstr "内容" - -msgid "Extra Params" -msgstr "参数" - -msgid "Result" -msgstr "结果" - -msgid "Send At" -msgstr "发送时间" - -msgid "Notice Log" -msgstr "通知记录" - -msgid "Webhook" -msgstr "回调地址" - -msgid "Name" -msgstr "名称" - -msgid "Robot" -msgstr "机器人" - -msgid "Username List" -msgstr "用户名列表" - -msgid "Title" -msgstr "标题" - -msgid "Template ID" -msgstr "模板ID" - -msgid "Params" -msgstr "参数" - -msgid "Robot ID" -msgstr "机器人ID" - -#, python-format -msgid "Notice Type not Exists => %s" -msgstr "通知类型不存在 => %s" - -msgid "TCloud" -msgstr "腾讯云" - -msgid "Review Image" -msgstr "图片审核" - -msgid "Review Document" -msgstr "文档审核" - -msgid "Review Video" -msgstr "视频审核" - -msgid "Review Audio" -msgstr "音频审核" - -msgid "Review Text" -msgstr "文本审核" - -msgid "Review HTML" -msgstr "网页审核" - -msgid "Bucket Region" -msgstr "地域" - -msgid "Bucket ID" -msgstr "存储桶ID" - -msgid "Event Name" -msgstr "事件名" - -msgid "Audit ID" -msgstr "审核ID" - -msgid "Is Sensitive" -msgstr "敏感" - -msgid "Detail" -msgstr "详情" - -msgid "Audit Time" -msgstr "审核时间" - -msgid "Callback Time" -msgstr "回调时间" - -msgid "Updated Time" -msgstr "更新时间" - -msgid "Handled" -msgstr "已处理" - -msgid "Audit Callback" -msgstr "审核回调" - -#, python-format -msgid "" -"Sensitive Object Notification\n" -"Bucket Region: %(region)s\n" -"Bucket ID: %(bucket)s\n" -"Event Name: %(event)s\n" -"Audit ID: %(audit_id)s\n" -"Creation Time: %(creation_time)s\n" -msgstr "" -"敏感对象通知\n" -"地域: %(region)s\n" -"存储桶: %(bucket)s\n" -"事件: %(event)s\n" -"审核ID: %(audit_id)s\n" -"审核时间: %(creation_time)s\n" - -msgid "App Auth Headers Not Exist" -msgstr "应用鉴权参数不存在" - -msgid "App Not Exist" -msgstr "应用不存在" - -msgid "Signature Invalid" -msgstr "签名非法" - -msgid "Auth Token Invalid" -msgstr "认证 Token 非法" - -msgid "App Auth Failed" -msgstr "应用鉴权失败" - -msgid "Mail Address" -msgstr "邮件地址" - -msgid "Code Verify Failed" -msgstr "代码验证失败" - -msgid "LIST" -msgstr "LIST" - -msgid "CREATE" -msgstr "CREATE" - -msgid "UPDATE" -msgstr "UPDATE" - -msgid "PARTIAL_UPDATE" -msgstr "PARTIAL_UPDATE" - -msgid "DESTROY" -msgstr "DESTROY" - -msgid "Request Failed" -msgstr "请求失败" - -msgid "Server Error" -msgstr "服务器异常" - -msgid "Login Required" -msgstr "未登录" - -msgid "Login Failed" -msgstr "登录失败" - -msgid "Permission Denied" -msgstr "未经授权" - -msgid "Operate Error" -msgstr "异常操作" - -msgid "Resource Not Found" -msgstr "资源未找到" - -msgid "Service Closed" -msgstr "服务已关闭" - -msgid "Soft Delete" -msgstr "软删除" - -msgid "Invalid Page Number" -msgstr "页码错误" - -msgid "TCaptcha" -msgstr "腾讯云验证码" - -msgid "Success" -msgstr "成功" - -msgid "Low" -msgstr "低" - -msgid "High" -msgstr "高" - -msgid "TCaptcha Verify Failed" -msgstr "验证码校验失败" - -msgid "App Performance" -msgstr "应用性能" diff --git a/manage.py b/manage.py deleted file mode 100755 index 0b92991..0000000 --- a/manage.py +++ /dev/null @@ -1,17 +0,0 @@ -# pylint: disable=R0801,C0415 - -import os -import sys - - -def main(): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "entry.settings") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError("Couldn't import Django") from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index de673b8..0000000 --- a/requirements.txt +++ /dev/null @@ -1,27 +0,0 @@ -# ovinc -ovinc-client==0.4.0 - -# Celery -celery==5.4.0 - -# ASGI -asgiref==3.8.1 -channels[daphne]==4.2.0 -channels_redis==4.2.1 - -# Server -gunicorn==23.0.0 -uvicorn-worker==0.2.0 -uvicorn==0.34.0 - -# Datetime -arrow==1.3.0 - -# RSA -pycryptodome==3.21.0 - -# tencent cloud -tencentcloud-sdk-python==3.0.1282 - -# oidc -django-oidc-provider==0.8.3 diff --git a/scripts/messages.sh b/scripts/messages.sh deleted file mode 100755 index e9c425e..0000000 --- a/scripts/messages.sh +++ /dev/null @@ -1,7 +0,0 @@ -export PACKAGE_PATH=`python -c "import site; print(site.getsitepackages()[0])"` - -ln -s "$PACKAGE_PATH/ovinc_client" ovinc_client - -python manage.py makemessages -l zh_Hans --no-wrap --no-location -s - -rm ovinc_client diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh deleted file mode 100755 index 07c0956..0000000 --- a/scripts/pre-commit.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -pre-commit run -a diff --git a/scripts/pylint.sh b/scripts/pylint.sh deleted file mode 100755 index db38b83..0000000 --- a/scripts/pylint.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -pylint --disable=C0114,W0613,C0115,W1113,W0223,C0116,R0901,R0903,C0209 --max-line-length=120 $(git ls-files '*.py') From 078182e2ab5cbc12cf7e85ca0c39a8e4c853ca23 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 17:33:48 +0800 Subject: [PATCH 02/22] chore(repo): init golang version --- Makefile | 2 + env.example | 2 + go.mod | 70 +++++++++++++ go.sum | 169 ++++++++++++++++++++++++++++++++ internal/apps/home/routers.go | 10 ++ internal/apps/home/tasks.go | 32 ++++++ internal/cmd/api.go | 14 +++ internal/cmd/beat.go | 14 +++ internal/cmd/root.go | 51 ++++++++++ internal/cmd/worker.go | 14 +++ internal/config/config.go | 22 +++++ internal/config/models.go | 18 ++++ internal/crond/beat/beat.go | 47 +++++++++ internal/crond/constants.go | 31 ++++++ internal/crond/crond.go | 20 ++++ internal/crond/middlewares.go | 84 ++++++++++++++++ internal/crond/model.go | 8 ++ internal/crond/utils.go | 72 ++++++++++++++ internal/crond/worker/worker.go | 34 +++++++ internal/httpd/httpd.go | 41 ++++++++ internal/version/VERSION | 1 + internal/version/version.go | 18 ++++ main.go | 7 ++ pkg/commonUtils/string.go | 27 +++++ pkg/configUtils/config.go | 57 +++++++++++ pkg/configUtils/utils.go | 111 +++++++++++++++++++++ pkg/ginUtils/constants.go | 3 + pkg/ginUtils/middlewares.go | 59 +++++++++++ pkg/ginUtils/models.go | 10 ++ pkg/ginUtils/response.go | 27 +++++ pkg/logger/logger.go | 48 +++++++++ pkg/logger/utils.go | 32 ++++++ pkg/redisUtils/commands.go | 111 +++++++++++++++++++++ pkg/redisUtils/errs.go | 11 +++ pkg/redisUtils/lock.go | 45 +++++++++ pkg/redisUtils/redis.go | 32 ++++++ pkg/traceUtils/constants.go | 8 ++ pkg/traceUtils/span.go | 21 ++++ pkg/traceUtils/trace.go | 57 +++++++++++ 39 files changed, 1440 insertions(+) create mode 100644 Makefile create mode 100644 env.example create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/apps/home/routers.go create mode 100644 internal/apps/home/tasks.go create mode 100644 internal/cmd/api.go create mode 100644 internal/cmd/beat.go create mode 100644 internal/cmd/root.go create mode 100644 internal/cmd/worker.go create mode 100644 internal/config/config.go create mode 100644 internal/config/models.go create mode 100644 internal/crond/beat/beat.go create mode 100644 internal/crond/constants.go create mode 100644 internal/crond/crond.go create mode 100644 internal/crond/middlewares.go create mode 100644 internal/crond/model.go create mode 100644 internal/crond/utils.go create mode 100644 internal/crond/worker/worker.go create mode 100644 internal/httpd/httpd.go create mode 100644 internal/version/VERSION create mode 100644 internal/version/version.go create mode 100644 main.go create mode 100644 pkg/commonUtils/string.go create mode 100644 pkg/configUtils/config.go create mode 100644 pkg/configUtils/utils.go create mode 100644 pkg/ginUtils/constants.go create mode 100644 pkg/ginUtils/middlewares.go create mode 100644 pkg/ginUtils/models.go create mode 100644 pkg/ginUtils/response.go create mode 100644 pkg/logger/logger.go create mode 100644 pkg/logger/utils.go create mode 100644 pkg/redisUtils/commands.go create mode 100644 pkg/redisUtils/errs.go create mode 100644 pkg/redisUtils/lock.go create mode 100644 pkg/redisUtils/redis.go create mode 100644 pkg/traceUtils/constants.go create mode 100644 pkg/traceUtils/span.go create mode 100644 pkg/traceUtils/trace.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..23b1e43 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +tidy: + go mod tidy \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000..d297f75 --- /dev/null +++ b/env.example @@ -0,0 +1,2 @@ +APP_NAME=api-center +APP_DEBUG=false diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8649086 --- /dev/null +++ b/go.mod @@ -0,0 +1,70 @@ +module github.com/ovinc-cn/apicenter/v2 + +go 1.23.0 + +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/hibiken/asynq v0.25.1 + github.com/joho/godotenv v1.5.1 + github.com/redis/go-redis/v9 v9.7.2 + github.com/spf13/cobra v1.10.1 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.uber.org/mock v0.5.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.8.0 // indirect + golang.org/x/tools v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b56c761 --- /dev/null +++ b/go.sum @@ -0,0 +1,169 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw= +github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/redis/go-redis/v9 v9.7.2 h1:PSGhv13dJyrTCw1+55H0pIKM3WFov7HuUrKUmInGL0o= +github.com/redis/go-redis/v9 v9.7.2/go.mod h1:yp5+a5FnEEP0/zTYuw6u6/2nn3zivwhv274qYgWQhDM= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/apps/home/routers.go b/internal/apps/home/routers.go new file mode 100644 index 0000000..e41ae2e --- /dev/null +++ b/internal/apps/home/routers.go @@ -0,0 +1,10 @@ +package home + +import ( + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/pkg/ginUtils" +) + +func HealthZ(c *gin.Context) { + ginUtils.ResponseSuccess(c, nil) +} diff --git a/internal/apps/home/tasks.go b/internal/apps/home/tasks.go new file mode 100644 index 0000000..4c769b5 --- /dev/null +++ b/internal/apps/home/tasks.go @@ -0,0 +1,32 @@ +package home + +import ( + "context" + "fmt" + "time" + + "github.com/hibiken/asynq" + "github.com/ovinc-cn/apicenter/v2/pkg/redisUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +var DebugTaskLockKey = "task:home:debug" +var DebugTaskLockTTL = 1 * time.Hour + +func DebugTask(ctx context.Context, t *asynq.Task) error { + // trace + ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("Task#%s", t.Type()), trace.SpanKindConsumer) + defer span.End() + + // lock + lock := redisUtils.NewLock(ctx, redisUtils.Client(), DebugTaskLockKey, DebugTaskLockTTL) + defer lock.Release() + if err := lock.Lock(); err != nil { + span.SetStatus(codes.Error, err.Error()) + return err + } + + return nil +} diff --git a/internal/cmd/api.go b/internal/cmd/api.go new file mode 100644 index 0000000..8d1cdc0 --- /dev/null +++ b/internal/cmd/api.go @@ -0,0 +1,14 @@ +package cmd + +import ( + "github.com/ovinc-cn/apicenter/v2/internal/httpd" + "github.com/spf13/cobra" +) + +var apiCmd = &cobra.Command{ + Use: "api", + Short: "Api Center HttpServer", + Run: func(cmd *cobra.Command, args []string) { + httpd.Serve() + }, +} diff --git a/internal/cmd/beat.go b/internal/cmd/beat.go new file mode 100644 index 0000000..dbbb8b8 --- /dev/null +++ b/internal/cmd/beat.go @@ -0,0 +1,14 @@ +package cmd + +import ( + "github.com/ovinc-cn/apicenter/v2/internal/crond/beat" + "github.com/spf13/cobra" +) + +var beatCmd = &cobra.Command{ + Use: "beat", + Short: "Api Center Scheduler", + Run: func(cmd *cobra.Command, args []string) { + beat.Serve() + }, +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..f968030 --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "log" + "os" + + "github.com/ovinc-cn/apicenter/v2/internal/crond" + "github.com/ovinc-cn/apicenter/v2/internal/version" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "api-center", + Short: "API Center", + Version: version.Version, + Run: func(cmd *cobra.Command, args []string) { + if len(args) > 0 && cmd.CalledAs() == "api-center" { + appMode := args[0] + args = args[1:] + switch appMode { + case "api": + apiCmd.Run(apiCmd, args) + case "worker": + workerCmd.Run(apiCmd, args) + case "beat": + beatCmd.Run(apiCmd, args) + default: + log.Fatal("[CMD] unknown app mode") + } + } else { + log.Fatal("[CMD] please specify app mode") + } + }, + PostRun: func(cmd *cobra.Command, args []string) { + _ = crond.AsyncClient.Close() + traceUtils.OnExit() + os.Exit(0) + }, +} + +func init() { + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.SetVersionTemplate(`{{printf "API Center "}}{{printf "%s" .Version}}`) +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + log.Fatalf("[CMD] execute failed; %s", err) + } +} diff --git a/internal/cmd/worker.go b/internal/cmd/worker.go new file mode 100644 index 0000000..d1941c0 --- /dev/null +++ b/internal/cmd/worker.go @@ -0,0 +1,14 @@ +package cmd + +import ( + "github.com/ovinc-cn/apicenter/v2/internal/crond/worker" + "github.com/spf13/cobra" +) + +var workerCmd = &cobra.Command{ + Use: "worker", + Short: "Api Center Worker", + Run: func(cmd *cobra.Command, args []string) { + worker.Serve() + }, +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0cf24cd --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "time" + + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" +) + +var Config configModel + +func init() { + Config = configModel{ + API: apiConfig{ + Addr: configUtils.GetConfigVal("APP_API_ADDR", "0.0.0.0:8000"), + }, + Cron: cronConfig{ + WorkerConcurrency: configUtils.GetConfigInt("APP_CRON_WORKER_CONCURRENCY", 1), + WorkerQueues: configUtils.GetConfigStruct("APP_CRON_WORKER_QUEUES", "{}", make(map[string]int)), + WorkerShutDownTimeout: time.Duration(configUtils.GetConfigUint64("APP_CRON_WORKER_SHUTDOWN_TIMEOUT", 60)) * time.Second, + }, + } +} diff --git a/internal/config/models.go b/internal/config/models.go new file mode 100644 index 0000000..c32e9e9 --- /dev/null +++ b/internal/config/models.go @@ -0,0 +1,18 @@ +package config + +import "time" + +type apiConfig struct { + Addr string +} + +type cronConfig struct { + WorkerConcurrency int + WorkerQueues map[string]int + WorkerShutDownTimeout time.Duration +} + +type configModel struct { + API apiConfig + Cron cronConfig +} diff --git a/internal/crond/beat/beat.go b/internal/crond/beat/beat.go new file mode 100644 index 0000000..1bddcb6 --- /dev/null +++ b/internal/crond/beat/beat.go @@ -0,0 +1,47 @@ +package beat + +import ( + "context" + "log" + + "github.com/hibiken/asynq" + "github.com/ovinc-cn/apicenter/v2/internal/crond" + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" +) + +type taskConfig struct { + TaskType crond.TaskType + Data interface{} + CronSpec string + Ops []asynq.Option +} + +var periodTasks = []taskConfig{ + { + TaskType: crond.TaskTypeDebug, + CronSpec: "* * * * *", + }, +} + +func Serve() { + // init beat + scheduler := asynq.NewScheduler(crond.RedisClientOption, &asynq.SchedulerOpts{Location: configUtils.AppTimezone()}) + + // register task + for _, periodTask := range periodTasks { + // register task + task, err := crond.NewTask(context.Background(), periodTask.TaskType, periodTask.Data, periodTask.Ops...) + if err != nil { + log.Fatalf("[Beat] create %s task failed: %s", periodTask.TaskType, err) + } + if _, err = scheduler.Register(periodTask.CronSpec, task); err != nil { + log.Fatalf("[Beat] register %s task failed: %s", periodTask.TaskType, err) + } + log.Printf("[Beat] register %s task success", periodTask.TaskType) + } + + // start + if err := scheduler.Run(); err != nil { + log.Fatalf("[Beat] start beat failed: %s", err) + } +} diff --git a/internal/crond/constants.go b/internal/crond/constants.go new file mode 100644 index 0000000..1f51718 --- /dev/null +++ b/internal/crond/constants.go @@ -0,0 +1,31 @@ +package crond + +import ( + "github.com/hibiken/asynq" +) + +type TaskType = string + +const ( + TaskTypeDebug TaskType = "DebugTask" +) + +type QueueType = string + +const ( + QueueTypeDefault QueueType = "default" +) + +func getQueue(t TaskType) QueueType { + switch t { + default: + return QueueTypeDefault + } +} + +func getOps(t TaskType) []asynq.Option { + switch t { + default: + return []asynq.Option{} + } +} diff --git a/internal/crond/crond.go b/internal/crond/crond.go new file mode 100644 index 0000000..65e97f8 --- /dev/null +++ b/internal/crond/crond.go @@ -0,0 +1,20 @@ +package crond + +import ( + "log" + + "github.com/hibiken/asynq" + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" +) + +var AsyncClient *asynq.Client +var RedisClientOption = asynq.RedisClientOpt{ + Addr: configUtils.RedisAddr(), + Password: configUtils.RedisPassword(), + DB: configUtils.RedisDB(), +} + +func init() { + AsyncClient = asynq.NewClient(RedisClientOption) + log.Printf("[AsyncClinet] init success\n") +} diff --git a/internal/crond/middlewares.go b/internal/crond/middlewares.go new file mode 100644 index 0000000..58e930a --- /dev/null +++ b/internal/crond/middlewares.go @@ -0,0 +1,84 @@ +package crond + +import ( + "context" + "encoding/json" + "fmt" + "runtime" + + "github.com/hibiken/asynq" + "github.com/ovinc-cn/apicenter/v2/pkg/commonUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +type ScheduleTaskFailed struct { + TaskName string +} + +func (e ScheduleTaskFailed) Error() string { + return fmt.Sprintf("%sFailed", e.TaskName) +} + +func ObserveMiddleware(next asynq.Handler) asynq.Handler { + propagator := otel.GetTextMapPropagator() + return asynq.HandlerFunc(func(ctx context.Context, task *asynq.Task) error { + // parse payload to map + var payload TaskPayload + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.Logger(ctx).Error( + fmt.Sprintf("[%s] failed\npayload: %s\nerr: %s", task.Type(), commonUtils.ForceStringMaxLength(task.Payload()), err), + ) + return err + } + + // load trace if exists + if len(payload.Carrier) > 0 { + ctx = propagator.Extract(ctx, payload.Carrier) + } + + // span + ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("ObserveTask#%s", task.Type()), trace.SpanKindInternal) + defer span.End() + + // pre exec + logger.Logger(ctx).Info(fmt.Sprintf("[%s] start", task.Type())) + + // execute + err := next.ProcessTask(ctx, task) + + // post exec + if err != nil { + logger.Logger(ctx).Error( + fmt.Sprintf("[%s] failed\npayload: %s\nerr: %s", task.Type(), commonUtils.ForceStringMaxLength(task.Payload()), err), + ) + } else { + logger.Logger(ctx).Info(fmt.Sprintf("[%s] finished", task.Type())) + } + return err + }) +} + +func RecoverMiddleware(next asynq.Handler) asynq.Handler { + return asynq.HandlerFunc(func(ctx context.Context, task *asynq.Task) (err error) { + defer func() { + if r := recover(); r != nil { + ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("Task#%s#Panic", task.Type()), trace.SpanKindInternal) + defer span.End() + + var stack []byte + var length int + stack = make([]byte, 4096) + length = runtime.Stack(stack, true) + stack = stack[:length] + + logger.Logger(ctx).Error( + fmt.Sprintf("[%s] panic\nerr: %s\n%s", task.Type(), commonUtils.ForceStringMaxLength(r), stack), + ) + } + }() + return next.ProcessTask(ctx, task) + }) +} diff --git a/internal/crond/model.go b/internal/crond/model.go new file mode 100644 index 0000000..1990e34 --- /dev/null +++ b/internal/crond/model.go @@ -0,0 +1,8 @@ +package crond + +import "go.opentelemetry.io/otel/propagation" + +type TaskPayload struct { + Carrier propagation.MapCarrier + Data interface{} +} diff --git a/internal/crond/utils.go b/internal/crond/utils.go new file mode 100644 index 0000000..c6914d2 --- /dev/null +++ b/internal/crond/utils.go @@ -0,0 +1,72 @@ +package crond + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/hibiken/asynq" + "github.com/ovinc-cn/apicenter/v2/pkg/commonUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +func NewTask(ctx context.Context, taskType TaskType, data interface{}, customOps ...asynq.Option) (*asynq.Task, error) { + // init trace + ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("NewTask#%s", taskType), trace.SpanKindInternal) + defer span.End() + + // init payload + payload := TaskPayload{Carrier: propagation.MapCarrier{}, Data: data} + + // inject carrier + otel.GetTextMapPropagator().Inject(ctx, payload.Carrier) + + // build payload + payloadData, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + // build options + ops := []asynq.Option{ + asynq.Queue(getQueue(taskType)), + asynq.MaxRetry(0), + } + ops = append(ops, getOps(taskType)...) + ops = append(ops, customOps...) + + // build task + return asynq.NewTask(taskType, payloadData, ops...), nil +} + +func RunAsyncTask(ctx context.Context, taskType TaskType, data interface{}, ops ...asynq.Option) error { + // trace + ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("RunAsyncTask#%s", taskType), trace.SpanKindProducer) + defer span.End() + + // init task + task, err := NewTask(ctx, taskType, data, ops...) + if err != nil { + return err + } + + // run task + taskInfo, err := AsyncClient.Enqueue(task) + if err != nil { + if errors.Is(err, asynq.ErrDuplicateTask) { + logger.Logger(ctx).Warn(fmt.Sprintf("[RunAsyncTask] duplicate %s task", taskType)) + return nil + } + span.SetStatus(codes.Error, err.Error()) + logger.Logger(ctx).Error(fmt.Sprintf("[RunAsyncTask] enqueue %s task failed\nerr: %s", taskType, err)) + return err + } + logger.Logger(ctx).Info(fmt.Sprintf("[RunAsyncTask] enqueue %s task success\ntask: %s", taskType, commonUtils.ForceStringMaxLength(taskInfo))) + return nil +} diff --git a/internal/crond/worker/worker.go b/internal/crond/worker/worker.go new file mode 100644 index 0000000..5071e57 --- /dev/null +++ b/internal/crond/worker/worker.go @@ -0,0 +1,34 @@ +package worker + +import ( + "log" + + "github.com/hibiken/asynq" + "github.com/ovinc-cn/apicenter/v2/internal/apps/home" + "github.com/ovinc-cn/apicenter/v2/internal/config" + "github.com/ovinc-cn/apicenter/v2/internal/crond" +) + +func Serve() { + // init worker + srv := asynq.NewServer( + crond.RedisClientOption, + asynq.Config{ + Concurrency: config.Config.Cron.WorkerConcurrency, + Queues: config.Config.Cron.WorkerQueues, + ShutdownTimeout: config.Config.Cron.WorkerShutDownTimeout, + }, + ) + + // init + mux := asynq.NewServeMux() + mux.Use(crond.RecoverMiddleware, crond.ObserveMiddleware) + + // home + mux.HandleFunc(crond.TaskTypeDebug, home.DebugTask) + + // run worker + if err := srv.Run(mux); err != nil { + log.Fatalf("[Worker] start failed: %s", err) + } +} diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go new file mode 100644 index 0000000..f44bc49 --- /dev/null +++ b/internal/httpd/httpd.go @@ -0,0 +1,41 @@ +package httpd + +import ( + "log" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/internal/apps/home" + "github.com/ovinc-cn/apicenter/v2/internal/config" + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/ginUtils" +) + +func Serve() { + // release mode + if configUtils.AppDebug() { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + + // init gin + r := gin.New() + + // middlewares + r.Use(gin.Recovery(), ginUtils.ObserveMiddleware()) + + // routers + apiV1Group := r.Group("/v1") + { + // home + homeGroup := apiV1Group.Group("") + { + homeGroup.GET("/healthz", home.HealthZ) + } + } + + // start server + if err := r.Run(config.Config.API.Addr); err != nil { + log.Fatalf("[Http] server failed; %s", err) + } +} diff --git a/internal/version/VERSION b/internal/version/VERSION new file mode 100644 index 0000000..359a5b9 --- /dev/null +++ b/internal/version/VERSION @@ -0,0 +1 @@ +2.0.0 \ No newline at end of file diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..36ca974 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,18 @@ +package version + +import ( + _ "embed" + "log" + "strings" +) + +//go:embed VERSION +var Version string + +func init() { + Version = strings.TrimSpace(Version) + if Version == "" { + log.Fatal("[Version] version is empty") + } + log.Printf("[Version] %s\n", Version) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a62c20c --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/ovinc-cn/apicenter/v2/internal/cmd" + +func main() { + cmd.Execute() +} diff --git a/pkg/commonUtils/string.go b/pkg/commonUtils/string.go new file mode 100644 index 0000000..1254c1c --- /dev/null +++ b/pkg/commonUtils/string.go @@ -0,0 +1,27 @@ +package commonUtils + +import ( + "encoding/json" + "fmt" +) + +const maxLength = 1024 + +func MaxLength(s string) string { + if len(s) > maxLength { + s = fmt.Sprintf("%s...", s[:maxLength]) + } + return s +} + +func ForceString(model interface{}) string { + data, err := json.Marshal(model) + if err != nil { + return fmt.Sprintf("%+v", model) + } + return string(data) +} + +func ForceStringMaxLength(model interface{}) string { + return MaxLength(ForceString(model)) +} diff --git a/pkg/configUtils/config.go b/pkg/configUtils/config.go new file mode 100644 index 0000000..2d7edf0 --- /dev/null +++ b/pkg/configUtils/config.go @@ -0,0 +1,57 @@ +package configUtils + +import ( + "log" + "time" +) + +// Base +var ( + AppName = func() string { + return MustGetConfigVal("APP_NAME") + } + AppDebug = func() bool { + return GetConfigBool("APP_DEBUG", false) + } + AppTimezone = func() *time.Location { + tz := GetConfigVal("APP_TIMEZONE", "Asia/Shanghai") + loc, err := time.LoadLocation(tz) + if err != nil { + log.Fatalf("[Config] load timezone %s failed; %s", tz, err) + } + return loc + } +) + +// Log +var ( + AppLogLevel = func() string { + return GetConfigVal("APP_LOG_LEVEL", "INFO") + } + AppLogEncoding = func() string { + return GetConfigVal("APP_LOG_ENCODING", "json") + } + AppLogOutput = func() string { + return GetConfigVal("APP_LOG_OUTPUT", "stdout") + } +) + +// db +var ( + RedisAddr = func() string { + return GetConfigVal("APP_REDIS_ADDR", "127.0.0.1:6379") + } + RedisPassword = func() string { + return GetConfigVal("APP_REDIS_PASSWORD", "") + } + RedisDB = func() int { + return GetConfigInt("APP_REDIS_DB", 0) + } +) + +// Trace +var ( + TraceEndpoint = func() string { + return GetConfigVal("APP_TRACE_ENDPOINT", "127.0.0.1:4317") + } +) diff --git a/pkg/configUtils/utils.go b/pkg/configUtils/utils.go new file mode 100644 index 0000000..0c8fc3b --- /dev/null +++ b/pkg/configUtils/utils.go @@ -0,0 +1,111 @@ +package configUtils + +import ( + "encoding/json" + "log" + "os" + "strconv" + "strings" + + "github.com/joho/godotenv" +) + +func init() { + // load from env file + if err := godotenv.Load(); err != nil { + log.Printf("[Config] error reading config: %s", err) + } +} + +func GetConfigVal(key string, defaultVal string) string { + // load from env + if val := os.Getenv(key); val != "" { + return val + } + // default val + return defaultVal +} + +func GetConfigBool(key string, defaultVal bool) bool { + val := GetConfigVal(key, "") + if val == "" { + return defaultVal + } + return strings.ToLower(val) == "true" +} + +func GetConfigInt(key string, defaultVal int) int { + val := GetConfigVal(key, "") + if val == "" { + return defaultVal + } + intVal, err := strconv.Atoi(val) + if err != nil { + log.Fatalf("[Config] parse %s of %s to int failed; %s", key, val, err) + } + return intVal +} + +func GetConfigInt64(key string, defaultVal int64) int64 { + val := GetConfigVal(key, "") + if val == "" { + return defaultVal + } + intVal, err := strconv.ParseInt(val, 10, 64) + if err != nil { + log.Fatalf("[Config] parse %s of %s to int64 failed; %s", key, val, err) + } + return intVal +} + +func GetConfigUint(key string, defaultVal uint) uint { + val := GetConfigVal(key, "") + if val == "" { + return defaultVal + } + uintVal, err := strconv.ParseUint(val, 10, strconv.IntSize) + if err != nil { + log.Fatalf("[Config] parse %s of %s to uint failed; %s", key, val, err) + } + return uint(uintVal) +} + +func GetConfigUint64(key string, defaultVal uint64) uint64 { + val := GetConfigVal(key, "") + if val == "" { + return defaultVal + } + uintVal, err := strconv.ParseUint(val, 10, 64) + if err != nil { + log.Fatalf("[Config] parse %s of %s to uint64 failed; %s", key, val, err) + } + return uintVal +} + +func GetConfigFloat64(key string, defaultVal float64) float64 { + val := GetConfigVal(key, "") + if val == "" { + return defaultVal + } + floatVal, err := strconv.ParseFloat(val, 64) + if err != nil { + log.Fatalf("[Config] parse %s of %s to float64 failed; %s", key, val, err) + } + return floatVal +} + +func GetConfigStruct[T interface{}](key string, defaultVal string, out T) T { + val := GetConfigVal(key, defaultVal) + if err := json.Unmarshal([]byte(val), &out); err != nil { + log.Fatalf("[Config] parse %s of `%s` to struct failed; %s", key, val, err) + } + return out +} + +func MustGetConfigVal(key string) string { + if val := GetConfigVal(key, ""); val != "" { + return val + } + log.Fatalf("[Config] %s is empty", key) + return "" +} diff --git a/pkg/ginUtils/constants.go b/pkg/ginUtils/constants.go new file mode 100644 index 0000000..a4c50a3 --- /dev/null +++ b/pkg/ginUtils/constants.go @@ -0,0 +1,3 @@ +package ginUtils + +const TraceContextKey = "otel_trace_context" diff --git a/pkg/ginUtils/middlewares.go b/pkg/ginUtils/middlewares.go new file mode 100644 index 0000000..c526481 --- /dev/null +++ b/pkg/ginUtils/middlewares.go @@ -0,0 +1,59 @@ +package ginUtils + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/trace" +) + +func ObserveMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // load path + path := c.FullPath() + if path == "" { + path = "Unknown" + } + + // init trace + ctx, span := traceUtils.StartSpan(context.Background(), fmt.Sprintf("Router#%s#%s", c.Request.Method, path), trace.SpanKindServer) + defer span.End() + + // add trace to context + c.Set(TraceContextKey, ctx) + + // record time + startTime := time.Now() + + // do next + c.Next() + + // record duration + duration := time.Since(startTime) + + // response status + status := c.Writer.Status() + if status >= http.StatusBadRequest { + span.SetStatus(codes.Error, fmt.Sprintf("status %d", status)) + } + + // add attributes + span.SetAttributes( + semconv.HTTPResponseStatusCode(status), + attribute.Int(traceUtils.AttributeStatusCode, status), + ) + + // log + logger.Logger(ctx).Info( + fmt.Sprintf("[ObserveMiddleware] %d | %v | %s | %s", status, duration, c.Request.Method, c.Request.RequestURI), + ) + } +} diff --git a/pkg/ginUtils/models.go b/pkg/ginUtils/models.go new file mode 100644 index 0000000..220a395 --- /dev/null +++ b/pkg/ginUtils/models.go @@ -0,0 +1,10 @@ +package ginUtils + +type APIError struct { + Code int + Msg string +} + +func (e *APIError) Error() string { + return e.Msg +} diff --git a/pkg/ginUtils/response.go b/pkg/ginUtils/response.go new file mode 100644 index 0000000..fea93d8 --- /dev/null +++ b/pkg/ginUtils/response.go @@ -0,0 +1,27 @@ +package ginUtils + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" +) + +func Response(c *gin.Context, status int, msg string, data interface{}) { + // load context + ctx := c.MustGet(TraceContextKey).(context.Context) + span := traceUtils.SpanFromContext(ctx) + traceID := span.SpanContext().TraceID().String() + + // response + c.JSON(status, gin.H{"trace_id": traceID, "message": msg, "data": data}) +} + +func ResponseSuccess(c *gin.Context, data interface{}) { + Response(c, http.StatusOK, "", data) +} + +func ResponseError(c *gin.Context, error *APIError) { + Response(c, error.Code, error.Msg, nil) +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..9f7fcc5 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,48 @@ +package logger + +import ( + "context" + "log" + + configUtils "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var logger *zap.Logger + +func init() { + var err error + config := zap.Config{ + Level: LogLevel(), + Development: configUtils.AppDebug(), + Encoding: configUtils.AppLogEncoding(), + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "time", + NameKey: "logger", + CallerKey: "caller", + StacktraceKey: "stacktrace", + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.RFC3339NanoTimeEncoder, + EncodeDuration: zapcore.MillisDurationEncoder, + EncodeCaller: zapcore.FullCallerEncoder, + EncodeName: zapcore.FullNameEncoder, + }, + OutputPaths: []string{configUtils.AppLogOutput()}, + } + if logger, err = config.Build(); err != nil { + log.Fatalf("[Logger] init failed; %s", err) + } +} + +func Logger(ctx context.Context) *zap.Logger { + span := traceUtils.SpanFromContext(ctx) + spanCtx := span.SpanContext() + return logger.With( + zap.String("trace_id", spanCtx.TraceID().String()), + zap.String("span_id", spanCtx.SpanID().String()), + ) +} diff --git a/pkg/logger/utils.go b/pkg/logger/utils.go new file mode 100644 index 0000000..6819df9 --- /dev/null +++ b/pkg/logger/utils.go @@ -0,0 +1,32 @@ +package logger + +import ( + "log" + "strings" + + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "go.uber.org/zap" +) + +func LogLevel() zap.AtomicLevel { + level := strings.ToUpper(configUtils.AppLogLevel()) + switch level { + case "DEBUG": + return zap.NewAtomicLevelAt(zap.DebugLevel) + case "INFO": + return zap.NewAtomicLevelAt(zap.InfoLevel) + case "WARN", "WARNING": + return zap.NewAtomicLevelAt(zap.WarnLevel) + case "ERROR": + return zap.NewAtomicLevelAt(zap.ErrorLevel) + case "DPANIC": + return zap.NewAtomicLevelAt(zap.DPanicLevel) + case "PANIC": + return zap.NewAtomicLevelAt(zap.PanicLevel) + case "FATAL": + return zap.NewAtomicLevelAt(zap.FatalLevel) + default: + log.Printf("[Logger] unknown log level %s, use INFO level", level) + return zap.NewAtomicLevelAt(zap.InfoLevel) + } +} diff --git a/pkg/redisUtils/commands.go b/pkg/redisUtils/commands.go new file mode 100644 index 0000000..ef37fd2 --- /dev/null +++ b/pkg/redisUtils/commands.go @@ -0,0 +1,111 @@ +package redisUtils + +import ( + "context" + "errors" + "time" + + "github.com/ovinc-cn/apicenter/v2/pkg/commonUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/redis/go-redis/v9" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +func buildAttributes(attrs ...attribute.KeyValue) []attribute.KeyValue { + return append( + []attribute.KeyValue{ + attribute.String(traceUtils.AttributeDBSystem, "redis"), + attribute.String(traceUtils.AttributeDBIP, configUtils.RedisAddr()), + attribute.Int(traceUtils.AttributeDBInstance, configUtils.RedisDB()), + }, + attrs..., + ) +} + +func Set(ctx context.Context, conn redis.Cmdable, k string, v interface{}, ex time.Duration) (string, error) { + // trace + ctx, span := traceUtils.StartSpan(ctx, "Redis#Set", trace.SpanKindClient) + defer span.End() + + // add data + span.SetAttributes( + buildAttributes( + attribute.String("key", k), + attribute.String("val", commonUtils.ForceStringMaxLength(v)), + attribute.String("ex", ex.String()), + )..., + ) + + // exec + cmd := conn.Set(ctx, k, v, ex) + if err := cmd.Err(); err != nil { + span.SetStatus(codes.Error, err.Error()) + return "", err + } + return cmd.Val(), nil +} + +func SetNX(ctx context.Context, conn redis.Cmdable, k string, v interface{}, ex time.Duration) (bool, error) { + // trace + ctx, span := traceUtils.StartSpan(ctx, "Redis#SetNX", trace.SpanKindClient) + defer span.End() + + // add data + span.SetAttributes( + buildAttributes( + attribute.String("key", k), + attribute.String("val", commonUtils.ForceStringMaxLength(v)), + attribute.String("ex", ex.String()), + )..., + ) + + // exec + cmd := conn.SetNX(ctx, k, v, ex) + if err := cmd.Err(); err != nil { + span.SetStatus(codes.Error, err.Error()) + return false, err + } + return cmd.Val(), nil +} + +func Get(ctx context.Context, conn redis.Cmdable, k string) (string, error) { + // trace + ctx, span := traceUtils.StartSpan(ctx, "Redis#Get", trace.SpanKindClient) + defer span.End() + + // add data + span.SetAttributes(buildAttributes(attribute.String("key", k))...) + + // exec + cmd := conn.Get(ctx, k) + if err := cmd.Err(); err != nil { + if errors.Is(err, redis.Nil) { + return "", nil + } + span.SetStatus(codes.Error, err.Error()) + return "", err + } + // result + return cmd.Val(), nil +} + +func Del(ctx context.Context, conn redis.Cmdable, keys ...string) (int64, error) { + // trace + ctx, span := traceUtils.StartSpan(ctx, "Redis#Del", trace.SpanKindClient) + defer span.End() + + // add data + span.SetAttributes(buildAttributes(attribute.StringSlice("keys", keys))...) + + // exec + cmd := conn.Del(ctx, keys...) + if err := cmd.Err(); err != nil { + span.SetStatus(codes.Error, err.Error()) + return 0, err + } + // result + return cmd.Val(), nil +} diff --git a/pkg/redisUtils/errs.go b/pkg/redisUtils/errs.go new file mode 100644 index 0000000..0998c5c --- /dev/null +++ b/pkg/redisUtils/errs.go @@ -0,0 +1,11 @@ +package redisUtils + +import "fmt" + +type ConcurrencyLocked struct { + Key string +} + +func (c *ConcurrencyLocked) Error() string { + return fmt.Sprintf("concurrency locked for %s", c.Key) +} diff --git a/pkg/redisUtils/lock.go b/pkg/redisUtils/lock.go new file mode 100644 index 0000000..79a7d82 --- /dev/null +++ b/pkg/redisUtils/lock.go @@ -0,0 +1,45 @@ +package redisUtils + +import ( + "context" + "fmt" + "time" + + "github.com/ovinc-cn/apicenter/v2/pkg/logger" + "github.com/redis/go-redis/v9" +) + +type RedisLock struct { + locked bool + + ctx context.Context + conn redis.Cmdable + + key string + expiration time.Duration +} + +func (l *RedisLock) Lock() error { + result, err := SetNX(l.ctx, l.conn, l.key, time.Now().Format(time.RFC3339Nano), l.expiration) + if err != nil { + return err + } + if !result { + return &ConcurrencyLocked{Key: l.key} + } + l.locked = true + return nil +} + +func (l *RedisLock) Release() { + if l.locked { + _, err := Del(l.ctx, l.conn, l.key) + if err != nil { + logger.Logger(l.ctx).Error(fmt.Sprintf("[RedisLock] release lock %s failed; %s", l.key, err.Error())) + } + } +} + +func NewLock(ctx context.Context, conn redis.Cmdable, key string, expiration time.Duration) *RedisLock { + return &RedisLock{ctx: ctx, conn: conn, key: key, expiration: expiration} +} diff --git a/pkg/redisUtils/redis.go b/pkg/redisUtils/redis.go new file mode 100644 index 0000000..fb30e08 --- /dev/null +++ b/pkg/redisUtils/redis.go @@ -0,0 +1,32 @@ +package redisUtils + +import ( + "context" + "log" + + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/redis/go-redis/v9" +) + +var rdb *redis.Client + +func init() { + // init client + rdb = redis.NewClient( + &redis.Options{ + Addr: configUtils.RedisAddr(), + Password: configUtils.RedisPassword(), + DB: configUtils.RedisDB(), + }, + ) + if err := rdb.Ping(context.Background()).Err(); err != nil { + log.Fatalf("[RedisClient] failed to ping; %s", err) + } + + // log + log.Printf("[RedisClient] connect success\n") +} + +func Client() *redis.Client { + return rdb +} diff --git a/pkg/traceUtils/constants.go b/pkg/traceUtils/constants.go new file mode 100644 index 0000000..252efb0 --- /dev/null +++ b/pkg/traceUtils/constants.go @@ -0,0 +1,8 @@ +package traceUtils + +const ( + AttributeStatusCode = "status.code" + AttributeDBInstance = "db.instance" + AttributeDBIP = "db.ip" + AttributeDBSystem = "db.system" +) diff --git a/pkg/traceUtils/span.go b/pkg/traceUtils/span.go new file mode 100644 index 0000000..ca6ffe5 --- /dev/null +++ b/pkg/traceUtils/span.go @@ -0,0 +1,21 @@ +package traceUtils + +import ( + "context" + + "go.opentelemetry.io/otel/trace" +) + +func StartSpan( + ctx context.Context, + spanName string, + spanKind trace.SpanKind, + options ...trace.SpanStartOption, +) (context.Context, trace.Span) { + options = append(options, trace.WithSpanKind(spanKind)) + return tracer.Start(ctx, spanName, options...) +} + +func SpanFromContext(ctx context.Context) trace.Span { + return trace.SpanFromContext(ctx) +} diff --git a/pkg/traceUtils/trace.go b/pkg/traceUtils/trace.go new file mode 100644 index 0000000..5c77108 --- /dev/null +++ b/pkg/traceUtils/trace.go @@ -0,0 +1,57 @@ +package traceUtils + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer +var traceProvider *sdktrace.TracerProvider + +func init() { + // init ctx + ctx := context.Background() + + // load process name + if len(os.Args) < 2 { + log.Fatalf("[Trace] process name not found") + } + processName := os.Args[1] + + // resource + r, err := resource.New(ctx, resource.WithAttributes( + semconv.ServiceName(fmt.Sprintf("%s-%s", configUtils.AppName(), processName))), + ) + if err != nil { + log.Fatalf("[Trace] create resource failed; %s", err) + } + + // init exporter + e, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(configUtils.TraceEndpoint()), otlptracegrpc.WithInsecure()) + if err != nil { + log.Fatalf("[Trace] create otlp grpc exporter failed; %s", err) + } + + // init trace provider + traceProvider = sdktrace.NewTracerProvider(sdktrace.WithBatcher(e), sdktrace.WithResource(r)) + otel.SetTracerProvider(traceProvider) + otel.SetTextMapPropagator(propagation.TraceContext{}) + + // init tracer + tracer = otel.Tracer(configUtils.AppName()) +} + +func OnExit() { + _ = traceProvider.Shutdown(context.Background()) +} From c62421b3965724f03a2443f2ef0d678beb8e4fc8 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 17:35:13 +0800 Subject: [PATCH 03/22] chore(repo): add codeql workflow --- .github/workflows/codeql.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..bbae956 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,36 @@ +name: "CodeQL" + +on: + push: + branches: + - "main" + pull_request: + branches: + - "*" + +jobs: + analyze: + name: Analyze + runs-on: 'ubuntu-latest' + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 2c678e82a2d3532d447b51e9f78cf68032c7c67b Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 17:38:45 +0800 Subject: [PATCH 04/22] fix(config): change timeout to int64 --- internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index 0cf24cd..eff0581 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,7 +16,7 @@ func init() { Cron: cronConfig{ WorkerConcurrency: configUtils.GetConfigInt("APP_CRON_WORKER_CONCURRENCY", 1), WorkerQueues: configUtils.GetConfigStruct("APP_CRON_WORKER_QUEUES", "{}", make(map[string]int)), - WorkerShutDownTimeout: time.Duration(configUtils.GetConfigUint64("APP_CRON_WORKER_SHUTDOWN_TIMEOUT", 60)) * time.Second, + WorkerShutDownTimeout: time.Duration(configUtils.GetConfigInt64("APP_CRON_WORKER_SHUTDOWN_TIMEOUT", 60)) * time.Second, }, } } From d56acbcc624626a36024c439171cf057d599fd1a Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 17:38:59 +0800 Subject: [PATCH 05/22] chore(repo): update codeql version --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bbae956..dd55a9e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,7 +25,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild From 06c552c39e17c45f9f5773b8cf4160ae529f38b9 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 17:46:12 +0800 Subject: [PATCH 06/22] refactor(cmd): change cmd runner --- internal/cmd/root.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index f968030..dd96ed5 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -22,9 +22,9 @@ var rootCmd = &cobra.Command{ case "api": apiCmd.Run(apiCmd, args) case "worker": - workerCmd.Run(apiCmd, args) + workerCmd.Run(workerCmd, args) case "beat": - beatCmd.Run(apiCmd, args) + beatCmd.Run(beatCmd, args) default: log.Fatal("[CMD] unknown app mode") } From a62a89bc4f1afc8d27a6818790f21ba9ca56ee89 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 17:46:29 +0800 Subject: [PATCH 07/22] refactor(crond): fix log content --- internal/crond/crond.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/crond/crond.go b/internal/crond/crond.go index 65e97f8..6d7dda1 100644 --- a/internal/crond/crond.go +++ b/internal/crond/crond.go @@ -16,5 +16,5 @@ var RedisClientOption = asynq.RedisClientOpt{ func init() { AsyncClient = asynq.NewClient(RedisClientOption) - log.Printf("[AsyncClinet] init success\n") + log.Printf("[AsyncClient] init success\n") } From 8f4c480815b54ff7d9508f7141e1947878136e4e Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sat, 4 Oct 2025 20:10:57 +0800 Subject: [PATCH 08/22] feat(mysql): init --- go.mod | 39 ++++- go.sum | 138 ++++++++++++++++-- internal/apps/home/errs.go | 17 +++ internal/apps/home/routers.go | 23 ++- internal/apps/home/tasks.go | 9 +- internal/cmd/migrate.go | 23 +++ internal/cmd/root.go | 6 +- internal/config/config.go | 10 +- internal/crond/beat/beat.go | 4 +- internal/crond/crond.go | 8 +- internal/crond/utils.go | 14 +- internal/crond/worker/worker.go | 3 +- internal/httpd/httpd.go | 8 +- pkg/{ginUtils => apiMixin}/constants.go | 2 +- pkg/{ginUtils => apiMixin}/middlewares.go | 9 +- pkg/{ginUtils => apiMixin}/models.go | 2 +- pkg/{ginUtils => apiMixin}/response.go | 6 +- .../crond => pkg/asyncTask}/middlewares.go | 17 +-- .../crond/model.go => pkg/asyncTask/models.go | 2 +- pkg/{configUtils => cfg}/config.go | 35 ++++- pkg/{configUtils => cfg}/utils.go | 2 +- pkg/logger/logger.go | 12 +- pkg/logger/utils.go | 4 +- pkg/mysql/mysql.go | 37 +++++ pkg/mysql/utils.go | 45 ++++++ pkg/{redisUtils => redis}/commands.go | 41 ++++-- pkg/{redisUtils => redis}/errs.go | 2 +- pkg/{redisUtils => redis}/lock.go | 2 +- pkg/{redisUtils => redis}/redis.go | 12 +- pkg/trace/constants.go | 18 +++ pkg/{traceUtils => trace}/span.go | 2 +- pkg/{traceUtils => trace}/trace.go | 10 +- pkg/traceUtils/constants.go | 8 - pkg/{commonUtils => utils}/string.go | 2 +- 34 files changed, 456 insertions(+), 116 deletions(-) create mode 100644 internal/apps/home/errs.go create mode 100644 internal/cmd/migrate.go rename pkg/{ginUtils => apiMixin}/constants.go (73%) rename pkg/{ginUtils => apiMixin}/middlewares.go (77%) rename pkg/{ginUtils => apiMixin}/models.go (85%) rename pkg/{ginUtils => apiMixin}/response.go (83%) rename {internal/crond => pkg/asyncTask}/middlewares.go (74%) rename internal/crond/model.go => pkg/asyncTask/models.go (87%) rename pkg/{configUtils => cfg}/config.go (53%) rename pkg/{configUtils => cfg}/utils.go (99%) create mode 100644 pkg/mysql/mysql.go create mode 100644 pkg/mysql/utils.go rename pkg/{redisUtils => redis}/commands.go (65%) rename pkg/{redisUtils => redis}/errs.go (89%) rename pkg/{redisUtils => redis}/lock.go (97%) rename pkg/{redisUtils => redis}/redis.go (56%) create mode 100644 pkg/trace/constants.go rename pkg/{traceUtils => trace}/span.go (95%) rename pkg/{traceUtils => trace}/trace.go (78%) delete mode 100644 pkg/traceUtils/constants.go rename pkg/{commonUtils => utils}/string.go (95%) diff --git a/go.mod b/go.mod index 8649086..c7488bd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ovinc-cn/apicenter/v2 -go 1.23.0 +go 1.24.0 require ( github.com/gin-gonic/gin v1.11.0 @@ -13,9 +13,16 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/zap v1.27.0 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.31.0 + gorm.io/plugin/opentelemetry v0.1.16 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/ClickHouse/ch-go v0.61.5 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.30.0 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -24,26 +31,43 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -53,18 +77,21 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/mock v0.5.0 // indirect - go.uber.org/multierr v1.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.41.0 // indirect - golang.org/x/mod v0.26.0 // indirect + golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.8.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/grpc v1.75.0 // indirect google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/clickhouse v0.7.0 // indirect + gorm.io/driver/postgres v1.5.11 // indirect ) diff --git a/go.sum b/go.sum index b56c761..ce22ba5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,11 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= +github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= +github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo= +github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -26,6 +34,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -39,12 +51,19 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -52,30 +71,62 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw= github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= @@ -89,6 +140,10 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= @@ -99,15 +154,27 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -130,29 +197,63 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= @@ -161,9 +262,26 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/clickhouse v0.7.0 h1:BCrqvgONayvZRgtuA6hdya+eAW5P2QVagV3OlEp1vtA= +gorm.io/driver/clickhouse v0.7.0/go.mod h1:TmNo0wcVTsD4BBObiRnCahUgHJHjBIwuRejHwYt3JRs= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/plugin/opentelemetry v0.1.16 h1:Kypj2YYAliJqkIczDZDde6P6sFMhKSlG5IpngMFQGpc= +gorm.io/plugin/opentelemetry v0.1.16/go.mod h1:P3RmTeZXT+9n0F1ccUqR5uuTvEXDxF8k2UpO7mTIB2Y= diff --git a/internal/apps/home/errs.go b/internal/apps/home/errs.go new file mode 100644 index 0000000..af88482 --- /dev/null +++ b/internal/apps/home/errs.go @@ -0,0 +1,17 @@ +package home + +import ( + "net/http" + + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" +) + +var MySQLConnFailed = &apiMixin.APIError{ + Code: http.StatusInternalServerError, + Msg: "database connection failed", +} + +var RedisConnFailed = &apiMixin.APIError{ + Code: http.StatusInternalServerError, + Msg: "redis connection failed", +} diff --git a/internal/apps/home/routers.go b/internal/apps/home/routers.go index e41ae2e..2118d80 100644 --- a/internal/apps/home/routers.go +++ b/internal/apps/home/routers.go @@ -1,10 +1,29 @@ package home import ( + "context" + "github.com/gin-gonic/gin" - "github.com/ovinc-cn/apicenter/v2/pkg/ginUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/mysql" + "github.com/ovinc-cn/apicenter/v2/pkg/redis" ) func HealthZ(c *gin.Context) { - ginUtils.ResponseSuccess(c, nil) + // trace + ctx := c.MustGet(apiMixin.TraceContextKey).(context.Context) + + // check for db + if mysql.DB().Ping() != nil { + apiMixin.ResponseError(c, MySQLConnFailed) + return + } + + // check for redis + if err := redis.Ping(ctx, redis.Client()); err != nil { + apiMixin.ResponseError(c, RedisConnFailed) + return + } + + apiMixin.ResponseSuccess(c, nil) } diff --git a/internal/apps/home/tasks.go b/internal/apps/home/tasks.go index 4c769b5..3411d31 100644 --- a/internal/apps/home/tasks.go +++ b/internal/apps/home/tasks.go @@ -6,10 +6,9 @@ import ( "time" "github.com/hibiken/asynq" - "github.com/ovinc-cn/apicenter/v2/pkg/redisUtils" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/redis" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" ) var DebugTaskLockKey = "task:home:debug" @@ -17,11 +16,11 @@ var DebugTaskLockTTL = 1 * time.Hour func DebugTask(ctx context.Context, t *asynq.Task) error { // trace - ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("Task#%s", t.Type()), trace.SpanKindConsumer) + ctx, span := trace.StartSpan(ctx, fmt.Sprintf("Task#%s", t.Type()), trace.SpanKindConsumer) defer span.End() // lock - lock := redisUtils.NewLock(ctx, redisUtils.Client(), DebugTaskLockKey, DebugTaskLockTTL) + lock := redis.NewLock(ctx, redis.Client(), DebugTaskLockKey, DebugTaskLockTTL) defer lock.Release() if err := lock.Lock(); err != nil { span.SetStatus(codes.Error, err.Error()) diff --git a/internal/cmd/migrate.go b/internal/cmd/migrate.go new file mode 100644 index 0000000..77ffbb1 --- /dev/null +++ b/internal/cmd/migrate.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "log" + + "github.com/ovinc-cn/apicenter/v2/pkg/mysql" + "github.com/spf13/cobra" +) + +var migrateCmd = &cobra.Command{ + Use: "migrator", + Short: "Api Center Migrator", + Run: func(cmd *cobra.Command, args []string) { + gormDB, err := mysql.GormDB() + if err != nil { + log.Fatalf("[Migrate] connect to mysql failed; %s", err) + } + if err := gormDB.AutoMigrate(); err != nil { + log.Fatalf("[Migrate] auto migrate failed; %s", err) + } + log.Println("[Migrate] auto migrate success") + }, +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index dd96ed5..fa1aefa 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,7 +6,7 @@ import ( "github.com/ovinc-cn/apicenter/v2/internal/crond" "github.com/ovinc-cn/apicenter/v2/internal/version" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" "github.com/spf13/cobra" ) @@ -25,6 +25,8 @@ var rootCmd = &cobra.Command{ workerCmd.Run(workerCmd, args) case "beat": beatCmd.Run(beatCmd, args) + case "migrate": + migrateCmd.Run(migrateCmd, args) default: log.Fatal("[CMD] unknown app mode") } @@ -34,7 +36,7 @@ var rootCmd = &cobra.Command{ }, PostRun: func(cmd *cobra.Command, args []string) { _ = crond.AsyncClient.Close() - traceUtils.OnExit() + trace.OnExit() os.Exit(0) }, } diff --git a/internal/config/config.go b/internal/config/config.go index eff0581..df8af8e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,7 +3,7 @@ package config import ( "time" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" ) var Config configModel @@ -11,12 +11,12 @@ var Config configModel func init() { Config = configModel{ API: apiConfig{ - Addr: configUtils.GetConfigVal("APP_API_ADDR", "0.0.0.0:8000"), + Addr: cfg.GetConfigVal("APP_API_ADDR", "0.0.0.0:8000"), }, Cron: cronConfig{ - WorkerConcurrency: configUtils.GetConfigInt("APP_CRON_WORKER_CONCURRENCY", 1), - WorkerQueues: configUtils.GetConfigStruct("APP_CRON_WORKER_QUEUES", "{}", make(map[string]int)), - WorkerShutDownTimeout: time.Duration(configUtils.GetConfigInt64("APP_CRON_WORKER_SHUTDOWN_TIMEOUT", 60)) * time.Second, + WorkerConcurrency: cfg.GetConfigInt("APP_CRON_WORKER_CONCURRENCY", 1), + WorkerQueues: cfg.GetConfigStruct("APP_CRON_WORKER_QUEUES", "{}", make(map[string]int)), + WorkerShutDownTimeout: time.Duration(cfg.GetConfigInt64("APP_CRON_WORKER_SHUTDOWN_TIMEOUT", 60)) * time.Second, }, } } diff --git a/internal/crond/beat/beat.go b/internal/crond/beat/beat.go index 1bddcb6..a78c00a 100644 --- a/internal/crond/beat/beat.go +++ b/internal/crond/beat/beat.go @@ -6,7 +6,7 @@ import ( "github.com/hibiken/asynq" "github.com/ovinc-cn/apicenter/v2/internal/crond" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" ) type taskConfig struct { @@ -25,7 +25,7 @@ var periodTasks = []taskConfig{ func Serve() { // init beat - scheduler := asynq.NewScheduler(crond.RedisClientOption, &asynq.SchedulerOpts{Location: configUtils.AppTimezone()}) + scheduler := asynq.NewScheduler(crond.RedisClientOption, &asynq.SchedulerOpts{Location: cfg.AppTimezone()}) // register task for _, periodTask := range periodTasks { diff --git a/internal/crond/crond.go b/internal/crond/crond.go index 6d7dda1..b4ce8d8 100644 --- a/internal/crond/crond.go +++ b/internal/crond/crond.go @@ -4,14 +4,14 @@ import ( "log" "github.com/hibiken/asynq" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" ) var AsyncClient *asynq.Client var RedisClientOption = asynq.RedisClientOpt{ - Addr: configUtils.RedisAddr(), - Password: configUtils.RedisPassword(), - DB: configUtils.RedisDB(), + Addr: cfg.RedisAddr(), + Password: cfg.RedisPassword(), + DB: cfg.RedisDB(), } func init() { diff --git a/internal/crond/utils.go b/internal/crond/utils.go index c6914d2..a90b94e 100644 --- a/internal/crond/utils.go +++ b/internal/crond/utils.go @@ -7,22 +7,22 @@ import ( "fmt" "github.com/hibiken/asynq" - "github.com/ovinc-cn/apicenter/v2/pkg/commonUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/asyncTask" "github.com/ovinc-cn/apicenter/v2/pkg/logger" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" + "github.com/ovinc-cn/apicenter/v2/pkg/utils" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" ) func NewTask(ctx context.Context, taskType TaskType, data interface{}, customOps ...asynq.Option) (*asynq.Task, error) { // init trace - ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("NewTask#%s", taskType), trace.SpanKindInternal) + ctx, span := trace.StartSpan(ctx, fmt.Sprintf("NewTask#%s", taskType), trace.SpanKindInternal) defer span.End() // init payload - payload := TaskPayload{Carrier: propagation.MapCarrier{}, Data: data} + payload := asyncTask.TaskPayload{Carrier: propagation.MapCarrier{}, Data: data} // inject carrier otel.GetTextMapPropagator().Inject(ctx, payload.Carrier) @@ -47,7 +47,7 @@ func NewTask(ctx context.Context, taskType TaskType, data interface{}, customOps func RunAsyncTask(ctx context.Context, taskType TaskType, data interface{}, ops ...asynq.Option) error { // trace - ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("RunAsyncTask#%s", taskType), trace.SpanKindProducer) + ctx, span := trace.StartSpan(ctx, fmt.Sprintf("RunAsyncTask#%s", taskType), trace.SpanKindProducer) defer span.End() // init task @@ -67,6 +67,6 @@ func RunAsyncTask(ctx context.Context, taskType TaskType, data interface{}, ops logger.Logger(ctx).Error(fmt.Sprintf("[RunAsyncTask] enqueue %s task failed\nerr: %s", taskType, err)) return err } - logger.Logger(ctx).Info(fmt.Sprintf("[RunAsyncTask] enqueue %s task success\ntask: %s", taskType, commonUtils.ForceStringMaxLength(taskInfo))) + logger.Logger(ctx).Info(fmt.Sprintf("[RunAsyncTask] enqueue %s task success\ntask: %s", taskType, utils.ForceStringMaxLength(taskInfo))) return nil } diff --git a/internal/crond/worker/worker.go b/internal/crond/worker/worker.go index 5071e57..ff37f2f 100644 --- a/internal/crond/worker/worker.go +++ b/internal/crond/worker/worker.go @@ -7,6 +7,7 @@ import ( "github.com/ovinc-cn/apicenter/v2/internal/apps/home" "github.com/ovinc-cn/apicenter/v2/internal/config" "github.com/ovinc-cn/apicenter/v2/internal/crond" + "github.com/ovinc-cn/apicenter/v2/pkg/asyncTask" ) func Serve() { @@ -22,7 +23,7 @@ func Serve() { // init mux := asynq.NewServeMux() - mux.Use(crond.RecoverMiddleware, crond.ObserveMiddleware) + mux.Use(asyncTask.RecoverMiddleware, asyncTask.ObserveMiddleware) // home mux.HandleFunc(crond.TaskTypeDebug, home.DebugTask) diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go index f44bc49..7baa00b 100644 --- a/internal/httpd/httpd.go +++ b/internal/httpd/httpd.go @@ -6,13 +6,13 @@ import ( "github.com/gin-gonic/gin" "github.com/ovinc-cn/apicenter/v2/internal/apps/home" "github.com/ovinc-cn/apicenter/v2/internal/config" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" - "github.com/ovinc-cn/apicenter/v2/pkg/ginUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" ) func Serve() { // release mode - if configUtils.AppDebug() { + if cfg.AppDebug() { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) @@ -22,7 +22,7 @@ func Serve() { r := gin.New() // middlewares - r.Use(gin.Recovery(), ginUtils.ObserveMiddleware()) + r.Use(gin.Recovery(), apiMixin.ObserveMiddleware()) // routers apiV1Group := r.Group("/v1") diff --git a/pkg/ginUtils/constants.go b/pkg/apiMixin/constants.go similarity index 73% rename from pkg/ginUtils/constants.go rename to pkg/apiMixin/constants.go index a4c50a3..25c43f4 100644 --- a/pkg/ginUtils/constants.go +++ b/pkg/apiMixin/constants.go @@ -1,3 +1,3 @@ -package ginUtils +package apiMixin const TraceContextKey = "otel_trace_context" diff --git a/pkg/ginUtils/middlewares.go b/pkg/apiMixin/middlewares.go similarity index 77% rename from pkg/ginUtils/middlewares.go rename to pkg/apiMixin/middlewares.go index c526481..6dbbcb3 100644 --- a/pkg/ginUtils/middlewares.go +++ b/pkg/apiMixin/middlewares.go @@ -1,4 +1,4 @@ -package ginUtils +package apiMixin import ( "context" @@ -8,11 +8,10 @@ import ( "github.com/gin-gonic/gin" "github.com/ovinc-cn/apicenter/v2/pkg/logger" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.37.0" - "go.opentelemetry.io/otel/trace" ) func ObserveMiddleware() gin.HandlerFunc { @@ -24,7 +23,7 @@ func ObserveMiddleware() gin.HandlerFunc { } // init trace - ctx, span := traceUtils.StartSpan(context.Background(), fmt.Sprintf("Router#%s#%s", c.Request.Method, path), trace.SpanKindServer) + ctx, span := trace.StartSpan(context.Background(), fmt.Sprintf("Router#%s#%s", c.Request.Method, path), trace.SpanKindServer) defer span.End() // add trace to context @@ -48,7 +47,7 @@ func ObserveMiddleware() gin.HandlerFunc { // add attributes span.SetAttributes( semconv.HTTPResponseStatusCode(status), - attribute.Int(traceUtils.AttributeStatusCode, status), + attribute.Int(trace.AttributeStatusCode, status), ) // log diff --git a/pkg/ginUtils/models.go b/pkg/apiMixin/models.go similarity index 85% rename from pkg/ginUtils/models.go rename to pkg/apiMixin/models.go index 220a395..5bf9a21 100644 --- a/pkg/ginUtils/models.go +++ b/pkg/apiMixin/models.go @@ -1,4 +1,4 @@ -package ginUtils +package apiMixin type APIError struct { Code int diff --git a/pkg/ginUtils/response.go b/pkg/apiMixin/response.go similarity index 83% rename from pkg/ginUtils/response.go rename to pkg/apiMixin/response.go index fea93d8..c79f0b2 100644 --- a/pkg/ginUtils/response.go +++ b/pkg/apiMixin/response.go @@ -1,17 +1,17 @@ -package ginUtils +package apiMixin import ( "context" "net/http" "github.com/gin-gonic/gin" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" ) func Response(c *gin.Context, status int, msg string, data interface{}) { // load context ctx := c.MustGet(TraceContextKey).(context.Context) - span := traceUtils.SpanFromContext(ctx) + span := trace.SpanFromContext(ctx) traceID := span.SpanContext().TraceID().String() // response diff --git a/internal/crond/middlewares.go b/pkg/asyncTask/middlewares.go similarity index 74% rename from internal/crond/middlewares.go rename to pkg/asyncTask/middlewares.go index 58e930a..b4fe031 100644 --- a/internal/crond/middlewares.go +++ b/pkg/asyncTask/middlewares.go @@ -1,4 +1,4 @@ -package crond +package asyncTask import ( "context" @@ -7,11 +7,10 @@ import ( "runtime" "github.com/hibiken/asynq" - "github.com/ovinc-cn/apicenter/v2/pkg/commonUtils" "github.com/ovinc-cn/apicenter/v2/pkg/logger" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" + "github.com/ovinc-cn/apicenter/v2/pkg/utils" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" ) type ScheduleTaskFailed struct { @@ -29,7 +28,7 @@ func ObserveMiddleware(next asynq.Handler) asynq.Handler { var payload TaskPayload if err := json.Unmarshal(task.Payload(), &payload); err != nil { logger.Logger(ctx).Error( - fmt.Sprintf("[%s] failed\npayload: %s\nerr: %s", task.Type(), commonUtils.ForceStringMaxLength(task.Payload()), err), + fmt.Sprintf("[%s] failed\npayload: %s\nerr: %s", task.Type(), utils.ForceStringMaxLength(task.Payload()), err), ) return err } @@ -40,7 +39,7 @@ func ObserveMiddleware(next asynq.Handler) asynq.Handler { } // span - ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("ObserveTask#%s", task.Type()), trace.SpanKindInternal) + ctx, span := trace.StartSpan(ctx, fmt.Sprintf("ObserveTask#%s", task.Type()), trace.SpanKindInternal) defer span.End() // pre exec @@ -52,7 +51,7 @@ func ObserveMiddleware(next asynq.Handler) asynq.Handler { // post exec if err != nil { logger.Logger(ctx).Error( - fmt.Sprintf("[%s] failed\npayload: %s\nerr: %s", task.Type(), commonUtils.ForceStringMaxLength(task.Payload()), err), + fmt.Sprintf("[%s] failed\npayload: %s\nerr: %s", task.Type(), utils.ForceStringMaxLength(task.Payload()), err), ) } else { logger.Logger(ctx).Info(fmt.Sprintf("[%s] finished", task.Type())) @@ -65,7 +64,7 @@ func RecoverMiddleware(next asynq.Handler) asynq.Handler { return asynq.HandlerFunc(func(ctx context.Context, task *asynq.Task) (err error) { defer func() { if r := recover(); r != nil { - ctx, span := traceUtils.StartSpan(ctx, fmt.Sprintf("Task#%s#Panic", task.Type()), trace.SpanKindInternal) + ctx, span := trace.StartSpan(ctx, fmt.Sprintf("Task#%s#Panic", task.Type()), trace.SpanKindInternal) defer span.End() var stack []byte @@ -75,7 +74,7 @@ func RecoverMiddleware(next asynq.Handler) asynq.Handler { stack = stack[:length] logger.Logger(ctx).Error( - fmt.Sprintf("[%s] panic\nerr: %s\n%s", task.Type(), commonUtils.ForceStringMaxLength(r), stack), + fmt.Sprintf("[%s] panic\nerr: %s\n%s", task.Type(), utils.ForceStringMaxLength(r), stack), ) } }() diff --git a/internal/crond/model.go b/pkg/asyncTask/models.go similarity index 87% rename from internal/crond/model.go rename to pkg/asyncTask/models.go index 1990e34..0b66633 100644 --- a/internal/crond/model.go +++ b/pkg/asyncTask/models.go @@ -1,4 +1,4 @@ -package crond +package asyncTask import "go.opentelemetry.io/otel/propagation" diff --git a/pkg/configUtils/config.go b/pkg/cfg/config.go similarity index 53% rename from pkg/configUtils/config.go rename to pkg/cfg/config.go index 2d7edf0..201948e 100644 --- a/pkg/configUtils/config.go +++ b/pkg/cfg/config.go @@ -1,4 +1,4 @@ -package configUtils +package cfg import ( "log" @@ -36,7 +36,38 @@ var ( } ) -// db +// mysql +var ( + MySQLUsername = func() string { + return GetConfigVal("APP_MYSQL_USERNAME", "root") + } + MySQLPassword = func() string { + return GetConfigVal("APP_MYSQL_PASSWORD", "") + } + MySQLHost = func() string { + return GetConfigVal("APP_MYSQL_HOST", "127.0.0.1") + } + MySQLPort = func() int { + return GetConfigInt("APP_MYSQL_PORT", 3306) + } + MySQLDatabase = func() string { + return GetConfigVal("APP_MYSQL_DATABASE", "api_center") + } + MySQLParams = func() string { + return GetConfigVal("APP_MYSQL_CONNECT_ARGS", "charset=utf8mb4&parseTime=True&loc=UTC") + } + MySQLMaxOpenConns = func() int { + return GetConfigInt("APP_MYSQL_MAX_OPEN_CONNS", 100) + } + MySQLMaxIdleConns = func() int { + return GetConfigInt("APP_MYSQL_MAX_IDLE_CONNS", 10) + } + MySQLConnMaxLifetime = func() time.Duration { + return time.Duration(GetConfigInt("APP_MYSQL_CONN_MAX_LIFETIME", 3600)) * time.Second + } +) + +// redis var ( RedisAddr = func() string { return GetConfigVal("APP_REDIS_ADDR", "127.0.0.1:6379") diff --git a/pkg/configUtils/utils.go b/pkg/cfg/utils.go similarity index 99% rename from pkg/configUtils/utils.go rename to pkg/cfg/utils.go index 0c8fc3b..cbf65fc 100644 --- a/pkg/configUtils/utils.go +++ b/pkg/cfg/utils.go @@ -1,4 +1,4 @@ -package configUtils +package cfg import ( "encoding/json" diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 9f7fcc5..4c4c929 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -4,8 +4,8 @@ import ( "context" "log" - configUtils "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -16,8 +16,8 @@ func init() { var err error config := zap.Config{ Level: LogLevel(), - Development: configUtils.AppDebug(), - Encoding: configUtils.AppLogEncoding(), + Development: cfg.AppDebug(), + Encoding: cfg.AppLogEncoding(), EncoderConfig: zapcore.EncoderConfig{ MessageKey: "message", LevelKey: "level", @@ -31,7 +31,7 @@ func init() { EncodeCaller: zapcore.FullCallerEncoder, EncodeName: zapcore.FullNameEncoder, }, - OutputPaths: []string{configUtils.AppLogOutput()}, + OutputPaths: []string{cfg.AppLogOutput()}, } if logger, err = config.Build(); err != nil { log.Fatalf("[Logger] init failed; %s", err) @@ -39,7 +39,7 @@ func init() { } func Logger(ctx context.Context) *zap.Logger { - span := traceUtils.SpanFromContext(ctx) + span := trace.SpanFromContext(ctx) spanCtx := span.SpanContext() return logger.With( zap.String("trace_id", spanCtx.TraceID().String()), diff --git a/pkg/logger/utils.go b/pkg/logger/utils.go index 6819df9..a870fc3 100644 --- a/pkg/logger/utils.go +++ b/pkg/logger/utils.go @@ -4,12 +4,12 @@ import ( "log" "strings" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" "go.uber.org/zap" ) func LogLevel() zap.AtomicLevel { - level := strings.ToUpper(configUtils.AppLogLevel()) + level := strings.ToUpper(cfg.AppLogLevel()) switch level { case "DEBUG": return zap.NewAtomicLevelAt(zap.DebugLevel) diff --git a/pkg/mysql/mysql.go b/pkg/mysql/mysql.go new file mode 100644 index 0000000..9b19238 --- /dev/null +++ b/pkg/mysql/mysql.go @@ -0,0 +1,37 @@ +package mysql + +import ( + "database/sql" + "log" + + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" + "gorm.io/gorm" +) + +var db *sql.DB + +func init() { + gormDB, err := openConn() + if err != nil { + log.Fatalf("[MySQL] connect to mysql failed; %s", err) + } + // try to connect + db, err = gormDB.DB() + if err != nil || db.Ping() != nil { + log.Fatalf("[MySQL] ping mysql failed; %s", err) + } + // set config + db.SetMaxOpenConns(cfg.MySQLMaxOpenConns()) + db.SetMaxIdleConns(cfg.MySQLMaxIdleConns()) + db.SetConnMaxLifetime(cfg.MySQLConnMaxLifetime()) + // log + log.Printf("[MySQL] connect to mysql success\n") +} + +func DB() *sql.DB { + return db +} + +func GormDB() (*gorm.DB, error) { + return openConn() +} diff --git a/pkg/mysql/utils.go b/pkg/mysql/utils.go new file mode 100644 index 0000000..9f810df --- /dev/null +++ b/pkg/mysql/utils.go @@ -0,0 +1,45 @@ +package mysql + +import ( + "fmt" + + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" + "go.opentelemetry.io/otel/attribute" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/plugin/opentelemetry/tracing" +) + +func openConn() (*gorm.DB, error) { + // init dsn + dsn := fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?%s", + cfg.MySQLUsername(), + cfg.MySQLPassword(), + cfg.MySQLHost(), + cfg.MySQLPort(), + cfg.MySQLDatabase(), + cfg.MySQLParams(), + ) + // init db + gormDB, err := gorm.Open( + mysql.Open(dsn), + &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}, + ) + if err != nil { + return nil, err + } + // add trace + if err := gormDB.Use(tracing.NewPlugin( + tracing.WithoutMetrics(), + tracing.WithAttributes( + attribute.String(trace.AttributeDBSystem, "mysql"), + attribute.String(trace.AttributeDBIP, fmt.Sprintf("%s:%d", cfg.MySQLHost(), cfg.MySQLPort())), + attribute.String(trace.AttributeDBInstance, cfg.MySQLDatabase()), + ), + )); err != nil { + return nil, err + } + return gormDB, nil +} diff --git a/pkg/redisUtils/commands.go b/pkg/redis/commands.go similarity index 65% rename from pkg/redisUtils/commands.go rename to pkg/redis/commands.go index ef37fd2..5335258 100644 --- a/pkg/redisUtils/commands.go +++ b/pkg/redis/commands.go @@ -1,40 +1,53 @@ -package redisUtils +package redis import ( "context" "errors" "time" - "github.com/ovinc-cn/apicenter/v2/pkg/commonUtils" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" - "github.com/ovinc-cn/apicenter/v2/pkg/traceUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" + "github.com/ovinc-cn/apicenter/v2/pkg/utils" "github.com/redis/go-redis/v9" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" ) func buildAttributes(attrs ...attribute.KeyValue) []attribute.KeyValue { return append( []attribute.KeyValue{ - attribute.String(traceUtils.AttributeDBSystem, "redis"), - attribute.String(traceUtils.AttributeDBIP, configUtils.RedisAddr()), - attribute.Int(traceUtils.AttributeDBInstance, configUtils.RedisDB()), + attribute.String(trace.AttributeDBSystem, "redis"), + attribute.String(trace.AttributeDBIP, cfg.RedisAddr()), + attribute.Int(trace.AttributeDBInstance, cfg.RedisDB()), }, attrs..., ) } +func Ping(ctx context.Context, conn redis.Cmdable) error { + // trace + ctx, span := trace.StartSpan(ctx, "Redis#Ping", trace.SpanKindClient) + defer span.End() + + // exec + cmd := conn.Ping(ctx) + if err := cmd.Err(); err != nil { + span.SetStatus(codes.Error, err.Error()) + return err + } + return nil +} + func Set(ctx context.Context, conn redis.Cmdable, k string, v interface{}, ex time.Duration) (string, error) { // trace - ctx, span := traceUtils.StartSpan(ctx, "Redis#Set", trace.SpanKindClient) + ctx, span := trace.StartSpan(ctx, "Redis#Set", trace.SpanKindClient) defer span.End() // add data span.SetAttributes( buildAttributes( attribute.String("key", k), - attribute.String("val", commonUtils.ForceStringMaxLength(v)), + attribute.String("val", utils.ForceStringMaxLength(v)), attribute.String("ex", ex.String()), )..., ) @@ -50,14 +63,14 @@ func Set(ctx context.Context, conn redis.Cmdable, k string, v interface{}, ex ti func SetNX(ctx context.Context, conn redis.Cmdable, k string, v interface{}, ex time.Duration) (bool, error) { // trace - ctx, span := traceUtils.StartSpan(ctx, "Redis#SetNX", trace.SpanKindClient) + ctx, span := trace.StartSpan(ctx, "Redis#SetNX", trace.SpanKindClient) defer span.End() // add data span.SetAttributes( buildAttributes( attribute.String("key", k), - attribute.String("val", commonUtils.ForceStringMaxLength(v)), + attribute.String("val", utils.ForceStringMaxLength(v)), attribute.String("ex", ex.String()), )..., ) @@ -73,7 +86,7 @@ func SetNX(ctx context.Context, conn redis.Cmdable, k string, v interface{}, ex func Get(ctx context.Context, conn redis.Cmdable, k string) (string, error) { // trace - ctx, span := traceUtils.StartSpan(ctx, "Redis#Get", trace.SpanKindClient) + ctx, span := trace.StartSpan(ctx, "Redis#Get", trace.SpanKindClient) defer span.End() // add data @@ -94,7 +107,7 @@ func Get(ctx context.Context, conn redis.Cmdable, k string) (string, error) { func Del(ctx context.Context, conn redis.Cmdable, keys ...string) (int64, error) { // trace - ctx, span := traceUtils.StartSpan(ctx, "Redis#Del", trace.SpanKindClient) + ctx, span := trace.StartSpan(ctx, "Redis#Del", trace.SpanKindClient) defer span.End() // add data diff --git a/pkg/redisUtils/errs.go b/pkg/redis/errs.go similarity index 89% rename from pkg/redisUtils/errs.go rename to pkg/redis/errs.go index 0998c5c..79f7286 100644 --- a/pkg/redisUtils/errs.go +++ b/pkg/redis/errs.go @@ -1,4 +1,4 @@ -package redisUtils +package redis import "fmt" diff --git a/pkg/redisUtils/lock.go b/pkg/redis/lock.go similarity index 97% rename from pkg/redisUtils/lock.go rename to pkg/redis/lock.go index 79a7d82..67eb9b5 100644 --- a/pkg/redisUtils/lock.go +++ b/pkg/redis/lock.go @@ -1,4 +1,4 @@ -package redisUtils +package redis import ( "context" diff --git a/pkg/redisUtils/redis.go b/pkg/redis/redis.go similarity index 56% rename from pkg/redisUtils/redis.go rename to pkg/redis/redis.go index fb30e08..0b70e44 100644 --- a/pkg/redisUtils/redis.go +++ b/pkg/redis/redis.go @@ -1,10 +1,10 @@ -package redisUtils +package redis import ( "context" "log" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" "github.com/redis/go-redis/v9" ) @@ -14,12 +14,12 @@ func init() { // init client rdb = redis.NewClient( &redis.Options{ - Addr: configUtils.RedisAddr(), - Password: configUtils.RedisPassword(), - DB: configUtils.RedisDB(), + Addr: cfg.RedisAddr(), + Password: cfg.RedisPassword(), + DB: cfg.RedisDB(), }, ) - if err := rdb.Ping(context.Background()).Err(); err != nil { + if err := Ping(context.Background(), rdb); err != nil { log.Fatalf("[RedisClient] failed to ping; %s", err) } diff --git a/pkg/trace/constants.go b/pkg/trace/constants.go new file mode 100644 index 0000000..f9fca7d --- /dev/null +++ b/pkg/trace/constants.go @@ -0,0 +1,18 @@ +package trace + +import otTrace "go.opentelemetry.io/otel/trace" + +const ( + AttributeStatusCode = "status.code" + AttributeDBInstance = "db.instance" + AttributeDBIP = "db.ip" + AttributeDBSystem = "db.system" +) + +const ( + SpanKindInternal = otTrace.SpanKindInternal + SpanKindServer = otTrace.SpanKindServer + SpanKindClient = otTrace.SpanKindClient + SpanKindProducer = otTrace.SpanKindProducer + SpanKindConsumer = otTrace.SpanKindConsumer +) diff --git a/pkg/traceUtils/span.go b/pkg/trace/span.go similarity index 95% rename from pkg/traceUtils/span.go rename to pkg/trace/span.go index ca6ffe5..8f1bda9 100644 --- a/pkg/traceUtils/span.go +++ b/pkg/trace/span.go @@ -1,4 +1,4 @@ -package traceUtils +package trace import ( "context" diff --git a/pkg/traceUtils/trace.go b/pkg/trace/trace.go similarity index 78% rename from pkg/traceUtils/trace.go rename to pkg/trace/trace.go index 5c77108..45965b6 100644 --- a/pkg/traceUtils/trace.go +++ b/pkg/trace/trace.go @@ -1,4 +1,4 @@ -package traceUtils +package trace import ( "context" @@ -6,7 +6,7 @@ import ( "log" "os" - "github.com/ovinc-cn/apicenter/v2/pkg/configUtils" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" @@ -31,14 +31,14 @@ func init() { // resource r, err := resource.New(ctx, resource.WithAttributes( - semconv.ServiceName(fmt.Sprintf("%s-%s", configUtils.AppName(), processName))), + semconv.ServiceName(fmt.Sprintf("%s-%s", cfg.AppName(), processName))), ) if err != nil { log.Fatalf("[Trace] create resource failed; %s", err) } // init exporter - e, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(configUtils.TraceEndpoint()), otlptracegrpc.WithInsecure()) + e, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(cfg.TraceEndpoint()), otlptracegrpc.WithInsecure()) if err != nil { log.Fatalf("[Trace] create otlp grpc exporter failed; %s", err) } @@ -49,7 +49,7 @@ func init() { otel.SetTextMapPropagator(propagation.TraceContext{}) // init tracer - tracer = otel.Tracer(configUtils.AppName()) + tracer = otel.Tracer(cfg.AppName()) } func OnExit() { diff --git a/pkg/traceUtils/constants.go b/pkg/traceUtils/constants.go deleted file mode 100644 index 252efb0..0000000 --- a/pkg/traceUtils/constants.go +++ /dev/null @@ -1,8 +0,0 @@ -package traceUtils - -const ( - AttributeStatusCode = "status.code" - AttributeDBInstance = "db.instance" - AttributeDBIP = "db.ip" - AttributeDBSystem = "db.system" -) diff --git a/pkg/commonUtils/string.go b/pkg/utils/string.go similarity index 95% rename from pkg/commonUtils/string.go rename to pkg/utils/string.go index 1254c1c..6438055 100644 --- a/pkg/commonUtils/string.go +++ b/pkg/utils/string.go @@ -1,4 +1,4 @@ -package commonUtils +package utils import ( "encoding/json" From 4d9543ebd01dfd4ea488b4c7b043c0b9f18968e8 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:01:03 +0800 Subject: [PATCH 09/22] feat(account): init --- .../apps/account/accountRouters/sign_in.go | 55 +++++++++ .../apps/account/accountRouters/sign_up.go | 115 ++++++++++++++++++ .../apps/account/accountRouters/user_info.go | 52 ++++++++ internal/apps/account/constants.go | 7 ++ internal/apps/account/middlewares.go | 55 +++++++++ internal/apps/account/models.go | 102 ++++++++++++++++ 6 files changed, 386 insertions(+) create mode 100644 internal/apps/account/accountRouters/sign_in.go create mode 100644 internal/apps/account/accountRouters/sign_up.go create mode 100644 internal/apps/account/accountRouters/user_info.go create mode 100644 internal/apps/account/constants.go create mode 100644 internal/apps/account/middlewares.go create mode 100644 internal/apps/account/models.go diff --git a/internal/apps/account/accountRouters/sign_in.go b/internal/apps/account/accountRouters/sign_in.go new file mode 100644 index 0000000..fb4f8c7 --- /dev/null +++ b/internal/apps/account/accountRouters/sign_in.go @@ -0,0 +1,55 @@ +package accountRouters + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/internal/apps/account" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" +) + +type SignInRequest struct { + Username string `json:"username" binding:"required,min=4,max=32"` + Password string `json:"password" binding:"required,min=64,max=64"` +} + +// SignIn godoc +// +// @Tags Account +// @Param request body SignInRequest true "request body" +// @Produce json +// @Success 200 {object} apiMixin.ResponseModel +// @Router /v1/account/sign-in [post] +func SignIn(c *gin.Context) { + // init + ctx := apiMixin.GetTraceCtx(c) + + // req + var req SignInRequest + if err := c.ShouldBindJSON(&req); err != nil { + apiMixin.ResponseBadRequest(c, err.Error()) + return + } + + // load user from db + var user account.User + if err := user.ExactByUsernameAndPassword(ctx, req.Username, req.Password); err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignIn] load user from db failed\n%s", err)) + apiMixin.ResponseBadRequest(c, apiMixin.Translate(c, "account.loginFailed", nil)) + return + } + + // set up cookie + token, err := user.MakeNewToken(ctx) + if err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignUp] make token failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + c.SetCookie(user.CookieName(), token, user.CacheKeyTimeout(), "", user.CookieDomain(), user.CookieSecure(), user.CookieHTTPOnly()) + + // response + apiMixin.ResponseSuccess(c, nil) + return +} diff --git a/internal/apps/account/accountRouters/sign_up.go b/internal/apps/account/accountRouters/sign_up.go new file mode 100644 index 0000000..78d98a2 --- /dev/null +++ b/internal/apps/account/accountRouters/sign_up.go @@ -0,0 +1,115 @@ +package accountRouters + +import ( + "database/sql" + "fmt" + "time" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/internal/apps/account" + "github.com/ovinc-cn/apicenter/v2/internal/config" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" + "github.com/ovinc-cn/apicenter/v2/pkg/mysql" + "github.com/ovinc-cn/apicenter/v2/pkg/password" +) + +type SignUpRequest struct { + SignInRequest + NickName string `json:"nickname" binding:"min=4,max=32"` + PhoneNumber string `json:"phone_number" binding:"required,min=11,max=11"` +} + +// SignUp godoc +// +// @Tags Account +// @Param request body SignUpRequest true "request body" +// @Produce json +// @Success 200 {object} apiMixin.ResponseModel +// @Router /v1/account/sign-up [post] +func SignUp(c *gin.Context) { + // trace + ctx := apiMixin.GetTraceCtx(c) + + // req + var req SignUpRequest + if err := c.ShouldBindJSON(&req); err != nil { + apiMixin.ResponseBadRequest(c, err.Error()) + return + } + + // check username pattern + if !account.UsernameRegex.MatchString(req.Username) { + apiMixin.ResponseBadRequest(c, apiMixin.Translate(c, "account.usernameInvalid", nil)) + return + } + + // check username exists + var userCount int64 + if err := mysql.Count( + ctx, + mysql.DB().Model(&account.User{}).Where("username = ?", req.Username).Or("nick_name = ?", req.NickName), + &userCount, + ); err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignUp] check username duplicate failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + if userCount > 0 { + apiMixin.ResponseBadRequest(c, apiMixin.Translate(c, "account.usernameDuplicate", nil)) + return + } + + // check phone number exists + var phoneCount int64 + if err := mysql.Count( + ctx, + mysql.DB().Model(&account.User{}).Where("phone_number = ?", req.PhoneNumber), + &phoneCount, + ); err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignUp] check phone number duplicate failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + if phoneCount > 0 { + apiMixin.ResponseBadRequest(c, apiMixin.Translate(c, "account.phoneNumberDuplicate", nil)) + return + } + + // make password + hashedPassword, err := password.MakePassword(req.Password) + if err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignUp] hash password failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + + // insert into db + user := account.User{ + Username: req.Username, + NickName: sql.NullString{String: req.NickName, Valid: true}, + Password: hashedPassword, + DateJoined: time.Now(), + LastLogin: time.Now(), + PhoneNumber: sql.NullString{String: req.PhoneNumber, Valid: true}, + EmailAddress: sql.NullString{String: fmt.Sprintf("%s@%s", req.Username, config.Config.AppAccount.EmailDomain), Valid: true}, + } + if err := mysql.Create(ctx, mysql.DB(), &user); err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignUp] insert user into db failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + + // set up cookie + token, err := user.MakeNewToken(ctx) + if err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignUp] make token failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + c.SetCookie(user.CookieName(), token, user.CacheKeyTimeout(), "", user.CookieDomain(), user.CookieSecure(), user.CookieHTTPOnly()) + + // response + apiMixin.ResponseSuccess(c, nil) + return +} diff --git a/internal/apps/account/accountRouters/user_info.go b/internal/apps/account/accountRouters/user_info.go new file mode 100644 index 0000000..58431e4 --- /dev/null +++ b/internal/apps/account/accountRouters/user_info.go @@ -0,0 +1,52 @@ +package accountRouters + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/internal/apps/account" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" +) + +type UserInfoData struct { + UserID uint64 `json:"user_id"` + Username string `json:"username"` + NickName string `json:"nick_name"` + LastLogin string `json:"last_login"` +} + +type UserInfoResponse struct { + apiMixin.ResponseModel + Data UserInfoData `json:"data"` +} + +// UserInfo godoc +// +// @Tags Account +// @Produce json +// @Success 200 {object} UserInfoResponse +// @Router /v1/account/user-info [get] +func UserInfo(c *gin.Context) { + // load user from context + var user account.User + if err := apiMixin.GetUser(c, &user); err != nil { + apiMixin.ResponseInternalError(c) + return + } + + // response + nickName := "" + if user.NickName.Valid { + nickName = user.NickName.String + } + apiMixin.ResponseSuccess( + c, + UserInfoData{ + UserID: apiMixin.GetUserID(c), + Username: apiMixin.GetUsername(c), + NickName: nickName, + LastLogin: user.LastLogin.In(cfg.AppTimezone()).Format(time.RFC3339), + }, + ) +} diff --git a/internal/apps/account/constants.go b/internal/apps/account/constants.go new file mode 100644 index 0000000..e33d783 --- /dev/null +++ b/internal/apps/account/constants.go @@ -0,0 +1,7 @@ +package account + +import "regexp" + +var ( + UsernameRegex = regexp.MustCompile(`^[a-zA-Z0-9]{4,32}$`) +) diff --git a/internal/apps/account/middlewares.go b/internal/apps/account/middlewares.go new file mode 100644 index 0000000..70d4557 --- /dev/null +++ b/internal/apps/account/middlewares.go @@ -0,0 +1,55 @@ +package account + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" + "go.opentelemetry.io/otel/attribute" +) + +func LoginRequiredMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // init + ctx, span := trace.StartSpan(apiMixin.GetTraceCtx(c), "LoginRequiredMiddleware", trace.SpanKindInternal) + defer span.End() + + // user + user := &User{} + + // get cookie + cookie, err := c.Cookie(user.CookieName()) + if err != nil || cookie == "" { + apiMixin.ResponseUnauthorized(c) + return + } + + // validate token + if err = user.ValidateToken(ctx, cookie); err != nil { + logger.Logger(ctx).Info(fmt.Sprintf("[LoginRequiredMiddleware] validate token failed\n%v", err)) + apiMixin.ResponseUnauthorized(c) + return + } + + // set user to context + apiMixin.SetUsername(c, user.Username) + apiMixin.SetUserID(c, user.ID) + if err := apiMixin.SetUser(c, user); err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[LoginRequiredMiddleware] set user to context failed\n%v", err)) + apiMixin.ResponseInternalError(c) + return + } + + // trace + span.SetAttributes( + attribute.String("username", user.Username), + attribute.String("user_id", fmt.Sprintf("%d", user.ID)), + ) + span.End() + + // next + c.Next() + } +} diff --git a/internal/apps/account/models.go b/internal/apps/account/models.go new file mode 100644 index 0000000..82af88d --- /dev/null +++ b/internal/apps/account/models.go @@ -0,0 +1,102 @@ +package account + +import ( + "context" + "crypto/sha256" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/ovinc-cn/apicenter/v2/internal/config" + "github.com/ovinc-cn/apicenter/v2/pkg/mysql" + "github.com/ovinc-cn/apicenter/v2/pkg/password" + "github.com/ovinc-cn/apicenter/v2/pkg/redis" +) + +type User struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;"` + Username string `json:"username" gorm:"type:varchar(32);unique;"` + NickName sql.NullString `json:"nick_name" gorm:"type:varchar(32);default:null;unique;"` + Password string `json:"password" gorm:"type:varchar(255);"` + DateJoined time.Time `json:"date_joined" gorm:"type:datetime;autoCreateTime;"` + LastLogin time.Time `json:"last_login" gorm:"type:datetime;"` + PhoneNumber sql.NullString `json:"phone_number" gorm:"type:varchar(32);default:null;unique;"` + EmailAddress sql.NullString `json:"email_address" gorm:"type:varchar(255);default:null;unique;"` + WeChatOpenID sql.NullString `json:"wechat_open_id" gorm:"column:wechat_open_id;type:varchar(255);default:null;unique;"` + WeChatUnionID sql.NullString `json:"wechat_union_id" gorm:"column:wechat_union_id;type:varchar(255);default:null;unique;"` + Avatar sql.NullString `json:"avatar" gorm:"type:varchar(255);default:null;"` +} + +func (u *User) ExactByUsername(ctx context.Context, username string) error { + return mysql.Select(ctx, mysql.DB(), u, "username = ?", username) +} + +func (u *User) ExactByUsernameAndPassword(ctx context.Context, username, passWd string) error { + // load user from db + if err := u.ExactByUsername(ctx, username); err != nil { + return err + } + // check password + isMatch, err := password.CheckPassword(passWd, u.Password) + if err != nil { + return err + } + if !isMatch { + return fmt.Errorf("password not match") + } + // update last login time + u.LastLogin = time.Now() + if err := mysql.Update(ctx, mysql.DB(), u, "last_login", time.Now().UTC()); err != nil { + return err + } + return nil +} + +func (u *User) MakeNewToken(ctx context.Context) (string, error) { + h := sha256.New() + h.Write([]byte(fmt.Sprintf("%d", time.Now().UnixMilli()))) + token := fmt.Sprintf("%x#%s", h.Sum(nil), uuid.NewString()) + if _, err := redis.Set(ctx, redis.Client(), u.TokenCacheKey(token), u.Username, time.Duration(u.CacheKeyTimeout())*time.Second); err != nil { + return "", err + } + return token, nil +} + +func (u *User) ValidateToken(ctx context.Context, token string) error { + // load from cache + username, err := redis.Get(ctx, redis.Client(), u.TokenCacheKey(token)) + if err != nil { + return err + } + if username == "" { + return fmt.Errorf("invalid token: %s", token) + } + + // load user from db + return u.ExactByUsername(ctx, username) +} + +func (u *User) TokenCacheKey(token string) string { + return fmt.Sprintf("account:user:token:%s", token) +} + +func (u *User) CacheKeyTimeout() int { + return config.Config.AppAccount.AuthTokenTimeout +} + +func (u *User) CookieName() string { + return config.Config.AppAccount.AuthTokenCookieName +} + +func (u *User) CookieDomain() string { + return config.Config.AppAccount.AuthTokenCookieDomain +} + +func (u *User) CookieSecure() bool { + return config.Config.AppAccount.AuthTokenCookieSecure +} + +func (u *User) CookieHTTPOnly() bool { + return config.Config.AppAccount.AuthTokenCookieHTTPOnly +} From 4a05154805cf818d0cf5b072bd0616c4df79b195 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:01:12 +0800 Subject: [PATCH 10/22] feat(i18n): init --- locales/zh_hans.toml | 23 ++++++++++++++ pkg/i18n/i18n.go | 71 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 locales/zh_hans.toml create mode 100644 pkg/i18n/i18n.go diff --git a/locales/zh_hans.toml b/locales/zh_hans.toml new file mode 100644 index 0000000..bb5a604 --- /dev/null +++ b/locales/zh_hans.toml @@ -0,0 +1,23 @@ +[apiMixin.internalServerError] +other = "服务器异常,请联系管理员" + +[apiMixin.notFound] +other = "资源不存在" + +[apiMixin.noPermission] +other = "无权限" + +[apiMixin.unAuthorized] +other = "未登录" + +[account.usernameDuplicate] +other = "用户名或昵称已被使用" + +[account.usernameInvalid] +other = "用户名只能包含字母或数字" + +[account.phoneNumberDuplicate] +other = "手机号已被使用" + +[account.loginFailed] +other = "用户名或密码错误" \ No newline at end of file diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go new file mode 100644 index 0000000..30cc5e2 --- /dev/null +++ b/pkg/i18n/i18n.go @@ -0,0 +1,71 @@ +package i18n + +import ( + "io/fs" + "log" + "path/filepath" + "strings" + "text/template" + + "github.com/BurntSushi/toml" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +var bundle *i18n.Bundle +var DefaultLang = language.SimplifiedChinese +var localeFilePath = "locales" + +func init() { + // init + bundle = i18n.NewBundle(DefaultLang) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + if err := filepath.WalkDir( + localeFilePath, + func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + if _, err := bundle.LoadMessageFile(path); err != nil { + log.Printf("[I18n] load locales %s fail: %s\n", path, err.Error()) + } + log.Printf("[I18n] load locales %s success\n", path) + } + return nil + }, + ); err != nil { + log.Printf("[I18n] read locales dir %s fail: %s\n", localeFilePath, err.Error()) + } + log.Printf("[I18n] init success\n") +} + +func getLocalizer(lang string) *i18n.Localizer { + if lang == "" { + return getLocalizer(DefaultLang.String()) + } + return i18n.NewLocalizer(bundle, lang) +} + +func Translate(lang string, messageID string, data map[string]string) string { + msg, err := getLocalizer(lang).Localize( + &i18n.LocalizeConfig{ + MessageID: messageID, + TemplateData: data, + DefaultMessage: &i18n.Message{ID: messageID, Other: messageID}, + }, + ) + if err != nil { + tmpl := template.New("msg").Option("missingkey=zero") + t, err := tmpl.Parse(messageID) + if err != nil { + return messageID + } + var buf strings.Builder + if err := t.Execute(&buf, data); err != nil { + return messageID + } + return buf.String() + } + return msg +} From 65e587cb1e56e207c70d860a554500e6684ba202 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:01:26 +0800 Subject: [PATCH 11/22] feat(repo): init --- .env.example | 34 ++++ Makefile | 5 +- env.example | 2 - go.mod | 26 +++- go.sum | 68 +++++++- internal/apps/home/errs.go | 17 -- internal/apps/home/routers.go | 25 ++- internal/cmd/migrate.go | 7 +- internal/config/config.go | 10 +- internal/config/models.go | 14 +- internal/httpd/httpd.go | 27 +++- main.go | 7 + pkg/apiMixin/constants.go | 8 +- pkg/apiMixin/i18n.go | 17 ++ pkg/apiMixin/middlewares.go | 2 +- pkg/apiMixin/models.go | 6 + pkg/apiMixin/response.go | 23 ++- pkg/apiMixin/utils.go | 46 ++++++ pkg/cfg/config.go | 7 +- pkg/mysql/commands.go | 112 +++++++++++++ pkg/mysql/mysql.go | 27 ++-- pkg/mysql/utils.go | 7 +- pkg/password/check.go | 40 +++++ pkg/password/make.go | 62 ++++++++ scripts/swagger.sh | 27 ++++ scripts/tidy.sh | 5 + support_files/api_docs/docs.go | 233 ++++++++++++++++++++++++++++ support_files/api_docs/swagger.json | 206 ++++++++++++++++++++++++ support_files/api_docs/swagger.yaml | 136 ++++++++++++++++ 29 files changed, 1129 insertions(+), 77 deletions(-) create mode 100644 .env.example delete mode 100644 env.example delete mode 100644 internal/apps/home/errs.go create mode 100644 pkg/apiMixin/i18n.go create mode 100644 pkg/apiMixin/utils.go create mode 100644 pkg/mysql/commands.go create mode 100644 pkg/password/check.go create mode 100644 pkg/password/make.go create mode 100755 scripts/swagger.sh create mode 100755 scripts/tidy.sh create mode 100644 support_files/api_docs/docs.go create mode 100644 support_files/api_docs/swagger.json create mode 100644 support_files/api_docs/swagger.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..013755c --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# Build In Base +APP_NAME=api-center +APP_DEBUG=false +APP_TIMEZONE=Asia/Shanghai +# Build In Log +APP_LOG_LEVEL=INFO +APP_LOG_ENCODING=json +APP_LOG_OUTPUT=stdout +# Build In MySQL +APP_MYSQL_USERNAME=root +APP_MYSQL_PASSWORD= +APP_MYSQL_ADDR=127.0.0.1:3306 +APP_MYSQL_DATABASE=api_center +APP_MYSQL_CONNECT_ARGS=charset=utf8mb4&parseTime=True&loc=UTC +APP_MYSQL_MAX_OPEN_CONNS=100 +APP_MYSQL_MAX_IDLE_CONNS=10 +APP_MYSQL_CONN_MAX_LIFETIME=3600 +# Build In Redis +APP_REDIS_ADDR=127.0.0.1:6379 +APP_REDIS_PASSWORD= +APP_REDIS_DB=0 +# Api +APP_API_ADDR=0.0.0.0:8000 +# Cron +APP_CRON_WORKER_CONCURRENCY=1 +APP_CRON_WORKER_QUEUES={"default": 1} +APP_CRON_WORKER_SHUTDOWN_TIMEOUT=60 +# App Account +APP_ACCOUNT_EMAIL_DOMAIN=example.com +APP_ACCOUNT_AUTH_TOKEN_TIMEOUT=604800 +APP_ACCOUNT_AUTH_TOKEN_COOKIE_NAME=auth-token +APP_ACCOUNT_AUTH_TOKEN_COOKIE_DOMAIN= +APP_ACCOUNT_AUTH_TOKEN_COOKIE_SECURE=true +APP_ACCOUNT_AUTH_TOKEN_COOKIE_HTTPONLY=true \ No newline at end of file diff --git a/Makefile b/Makefile index 23b1e43..acd83c6 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,5 @@ +swagger: + scripts/swagger.sh + tidy: - go mod tidy \ No newline at end of file + scripts/tidy.sh \ No newline at end of file diff --git a/env.example b/env.example deleted file mode 100644 index d297f75..0000000 --- a/env.example +++ /dev/null @@ -1,2 +0,0 @@ -APP_NAME=api-center -APP_DEBUG=false diff --git a/go.mod b/go.mod index c7488bd..33a0cee 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,24 @@ module github.com/ovinc-cn/apicenter/v2 go 1.24.0 require ( + github.com/BurntSushi/toml v1.5.0 github.com/gin-gonic/gin v1.11.0 + github.com/google/uuid v1.6.0 github.com/hibiken/asynq v0.25.1 github.com/joho/godotenv v1.5.1 - github.com/redis/go-redis/v9 v9.7.2 + github.com/nicksnyder/go-i18n/v2 v2.6.0 + github.com/redis/go-redis/v9 v9.7.3 github.com/spf13/cobra v1.10.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.41.0 + golang.org/x/text v0.29.0 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.0 gorm.io/plugin/opentelemetry v0.1.16 @@ -22,6 +30,9 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/ClickHouse/ch-go v0.61.5 // indirect github.com/ClickHouse/clickhouse-go/v2 v2.30.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect @@ -35,13 +46,16 @@ require ( github.com/go-faster/errors v0.7.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-yaml v1.18.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -51,13 +65,14 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -79,18 +94,17 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.41.0 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/grpc v1.75.0 // indirect google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/postgres v1.5.11 // indirect diff --git a/go.sum b/go.sum index ce22ba5..0b60da3 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,17 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo= github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -21,6 +29,7 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -30,6 +39,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= @@ -43,6 +54,16 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -91,6 +112,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -109,15 +132,23 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= +github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= @@ -133,8 +164,8 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/redis/go-redis/v9 v9.7.2 h1:PSGhv13dJyrTCw1+55H0pIKM3WFov7HuUrKUmInGL0o= -github.com/redis/go-redis/v9 v9.7.2/go.mod h1:yp5+a5FnEEP0/zTYuw6u6/2nn3zivwhv274qYgWQhDM= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -161,6 +192,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -174,6 +211,7 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= @@ -206,40 +244,54 @@ golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= @@ -248,6 +300,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -268,9 +321,14 @@ google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7I google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/clickhouse v0.7.0 h1:BCrqvgONayvZRgtuA6hdya+eAW5P2QVagV3OlEp1vtA= diff --git a/internal/apps/home/errs.go b/internal/apps/home/errs.go deleted file mode 100644 index af88482..0000000 --- a/internal/apps/home/errs.go +++ /dev/null @@ -1,17 +0,0 @@ -package home - -import ( - "net/http" - - "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" -) - -var MySQLConnFailed = &apiMixin.APIError{ - Code: http.StatusInternalServerError, - Msg: "database connection failed", -} - -var RedisConnFailed = &apiMixin.APIError{ - Code: http.StatusInternalServerError, - Msg: "redis connection failed", -} diff --git a/internal/apps/home/routers.go b/internal/apps/home/routers.go index 2118d80..c8cec1f 100644 --- a/internal/apps/home/routers.go +++ b/internal/apps/home/routers.go @@ -1,27 +1,42 @@ package home import ( - "context" + "fmt" "github.com/gin-gonic/gin" "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" "github.com/ovinc-cn/apicenter/v2/pkg/mysql" "github.com/ovinc-cn/apicenter/v2/pkg/redis" ) +// HealthZ godoc +// +// @Tags Home +// @Produce json +// @Success 200 {object} apiMixin.ResponseModel +// @Router /v1/healthz [get] func HealthZ(c *gin.Context) { // trace - ctx := c.MustGet(apiMixin.TraceContextKey).(context.Context) + ctx := apiMixin.GetTraceCtx(c) // check for db - if mysql.DB().Ping() != nil { - apiMixin.ResponseError(c, MySQLConnFailed) + sqlDB, err := mysql.DB().DB() + if err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[HealthZ] get sql db failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + if err := sqlDB.Ping(); err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[HealthZ] ping mysql failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) return } // check for redis if err := redis.Ping(ctx, redis.Client()); err != nil { - apiMixin.ResponseError(c, RedisConnFailed) + logger.Logger(ctx).Error(fmt.Sprintf("[HealthZ] ping redis failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) return } diff --git a/internal/cmd/migrate.go b/internal/cmd/migrate.go index 77ffbb1..d2160b6 100644 --- a/internal/cmd/migrate.go +++ b/internal/cmd/migrate.go @@ -3,6 +3,7 @@ package cmd import ( "log" + "github.com/ovinc-cn/apicenter/v2/internal/apps/account" "github.com/ovinc-cn/apicenter/v2/pkg/mysql" "github.com/spf13/cobra" ) @@ -11,11 +12,7 @@ var migrateCmd = &cobra.Command{ Use: "migrator", Short: "Api Center Migrator", Run: func(cmd *cobra.Command, args []string) { - gormDB, err := mysql.GormDB() - if err != nil { - log.Fatalf("[Migrate] connect to mysql failed; %s", err) - } - if err := gormDB.AutoMigrate(); err != nil { + if err := mysql.DB().AutoMigrate(&account.User{}); err != nil { log.Fatalf("[Migrate] auto migrate failed; %s", err) } log.Println("[Migrate] auto migrate success") diff --git a/internal/config/config.go b/internal/config/config.go index df8af8e..29b2346 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,8 +15,16 @@ func init() { }, Cron: cronConfig{ WorkerConcurrency: cfg.GetConfigInt("APP_CRON_WORKER_CONCURRENCY", 1), - WorkerQueues: cfg.GetConfigStruct("APP_CRON_WORKER_QUEUES", "{}", make(map[string]int)), + WorkerQueues: cfg.GetConfigStruct("APP_CRON_WORKER_QUEUES", `{"default": 1}`, make(map[string]int)), WorkerShutDownTimeout: time.Duration(cfg.GetConfigInt64("APP_CRON_WORKER_SHUTDOWN_TIMEOUT", 60)) * time.Second, }, + AppAccount: appAccountConfig{ + EmailDomain: cfg.GetConfigVal("APP_ACCOUNT_EMAIL_DOMAIN", "example.com"), + AuthTokenTimeout: cfg.GetConfigInt("APP_ACCOUNT_AUTH_TOKEN_TIMEOUT", 7*24*60*60), + AuthTokenCookieName: cfg.GetConfigVal("APP_ACCOUNT_AUTH_TOKEN_COOKIE_NAME", "auth-token"), + AuthTokenCookieDomain: cfg.GetConfigVal("APP_ACCOUNT_AUTH_TOKEN_COOKIE_DOMAIN", ""), + AuthTokenCookieSecure: cfg.GetConfigBool("APP_ACCOUNT_AUTH_TOKEN_COOKIE_SECURE", true), + AuthTokenCookieHTTPOnly: cfg.GetConfigBool("APP_ACCOUNT_AUTH_TOKEN_COOKIE_HTTPONLY", true), + }, } } diff --git a/internal/config/models.go b/internal/config/models.go index c32e9e9..a851234 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -12,7 +12,17 @@ type cronConfig struct { WorkerShutDownTimeout time.Duration } +type appAccountConfig struct { + EmailDomain string + AuthTokenTimeout int + AuthTokenCookieName string + AuthTokenCookieDomain string + AuthTokenCookieSecure bool + AuthTokenCookieHTTPOnly bool +} + type configModel struct { - API apiConfig - Cron cronConfig + API apiConfig + Cron cronConfig + AppAccount appAccountConfig } diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go index 7baa00b..7d0c7a4 100644 --- a/internal/httpd/httpd.go +++ b/internal/httpd/httpd.go @@ -4,20 +4,18 @@ import ( "log" "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/internal/apps/account" + "github.com/ovinc-cn/apicenter/v2/internal/apps/account/accountRouters" "github.com/ovinc-cn/apicenter/v2/internal/apps/home" "github.com/ovinc-cn/apicenter/v2/internal/config" "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" "github.com/ovinc-cn/apicenter/v2/pkg/cfg" + _ "github.com/ovinc-cn/apicenter/v2/support_files/api_docs" + "github.com/swaggo/files" + "github.com/swaggo/gin-swagger" ) func Serve() { - // release mode - if cfg.AppDebug() { - gin.SetMode(gin.DebugMode) - } else { - gin.SetMode(gin.ReleaseMode) - } - // init gin r := gin.New() @@ -32,6 +30,21 @@ func Serve() { { homeGroup.GET("/healthz", home.HealthZ) } + // account + accountGroup := apiV1Group.Group("/account") + { + accountGroup.POST("/sign-up", accountRouters.SignUp) + accountGroup.POST("/sign-in", accountRouters.SignIn) + accountGroup.GET("/user-info", account.LoginRequiredMiddleware(), accountRouters.UserInfo) + } + } + + // release mode + if cfg.AppDebug() { + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) } // start server diff --git a/main.go b/main.go index a62c20c..f32a9b8 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,13 @@ package main import "github.com/ovinc-cn/apicenter/v2/internal/cmd" +// @title Api Center +// @version 2.0.0 +// @contact.name OVINC CN +// @contact.url https://github.com/OVINC-CN +// @contact.email contact@ovinc.cn +// @license.name MIT License +// @license.url https://github.com/OVINC-CN/APICenter/blob/main/LICENSE func main() { cmd.Execute() } diff --git a/pkg/apiMixin/constants.go b/pkg/apiMixin/constants.go index 25c43f4..721a5b8 100644 --- a/pkg/apiMixin/constants.go +++ b/pkg/apiMixin/constants.go @@ -1,3 +1,9 @@ package apiMixin -const TraceContextKey = "otel_trace_context" +const ( + TraceContextKey = "otel_trace_context" + UsernameContextKey = "username" + UserIDContextKey = "user_id" + UserContextKey = "user" + I18nLangCookieKey = "language" +) diff --git a/pkg/apiMixin/i18n.go b/pkg/apiMixin/i18n.go new file mode 100644 index 0000000..71922a2 --- /dev/null +++ b/pkg/apiMixin/i18n.go @@ -0,0 +1,17 @@ +package apiMixin + +import ( + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/pkg/i18n" +) + +func Translate(c *gin.Context, messageID string, data map[string]string) string { + // load language + lang, _ := c.Cookie(I18nLangCookieKey) + if lang == "" { + lang = i18n.DefaultLang.String() + } + + // translate + return i18n.Translate(lang, messageID, data) +} diff --git a/pkg/apiMixin/middlewares.go b/pkg/apiMixin/middlewares.go index 6dbbcb3..16f7fea 100644 --- a/pkg/apiMixin/middlewares.go +++ b/pkg/apiMixin/middlewares.go @@ -27,7 +27,7 @@ func ObserveMiddleware() gin.HandlerFunc { defer span.End() // add trace to context - c.Set(TraceContextKey, ctx) + SetTraceCtx(c, ctx) // record time startTime := time.Now() diff --git a/pkg/apiMixin/models.go b/pkg/apiMixin/models.go index 5bf9a21..d7ba3ed 100644 --- a/pkg/apiMixin/models.go +++ b/pkg/apiMixin/models.go @@ -8,3 +8,9 @@ type APIError struct { func (e *APIError) Error() string { return e.Msg } + +type ResponseModel struct { + TraceID string `json:"trace_id"` + Message string `json:"message"` + Data interface{} `json:"data"` +} diff --git a/pkg/apiMixin/response.go b/pkg/apiMixin/response.go index c79f0b2..ba39f0c 100644 --- a/pkg/apiMixin/response.go +++ b/pkg/apiMixin/response.go @@ -1,7 +1,6 @@ package apiMixin import ( - "context" "net/http" "github.com/gin-gonic/gin" @@ -10,7 +9,7 @@ import ( func Response(c *gin.Context, status int, msg string, data interface{}) { // load context - ctx := c.MustGet(TraceContextKey).(context.Context) + ctx := GetTraceCtx(c) span := trace.SpanFromContext(ctx) traceID := span.SpanContext().TraceID().String() @@ -25,3 +24,23 @@ func ResponseSuccess(c *gin.Context, data interface{}) { func ResponseError(c *gin.Context, error *APIError) { Response(c, error.Code, error.Msg, nil) } + +func ResponseBadRequest(c *gin.Context, msg string) { + Response(c, http.StatusBadRequest, msg, nil) +} + +func ResponseUnauthorized(c *gin.Context) { + Response(c, http.StatusUnauthorized, Translate(c, "apiMixin.unAuthorized", nil), nil) +} + +func ResponseForbidden(c *gin.Context) { + Response(c, http.StatusForbidden, Translate(c, "apiMixin.noPermission", nil), nil) +} + +func ResponseNotFound(c *gin.Context) { + Response(c, http.StatusNotFound, Translate(c, "apiMixin.notFound", nil), nil) +} + +func ResponseInternalError(c *gin.Context) { + Response(c, http.StatusInternalServerError, Translate(c, "apiMixin.internalServerError", nil), nil) +} diff --git a/pkg/apiMixin/utils.go b/pkg/apiMixin/utils.go new file mode 100644 index 0000000..606e29c --- /dev/null +++ b/pkg/apiMixin/utils.go @@ -0,0 +1,46 @@ +package apiMixin + +import ( + "context" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func SetTraceCtx(c *gin.Context, ctx context.Context) { + c.Set(TraceContextKey, ctx) +} + +func GetTraceCtx(c *gin.Context) context.Context { + return c.MustGet(TraceContextKey).(context.Context) +} + +func SetUsername(c *gin.Context, username string) { + c.Set(UsernameContextKey, username) +} + +func GetUsername(c *gin.Context) string { + return c.MustGet(UsernameContextKey).(string) +} + +func SetUserID(c *gin.Context, userID uint64) { + c.Set(UserIDContextKey, userID) +} + +func GetUserID(c *gin.Context) uint64 { + return c.MustGet(UserIDContextKey).(uint64) +} + +func SetUser(c *gin.Context, user interface{}) error { + userBytes, err := json.Marshal(user) + if err != nil { + return err + } + c.Set(UserContextKey, userBytes) + return nil +} + +func GetUser[T interface{}](c *gin.Context, user *T) error { + userBytes := c.MustGet(UserContextKey).([]byte) + return json.Unmarshal(userBytes, user) +} diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index 201948e..b489921 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -44,11 +44,8 @@ var ( MySQLPassword = func() string { return GetConfigVal("APP_MYSQL_PASSWORD", "") } - MySQLHost = func() string { - return GetConfigVal("APP_MYSQL_HOST", "127.0.0.1") - } - MySQLPort = func() int { - return GetConfigInt("APP_MYSQL_PORT", 3306) + MySQLAddr = func() string { + return GetConfigVal("APP_MYSQL_ADDR", "127.0.0.1:3306") } MySQLDatabase = func() string { return GetConfigVal("APP_MYSQL_DATABASE", "api_center") diff --git a/pkg/mysql/commands.go b/pkg/mysql/commands.go new file mode 100644 index 0000000..57110eb --- /dev/null +++ b/pkg/mysql/commands.go @@ -0,0 +1,112 @@ +package mysql + +import ( + "context" + + "github.com/ovinc-cn/apicenter/v2/pkg/cfg" + "github.com/ovinc-cn/apicenter/v2/pkg/trace" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "gorm.io/gorm" +) + +func buildAttributes(attributes ...attribute.KeyValue) []attribute.KeyValue { + return append( + []attribute.KeyValue{ + attribute.String(trace.AttributeDBSystem, "mysql"), + attribute.String(trace.AttributeDBIP, cfg.MySQLAddr()), + attribute.String(trace.AttributeDBInstance, cfg.MySQLDatabase()), + }, + attributes..., + ) +} + +func Count(ctx context.Context, db *gorm.DB, count *int64) error { + // trace + ctx, span := trace.StartSpan(ctx, "MySQL#Count", trace.SpanKindClient) + defer span.End() + + // exec + result := db.WithContext(ctx).Count(count) + err := result.Error + if err != nil { + span.SetStatus(codes.Error, err.Error()) + } + + // add attributes + span.SetAttributes(buildAttributes()...) + + return err +} + +func Create(ctx context.Context, db *gorm.DB, value interface{}) error { + // trace + ctx, span := trace.StartSpan(ctx, "MySQL#Create", trace.SpanKindClient) + defer span.End() + + // exec + result := db.WithContext(ctx).Create(value) + err := result.Error + if err != nil { + span.SetStatus(codes.Error, err.Error()) + } + + // add attributes + span.SetAttributes(buildAttributes()...) + + return err +} + +func Select(ctx context.Context, db *gorm.DB, dest interface{}, conditions ...interface{}) error { + // trace + ctx, span := trace.StartSpan(ctx, "MySQL#Select", trace.SpanKindClient) + defer span.End() + + // exec + result := db.WithContext(ctx).Find(dest, conditions...) + err := result.Error + if err != nil { + span.SetStatus(codes.Error, err.Error()) + } + + // add attributes + span.SetAttributes(buildAttributes()...) + + return err +} + +func Update[T interface{}](ctx context.Context, db *gorm.DB, model *T, column string, value interface{}) error { + // trace + ctx, span := trace.StartSpan(ctx, "MySQL#Update", trace.SpanKindClient) + defer span.End() + + // exec + result := db.WithContext(ctx).Model(model).Update(column, value) + err := result.Error + if err != nil { + span.SetStatus(codes.Error, err.Error()) + } + + // add attributes + span.SetAttributes(buildAttributes()...) + + return err +} + +func Save(ctx context.Context, db *gorm.DB, value interface{}) error { + // trace + ctx, span := trace.StartSpan(ctx, "MySQL#Save", trace.SpanKindClient) + defer span.End() + + // exec + result := db.WithContext(ctx).Save(value) + err := result.Error + if err != nil { + span.SetStatus(codes.Error, err.Error()) + } + + // add attributes + span.SetAttributes(buildAttributes()...) + + return err +} diff --git a/pkg/mysql/mysql.go b/pkg/mysql/mysql.go index 9b19238..040a3fd 100644 --- a/pkg/mysql/mysql.go +++ b/pkg/mysql/mysql.go @@ -1,37 +1,38 @@ package mysql import ( - "database/sql" "log" "github.com/ovinc-cn/apicenter/v2/pkg/cfg" "gorm.io/gorm" ) -var db *sql.DB +var db *gorm.DB func init() { - gormDB, err := openConn() + // open conn + var err error + db, err = openConn() if err != nil { log.Fatalf("[MySQL] connect to mysql failed; %s", err) } + // debug + if cfg.AppDebug() { + db = db.Debug() + } // try to connect - db, err = gormDB.DB() - if err != nil || db.Ping() != nil { + sqlDB, err := db.DB() + if err != nil || sqlDB.Ping() != nil { log.Fatalf("[MySQL] ping mysql failed; %s", err) } // set config - db.SetMaxOpenConns(cfg.MySQLMaxOpenConns()) - db.SetMaxIdleConns(cfg.MySQLMaxIdleConns()) - db.SetConnMaxLifetime(cfg.MySQLConnMaxLifetime()) + sqlDB.SetMaxOpenConns(cfg.MySQLMaxOpenConns()) + sqlDB.SetMaxIdleConns(cfg.MySQLMaxIdleConns()) + sqlDB.SetConnMaxLifetime(cfg.MySQLConnMaxLifetime()) // log log.Printf("[MySQL] connect to mysql success\n") } -func DB() *sql.DB { +func DB() *gorm.DB { return db } - -func GormDB() (*gorm.DB, error) { - return openConn() -} diff --git a/pkg/mysql/utils.go b/pkg/mysql/utils.go index 9f810df..80c94ea 100644 --- a/pkg/mysql/utils.go +++ b/pkg/mysql/utils.go @@ -14,11 +14,10 @@ import ( func openConn() (*gorm.DB, error) { // init dsn dsn := fmt.Sprintf( - "%s:%s@tcp(%s:%d)/%s?%s", + "%s:%s@tcp(%s)/%s?%s", cfg.MySQLUsername(), cfg.MySQLPassword(), - cfg.MySQLHost(), - cfg.MySQLPort(), + cfg.MySQLAddr(), cfg.MySQLDatabase(), cfg.MySQLParams(), ) @@ -35,7 +34,7 @@ func openConn() (*gorm.DB, error) { tracing.WithoutMetrics(), tracing.WithAttributes( attribute.String(trace.AttributeDBSystem, "mysql"), - attribute.String(trace.AttributeDBIP, fmt.Sprintf("%s:%d", cfg.MySQLHost(), cfg.MySQLPort())), + attribute.String(trace.AttributeDBIP, cfg.MySQLAddr()), attribute.String(trace.AttributeDBInstance, cfg.MySQLDatabase()), ), )); err != nil { diff --git a/pkg/password/check.go b/pkg/password/check.go new file mode 100644 index 0000000..2047b95 --- /dev/null +++ b/pkg/password/check.go @@ -0,0 +1,40 @@ +package password + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "hash" + "strconv" + "strings" + + "golang.org/x/crypto/pbkdf2" +) + +func CheckPassword(password, encoded string) (bool, error) { + if strings.Index(encoded, unusableHash) == 0 { + _, _ = MakePassword(password) + return false, nil + } + return checkPbkdf2(password, encoded, sha256.Size, sha256.New) +} + +func checkPbkdf2(password, encoded string, keyLen int, h func() hash.Hash) (bool, error) { + parts := strings.SplitN(encoded, "$", 4) + if len(parts) != 4 { + return false, errors.New("hash must consist of 4 segments") + } + iter, err := strconv.Atoi(parts[1]) + if err != nil { + return false, fmt.Errorf("wrong number of iterations: %v", err) + } + salt := []byte(parts[2]) + k, err := base64.StdEncoding.DecodeString(parts[3]) + if err != nil { + return false, fmt.Errorf("wrong hash encoding: %v", err) + } + dk := pbkdf2.Key([]byte(password), salt, iter, keyLen, h) + return bytes.Equal(k, dk), nil +} diff --git a/pkg/password/make.go b/pkg/password/make.go new file mode 100644 index 0000000..0b98ddc --- /dev/null +++ b/pkg/password/make.go @@ -0,0 +1,62 @@ +package password + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "hash" + "math/rand" + + "golang.org/x/crypto/pbkdf2" +) + +const ( + iter = 100000 + saltSize = 22 + hashAlg = "pbkdf2_sha256" + allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + unusableHash = "!" +) + +func Iter() int { + return iter +} + +func SaltSize() int { + return saltSize +} + +func HashAlg() string { + return hashAlg +} + +func AllowedChars() string { + return allowedChars +} + +func UnusableHash() string { + return unusableHash +} + +func MakePassword(password string) (string, error) { + if password == "" { + return unusableHash, nil + } + return makePbkdf2(password, sha256.Size, sha256.New) +} + +func makePbkdf2(password string, keyLen int, h func() hash.Hash) (string, error) { + salt := getRandomSalt(saltSize) + dk := pbkdf2.Key([]byte(password), salt, iter, keyLen, h) + b64Hash := base64.StdEncoding.EncodeToString(dk) + return fmt.Sprintf("%s$%d$%s$%s", hashAlg, iter, salt, b64Hash), nil +} + +func getRandomSalt(size int) []byte { + salt := make([]byte, size) + l := len(allowedChars) + for i := range salt { + salt[i] = allowedChars[rand.Intn(l)] + } + return salt +} diff --git a/scripts/swagger.sh b/scripts/swagger.sh new file mode 100755 index 0000000..49899c3 --- /dev/null +++ b/scripts/swagger.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -e + +swag fmt + +if [ ! -f internal/version/VERSION ]; then + echo "internal/version/VERSION not exists" + exit 1 +fi + +VERSION=$(cat internal/version/VERSION) +echo "Version: $VERSION" + +if [ ! -f main.go ]; then + echo "main.go not exists" + exit 1 +fi + +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|// @version .*|// @version $VERSION|" main.go +else + sed -i "s|// @version .*|// @version $VERSION|" main.go +fi +cat main.go | grep $VERSION > /dev/null || (echo "update version failed" && exit 1) + +swag init -o support_files/api_docs --parseDependency --parseInternal diff --git a/scripts/tidy.sh b/scripts/tidy.sh new file mode 100755 index 0000000..1a56c80 --- /dev/null +++ b/scripts/tidy.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -ex + +go mod tidy \ No newline at end of file diff --git a/support_files/api_docs/docs.go b/support_files/api_docs/docs.go new file mode 100644 index 0000000..f838725 --- /dev/null +++ b/support_files/api_docs/docs.go @@ -0,0 +1,233 @@ +// Package api_docs Code generated by swaggo/swag. DO NOT EDIT +package api_docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "OVINC CN", + "url": "https://github.com/OVINC-CN", + "email": "contact@ovinc.cn" + }, + "license": { + "name": "MIT License", + "url": "https://github.com/OVINC-CN/APICenter/blob/main/LICENSE" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/v1/account/sign-in": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/accountRouters.SignInRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + }, + "/v1/account/sign-up": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/accountRouters.SignUpRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + }, + "/v1/account/user-info": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/accountRouters.UserInfoResponse" + } + } + } + } + }, + "/v1/healthz": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Home" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + } + }, + "definitions": { + "accountRouters.SignInRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "maxLength": 64, + "minLength": 64 + }, + "username": { + "type": "string", + "maxLength": 32, + "minLength": 4 + } + } + }, + "accountRouters.SignUpRequest": { + "type": "object", + "required": [ + "password", + "phone_number", + "username" + ], + "properties": { + "nickname": { + "type": "string", + "maxLength": 32, + "minLength": 4 + }, + "password": { + "type": "string", + "maxLength": 64, + "minLength": 64 + }, + "phone_number": { + "type": "string", + "maxLength": 11, + "minLength": 11 + }, + "username": { + "type": "string", + "maxLength": 32, + "minLength": 4 + } + } + }, + "accountRouters.UserInfoData": { + "type": "object", + "properties": { + "last_login": { + "type": "string" + }, + "nick_name": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "accountRouters.UserInfoResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/accountRouters.UserInfoData" + }, + "message": { + "type": "string" + }, + "trace_id": { + "type": "string" + } + } + }, + "apiMixin.ResponseModel": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "trace_id": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "2.0.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Api Center", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/support_files/api_docs/swagger.json b/support_files/api_docs/swagger.json new file mode 100644 index 0000000..ae6d4c1 --- /dev/null +++ b/support_files/api_docs/swagger.json @@ -0,0 +1,206 @@ +{ + "swagger": "2.0", + "info": { + "title": "Api Center", + "contact": { + "name": "OVINC CN", + "url": "https://github.com/OVINC-CN", + "email": "contact@ovinc.cn" + }, + "license": { + "name": "MIT License", + "url": "https://github.com/OVINC-CN/APICenter/blob/main/LICENSE" + }, + "version": "2.0.0" + }, + "paths": { + "/v1/account/sign-in": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/accountRouters.SignInRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + }, + "/v1/account/sign-up": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/accountRouters.SignUpRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + }, + "/v1/account/user-info": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/accountRouters.UserInfoResponse" + } + } + } + } + }, + "/v1/healthz": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Home" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + } + }, + "definitions": { + "accountRouters.SignInRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "maxLength": 64, + "minLength": 64 + }, + "username": { + "type": "string", + "maxLength": 32, + "minLength": 4 + } + } + }, + "accountRouters.SignUpRequest": { + "type": "object", + "required": [ + "password", + "phone_number", + "username" + ], + "properties": { + "nickname": { + "type": "string", + "maxLength": 32, + "minLength": 4 + }, + "password": { + "type": "string", + "maxLength": 64, + "minLength": 64 + }, + "phone_number": { + "type": "string", + "maxLength": 11, + "minLength": 11 + }, + "username": { + "type": "string", + "maxLength": 32, + "minLength": 4 + } + } + }, + "accountRouters.UserInfoData": { + "type": "object", + "properties": { + "last_login": { + "type": "string" + }, + "nick_name": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "accountRouters.UserInfoResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/accountRouters.UserInfoData" + }, + "message": { + "type": "string" + }, + "trace_id": { + "type": "string" + } + } + }, + "apiMixin.ResponseModel": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "trace_id": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/support_files/api_docs/swagger.yaml b/support_files/api_docs/swagger.yaml new file mode 100644 index 0000000..cac76a1 --- /dev/null +++ b/support_files/api_docs/swagger.yaml @@ -0,0 +1,136 @@ +definitions: + accountRouters.SignInRequest: + properties: + password: + maxLength: 64 + minLength: 64 + type: string + username: + maxLength: 32 + minLength: 4 + type: string + required: + - password + - username + type: object + accountRouters.SignUpRequest: + properties: + nickname: + maxLength: 32 + minLength: 4 + type: string + password: + maxLength: 64 + minLength: 64 + type: string + phone_number: + maxLength: 11 + minLength: 11 + type: string + username: + maxLength: 32 + minLength: 4 + type: string + required: + - password + - phone_number + - username + type: object + accountRouters.UserInfoData: + properties: + last_login: + type: string + nick_name: + type: string + user_id: + type: integer + username: + type: string + type: object + accountRouters.UserInfoResponse: + properties: + data: + $ref: '#/definitions/accountRouters.UserInfoData' + message: + type: string + trace_id: + type: string + type: object + apiMixin.ResponseModel: + properties: + data: {} + message: + type: string + trace_id: + type: string + type: object +info: + contact: + email: contact@ovinc.cn + name: OVINC CN + url: https://github.com/OVINC-CN + license: + name: MIT License + url: https://github.com/OVINC-CN/APICenter/blob/main/LICENSE + title: Api Center + version: 2.0.0 +paths: + /v1/account/sign-in: + post: + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/accountRouters.SignInRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/apiMixin.ResponseModel' + tags: + - Account + /v1/account/sign-up: + post: + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/accountRouters.SignUpRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/apiMixin.ResponseModel' + tags: + - Account + /v1/account/user-info: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/accountRouters.UserInfoResponse' + tags: + - Account + /v1/healthz: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/apiMixin.ResponseModel' + tags: + - Home +swagger: "2.0" From 08a890d0841f5a8128265a7fdb080cfd4ef848b1 Mon Sep 17 00:00:00 2001 From: U8F69 <41963680+U8F69@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:04:10 +0800 Subject: [PATCH 12/22] fix(password): security update Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: U8F69 <41963680+U8F69@users.noreply.github.com> --- pkg/password/make.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/password/make.go b/pkg/password/make.go index 0b98ddc..ba37eec 100644 --- a/pkg/password/make.go +++ b/pkg/password/make.go @@ -2,10 +2,11 @@ package password import ( "crypto/sha256" + "crypto/rand" "encoding/base64" "fmt" "hash" - "math/rand" + "math/big" "golang.org/x/crypto/pbkdf2" ) @@ -54,9 +55,14 @@ func makePbkdf2(password string, keyLen int, h func() hash.Hash) (string, error) func getRandomSalt(size int) []byte { salt := make([]byte, size) - l := len(allowedChars) + l := int64(len(allowedChars)) for i := range salt { - salt[i] = allowedChars[rand.Intn(l)] + nBig, err := rand.Int(rand.Reader, big.NewInt(l)) + if err != nil { + // If we can't generate randomness, fail securely. + panic(fmt.Sprintf("failed to generate cryptographically secure salt: %v", err)) + } + salt[i] = allowedChars[nBig.Int64()] } return salt } From 134b6b9ea0582b125f5f513ba7c6ff36d3504365 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:07:23 +0800 Subject: [PATCH 13/22] refactor(password): code lint --- pkg/password/make.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/password/make.go b/pkg/password/make.go index ba37eec..fde0163 100644 --- a/pkg/password/make.go +++ b/pkg/password/make.go @@ -1,8 +1,8 @@ package password import ( - "crypto/sha256" "crypto/rand" + "crypto/sha256" "encoding/base64" "fmt" "hash" From 67c0041edde7387bad642c5f7e9d96296efa9d53 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:17:12 +0800 Subject: [PATCH 14/22] feat(home): add change language api --- .env.example | 8 ++-- internal/apps/account/models.go | 6 +-- .../{routers.go => homeRouters/healthz.go} | 2 +- internal/apps/home/homeRouters/i18n.go | 39 ++++++++++++++++ internal/config/config.go | 14 +++--- internal/config/models.go | 14 +++--- internal/httpd/httpd.go | 5 ++- locales/en.toml | 23 ++++++++++ pkg/apiMixin/constants.go | 1 + support_files/api_docs/docs.go | 44 +++++++++++++++++++ support_files/api_docs/swagger.json | 44 +++++++++++++++++++ support_files/api_docs/swagger.yaml | 28 ++++++++++++ 12 files changed, 204 insertions(+), 24 deletions(-) rename internal/apps/home/{routers.go => homeRouters/healthz.go} (98%) create mode 100644 internal/apps/home/homeRouters/i18n.go create mode 100644 locales/en.toml diff --git a/.env.example b/.env.example index 013755c..ecd2be1 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,9 @@ APP_REDIS_PASSWORD= APP_REDIS_DB=0 # Api APP_API_ADDR=0.0.0.0:8000 +APP_API_COOKIE_DOMAIN= +APP_API_COOKIE_SECURE=true +APP_API_COOKIE_HTTP_ONLY=true # Cron APP_CRON_WORKER_CONCURRENCY=1 APP_CRON_WORKER_QUEUES={"default": 1} @@ -28,7 +31,4 @@ APP_CRON_WORKER_SHUTDOWN_TIMEOUT=60 # App Account APP_ACCOUNT_EMAIL_DOMAIN=example.com APP_ACCOUNT_AUTH_TOKEN_TIMEOUT=604800 -APP_ACCOUNT_AUTH_TOKEN_COOKIE_NAME=auth-token -APP_ACCOUNT_AUTH_TOKEN_COOKIE_DOMAIN= -APP_ACCOUNT_AUTH_TOKEN_COOKIE_SECURE=true -APP_ACCOUNT_AUTH_TOKEN_COOKIE_HTTPONLY=true \ No newline at end of file +APP_ACCOUNT_AUTH_TOKEN_COOKIE_NAME=auth-token \ No newline at end of file diff --git a/internal/apps/account/models.go b/internal/apps/account/models.go index 82af88d..e765839 100644 --- a/internal/apps/account/models.go +++ b/internal/apps/account/models.go @@ -90,13 +90,13 @@ func (u *User) CookieName() string { } func (u *User) CookieDomain() string { - return config.Config.AppAccount.AuthTokenCookieDomain + return config.Config.API.CookieDomain } func (u *User) CookieSecure() bool { - return config.Config.AppAccount.AuthTokenCookieSecure + return config.Config.API.CookieSecure } func (u *User) CookieHTTPOnly() bool { - return config.Config.AppAccount.AuthTokenCookieHTTPOnly + return config.Config.API.CookieHTTPOnly } diff --git a/internal/apps/home/routers.go b/internal/apps/home/homeRouters/healthz.go similarity index 98% rename from internal/apps/home/routers.go rename to internal/apps/home/homeRouters/healthz.go index c8cec1f..856e3d3 100644 --- a/internal/apps/home/routers.go +++ b/internal/apps/home/homeRouters/healthz.go @@ -1,4 +1,4 @@ -package home +package homeRouters import ( "fmt" diff --git a/internal/apps/home/homeRouters/i18n.go b/internal/apps/home/homeRouters/i18n.go new file mode 100644 index 0000000..a9d3f97 --- /dev/null +++ b/internal/apps/home/homeRouters/i18n.go @@ -0,0 +1,39 @@ +package homeRouters + +import ( + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/internal/config" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" +) + +type ChangeLangRequest struct { + Lang string `json:"lang" binding:"required,oneof=zh-Hans en"` +} + +// ChangeLang godoc +// +// @Tags Account +// @Param request body ChangeLangRequest true "request body" +// @Produce json +// @Success 200 {object} apiMixin.ResponseModel +// @Router /v1/language [put] +func ChangeLang(c *gin.Context) { + // req + var req ChangeLangRequest + if err := c.ShouldBindJSON(&req); err != nil { + apiMixin.ResponseBadRequest(c, err.Error()) + return + } + + // set cookie + c.SetCookie( + apiMixin.I18nLangCookieKey, + req.Lang, + apiMixin.I18nLangCookieTTL, + "", + config.Config.API.CookieDomain, + config.Config.API.CookieSecure, + config.Config.API.CookieHTTPOnly, + ) + apiMixin.ResponseSuccess(c, nil) +} diff --git a/internal/config/config.go b/internal/config/config.go index 29b2346..4c36f8c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,7 +11,10 @@ var Config configModel func init() { Config = configModel{ API: apiConfig{ - Addr: cfg.GetConfigVal("APP_API_ADDR", "0.0.0.0:8000"), + Addr: cfg.GetConfigVal("APP_API_ADDR", "0.0.0.0:8000"), + CookieDomain: cfg.GetConfigVal("APP_API_COOKIE_DOMAIN", ""), + CookieSecure: cfg.GetConfigBool("APP_API_COOKIE_SECURE", true), + CookieHTTPOnly: cfg.GetConfigBool("APP_API_COOKIE_HTTP_ONLY", true), }, Cron: cronConfig{ WorkerConcurrency: cfg.GetConfigInt("APP_CRON_WORKER_CONCURRENCY", 1), @@ -19,12 +22,9 @@ func init() { WorkerShutDownTimeout: time.Duration(cfg.GetConfigInt64("APP_CRON_WORKER_SHUTDOWN_TIMEOUT", 60)) * time.Second, }, AppAccount: appAccountConfig{ - EmailDomain: cfg.GetConfigVal("APP_ACCOUNT_EMAIL_DOMAIN", "example.com"), - AuthTokenTimeout: cfg.GetConfigInt("APP_ACCOUNT_AUTH_TOKEN_TIMEOUT", 7*24*60*60), - AuthTokenCookieName: cfg.GetConfigVal("APP_ACCOUNT_AUTH_TOKEN_COOKIE_NAME", "auth-token"), - AuthTokenCookieDomain: cfg.GetConfigVal("APP_ACCOUNT_AUTH_TOKEN_COOKIE_DOMAIN", ""), - AuthTokenCookieSecure: cfg.GetConfigBool("APP_ACCOUNT_AUTH_TOKEN_COOKIE_SECURE", true), - AuthTokenCookieHTTPOnly: cfg.GetConfigBool("APP_ACCOUNT_AUTH_TOKEN_COOKIE_HTTPONLY", true), + EmailDomain: cfg.GetConfigVal("APP_ACCOUNT_EMAIL_DOMAIN", "example.com"), + AuthTokenTimeout: cfg.GetConfigInt("APP_ACCOUNT_AUTH_TOKEN_TIMEOUT", 7*24*60*60), + AuthTokenCookieName: cfg.GetConfigVal("APP_ACCOUNT_AUTH_TOKEN_COOKIE_NAME", "auth-token"), }, } } diff --git a/internal/config/models.go b/internal/config/models.go index a851234..93906a1 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -3,7 +3,10 @@ package config import "time" type apiConfig struct { - Addr string + Addr string + CookieDomain string + CookieSecure bool + CookieHTTPOnly bool } type cronConfig struct { @@ -13,12 +16,9 @@ type cronConfig struct { } type appAccountConfig struct { - EmailDomain string - AuthTokenTimeout int - AuthTokenCookieName string - AuthTokenCookieDomain string - AuthTokenCookieSecure bool - AuthTokenCookieHTTPOnly bool + EmailDomain string + AuthTokenTimeout int + AuthTokenCookieName string } type configModel struct { diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go index 7d0c7a4..7319209 100644 --- a/internal/httpd/httpd.go +++ b/internal/httpd/httpd.go @@ -6,7 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/ovinc-cn/apicenter/v2/internal/apps/account" "github.com/ovinc-cn/apicenter/v2/internal/apps/account/accountRouters" - "github.com/ovinc-cn/apicenter/v2/internal/apps/home" + "github.com/ovinc-cn/apicenter/v2/internal/apps/home/homeRouters" "github.com/ovinc-cn/apicenter/v2/internal/config" "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" "github.com/ovinc-cn/apicenter/v2/pkg/cfg" @@ -28,7 +28,8 @@ func Serve() { // home homeGroup := apiV1Group.Group("") { - homeGroup.GET("/healthz", home.HealthZ) + homeGroup.GET("/healthz", homeRouters.HealthZ) + homeGroup.PUT("/language", homeRouters.ChangeLang) } // account accountGroup := apiV1Group.Group("/account") diff --git a/locales/en.toml b/locales/en.toml new file mode 100644 index 0000000..0d1530a --- /dev/null +++ b/locales/en.toml @@ -0,0 +1,23 @@ +[apiMixin.internalServerError] +other = "internal server error" + +[apiMixin.notFound] +other = "resource not found" + +[apiMixin.noPermission] +other = "no permission" + +[apiMixin.unAuthorized] +other = "unauthorized" + +[account.usernameDuplicate] +other = "username already exists" + +[account.usernameInvalid] +other = "username can only contain letters, numbers" + +[account.phoneNumberDuplicate] +other = "phone number already exists" + +[account.loginFailed] +other = "login failed, please check your username and password" \ No newline at end of file diff --git a/pkg/apiMixin/constants.go b/pkg/apiMixin/constants.go index 721a5b8..b675616 100644 --- a/pkg/apiMixin/constants.go +++ b/pkg/apiMixin/constants.go @@ -6,4 +6,5 @@ const ( UserIDContextKey = "user_id" UserContextKey = "user" I18nLangCookieKey = "language" + I18nLangCookieTTL = 3600 * 24 * 365 ) diff --git a/support_files/api_docs/docs.go b/support_files/api_docs/docs.go index f838725..9f8291e 100644 --- a/support_files/api_docs/docs.go +++ b/support_files/api_docs/docs.go @@ -116,6 +116,35 @@ const docTemplate = `{ } } } + }, + "/v1/language": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/homeRouters.ChangeLangRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } } }, "definitions": { @@ -210,6 +239,21 @@ const docTemplate = `{ "type": "string" } } + }, + "homeRouters.ChangeLangRequest": { + "type": "object", + "required": [ + "lang" + ], + "properties": { + "lang": { + "type": "string", + "enum": [ + "zh-Hans", + "en" + ] + } + } } } }` diff --git a/support_files/api_docs/swagger.json b/support_files/api_docs/swagger.json index ae6d4c1..1416187 100644 --- a/support_files/api_docs/swagger.json +++ b/support_files/api_docs/swagger.json @@ -107,6 +107,35 @@ } } } + }, + "/v1/language": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/homeRouters.ChangeLangRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } } }, "definitions": { @@ -201,6 +230,21 @@ "type": "string" } } + }, + "homeRouters.ChangeLangRequest": { + "type": "object", + "required": [ + "lang" + ], + "properties": { + "lang": { + "type": "string", + "enum": [ + "zh-Hans", + "en" + ] + } + } } } } \ No newline at end of file diff --git a/support_files/api_docs/swagger.yaml b/support_files/api_docs/swagger.yaml index cac76a1..36fdfc7 100644 --- a/support_files/api_docs/swagger.yaml +++ b/support_files/api_docs/swagger.yaml @@ -64,6 +64,16 @@ definitions: trace_id: type: string type: object + homeRouters.ChangeLangRequest: + properties: + lang: + enum: + - zh-Hans + - en + type: string + required: + - lang + type: object info: contact: email: contact@ovinc.cn @@ -133,4 +143,22 @@ paths: $ref: '#/definitions/apiMixin.ResponseModel' tags: - Home + /v1/language: + put: + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/homeRouters.ChangeLangRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/apiMixin.ResponseModel' + tags: + - Account swagger: "2.0" From b9af42720c2ecd4afdcb29ac42d5acaae86bf74d Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:19:15 +0800 Subject: [PATCH 15/22] feat(home): update tags for i18n api --- internal/apps/home/homeRouters/i18n.go | 2 +- support_files/api_docs/docs.go | 2 +- support_files/api_docs/swagger.json | 2 +- support_files/api_docs/swagger.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/apps/home/homeRouters/i18n.go b/internal/apps/home/homeRouters/i18n.go index a9d3f97..ccdc6a2 100644 --- a/internal/apps/home/homeRouters/i18n.go +++ b/internal/apps/home/homeRouters/i18n.go @@ -12,7 +12,7 @@ type ChangeLangRequest struct { // ChangeLang godoc // -// @Tags Account +// @Tags Home // @Param request body ChangeLangRequest true "request body" // @Produce json // @Success 200 {object} apiMixin.ResponseModel diff --git a/support_files/api_docs/docs.go b/support_files/api_docs/docs.go index 9f8291e..8aee9f3 100644 --- a/support_files/api_docs/docs.go +++ b/support_files/api_docs/docs.go @@ -123,7 +123,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Account" + "Home" ], "parameters": [ { diff --git a/support_files/api_docs/swagger.json b/support_files/api_docs/swagger.json index 1416187..c356d34 100644 --- a/support_files/api_docs/swagger.json +++ b/support_files/api_docs/swagger.json @@ -114,7 +114,7 @@ "application/json" ], "tags": [ - "Account" + "Home" ], "parameters": [ { diff --git a/support_files/api_docs/swagger.yaml b/support_files/api_docs/swagger.yaml index 36fdfc7..706f125 100644 --- a/support_files/api_docs/swagger.yaml +++ b/support_files/api_docs/swagger.yaml @@ -160,5 +160,5 @@ paths: schema: $ref: '#/definitions/apiMixin.ResponseModel' tags: - - Account + - Home swagger: "2.0" From a1b5eae6229fc836eec96c57de73deaf2b68d8ee Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:30:38 +0800 Subject: [PATCH 16/22] feat(api mixing): add more attrs to span --- Makefile | 6 +++++- pkg/apiMixin/middlewares.go | 5 +++++ pkg/trace/constants.go | 11 +++++++---- scripts/tidy.sh | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index acd83c6..c837be6 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,8 @@ swagger: scripts/swagger.sh tidy: - scripts/tidy.sh \ No newline at end of file + scripts/tidy.sh + +lint: + scripts/tidy.sh + scripts/swagger.sh \ No newline at end of file diff --git a/pkg/apiMixin/middlewares.go b/pkg/apiMixin/middlewares.go index 16f7fea..a1a5514 100644 --- a/pkg/apiMixin/middlewares.go +++ b/pkg/apiMixin/middlewares.go @@ -25,6 +25,11 @@ func ObserveMiddleware() gin.HandlerFunc { // init trace ctx, span := trace.StartSpan(context.Background(), fmt.Sprintf("Router#%s#%s", c.Request.Method, path), trace.SpanKindServer) defer span.End() + span.SetAttributes( + attribute.String(trace.AttributeRequestURI, c.Request.RequestURI), + attribute.String(trace.AttributeRequestRemoteAddr, c.Request.RemoteAddr), + attribute.Int64(trace.AttributeRequestContentLength, c.Request.ContentLength), + ) // add trace to context SetTraceCtx(c, ctx) diff --git a/pkg/trace/constants.go b/pkg/trace/constants.go index f9fca7d..a0bb574 100644 --- a/pkg/trace/constants.go +++ b/pkg/trace/constants.go @@ -3,10 +3,13 @@ package trace import otTrace "go.opentelemetry.io/otel/trace" const ( - AttributeStatusCode = "status.code" - AttributeDBInstance = "db.instance" - AttributeDBIP = "db.ip" - AttributeDBSystem = "db.system" + AttributeRequestURI = "request.uri" + AttributeRequestRemoteAddr = "request.remote_addr" + AttributeRequestContentLength = "request.content_length" + AttributeStatusCode = "status.code" + AttributeDBInstance = "db.instance" + AttributeDBIP = "db.ip" + AttributeDBSystem = "db.system" ) const ( diff --git a/scripts/tidy.sh b/scripts/tidy.sh index 1a56c80..5d7764f 100755 --- a/scripts/tidy.sh +++ b/scripts/tidy.sh @@ -1,5 +1,5 @@ #!/bin/sh -set -ex +set -e go mod tidy \ No newline at end of file From aed83f255aab27dece17437e684415e7aafed514 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:45:43 +0800 Subject: [PATCH 17/22] chore(repo): add image build workflow --- .env.example | 2 ++ .github/workflows/release-image.yml | 54 +++++++++++++++++++++++++++++ Dockerfile | 34 ++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 .github/workflows/release-image.yml create mode 100644 Dockerfile diff --git a/.env.example b/.env.example index ecd2be1..6ccbf4e 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,8 @@ APP_MYSQL_CONN_MAX_LIFETIME=3600 APP_REDIS_ADDR=127.0.0.1:6379 APP_REDIS_PASSWORD= APP_REDIS_DB=0 +# Build In Trace +APP_TRACE_ENDPOINT=127.0.0.1:4317 # Api APP_API_ADDR=0.0.0.0:8000 APP_API_COOKIE_DOMAIN= diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml new file mode 100644 index 0000000..8f5d0a1 --- /dev/null +++ b/.github/workflows/release-image.yml @@ -0,0 +1,54 @@ +name: Release Image + +on: + push: + tags: + - "*" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install cosign + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + - name: Sign the published Docker image + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d682db0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# build stage +FROM golang:1.24-alpine AS builder + +# install ca-certificates for https requests +RUN apk --no-cache add ca-certificates tzdata + +# set working directory +WORKDIR /app + +# copy go mod files +COPY go.mod go.sum ./ + +# download dependencies +RUN go mod download + +# copy source code +COPY . . + +# build the application +RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o api-center . + +# final stage +FROM alpine:latest + +# install ca-certificates for https requests +RUN apk --no-cache add ca-certificates tzdata + +WORKDIR /app + +# copy the binary from builder stage +COPY --from=builder /app/api-center . + +# copy locales directory for i18n support +COPY --from=builder /app/locales ./locales From 2bb945294679865db4e0fb714dc5c4ff84482895 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 15:59:38 +0800 Subject: [PATCH 18/22] feat(account): sign out --- .../apps/account/accountRouters/sign_out.go | 50 +++++++++++++++++++ internal/httpd/httpd.go | 1 + support_files/api_docs/docs.go | 18 +++++++ support_files/api_docs/swagger.json | 18 +++++++ support_files/api_docs/swagger.yaml | 11 ++++ 5 files changed, 98 insertions(+) create mode 100644 internal/apps/account/accountRouters/sign_out.go diff --git a/internal/apps/account/accountRouters/sign_out.go b/internal/apps/account/accountRouters/sign_out.go new file mode 100644 index 0000000..33fe647 --- /dev/null +++ b/internal/apps/account/accountRouters/sign_out.go @@ -0,0 +1,50 @@ +package accountRouters + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/ovinc-cn/apicenter/v2/internal/apps/account" + "github.com/ovinc-cn/apicenter/v2/pkg/apiMixin" + "github.com/ovinc-cn/apicenter/v2/pkg/logger" + "github.com/ovinc-cn/apicenter/v2/pkg/redis" +) + +// SignOut godoc +// +// @Tags Account +// @Produce json +// @Success 200 {object} apiMixin.ResponseModel +// @Router /v1/account/sign-out [get] +func SignOut(c *gin.Context) { + // init + ctx := apiMixin.GetTraceCtx(c) + + // load cookie + user := &account.User{} + cookie, err := c.Cookie(user.CookieName()) + if err != nil { + if errors.Is(err, http.ErrNoCookie) { + apiMixin.ResponseSuccess(c, nil) + return + } + logger.Logger(ctx).Error(fmt.Sprintf("[SignOut] load cookie failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + + // delete redis cache + if _, err := redis.Del(ctx, redis.Client(), user.TokenCacheKey(cookie)); err != nil { + logger.Logger(ctx).Error(fmt.Sprintf("[SignOut] delete redis cache failed\n%s", err.Error())) + apiMixin.ResponseInternalError(c) + return + } + + // delete cookie + c.SetCookie(user.CookieName(), "", -1, "", user.CookieDomain(), user.CookieSecure(), user.CookieHTTPOnly()) + + // response + apiMixin.ResponseSuccess(c, nil) +} diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go index 7319209..7dcdd75 100644 --- a/internal/httpd/httpd.go +++ b/internal/httpd/httpd.go @@ -36,6 +36,7 @@ func Serve() { { accountGroup.POST("/sign-up", accountRouters.SignUp) accountGroup.POST("/sign-in", accountRouters.SignIn) + accountGroup.GET("/sign-out", accountRouters.SignOut) accountGroup.GET("/user-info", account.LoginRequiredMiddleware(), accountRouters.UserInfo) } } diff --git a/support_files/api_docs/docs.go b/support_files/api_docs/docs.go index 8aee9f3..421d804 100644 --- a/support_files/api_docs/docs.go +++ b/support_files/api_docs/docs.go @@ -52,6 +52,24 @@ const docTemplate = `{ } } }, + "/v1/account/sign-out": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + }, "/v1/account/sign-up": { "post": { "produces": [ diff --git a/support_files/api_docs/swagger.json b/support_files/api_docs/swagger.json index c356d34..a7f76ae 100644 --- a/support_files/api_docs/swagger.json +++ b/support_files/api_docs/swagger.json @@ -43,6 +43,24 @@ } } }, + "/v1/account/sign-out": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Account" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/apiMixin.ResponseModel" + } + } + } + } + }, "/v1/account/sign-up": { "post": { "produces": [ diff --git a/support_files/api_docs/swagger.yaml b/support_files/api_docs/swagger.yaml index 706f125..093a5f3 100644 --- a/support_files/api_docs/swagger.yaml +++ b/support_files/api_docs/swagger.yaml @@ -103,6 +103,17 @@ paths: $ref: '#/definitions/apiMixin.ResponseModel' tags: - Account + /v1/account/sign-out: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/apiMixin.ResponseModel' + tags: + - Account /v1/account/sign-up: post: parameters: From 28f60ffa447819a4920afefb70194b17e4602d42 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 16:00:09 +0800 Subject: [PATCH 19/22] feat(trace): trace toggle --- .env.example | 1 + pkg/cfg/config.go | 3 +++ pkg/trace/trace.go | 14 +++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 6ccbf4e..ade0662 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,7 @@ APP_REDIS_ADDR=127.0.0.1:6379 APP_REDIS_PASSWORD= APP_REDIS_DB=0 # Build In Trace +APP_ENABLE_TRACE=true APP_TRACE_ENDPOINT=127.0.0.1:4317 # Api APP_API_ADDR=0.0.0.0:8000 diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index b489921..20f933e 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -79,6 +79,9 @@ var ( // Trace var ( + EnableTrace = func() bool { + return GetConfigBool("APP_ENABLE_TRACE", true) + } TraceEndpoint = func() string { return GetConfigVal("APP_TRACE_ENDPOINT", "127.0.0.1:4317") } diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go index 45965b6..d194992 100644 --- a/pkg/trace/trace.go +++ b/pkg/trace/trace.go @@ -37,14 +37,18 @@ func init() { log.Fatalf("[Trace] create resource failed; %s", err) } - // init exporter - e, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(cfg.TraceEndpoint()), otlptracegrpc.WithInsecure()) - if err != nil { - log.Fatalf("[Trace] create otlp grpc exporter failed; %s", err) + // init ops + ops := []sdktrace.TracerProviderOption{sdktrace.WithResource(r)} + if cfg.EnableTrace() { + e, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(cfg.TraceEndpoint()), otlptracegrpc.WithInsecure()) + if err != nil { + log.Fatalf("[Trace] create otlp grpc exporter failed; %s", err) + } + ops = append(ops, sdktrace.WithBatcher(e)) } // init trace provider - traceProvider = sdktrace.NewTracerProvider(sdktrace.WithBatcher(e), sdktrace.WithResource(r)) + traceProvider = sdktrace.NewTracerProvider(ops...) otel.SetTracerProvider(traceProvider) otel.SetTextMapPropagator(propagation.TraceContext{}) From 294f506575bbf4e413cf2c9eb459d782c5afb967 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 16:11:15 +0800 Subject: [PATCH 20/22] feat(middleware): recovery middleware --- internal/apps/account/middlewares.go | 2 +- internal/httpd/httpd.go | 2 +- pkg/apiMixin/middlewares.go | 22 ++++++++++++++++++++++ pkg/apiMixin/response.go | 7 ++++++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/internal/apps/account/middlewares.go b/internal/apps/account/middlewares.go index 70d4557..443bd65 100644 --- a/internal/apps/account/middlewares.go +++ b/internal/apps/account/middlewares.go @@ -33,7 +33,7 @@ func LoginRequiredMiddleware() gin.HandlerFunc { return } - // set user to context + //set user to context apiMixin.SetUsername(c, user.Username) apiMixin.SetUserID(c, user.ID) if err := apiMixin.SetUser(c, user); err != nil { diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go index 7dcdd75..c386574 100644 --- a/internal/httpd/httpd.go +++ b/internal/httpd/httpd.go @@ -20,7 +20,7 @@ func Serve() { r := gin.New() // middlewares - r.Use(gin.Recovery(), apiMixin.ObserveMiddleware()) + r.Use(apiMixin.RecoveryMiddleware(), apiMixin.ObserveMiddleware()) // routers apiV1Group := r.Group("/v1") diff --git a/pkg/apiMixin/middlewares.go b/pkg/apiMixin/middlewares.go index a1a5514..a952722 100644 --- a/pkg/apiMixin/middlewares.go +++ b/pkg/apiMixin/middlewares.go @@ -61,3 +61,25 @@ func ObserveMiddleware() gin.HandlerFunc { ) } } + +func RecoveryMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if r := recover(); r != nil { + // init + ctx, span := trace.StartSpan(GetTraceCtx(c), "RecoveryMiddleware", trace.SpanKindInternal) + defer span.End() + + // set error + span.SetStatus(codes.Error, fmt.Sprintf("%T", r)) + + // log + logger.Logger(ctx).Error(fmt.Sprintf("[RecoveryMiddleware] server panic\n%v", r)) + + // return 500 + ResponseInternalError(c) + } + }() + c.Next() + } +} diff --git a/pkg/apiMixin/response.go b/pkg/apiMixin/response.go index ba39f0c..8ece20f 100644 --- a/pkg/apiMixin/response.go +++ b/pkg/apiMixin/response.go @@ -14,7 +14,12 @@ func Response(c *gin.Context, status int, msg string, data interface{}) { traceID := span.SpanContext().TraceID().String() // response - c.JSON(status, gin.H{"trace_id": traceID, "message": msg, "data": data}) + responseData := gin.H{"trace_id": traceID, "message": msg, "data": data} + if status >= http.StatusBadRequest { + c.AbortWithStatusJSON(status, responseData) + return + } + c.JSON(status, responseData) } func ResponseSuccess(c *gin.Context, data interface{}) { From 4c3a34b7b7a99d04d48efe038f3df6693ed2d6dc Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 16:15:03 +0800 Subject: [PATCH 21/22] fix(account): user query is empty --- internal/apps/account/models.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/apps/account/models.go b/internal/apps/account/models.go index e765839..86febaa 100644 --- a/internal/apps/account/models.go +++ b/internal/apps/account/models.go @@ -29,7 +29,13 @@ type User struct { } func (u *User) ExactByUsername(ctx context.Context, username string) error { - return mysql.Select(ctx, mysql.DB(), u, "username = ?", username) + if err := mysql.Select(ctx, mysql.DB(), u, "username = ?", username); err != nil { + return err + } + if u.ID == 0 { + return fmt.Errorf("user not found: %s", username) + } + return nil } func (u *User) ExactByUsernameAndPassword(ctx context.Context, username, passWd string) error { From ccf96b556786d53e29025a3fe6468bb36e1291f0 Mon Sep 17 00:00:00 2001 From: u8f69 Date: Sun, 5 Oct 2025 19:25:10 +0800 Subject: [PATCH 22/22] refactor(account): add nickname method --- go.mod | 5 ++--- go.sum | 6 ++++-- .../apps/account/accountRouters/user_info.go | 10 +++------- internal/apps/account/models.go | 14 ++++++++++++++ pkg/redis/commands.go | 19 ++++++++++++++++++- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 33a0cee..17c1afb 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/spec v0.20.4 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect @@ -70,7 +70,7 @@ require ( github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -104,7 +104,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/grpc v1.75.0 // indirect google.golang.org/protobuf v1.36.9 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/postgres v1.5.11 // indirect diff --git a/go.sum b/go.sum index 0b60da3..0397a61 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,9 @@ github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/a github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -134,8 +135,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= diff --git a/internal/apps/account/accountRouters/user_info.go b/internal/apps/account/accountRouters/user_info.go index 58431e4..d7aa7e4 100644 --- a/internal/apps/account/accountRouters/user_info.go +++ b/internal/apps/account/accountRouters/user_info.go @@ -36,16 +36,12 @@ func UserInfo(c *gin.Context) { } // response - nickName := "" - if user.NickName.Valid { - nickName = user.NickName.String - } apiMixin.ResponseSuccess( c, UserInfoData{ - UserID: apiMixin.GetUserID(c), - Username: apiMixin.GetUsername(c), - NickName: nickName, + UserID: user.ID, + Username: user.Username, + NickName: user.GetNickName(), LastLogin: user.LastLogin.In(cfg.AppTimezone()).Format(time.RFC3339), }, ) diff --git a/internal/apps/account/models.go b/internal/apps/account/models.go index 86febaa..3553f2b 100644 --- a/internal/apps/account/models.go +++ b/internal/apps/account/models.go @@ -106,3 +106,17 @@ func (u *User) CookieSecure() bool { func (u *User) CookieHTTPOnly() bool { return config.Config.API.CookieHTTPOnly } + +func (u *User) GetNickName() string { + if u.NickName.Valid { + return u.NickName.String + } + return "" +} + +func (u *User) GetEmail() string { + if u.EmailAddress.Valid { + return u.EmailAddress.String + } + return "" +} diff --git a/pkg/redis/commands.go b/pkg/redis/commands.go index 5335258..6d76390 100644 --- a/pkg/redis/commands.go +++ b/pkg/redis/commands.go @@ -101,6 +101,7 @@ func Get(ctx context.Context, conn redis.Cmdable, k string) (string, error) { span.SetStatus(codes.Error, err.Error()) return "", err } + // result return cmd.Val(), nil } @@ -119,6 +120,22 @@ func Del(ctx context.Context, conn redis.Cmdable, keys ...string) (int64, error) span.SetStatus(codes.Error, err.Error()) return 0, err } - // result + return cmd.Val(), nil +} + +func Exists(ctx context.Context, conn redis.Cmdable, keys ...string) (int64, error) { + // trace + ctx, span := trace.StartSpan(ctx, "Redis#Exists", trace.SpanKindClient) + defer span.End() + + // add data + span.SetAttributes(buildAttributes(attribute.StringSlice("keys", keys))...) + + // exec + cmd := conn.Exists(ctx, keys...) + if err := cmd.Err(); err != nil { + span.SetStatus(codes.Error, err.Error()) + return 0, err + } return cmd.Val(), nil }