Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
0ac2ca73ec | |||
7b67a2f758 | |||
8b86145519 | |||
ac7a8dd90e | |||
6903b7f768 | |||
2e8b8d7330 | |||
b4d5375954 |
28
.gitea/tools/render-docker-compose-ci.sh
Executable file
28
.gitea/tools/render-docker-compose-ci.sh
Executable file
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
set +x
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
cat >"./docker-compose-ci-${COMPOSE_PROJECT}.yaml" <<EOF
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-${COMPOSE_PROJECT}"
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-${COMPOSE_PROJECT}"
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-${COMPOSE_PROJECT}"
|
||||||
|
|
||||||
|
apple-ci:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-${COMPOSE_PROJECT}"
|
||||||
|
|
||||||
|
backend-ci:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-${COMPOSE_PROJECT}"
|
||||||
|
|
||||||
|
extension-ci:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-${COMPOSE_PROJECT}"
|
||||||
|
|
||||||
|
packages-ci:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-${COMPOSE_PROJECT}"
|
||||||
|
EOF
|
|
@ -17,8 +17,35 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout the code"
|
- name: "Checkout the code"
|
||||||
uses: "actions/checkout@v2"
|
uses: "actions/checkout@v2"
|
||||||
|
- name: "Get run info"
|
||||||
|
id: "get-run-info"
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
echo "COMPOSE_PROJECT=${{ vars.COMPOSE_PROJECT_BASE }}-${GITHUB_RUN_NUMBER}" >> $GITHUB_OUTPUT
|
||||||
|
- name: "Get build options"
|
||||||
|
id: "get-build-options"
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
SHORT_SHA="${GITHUB_SHA::8}"
|
||||||
|
BUILD_ARCH="amd64"
|
||||||
|
BUILD_PLATFORM="linux/amd64"
|
||||||
|
if [ "${RUNNER_ARCH}" = "ARM64" ];then
|
||||||
|
BUILD_ARCH="arm64"
|
||||||
|
BUILD_PLATFORM="linux/arm64"
|
||||||
|
fi
|
||||||
|
echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "BUILD_ARCH=$BUILD_ARCH" >> $GITHUB_OUTPUT
|
||||||
|
echo "BUILD_PLATFORM=$BUILD_PLATFORM" >> $GITHUB_OUTPUT
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
|
id: "setup-docker-buildx"
|
||||||
uses: "docker/setup-buildx-action@v3"
|
uses: "docker/setup-buildx-action@v3"
|
||||||
|
with:
|
||||||
|
driver: "remote"
|
||||||
|
endpoint: "tcp://builder-01.bthlab:2375"
|
||||||
|
platforms: "linux/amd64"
|
||||||
|
append: |
|
||||||
|
- endpoint: "tcp://builder-mac-01.bthlab:2375"
|
||||||
|
platforms: "linux/arm64"
|
||||||
- name: "Build `postgres` image"
|
- name: "Build `postgres` image"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
@ -26,7 +53,10 @@ jobs:
|
||||||
context: "services/"
|
context: "services/"
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-local"
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
|
||||||
|
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
|
||||||
|
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
|
||||||
- name: "Build `keycloak` image"
|
- name: "Build `keycloak` image"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
@ -34,7 +64,10 @@ jobs:
|
||||||
context: "services/"
|
context: "services/"
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-local"
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
|
||||||
|
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
|
||||||
|
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
|
||||||
- name: "Build `rabbitmq` image"
|
- name: "Build `rabbitmq` image"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
@ -42,7 +75,10 @@ jobs:
|
||||||
context: "services/"
|
context: "services/"
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-local"
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
|
||||||
|
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
|
||||||
|
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
|
||||||
- name: "Build `backend-ci` image"
|
- name: "Build `backend-ci` image"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
@ -51,7 +87,10 @@ jobs:
|
||||||
target: "ci"
|
target: "ci"
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-local"
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
|
||||||
|
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
|
||||||
|
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
|
||||||
- name: "Build `packages-ci` image"
|
- name: "Build `packages-ci` image"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
@ -60,7 +99,10 @@ jobs:
|
||||||
target: "ci"
|
target: "ci"
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-local"
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
|
||||||
|
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
|
||||||
|
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
|
||||||
- name: "Build `extension-ci` image"
|
- name: "Build `extension-ci` image"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
@ -69,23 +111,91 @@ jobs:
|
||||||
target: "ci"
|
target: "ci"
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-local"
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
|
||||||
|
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
|
||||||
|
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
|
||||||
|
- name: "Build `apple-ci` image"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: "services/apple/Dockerfile"
|
||||||
|
context: "services/"
|
||||||
|
target: "ci"
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
|
||||||
|
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
|
||||||
|
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
|
||||||
|
- name: "Prepare the build"
|
||||||
|
id: "prepare"
|
||||||
|
env:
|
||||||
|
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
./.gitea/tools/render-docker-compose-ci.sh
|
||||||
- name: "Run `backend` checks"
|
- name: "Run `backend` checks"
|
||||||
|
if: "steps.prepare.conclusion == 'success'"
|
||||||
|
env:
|
||||||
|
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm backend-ci inv ci
|
docker compose \
|
||||||
|
-p "${COMPOSE_PROJECT}" \
|
||||||
|
-f "docker-compose.yaml" \
|
||||||
|
-f "docker-compose-ci.yaml" \
|
||||||
|
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
|
||||||
|
run --rm \
|
||||||
|
backend-ci inv ci
|
||||||
- name: "Run `packages` checks"
|
- name: "Run `packages` checks"
|
||||||
if: always()
|
if: "steps.prepare.conclusion == 'success'"
|
||||||
|
env:
|
||||||
|
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm packages-ci inv ci
|
docker compose \
|
||||||
|
-p "${COMPOSE_PROJECT}" \
|
||||||
|
-f "docker-compose.yaml" \
|
||||||
|
-f "docker-compose-ci.yaml" \
|
||||||
|
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
|
||||||
|
run --rm \
|
||||||
|
packages-ci inv ci
|
||||||
- name: "Run `extension` checks"
|
- name: "Run `extension` checks"
|
||||||
if: always()
|
if: "steps.prepare.conclusion == 'success'"
|
||||||
|
env:
|
||||||
|
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm extension-ci inv ci
|
docker compose \
|
||||||
|
-p "${COMPOSE_PROJECT}" \
|
||||||
|
-f "docker-compose.yaml" \
|
||||||
|
-f "docker-compose-ci.yaml" \
|
||||||
|
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
|
||||||
|
run --rm \
|
||||||
|
extension-ci inv ci
|
||||||
|
- name: "Run `apple` checks"
|
||||||
|
if: "steps.prepare.conclusion == 'success'"
|
||||||
|
env:
|
||||||
|
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
docker compose \
|
||||||
|
-p "${COMPOSE_PROJECT}" \
|
||||||
|
-f "docker-compose.yaml" \
|
||||||
|
-f "docker-compose-ci.yaml" \
|
||||||
|
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
|
||||||
|
run --rm \
|
||||||
|
apple-ci inv ci
|
||||||
- name: "Clean up"
|
- name: "Clean up"
|
||||||
if: always()
|
if: always()
|
||||||
|
env:
|
||||||
|
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml down --volumes
|
docker compose \
|
||||||
|
-p "${COMPOSE_PROJECT}" \
|
||||||
|
-f "docker-compose.yaml" \
|
||||||
|
-f "docker-compose-ci.yaml" \
|
||||||
|
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
|
||||||
|
down --volumes --rmi all || true
|
||||||
|
rm -f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" || true
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
.envrc*
|
.envrc*
|
||||||
.ipythonhome/
|
.ipythonhome/
|
||||||
|
/docker-compose-ci-*.yaml
|
||||||
|
|
|
@ -86,3 +86,5 @@ Licensed under terms of the MIT License
|
||||||
Pepper Hot Solid icon
|
Pepper Hot Solid icon
|
||||||
Copyright (c) Icons8
|
Copyright (c) Icons8
|
||||||
Licensed under terms of the MIT License
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
Spinner Loader CSS from https://css-loaders.com/
|
||||||
|
|
|
@ -66,7 +66,7 @@ $ docker run --rm -it \
|
||||||
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \
|
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \
|
||||||
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \
|
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \
|
||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.10.4-01
|
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.10.13-01
|
||||||
```
|
```
|
||||||
|
|
||||||
The command above will set up and start the application. The SQLite file will
|
The command above will set up and start the application. The SQLite file will
|
||||||
|
@ -76,8 +76,7 @@ credentials. The Web app will be reachable at `http://127.0.0.1:8000/`.
|
||||||
The admin will be reachable at `http://127.0.0.1:8000/admin/`.
|
The admin will be reachable at `http://127.0.0.1:8000/admin/`.
|
||||||
|
|
||||||
The `DJANGO_SETTINGS_MODULE` environment variable defaults to
|
The `DJANGO_SETTINGS_MODULE` environment variable defaults to
|
||||||
`hotpocket_backend.settings.deployment.webapp`. This should be set to
|
`hotpocket_backend.settings.deployment.aio`.
|
||||||
`hotpocket_backend.settings.deployment.admin` in the Admin container.
|
|
||||||
|
|
||||||
**NOTE:** The command above specifies wildly insecure `SECRET_KEY` which is
|
**NOTE:** The command above specifies wildly insecure `SECRET_KEY` which is
|
||||||
used among other things to secure the session cookie. Please *please*
|
used among other things to secure the session cookie. Please *please*
|
||||||
|
@ -94,7 +93,8 @@ backend etc. The final deployment will require services for at least the Web
|
||||||
app, the Celery worker and Celery Beat. Admin is optional.
|
app, the Celery worker and Celery Beat. Admin is optional.
|
||||||
|
|
||||||
The `DJANGO_SETTINGS_MODULE` environment variable defaults to
|
The `DJANGO_SETTINGS_MODULE` environment variable defaults to
|
||||||
`hotpocket_backend.settings.deployment.aio`.
|
`hotpocket_backend.settings.deployment.webapp`. This should be set to
|
||||||
|
`hotpocket_backend.settings.deployment.admin` in the Admin container.
|
||||||
|
|
||||||
The `deployment/fullstack/docker-compose.yaml` file can be used as a
|
The `deployment/fullstack/docker-compose.yaml` file can be used as a
|
||||||
starting point for full-stack deployments.
|
starting point for full-stack deployments.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.10.4-01"
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.10.13-01"
|
||||||
environment:
|
environment:
|
||||||
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"
|
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"
|
||||||
|
|
|
@ -8,7 +8,7 @@ x-backend-environment: &x-backend-environment
|
||||||
|
|
||||||
services:
|
services:
|
||||||
webapp:
|
webapp:
|
||||||
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.4-01"
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-01"
|
||||||
environment:
|
environment:
|
||||||
<<: *x-backend-environment
|
<<: *x-backend-environment
|
||||||
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
||||||
|
@ -21,7 +21,7 @@ services:
|
||||||
restart: "unless-stopped"
|
restart: "unless-stopped"
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.4-01"
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-01"
|
||||||
environment:
|
environment:
|
||||||
<<: *x-backend-environment
|
<<: *x-backend-environment
|
||||||
HOTPOCKET_BACKEND_APP: "admin"
|
HOTPOCKET_BACKEND_APP: "admin"
|
||||||
|
@ -35,7 +35,7 @@ services:
|
||||||
restart: "unless-stopped"
|
restart: "unless-stopped"
|
||||||
|
|
||||||
celery-worker:
|
celery-worker:
|
||||||
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.4-01"
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-01"
|
||||||
command:
|
command:
|
||||||
- "/srv/venv/bin/celery"
|
- "/srv/venv/bin/celery"
|
||||||
- "-A"
|
- "-A"
|
||||||
|
@ -57,7 +57,7 @@ services:
|
||||||
restart: "unless-stopped"
|
restart: "unless-stopped"
|
||||||
|
|
||||||
celery-beat:
|
celery-beat:
|
||||||
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.4-01"
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-01"
|
||||||
command:
|
command:
|
||||||
- "/srv/venv/bin/celery"
|
- "/srv/venv/bin/celery"
|
||||||
- "-A"
|
- "-A"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"group": {
|
"group": {
|
||||||
"default": {
|
"default": {
|
||||||
"targets": [
|
"targets": [
|
||||||
|
"apple-management",
|
||||||
"backend-management",
|
"backend-management",
|
||||||
"caddy",
|
"caddy",
|
||||||
"extension-management",
|
"extension-management",
|
||||||
|
@ -13,6 +14,28 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
|
"apple-management": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "apple/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/apple:local"
|
||||||
|
],
|
||||||
|
"target": "development",
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"apple-ci": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "apple/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-local"
|
||||||
|
],
|
||||||
|
"target": "ci",
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
"backend-management": {
|
"backend-management": {
|
||||||
"context": "services/",
|
"context": "services/",
|
||||||
"dockerfile": "backend/Dockerfile",
|
"dockerfile": "backend/Dockerfile",
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
ports: []
|
ports: !override []
|
||||||
|
|
||||||
keycloak:
|
keycloak:
|
||||||
command: "echo 'NOOP'"
|
command: "echo 'NOOP'"
|
||||||
ports: []
|
ports: !override []
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
ports: []
|
ports: !override []
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- path: "./services/backend/docker-compose-ci.yaml"
|
- path: "./services/backend/docker-compose-ci.yaml"
|
||||||
- path: "./services/packages/docker-compose-ci.yaml"
|
- path: "./services/packages/docker-compose-ci.yaml"
|
||||||
- path: "./services/extension/docker-compose-ci.yaml"
|
- path: "./services/extension/docker-compose-ci.yaml"
|
||||||
|
- path: "./services/apple/docker-compose-ci.yaml"
|
||||||
|
|
|
@ -6,6 +6,7 @@ include:
|
||||||
- path: "./services/backend/docker-compose.yaml"
|
- path: "./services/backend/docker-compose.yaml"
|
||||||
- path: "./services/packages/docker-compose.yaml"
|
- path: "./services/packages/docker-compose.yaml"
|
||||||
- path: "./services/extension/docker-compose.yaml"
|
- path: "./services/extension/docker-compose.yaml"
|
||||||
|
- path: "./services/apple/docker-compose.yaml"
|
||||||
|
|
||||||
volumes: {}
|
volumes: {}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hotpocket-workspace"
|
name = "hotpocket-workspace"
|
||||||
version = "25.10.4"
|
version = "25.10.13"
|
||||||
description = "HotPocket Workspace"
|
description = "HotPocket Workspace"
|
||||||
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
_tmp/
|
_tmp/
|
||||||
apple/
|
apple/build/
|
||||||
|
apple/DerivedData/
|
||||||
backend/node_modules/
|
backend/node_modules/
|
||||||
backend/ops/metal/
|
backend/ops/metal/
|
||||||
backend/hotpocket_backend/playground.py
|
backend/hotpocket_backend/playground.py
|
||||||
|
|
19
services/apple/Dockerfile
Normal file
19
services/apple/Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
ARG APP_USER_UID=1000
|
||||||
|
ARG APP_USER_GID=1000
|
||||||
|
ARG IMAGE_ID=development.00000000
|
||||||
|
|
||||||
|
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-node-20250819-01 AS development
|
||||||
|
|
||||||
|
ARG APP_USER_UID
|
||||||
|
ARG APP_USER_GID
|
||||||
|
ARG IMAGE_ID
|
||||||
|
|
||||||
|
# COPY --chown=$APP_USER_UID:$APP_USER_GID apple/ops/bin/*.sh /srv/bin/
|
||||||
|
|
||||||
|
VOLUME ["/srv/node_modules", "/srv/venv"]
|
||||||
|
|
||||||
|
FROM development AS ci
|
||||||
|
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID apple/ /srv/app/
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/packages/
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
|
|
@ -713,7 +713,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
|
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
|
||||||
|
@ -726,7 +726,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
||||||
PRODUCT_NAME = "Save to HotPocket";
|
PRODUCT_NAME = "Save to HotPocket";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -746,7 +746,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
|
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
|
||||||
|
@ -759,7 +759,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
||||||
PRODUCT_NAME = "Save to HotPocket";
|
PRODUCT_NAME = "Save to HotPocket";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -779,7 +779,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
|
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
|
||||||
|
@ -792,7 +792,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -814,7 +814,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
|
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
|
||||||
|
@ -827,7 +827,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -853,7 +853,7 @@
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
||||||
|
@ -873,7 +873,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -899,7 +899,7 @@
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
||||||
|
@ -919,7 +919,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -945,7 +945,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
@ -960,7 +960,7 @@
|
||||||
"@executable_path/../../../../Frameworks",
|
"@executable_path/../../../../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -980,7 +980,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
@ -995,7 +995,7 @@
|
||||||
"@executable_path/../../../../Frameworks",
|
"@executable_path/../../../../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -1017,7 +1017,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
@ -1033,7 +1033,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -1056,7 +1056,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
@ -1072,7 +1072,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
|
@ -1206,7 +1206,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
@ -1220,7 +1220,7 @@
|
||||||
"@executable_path/../../../../Frameworks",
|
"@executable_path/../../../../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
||||||
PRODUCT_NAME = "Save to HotPocket";
|
PRODUCT_NAME = "Save to HotPocket";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
|
@ -1236,7 +1236,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2025100401;
|
CURRENT_PROJECT_VERSION = 2025101302;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
@ -1250,7 +1250,7 @@
|
||||||
"@executable_path/../../../../Frameworks",
|
"@executable_path/../../../../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 25.10.4;
|
MARKETING_VERSION = 25.10.13;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
|
||||||
PRODUCT_NAME = "Save to HotPocket";
|
PRODUCT_NAME = "Save to HotPocket";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
|
|
23
services/apple/docker-compose-ci.yaml
Normal file
23
services/apple/docker-compose-ci.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
services:
|
||||||
|
apple-ci:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "apple/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-local"
|
||||||
|
command: "echo 'NOOP'"
|
||||||
|
environment:
|
||||||
|
PYTHONBREAKPOINT: "ipdb.set_trace"
|
||||||
|
HOTPOCKET_PACKAGES_ENV: "${HOTPOCKET_EXTENSION_ENV:-docker}"
|
||||||
|
# REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
|
||||||
|
RUN_POETRY_INSTALL: "true"
|
||||||
|
RUN_YARN_INSTALL: "false"
|
||||||
|
SETUP_BACKEND: "true"
|
||||||
|
SETUP_FRONTEND: "false"
|
||||||
|
volumes:
|
||||||
|
- "apple_venv:/srv/venv"
|
||||||
|
- "apple_node_modules:/srv/node_modules"
|
||||||
|
- "../tls:/srv/tls"
|
||||||
|
restart: "no"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
29
services/apple/docker-compose.yaml
Normal file
29
services/apple/docker-compose.yaml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
services:
|
||||||
|
apple-management:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "apple/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:local"
|
||||||
|
command: "echo 'NOOP'"
|
||||||
|
environment: &apple-env
|
||||||
|
PYTHONBREAKPOINT: "ipdb.set_trace"
|
||||||
|
HOTPOCKET_EXTENSION_ENV: "${HOTPOCKET_EXTENSION_ENV:-docker}"
|
||||||
|
REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
|
||||||
|
RUN_POETRY_INSTALL: "true"
|
||||||
|
RUN_YARN_INSTALL: "false"
|
||||||
|
SETUP_BACKEND: "true"
|
||||||
|
SETUP_FRONTEND: "false"
|
||||||
|
volumes:
|
||||||
|
- "apple_venv:/srv/venv"
|
||||||
|
- "apple_node_modules:/srv/node_modules"
|
||||||
|
- ".:/srv/app"
|
||||||
|
- "../packages:/srv/packages"
|
||||||
|
- "../tls:/srv/tls"
|
||||||
|
restart: "no"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
apple_venv:
|
||||||
|
apple_node_modules:
|
0
services/apple/ops/bin/.placeholder
Normal file
0
services/apple/ops/bin/.placeholder
Normal file
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hotpocket-apple"
|
name = "hotpocket-apple"
|
||||||
version = "25.10.4"
|
version = "25.10.13"
|
||||||
description = "HotPocket Apple Integrations"
|
description = "HotPocket Apple Integrations"
|
||||||
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
|
@ -8,12 +8,7 @@ ARG APP_USER_UID
|
||||||
ARG APP_USER_GID
|
ARG APP_USER_GID
|
||||||
ARG IMAGE_ID
|
ARG IMAGE_ID
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ops/bin/*.sh /srv/bin/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ops/bin/*.sh /srv/bin/
|
||||||
RUN chown -R ${APP_USER_UID}:${APP_USER_GID} /srv
|
|
||||||
|
|
||||||
USER app
|
|
||||||
|
|
||||||
VOLUME ["/srv/node_modules", "/srv/venv"]
|
VOLUME ["/srv/node_modules", "/srv/venv"]
|
||||||
|
|
||||||
|
@ -50,7 +45,6 @@ COPY --from=deployment-build /srv/packages /srv/packages
|
||||||
COPY --from=deployment-build /srv/venv /srv/venv
|
COPY --from=deployment-build /srv/venv /srv/venv
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ops/bin/*.sh /srv/bin/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ops/bin/*.sh /srv/bin/
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ops/deployment/gunicorn.conf.py backend/ops/deployment/gunicorn.logging.conf /srv/lib/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ops/deployment/gunicorn.conf.py backend/ops/deployment/gunicorn.logging.conf /srv/lib/
|
||||||
RUN chown -R $APP_USER_UID:$APP_USER_GID /srv
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
@ -109,5 +103,4 @@ COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/packages/
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
|
||||||
|
|
||||||
RUN ln -s /srv/app/ops/docker/settings /srv/app/hotpocket_backend/settings/docker && \
|
RUN ln -s /srv/app/ops/docker/settings /srv/app/hotpocket_backend/settings/docker && \
|
||||||
ln -s /srv/app/ops/docker/secrets /srv/app/hotpocket_backend/secrets/docker && \
|
ln -s /srv/app/ops/docker/secrets /srv/app/hotpocket_backend/secrets/docker
|
||||||
chown -R $APP_USER_UID:$APP_USER_GID /srv
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
version = '25.10.4'
|
version = '25.10.13'
|
||||||
|
|
|
@ -68,4 +68,10 @@ class Account(AbstractUser):
|
||||||
else:
|
else:
|
||||||
result['auto_load_embeds'] = auto_load_embeds
|
result['auto_load_embeds'] = auto_load_embeds
|
||||||
|
|
||||||
|
light_mode = result.get('light_mode', None)
|
||||||
|
if isinstance(light_mode, str) is True:
|
||||||
|
result['light_mode'] = (light_mode == 'True')
|
||||||
|
else:
|
||||||
|
result['light_mode'] = light_mode
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -6,6 +6,7 @@ import hmac
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
import uuid6
|
import uuid6
|
||||||
|
|
||||||
|
@ -15,6 +16,10 @@ from hotpocket_soa.dto.accounts import (
|
||||||
AccessTokenMetaUpdateIn,
|
AccessTokenMetaUpdateIn,
|
||||||
AccessTokensQuery,
|
AccessTokensQuery,
|
||||||
)
|
)
|
||||||
|
from hotpocket_soa.exceptions.backend import (
|
||||||
|
Invalid as InvalidError,
|
||||||
|
NotFound as NotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -23,7 +28,10 @@ class AccessTokensService:
|
||||||
class AccessTokensServiceError(Exception):
|
class AccessTokensServiceError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AccessTokenNotFound(AccessTokensServiceError):
|
class Invalid(InvalidError, AccessTokensServiceError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotFound(NotFoundError, AccessTokensServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create(self,
|
def create(self,
|
||||||
|
@ -32,20 +40,23 @@ class AccessTokensService:
|
||||||
origin: str,
|
origin: str,
|
||||||
meta: dict,
|
meta: dict,
|
||||||
) -> AccessToken:
|
) -> AccessToken:
|
||||||
pk = uuid6.uuid7()
|
try:
|
||||||
key = hmac.new(
|
pk = uuid6.uuid7()
|
||||||
settings.SECRET_KEY.encode('ascii'),
|
key = hmac.new(
|
||||||
msg=pk.bytes,
|
settings.SECRET_KEY.encode('ascii'),
|
||||||
digestmod=hashlib.sha256,
|
msg=pk.bytes,
|
||||||
)
|
digestmod=hashlib.sha256,
|
||||||
|
)
|
||||||
|
|
||||||
return AccessToken.objects.create(
|
return AccessToken.objects.create(
|
||||||
pk=pk,
|
pk=pk,
|
||||||
account_uuid=account_uuid,
|
account_uuid=account_uuid,
|
||||||
key=key.hexdigest(),
|
key=key.hexdigest(),
|
||||||
origin=origin,
|
origin=origin,
|
||||||
meta=meta,
|
meta=meta,
|
||||||
)
|
)
|
||||||
|
except ValidationError as exception:
|
||||||
|
raise self.Invalid.from_django_validation_error(exception)
|
||||||
|
|
||||||
def get(self, *, pk: uuid.UUID) -> AccessToken:
|
def get(self, *, pk: uuid.UUID) -> AccessToken:
|
||||||
try:
|
try:
|
||||||
|
@ -53,7 +64,7 @@ class AccessTokensService:
|
||||||
|
|
||||||
return query_set.get(pk=pk)
|
return query_set.get(pk=pk)
|
||||||
except AccessToken.DoesNotExist as exception:
|
except AccessToken.DoesNotExist as exception:
|
||||||
raise self.AccessTokenNotFound(
|
raise self.NotFound(
|
||||||
f'Access Token not found: pk=`{pk}`',
|
f'Access Token not found: pk=`{pk}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
|
@ -63,7 +74,7 @@ class AccessTokensService:
|
||||||
|
|
||||||
return query_set.get(key=key)
|
return query_set.get(key=key)
|
||||||
except AccessToken.DoesNotExist as exception:
|
except AccessToken.DoesNotExist as exception:
|
||||||
raise self.AccessTokenNotFound(
|
raise self.NotFound(
|
||||||
f'Access Token not found: key=`{key}`',
|
f'Access Token not found: key=`{key}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
|
@ -98,7 +109,7 @@ class AccessTokensService:
|
||||||
pk: uuid.UUID,
|
pk: uuid.UUID,
|
||||||
update: AccessTokenMetaUpdateIn,
|
update: AccessTokenMetaUpdateIn,
|
||||||
) -> AccessToken:
|
) -> AccessToken:
|
||||||
access_token = AccessToken.active_objects.get(pk=pk)
|
access_token = self.get(pk=pk)
|
||||||
|
|
||||||
next_meta = {
|
next_meta = {
|
||||||
**(access_token.meta or {}),
|
**(access_token.meta or {}),
|
||||||
|
|
|
@ -5,6 +5,7 @@ import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from hotpocket_backend.apps.accounts.models import Account
|
from hotpocket_backend.apps.accounts.models import Account
|
||||||
|
from hotpocket_soa.exceptions.backend import NotFound as NotFoundError
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ class AccountsService:
|
||||||
class AccountsServiceError(Exception):
|
class AccountsServiceError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AccountNotFound(AccountsServiceError):
|
class NotFound(NotFoundError, AccountsServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get(self, *, pk: uuid.UUID) -> Account:
|
def get(self, *, pk: uuid.UUID) -> Account:
|
||||||
|
@ -22,6 +23,6 @@ class AccountsService:
|
||||||
|
|
||||||
return query_set.get(pk=pk)
|
return query_set.get(pk=pk)
|
||||||
except Account.DoesNotExist as exception:
|
except Account.DoesNotExist as exception:
|
||||||
raise self.AccountNotFound(
|
raise self.NotFound(
|
||||||
f'Account not found: pk=`{pk}`',
|
f'Account not found: pk=`{pk}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
|
@ -5,11 +5,17 @@ import datetime
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
import uuid6
|
import uuid6
|
||||||
|
|
||||||
from hotpocket_backend.apps.accounts.models import AuthKey
|
from hotpocket_backend.apps.accounts.models import AuthKey
|
||||||
from hotpocket_backend.apps.core.conf import settings
|
from hotpocket_backend.apps.core.conf import settings
|
||||||
|
from hotpocket_soa.exceptions.backend import (
|
||||||
|
InternalError,
|
||||||
|
Invalid as InvalidError,
|
||||||
|
NotFound as NotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -18,22 +24,25 @@ class AuthKeysService:
|
||||||
class AuthKeysServiceError(Exception):
|
class AuthKeysServiceError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AuthKeyNotFound(AuthKeysServiceError):
|
class Invalid(InvalidError, AuthKeysServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AuthKeyExpired(AuthKeysServiceError):
|
class NotFound(NotFoundError, AuthKeysServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AuthKeyAccessDenied(AuthKeysServiceError):
|
class Expired(InternalError, AuthKeysServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create(self, *, account_uuid: uuid.UUID) -> AuthKey:
|
def create(self, *, account_uuid: uuid.UUID) -> AuthKey:
|
||||||
key = str(uuid6.uuid7())
|
try:
|
||||||
|
key = str(uuid6.uuid7())
|
||||||
|
|
||||||
return AuthKey.objects.create(
|
return AuthKey.objects.create(
|
||||||
account_uuid=account_uuid,
|
account_uuid=account_uuid,
|
||||||
key=key,
|
key=key,
|
||||||
)
|
)
|
||||||
|
except ValidationError as exception:
|
||||||
|
raise self.Invalid.from_django_validation_error(exception)
|
||||||
|
|
||||||
def get(self, *, pk: uuid.UUID) -> AuthKey:
|
def get(self, *, pk: uuid.UUID) -> AuthKey:
|
||||||
try:
|
try:
|
||||||
|
@ -41,7 +50,7 @@ class AuthKeysService:
|
||||||
|
|
||||||
return query_set.get(pk=pk)
|
return query_set.get(pk=pk)
|
||||||
except AuthKey.DoesNotExist as exception:
|
except AuthKey.DoesNotExist as exception:
|
||||||
raise self.AuthKeyNotFound(
|
raise self.NotFound(
|
||||||
f'Auth Key not found: pk=`{pk}`',
|
f'Auth Key not found: pk=`{pk}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
|
@ -56,17 +65,17 @@ class AuthKeysService:
|
||||||
|
|
||||||
if ttl > 0:
|
if ttl > 0:
|
||||||
if result.created_at < now() - datetime.timedelta(seconds=ttl):
|
if result.created_at < now() - datetime.timedelta(seconds=ttl):
|
||||||
raise self.AuthKeyExpired(
|
raise self.Expired(
|
||||||
f'Auth Key expired: pk=`{key}`',
|
f'Auth Key expired: pk=`{key}`',
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.consumed_at is not None:
|
if result.consumed_at is not None:
|
||||||
raise self.AuthKeyExpired(
|
raise self.Expired(
|
||||||
f'Auth Key already consumed: pk=`{key}`',
|
f'Auth Key already consumed: pk=`{key}`',
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except AuthKey.DoesNotExist as exception:
|
except AuthKey.DoesNotExist as exception:
|
||||||
raise self.AuthKeyNotFound(
|
raise self.NotFound(
|
||||||
f'Auth Key not found: key=`{key}`',
|
f'Auth Key not found: key=`{key}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
|
@ -1,16 +1,39 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from bthlabs_jsonrpc_core.exceptions import BaseJSONRPCError
|
||||||
from bthlabs_jsonrpc_django import (
|
from bthlabs_jsonrpc_django import (
|
||||||
DjangoExecutor,
|
DjangoExecutor,
|
||||||
DjangoJSONRPCSerializer,
|
DjangoJSONRPCSerializer,
|
||||||
JSONRPCView as BaseJSONRPCView,
|
JSONRPCView as BaseJSONRPCView,
|
||||||
)
|
)
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
import uuid6
|
import uuid6
|
||||||
|
|
||||||
|
from hotpocket_soa.exceptions.frontend import SOAError
|
||||||
|
|
||||||
|
|
||||||
|
class SOAJSONRPCError(BaseJSONRPCError):
|
||||||
|
ERROR_CODE = -32000
|
||||||
|
ERROR_MESSAGE = 'SOA Error'
|
||||||
|
|
||||||
|
def to_rpc(self) -> dict:
|
||||||
|
exception = typing.cast(SOAError, self.data)
|
||||||
|
|
||||||
|
code = (
|
||||||
|
exception.code
|
||||||
|
if exception.code is not None
|
||||||
|
else self.ERROR_CODE
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'code': code,
|
||||||
|
'message': exception.message or self.ERROR_MESSAGE,
|
||||||
|
'data': exception.data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class JSONRPCSerializer(DjangoJSONRPCSerializer):
|
class JSONRPCSerializer(DjangoJSONRPCSerializer):
|
||||||
STRING_COERCIBLE_TYPES: typing.Any = (
|
STRING_COERCIBLE_TYPES: typing.Any = (
|
||||||
|
@ -18,30 +41,6 @@ class JSONRPCSerializer(DjangoJSONRPCSerializer):
|
||||||
uuid6.UUID,
|
uuid6.UUID,
|
||||||
)
|
)
|
||||||
|
|
||||||
def serialize_value(self, value: typing.Any) -> typing.Any:
|
|
||||||
if isinstance(value, ValidationError):
|
|
||||||
result: typing.Any = None
|
|
||||||
|
|
||||||
if hasattr(value, 'error_dict') is True:
|
|
||||||
result = {}
|
|
||||||
for field, errors in value.error_dict.items():
|
|
||||||
result[field] = [
|
|
||||||
error.code
|
|
||||||
for error
|
|
||||||
in errors
|
|
||||||
]
|
|
||||||
elif hasattr(value, 'error_list') is True:
|
|
||||||
result = [
|
|
||||||
error.code
|
|
||||||
for error in value.error_list
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
result = value.code
|
|
||||||
|
|
||||||
return self.serialize_value(result)
|
|
||||||
|
|
||||||
return super().serialize_value(value)
|
|
||||||
|
|
||||||
|
|
||||||
class Executor(DjangoExecutor):
|
class Executor(DjangoExecutor):
|
||||||
serializer = JSONRPCSerializer
|
serializer = JSONRPCSerializer
|
||||||
|
@ -49,3 +48,14 @@ class Executor(DjangoExecutor):
|
||||||
|
|
||||||
class JSONRPCView(BaseJSONRPCView):
|
class JSONRPCView(BaseJSONRPCView):
|
||||||
executor = Executor
|
executor = Executor
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_soa_errors(func: typing.Callable) -> typing.Callable:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except SOAError as exception:
|
||||||
|
raise SOAJSONRPCError(exception)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
|
@ -5,6 +5,7 @@ import datetime
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
@ -15,6 +16,10 @@ from hotpocket_soa.dto.associations import (
|
||||||
AssociationsQuery,
|
AssociationsQuery,
|
||||||
AssociationUpdateIn,
|
AssociationUpdateIn,
|
||||||
)
|
)
|
||||||
|
from hotpocket_soa.exceptions.backend import (
|
||||||
|
Invalid as InvalidError,
|
||||||
|
NotFound as NotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
from .saves import SavesService
|
from .saves import SavesService
|
||||||
|
|
||||||
|
@ -25,7 +30,10 @@ class AssociationsService:
|
||||||
class AssociationsServiceError(Exception):
|
class AssociationsServiceError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AssociationNotFound(AssociationsServiceError):
|
class Invalid(InvalidError, AssociationsServiceError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotFound(NotFoundError, AssociationsServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -46,30 +54,33 @@ class AssociationsService:
|
||||||
pk: uuid.UUID | None = None,
|
pk: uuid.UUID | None = None,
|
||||||
created_at: datetime.datetime | None = None,
|
created_at: datetime.datetime | None = None,
|
||||||
) -> Association:
|
) -> Association:
|
||||||
save = SavesService().get(pk=save_uuid)
|
try:
|
||||||
|
save = SavesService().get(pk=save_uuid)
|
||||||
|
|
||||||
defaults = dict(
|
defaults = dict(
|
||||||
account_uuid=account_uuid,
|
account_uuid=account_uuid,
|
||||||
target=save,
|
target=save,
|
||||||
)
|
)
|
||||||
|
|
||||||
if pk is not None:
|
if pk is not None:
|
||||||
defaults['id'] = pk
|
defaults['id'] = pk
|
||||||
|
|
||||||
result, created = Association.objects.get_or_create(
|
result, created = Association.objects.get_or_create(
|
||||||
account_uuid=account_uuid,
|
account_uuid=account_uuid,
|
||||||
deleted_at__isnull=True,
|
deleted_at__isnull=True,
|
||||||
target=save,
|
target=save,
|
||||||
archived_at__isnull=True,
|
archived_at__isnull=True,
|
||||||
defaults=defaults,
|
defaults=defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
if created is True:
|
if created is True:
|
||||||
if created_at is not None:
|
if created_at is not None:
|
||||||
result.created_at = created_at
|
result.created_at = created_at
|
||||||
result.save()
|
result.save()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
except ValidationError as exception:
|
||||||
|
raise self.Invalid.from_django_validation_error(exception)
|
||||||
|
|
||||||
def get(self,
|
def get(self,
|
||||||
*,
|
*,
|
||||||
|
@ -87,7 +98,7 @@ class AssociationsService:
|
||||||
|
|
||||||
return query_set.get(pk=pk)
|
return query_set.get(pk=pk)
|
||||||
except Association.DoesNotExist as exception:
|
except Association.DoesNotExist as exception:
|
||||||
raise self.AssociationNotFound(
|
raise self.NotFound(
|
||||||
f'Association not found: pk=`{pk}`',
|
f'Association not found: pk=`{pk}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
|
@ -112,21 +123,24 @@ class AssociationsService:
|
||||||
pk: uuid.UUID,
|
pk: uuid.UUID,
|
||||||
update: AssociationUpdateIn,
|
update: AssociationUpdateIn,
|
||||||
) -> Association:
|
) -> Association:
|
||||||
association = self.get(pk=pk)
|
try:
|
||||||
association.target_title = update.target_title
|
association = self.get(pk=pk)
|
||||||
association.target_description = update.target_description
|
association.target_title = update.target_title
|
||||||
|
association.target_description = update.target_description
|
||||||
|
|
||||||
next_target_meta = {
|
next_target_meta = {
|
||||||
**(association.target_meta or {}),
|
**(association.target_meta or {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
next_target_meta.pop('title', None)
|
next_target_meta.pop('title', None)
|
||||||
next_target_meta.pop('description', None)
|
next_target_meta.pop('description', None)
|
||||||
association.target_meta = next_target_meta
|
association.target_meta = next_target_meta
|
||||||
|
|
||||||
association.save()
|
association.save()
|
||||||
|
|
||||||
return association
|
return association
|
||||||
|
except ValidationError as exception:
|
||||||
|
raise self.Invalid.from_django_validation_error(exception)
|
||||||
|
|
||||||
def archive(self, *, pk: uuid.UUID) -> bool:
|
def archive(self, *, pk: uuid.UUID) -> bool:
|
||||||
association = self.get(pk=pk)
|
association = self.get(pk=pk)
|
||||||
|
|
|
@ -5,19 +5,27 @@ import hashlib
|
||||||
import typing
|
import typing
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from hotpocket_backend.apps.core.services import get_adapter
|
from hotpocket_backend.apps.core.services import get_adapter
|
||||||
from hotpocket_backend.apps.saves.models import Save
|
from hotpocket_backend.apps.saves.models import Save
|
||||||
from hotpocket_backend.apps.saves.types import PSaveAdapter
|
from hotpocket_backend.apps.saves.types import PSaveAdapter
|
||||||
from hotpocket_soa.dto.saves import ImportedSaveIn, SaveIn, SavesQuery
|
from hotpocket_soa.dto.saves import ImportedSaveIn, SaveIn, SavesQuery
|
||||||
|
from hotpocket_soa.exceptions.backend import (
|
||||||
|
Invalid as InvalidError,
|
||||||
|
NotFound as NotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SavesService:
|
class SavesService:
|
||||||
class SavesServiceError(Exception):
|
class SavesServiceError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class SaveNotFound(SavesServiceError):
|
class Invalid(InvalidError, SavesServiceError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotFound(NotFoundError, SavesServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -36,35 +44,38 @@ class SavesService:
|
||||||
account_uuid: uuid.UUID,
|
account_uuid: uuid.UUID,
|
||||||
save: SaveIn | ImportedSaveIn,
|
save: SaveIn | ImportedSaveIn,
|
||||||
) -> Save:
|
) -> Save:
|
||||||
key = hashlib.sha256(save.url.encode('utf-8')).hexdigest()
|
try:
|
||||||
|
key = hashlib.sha256(save.url.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
defaults = dict(
|
defaults = dict(
|
||||||
account_uuid=account_uuid,
|
account_uuid=account_uuid,
|
||||||
key=key,
|
key=key,
|
||||||
url=save.url,
|
url=save.url,
|
||||||
)
|
)
|
||||||
|
|
||||||
save_object, created = Save.objects.get_or_create(
|
save_object, created = Save.objects.get_or_create(
|
||||||
key=key,
|
key=key,
|
||||||
deleted_at__isnull=True,
|
deleted_at__isnull=True,
|
||||||
defaults=defaults,
|
defaults=defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
if created is True:
|
if created is True:
|
||||||
save_object.is_netloc_banned = save.is_netloc_banned
|
save_object.is_netloc_banned = save.is_netloc_banned
|
||||||
|
|
||||||
if isinstance(save, ImportedSaveIn) is True:
|
if isinstance(save, ImportedSaveIn) is True:
|
||||||
save_object.title = save.title # type: ignore[union-attr]
|
save_object.title = save.title # type: ignore[union-attr]
|
||||||
|
|
||||||
save_object.save()
|
save_object.save()
|
||||||
|
|
||||||
return save_object
|
return save_object
|
||||||
|
except ValidationError as exception:
|
||||||
|
raise self.Invalid.from_django_validation_error(exception)
|
||||||
|
|
||||||
def get(self, *, pk: uuid.UUID) -> Save:
|
def get(self, *, pk: uuid.UUID) -> Save:
|
||||||
try:
|
try:
|
||||||
return Save.active_objects.get(pk=pk)
|
return Save.active_objects.get(pk=pk)
|
||||||
except Save.DoesNotExist as exception:
|
except Save.DoesNotExist as exception:
|
||||||
raise self.SaveNotFound(
|
raise self.NotFound(
|
||||||
f'Save not found: pk=`{pk}`',
|
f'Save not found: pk=`{pk}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
|
|
|
@ -46,3 +46,33 @@ def version(request: HttpRequest) -> dict:
|
||||||
return {
|
return {
|
||||||
'VERSION': backend_version,
|
'VERSION': backend_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def appearance_settings(request: HttpRequest) -> dict:
|
||||||
|
theme = 'hotpocket'
|
||||||
|
result = {
|
||||||
|
'theme': theme,
|
||||||
|
'light_mode': False,
|
||||||
|
'theme_css': 'ui/css/bootstrap-hotpocket.min.css',
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.user.is_anonymous is False:
|
||||||
|
theme = request.user.settings.get('theme', 'hotpocket')
|
||||||
|
result.update({
|
||||||
|
'theme': theme,
|
||||||
|
'light_mode': request.user.settings['light_mode'],
|
||||||
|
})
|
||||||
|
|
||||||
|
match theme:
|
||||||
|
case 'bootstrap':
|
||||||
|
result['theme_css'] = 'ui/css/bootstrap.min.css'
|
||||||
|
|
||||||
|
case 'cosmo':
|
||||||
|
result['theme_css'] = 'ui/css/cosmo.min.css'
|
||||||
|
|
||||||
|
case 'solar':
|
||||||
|
result['theme_css'] = 'ui/css/solar.min.css'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'APPEARANCE_SETTINGS': result,
|
||||||
|
}
|
||||||
|
|
|
@ -143,13 +143,18 @@ class SettingsForm(Form):
|
||||||
|
|
||||||
theme = forms.ChoiceField(
|
theme = forms.ChoiceField(
|
||||||
label=_('Theme'),
|
label=_('Theme'),
|
||||||
disabled=True,
|
disabled=False,
|
||||||
required=False,
|
required=False,
|
||||||
choices=[
|
choices=[
|
||||||
(None, _('Bootstrap')),
|
(None, _('BTHLabs')),
|
||||||
|
('bootstrap', _('Bootstrap')),
|
||||||
('cosmo', _('Cosmo')),
|
('cosmo', _('Cosmo')),
|
||||||
|
('solar', _('Solar')),
|
||||||
],
|
],
|
||||||
show_hidden_initial=True,
|
)
|
||||||
|
light_mode = forms.BooleanField(
|
||||||
|
label=_('Use light mode'),
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
auto_load_embeds = forms.ChoiceField(
|
auto_load_embeds = forms.ChoiceField(
|
||||||
label=_('Auto load embedded content'),
|
label=_('Auto load embedded content'),
|
||||||
|
@ -168,6 +173,7 @@ class SettingsForm(Form):
|
||||||
def get_layout_fields(self) -> list[str]:
|
def get_layout_fields(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
'theme',
|
'theme',
|
||||||
|
'light_mode',
|
||||||
'auto_load_embeds',
|
'auto_load_embeds',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from bthlabs_jsonrpc_core import register_method
|
||||||
from django import db
|
from django import db
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
|
||||||
from hotpocket_soa.services import (
|
from hotpocket_soa.services import (
|
||||||
AccessTokensService,
|
AccessTokensService,
|
||||||
AccountsService,
|
AccountsService,
|
||||||
|
@ -17,6 +18,7 @@ LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@register_method('accounts.access_tokens.create', namespace='accounts')
|
@register_method('accounts.access_tokens.create', namespace='accounts')
|
||||||
|
@wrap_soa_errors
|
||||||
def create(request: HttpRequest,
|
def create(request: HttpRequest,
|
||||||
auth_key: str,
|
auth_key: str,
|
||||||
meta: dict,
|
meta: dict,
|
||||||
|
@ -27,7 +29,7 @@ def create(request: HttpRequest,
|
||||||
account_uuid=None,
|
account_uuid=None,
|
||||||
key=auth_key,
|
key=auth_key,
|
||||||
)
|
)
|
||||||
except AuthKeysService.AuthKeyNotFound as exception:
|
except AuthKeysService.NotFound as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Unable to issue access token: %s',
|
'Unable to issue access token: %s',
|
||||||
exception,
|
exception,
|
||||||
|
@ -37,7 +39,7 @@ def create(request: HttpRequest,
|
||||||
|
|
||||||
try:
|
try:
|
||||||
account = AccountsService().get(pk=auth_key_object.account_uuid)
|
account = AccountsService().get(pk=auth_key_object.account_uuid)
|
||||||
except AccountsService.AccountNotFound as exception:
|
except AccountsService.NotFound as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Unable to issue access token: %s',
|
'Unable to issue access token: %s',
|
||||||
exception,
|
exception,
|
||||||
|
|
|
@ -44,7 +44,7 @@ def check_access_token(request: HttpRequest,
|
||||||
access_token=access_token_object,
|
access_token=access_token_object,
|
||||||
update=meta_update,
|
update=meta_update,
|
||||||
)
|
)
|
||||||
except AccessTokensService.AccessTokenNotFound as exception:
|
except AccessTokensService.NotFound as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Access Token not found: account_uuid=`%s` key=`%s`',
|
'Access Token not found: account_uuid=`%s` key=`%s`',
|
||||||
request.user.pk,
|
request.user.pk,
|
||||||
|
@ -52,7 +52,7 @@ def check_access_token(request: HttpRequest,
|
||||||
exc_info=exception,
|
exc_info=exception,
|
||||||
)
|
)
|
||||||
result = False
|
result = False
|
||||||
except AccessTokensService.AccessTokenAccessDenied as exception:
|
except AccessTokensService.AccessDenied as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Access Token access denied: account_uuid=`%s` key=`%s`',
|
'Access Token access denied: account_uuid=`%s` key=`%s`',
|
||||||
request.user.pk,
|
request.user.pk,
|
||||||
|
|
|
@ -4,11 +4,13 @@ from __future__ import annotations
|
||||||
from bthlabs_jsonrpc_core import register_method
|
from bthlabs_jsonrpc_core import register_method
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
|
||||||
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
|
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
|
||||||
from hotpocket_soa.dto.associations import AssociationOut
|
from hotpocket_soa.dto.associations import AssociationOut
|
||||||
|
|
||||||
|
|
||||||
@register_method(method='saves.create')
|
@register_method(method='saves.create')
|
||||||
|
@wrap_soa_errors
|
||||||
def create(request: HttpRequest, url: str) -> AssociationOut:
|
def create(request: HttpRequest, url: str) -> AssociationOut:
|
||||||
association = CreateSaveWorkflow().run_rpc(
|
association = CreateSaveWorkflow().run_rpc(
|
||||||
request=request,
|
request=request,
|
||||||
|
|
|
@ -27,7 +27,7 @@ class UIAccessTokensService:
|
||||||
account_uuid=account_uuid,
|
account_uuid=account_uuid,
|
||||||
pk=pk,
|
pk=pk,
|
||||||
)
|
)
|
||||||
except AccessTokensService.AccessTokenNotFound as exception:
|
except AccessTokensService.NotFound as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Access Token not found: account_uuid=`%s` pk=`%s`',
|
'Access Token not found: account_uuid=`%s` pk=`%s`',
|
||||||
account_uuid,
|
account_uuid,
|
||||||
|
@ -35,7 +35,7 @@ class UIAccessTokensService:
|
||||||
exc_info=exception,
|
exc_info=exception,
|
||||||
)
|
)
|
||||||
raise Http404()
|
raise Http404()
|
||||||
except AccessTokensService.AccessTokenAccessDenied as exception:
|
except AccessTokensService.AccessDenied as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Access Token access denied: account_uuid=`%s` pk=`%s`',
|
'Access Token access denied: account_uuid=`%s` pk=`%s`',
|
||||||
account_uuid,
|
account_uuid,
|
||||||
|
|
|
@ -34,7 +34,7 @@ class UIAssociationsService:
|
||||||
with_target=True,
|
with_target=True,
|
||||||
allow_archived=allow_archived,
|
allow_archived=allow_archived,
|
||||||
)
|
)
|
||||||
except AssociationsService.AssociationNotFound as exception:
|
except AssociationsService.NotFound as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Association not found: account_uuid=`%s` pk=`%s`',
|
'Association not found: account_uuid=`%s` pk=`%s`',
|
||||||
account_uuid,
|
account_uuid,
|
||||||
|
@ -42,7 +42,7 @@ class UIAssociationsService:
|
||||||
exc_info=exception,
|
exc_info=exception,
|
||||||
)
|
)
|
||||||
raise Http404()
|
raise Http404()
|
||||||
except AssociationsService.AssociationAccessDenied as exception:
|
except AssociationsService.AccessDenied as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Association access denied: account_uuid=`%s` pk=`%s`',
|
'Association access denied: account_uuid=`%s` pk=`%s`',
|
||||||
account_uuid,
|
account_uuid,
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@ -13,16 +14,28 @@ from django.utils.timezone import get_current_timezone
|
||||||
from hotpocket_backend.apps.ui.services.workflows import ImportSaveWorkflow
|
from hotpocket_backend.apps.ui.services.workflows import ImportSaveWorkflow
|
||||||
from hotpocket_backend.apps.ui.tasks import import_from_pocket
|
from hotpocket_backend.apps.ui.tasks import import_from_pocket
|
||||||
from hotpocket_common.uuid import uuid7_from_timestamp
|
from hotpocket_common.uuid import uuid7_from_timestamp
|
||||||
|
from hotpocket_soa.services import SavesService
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UIImportsService:
|
class UIImportsService:
|
||||||
def import_from_pocket(self,
|
def import_from_pocket(self,
|
||||||
*,
|
*,
|
||||||
|
job: str,
|
||||||
account_uuid: uuid.UUID,
|
account_uuid: uuid.UUID,
|
||||||
csv_path: str,
|
csv_path: str,
|
||||||
) -> list[tuple[uuid.UUID, uuid.UUID]]:
|
) -> list[tuple[uuid.UUID, uuid.UUID]]:
|
||||||
result = []
|
LOGGER.info(
|
||||||
|
'Starting import job: job=`%s` account_uuid=`%s`',
|
||||||
|
job,
|
||||||
|
account_uuid,
|
||||||
|
extra={
|
||||||
|
'job': job,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
with db.transaction.atomic():
|
with db.transaction.atomic():
|
||||||
try:
|
try:
|
||||||
with open(csv_path, 'r', encoding='utf-8') as csv_file:
|
with open(csv_path, 'r', encoding='utf-8') as csv_file:
|
||||||
|
@ -34,22 +47,35 @@ class UIImportsService:
|
||||||
current_timezone = get_current_timezone()
|
current_timezone = get_current_timezone()
|
||||||
|
|
||||||
is_header = False
|
is_header = False
|
||||||
for row in csv_reader:
|
for row_number, row in enumerate(csv_reader, start=1):
|
||||||
if is_header is False:
|
if is_header is False:
|
||||||
is_header = True
|
is_header = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
timestamp = int(row['time_added'])
|
timestamp = int(row['time_added'])
|
||||||
|
|
||||||
save, association = ImportSaveWorkflow().run(
|
try:
|
||||||
account_uuid=account_uuid,
|
save, association = ImportSaveWorkflow().run(
|
||||||
url=row['url'],
|
account_uuid=account_uuid,
|
||||||
title=row['title'],
|
url=row['url'],
|
||||||
pk=uuid7_from_timestamp(timestamp),
|
title=row['title'],
|
||||||
created_at=datetime.datetime.fromtimestamp(
|
pk=uuid7_from_timestamp(timestamp),
|
||||||
timestamp, tz=current_timezone,
|
created_at=datetime.datetime.fromtimestamp(
|
||||||
),
|
timestamp, tz=current_timezone,
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
except SavesService.Invalid as exception:
|
||||||
|
LOGGER.error(
|
||||||
|
'Import error: row_number=`%d` url=`%s` exception=`%s`',
|
||||||
|
row_number,
|
||||||
|
row['url'],
|
||||||
|
exception,
|
||||||
|
exc_info=exception,
|
||||||
|
extra={
|
||||||
|
'job': job,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
result.append((save.pk, association.pk))
|
result.append((save.pk, association.pk))
|
||||||
finally:
|
finally:
|
||||||
|
@ -64,6 +90,7 @@ class UIImportsService:
|
||||||
) -> AsyncResult:
|
) -> AsyncResult:
|
||||||
return import_from_pocket.apply_async(
|
return import_from_pocket.apply_async(
|
||||||
kwargs={
|
kwargs={
|
||||||
|
'job': str(uuid.uuid4()),
|
||||||
'account_uuid': account_uuid,
|
'account_uuid': account_uuid,
|
||||||
'csv_path': csv_path,
|
'csv_path': csv_path,
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ class UISavesService:
|
||||||
def get_or_404(self, *, pk: uuid.UUID) -> SaveOut:
|
def get_or_404(self, *, pk: uuid.UUID) -> SaveOut:
|
||||||
try:
|
try:
|
||||||
return SavesService().get(pk=pk)
|
return SavesService().get(pk=pk)
|
||||||
except SavesService.SaveNotFound as exception:
|
except SavesService.NotFound as exception:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Save not found: pk=`%s`', pk, exc_info=exception,
|
'Save not found: pk=`%s`', pk, exc_info=exception,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from bthlabs_jsonrpc_core import JSONRPCInternalError
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
import django.db
|
import django.db
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -14,7 +12,6 @@ from hotpocket_backend.apps.accounts.types import PAccount
|
||||||
from hotpocket_soa.dto.associations import AssociationOut
|
from hotpocket_soa.dto.associations import AssociationOut
|
||||||
from hotpocket_soa.dto.celery import AsyncResultOut
|
from hotpocket_soa.dto.celery import AsyncResultOut
|
||||||
from hotpocket_soa.dto.saves import SaveIn, SaveOut
|
from hotpocket_soa.dto.saves import SaveIn, SaveOut
|
||||||
from hotpocket_soa.services import SavesService
|
|
||||||
|
|
||||||
from .base import SaveWorkflow
|
from .base import SaveWorkflow
|
||||||
|
|
||||||
|
@ -73,14 +70,8 @@ class CreateSaveWorkflow(SaveWorkflow):
|
||||||
account: PAccount,
|
account: PAccount,
|
||||||
url: str,
|
url: str,
|
||||||
) -> AssociationOut:
|
) -> AssociationOut:
|
||||||
try:
|
save, association, processing_result = self.create_associate_and_process(
|
||||||
save, association, processing_result = self.create_associate_and_process(
|
account, url,
|
||||||
account, url,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return association
|
return association
|
||||||
except SavesService.SavesServiceError as exception:
|
|
||||||
if isinstance(exception.__cause__, ValidationError) is True:
|
|
||||||
raise JSONRPCInternalError(data=exception.__cause__)
|
|
||||||
|
|
||||||
raise
|
|
||||||
|
|
6
services/backend/hotpocket_backend/apps/ui/static/ui/css/bootstrap-hotpocket.min.css
vendored
Normal file
6
services/backend/hotpocket_backend/apps/ui/static/ui/css/bootstrap-hotpocket.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
services/backend/hotpocket_backend/apps/ui/static/ui/css/cosmo.min.css
vendored
Normal file
12
services/backend/hotpocket_backend/apps/ui/static/ui/css/cosmo.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
services/backend/hotpocket_backend/apps/ui/static/ui/css/solar.min.css
vendored
Normal file
12
services/backend/hotpocket_backend/apps/ui/static/ui/css/solar.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -11,6 +11,7 @@ LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def import_from_pocket(*,
|
def import_from_pocket(*,
|
||||||
|
job: str,
|
||||||
account_uuid: uuid.UUID,
|
account_uuid: uuid.UUID,
|
||||||
csv_path: str,
|
csv_path: str,
|
||||||
) -> list[tuple[uuid.UUID, uuid.UUID]]:
|
) -> list[tuple[uuid.UUID, uuid.UUID]]:
|
||||||
|
@ -18,6 +19,7 @@ def import_from_pocket(*,
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return UIImportsService().import_from_pocket(
|
return UIImportsService().import_from_pocket(
|
||||||
|
job=job,
|
||||||
account_uuid=account_uuid,
|
account_uuid=account_uuid,
|
||||||
csv_path=csv_path,
|
csv_path=csv_path,
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,14 +10,18 @@
|
||||||
|_|
|
|_|
|
||||||
production
|
production
|
||||||
-->
|
-->
|
||||||
<html lang="en" data-bs-theme="dark">
|
<html lang="en" data-bs-theme="{% if APPEARANCE_SETTINGS.light_mode %}light{% else %}dark{% endif %}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
|
||||||
<meta name="generator" content="pl.bthlabs.HotPocket.backend@{{ IMAGE_ID }}">
|
<meta name="generator" content="pl.bthlabs.HotPocket.backend@{{ IMAGE_ID }}">
|
||||||
<meta name="theme-color" content="#2b3035"/>
|
{% if APPEARANCE_SETTINGS.light_mode %}
|
||||||
|
<meta name="theme-color" content="#f8f9fa" />
|
||||||
|
{% else %}
|
||||||
|
<meta name="theme-color" content="#2b3035" />
|
||||||
|
{% endif %}
|
||||||
<title>{% block title %}{% translate 'Not Found' %}{% endblock %} | {{ SITE_TITLE }}</title>
|
<title>{% block title %}{% translate 'Not Found' %}{% endblock %} | {{ SITE_TITLE }}</title>
|
||||||
<link href="{% static 'ui/css/bootstrap.min.css' %}" rel="stylesheet">
|
<link href="{% static APPEARANCE_SETTINGS.theme_css %}" rel="stylesheet">
|
||||||
<link href="{% static 'ui/css/bootstrap-icons.min.css' %}" rel="stylesheet">
|
<link href="{% static 'ui/css/bootstrap-icons.min.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'ui/css/hotpocket-backend.css' %}" rel="stylesheet">
|
<link href="{% static 'ui/css/hotpocket-backend.css' %}" rel="stylesheet">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
|
|
@ -9,6 +9,10 @@ from hotpocket_backend.apps.core.conf import settings
|
||||||
|
|
||||||
|
|
||||||
def manifest_json(request: HttpRequest) -> JsonResponse:
|
def manifest_json(request: HttpRequest) -> JsonResponse:
|
||||||
|
light_theme = False
|
||||||
|
if request.user.is_anonymous is False:
|
||||||
|
light_theme = request.user.settings.get('light_theme', None) or False
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'name': settings.SITE_TITLE,
|
'name': settings.SITE_TITLE,
|
||||||
'short_name': settings.SITE_SHORT_TITLE,
|
'short_name': settings.SITE_SHORT_TITLE,
|
||||||
|
@ -16,8 +20,17 @@ def manifest_json(request: HttpRequest) -> JsonResponse:
|
||||||
reverse('ui.associations.browse'),
|
reverse('ui.associations.browse'),
|
||||||
),
|
),
|
||||||
'display': 'standalone',
|
'display': 'standalone',
|
||||||
'background_color': '#212529',
|
'background_color': (
|
||||||
'theme_color': '#2b3035',
|
'#212529'
|
||||||
|
if light_theme is False
|
||||||
|
else '#ffffff'
|
||||||
|
),
|
||||||
|
'theme_color': (
|
||||||
|
'#2b3035'
|
||||||
|
if light_theme is False
|
||||||
|
else
|
||||||
|
'#f8f9fa'
|
||||||
|
),
|
||||||
'icons': [
|
'icons': [
|
||||||
{
|
{
|
||||||
'src': request.build_absolute_uri(
|
'src': request.build_absolute_uri(
|
||||||
|
|
|
@ -67,6 +67,7 @@ TEMPLATES = [
|
||||||
'hotpocket_backend.apps.ui.context_processors.htmx',
|
'hotpocket_backend.apps.ui.context_processors.htmx',
|
||||||
'hotpocket_backend.apps.ui.context_processors.debug',
|
'hotpocket_backend.apps.ui.context_processors.debug',
|
||||||
'hotpocket_backend.apps.ui.context_processors.version',
|
'hotpocket_backend.apps.ui.context_processors.version',
|
||||||
|
'hotpocket_backend.apps.ui.context_processors.appearance_settings',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,7 @@ cat <<EOF
|
||||||
|_|
|
|_|
|
||||||
production
|
production
|
||||||
|
|
||||||
HotPocket v25.10.4 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
|
HotPocket v25.10.13 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
|
||||||
Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/)
|
Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/)
|
||||||
Licensed under Apache-2.0
|
Licensed under Apache-2.0
|
||||||
EOF
|
EOF
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hotpocket-backend",
|
"name": "hotpocket-backend",
|
||||||
"version": "25.10.4",
|
"version": "25.10.13",
|
||||||
"description": "HotPocket Backend",
|
"description": "HotPocket Backend",
|
||||||
"main": "hotpocket_backend/apps/frontend/src/index.js",
|
"main": "hotpocket_backend/apps/frontend/src/index.js",
|
||||||
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",
|
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hotpocket-backend"
|
name = "hotpocket-backend"
|
||||||
version = "25.10.4"
|
version = "25.10.13"
|
||||||
description = "HotPocket Backend"
|
description = "HotPocket Backend"
|
||||||
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
|
@ -76,6 +76,18 @@ def starred_association_out(starred_association):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def deleted_save_association(association_factory, deleted_save):
|
||||||
|
return association_factory(target=deleted_save)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def deleted_save_association_out(deleted_save_association):
|
||||||
|
return AssociationWithTargetOut.model_validate(
|
||||||
|
deleted_save_association, from_attributes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def other_account_association(association_factory, other_account):
|
def other_account_association(association_factory, other_account):
|
||||||
return association_factory(account=other_account)
|
return association_factory(account=other_account)
|
||||||
|
@ -124,6 +136,18 @@ def other_account_starred_association_out(other_account_starred_association):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def other_account_deleted_save_association(association_factory, other_account, deleted_save):
|
||||||
|
return association_factory(account=other_account, target=deleted_save)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def other_account_deleted_save_association_out(other_account_deleted_save_association):
|
||||||
|
return AssociationWithTargetOut.model_validate(
|
||||||
|
other_account_deleted_save_association, from_attributes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def browsable_associations(association,
|
def browsable_associations(association,
|
||||||
deleted_association,
|
deleted_association,
|
||||||
|
|
|
@ -64,11 +64,25 @@ def pocket_import_banned_netloc_save_spec():
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pocket_import_invalid_url_spec():
|
||||||
|
return PocketImportSaveSpec.model_validate({
|
||||||
|
'title': "This isn't right",
|
||||||
|
'url': 'thisisntright',
|
||||||
|
'time_added': datetime.datetime(
|
||||||
|
2021, 1, 18, 8, 0, 0, 0, tzinfo=datetime.UTC,
|
||||||
|
),
|
||||||
|
'tags': '',
|
||||||
|
'status': 'unread',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pocket_csv_content(pocket_import_created_save_spec,
|
def pocket_csv_content(pocket_import_created_save_spec,
|
||||||
pocket_import_reused_save_spec,
|
pocket_import_reused_save_spec,
|
||||||
pocket_import_other_account_save_spec,
|
pocket_import_other_account_save_spec,
|
||||||
pocket_import_banned_netloc_save_spec,
|
pocket_import_banned_netloc_save_spec,
|
||||||
|
pocket_import_invalid_url_spec,
|
||||||
):
|
):
|
||||||
with io.StringIO() as csv_f:
|
with io.StringIO() as csv_f:
|
||||||
field_names = [
|
field_names = [
|
||||||
|
@ -82,6 +96,7 @@ def pocket_csv_content(pocket_import_created_save_spec,
|
||||||
pocket_import_reused_save_spec.dict(),
|
pocket_import_reused_save_spec.dict(),
|
||||||
pocket_import_other_account_save_spec.dict(),
|
pocket_import_other_account_save_spec.dict(),
|
||||||
pocket_import_banned_netloc_save_spec.dict(),
|
pocket_import_banned_netloc_save_spec.dict(),
|
||||||
|
pocket_import_invalid_url_spec.dict(),
|
||||||
])
|
])
|
||||||
|
|
||||||
csv_f.seek(0)
|
csv_f.seek(0)
|
||||||
|
|
|
@ -29,6 +29,20 @@ class AssociationsTestingService:
|
||||||
if reference is not None:
|
if reference is not None:
|
||||||
assert association.updated_at > reference.updated_at
|
assert association.updated_at > reference.updated_at
|
||||||
|
|
||||||
|
def assert_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||||
|
association = Association.objects.get(pk=pk)
|
||||||
|
assert association.deleted_at is not None
|
||||||
|
|
||||||
|
if reference is not None:
|
||||||
|
assert association.updated_at > reference.updated_at
|
||||||
|
|
||||||
|
def assert_not_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||||
|
association = Association.objects.get(pk=pk)
|
||||||
|
assert association.deleted_at is None
|
||||||
|
|
||||||
|
if reference is not None:
|
||||||
|
assert association.updated_at == reference.updated_at
|
||||||
|
|
||||||
def assert_starred(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
def assert_starred(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||||
association = Association.objects.get(pk=pk)
|
association = Association.objects.get(pk=pk)
|
||||||
assert association.starred_at is not None
|
assert association.starred_at is not None
|
||||||
|
|
|
@ -45,10 +45,12 @@ def test_ok(account,
|
||||||
other_account_save_out,
|
other_account_save_out,
|
||||||
pocket_import_other_account_save_spec: PocketImportSaveSpec,
|
pocket_import_other_account_save_spec: PocketImportSaveSpec,
|
||||||
pocket_import_banned_netloc_save_spec: PocketImportSaveSpec,
|
pocket_import_banned_netloc_save_spec: PocketImportSaveSpec,
|
||||||
|
pocket_import_invalid_url_spec: PocketImportSaveSpec,
|
||||||
mock_saves_process_save_task_apply_async: mock.Mock,
|
mock_saves_process_save_task_apply_async: mock.Mock,
|
||||||
):
|
):
|
||||||
# When
|
# When
|
||||||
result = tasks_module.import_from_pocket(
|
result = tasks_module.import_from_pocket(
|
||||||
|
job='test',
|
||||||
account_uuid=account.pk,
|
account_uuid=account.pk,
|
||||||
csv_path=str(pocket_csv_file_path),
|
csv_path=str(pocket_csv_file_path),
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,7 @@ from hotpocket_backend_testing.services.accounts import AccountsTestingService
|
||||||
def payload():
|
def payload():
|
||||||
return {
|
return {
|
||||||
'theme': 'cosmo',
|
'theme': 'cosmo',
|
||||||
|
'light_mode': True,
|
||||||
'auto_load_embeds': 'True',
|
'auto_load_embeds': 'True',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +42,8 @@ def test_ok(authenticated_client: Client,
|
||||||
AccountsTestingService().assert_settings_edited(
|
AccountsTestingService().assert_settings_edited(
|
||||||
pk=account.pk,
|
pk=account.pk,
|
||||||
update={
|
update={
|
||||||
'theme': None, # TODO: Themes!
|
'theme': 'cosmo',
|
||||||
|
'light_mode': True,
|
||||||
'auto_load_embeds': True,
|
'auto_load_embeds': True,
|
||||||
},
|
},
|
||||||
reference=account,
|
reference=account,
|
||||||
|
|
|
@ -100,6 +100,54 @@ def test_invalid_all_empty(authenticated_client: Client,
|
||||||
assert 'canhazconfirm' in result.context['form'].errors
|
assert 'canhazconfirm' in result.context['form'].errors
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_archived(authenticated_client: Client,
|
||||||
|
archived_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.archive', args=(archived_association_out.pk,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deleted(authenticated_client: Client,
|
||||||
|
deleted_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.archive', args=(deleted_association_out.pk,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_found(authenticated_client: Client,
|
||||||
|
null_uuid,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.archive', args=(null_uuid,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_other_account_association(authenticated_client: Client,
|
def test_other_account_association(authenticated_client: Client,
|
||||||
other_account_association_out,
|
other_account_association_out,
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.urls import reverse
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_django import asserts
|
from pytest_django import asserts
|
||||||
|
|
||||||
from hotpocket_backend.apps.saves.models import Association
|
from hotpocket_backend_testing.services.saves import AssociationsTestingService
|
||||||
from hotpocket_common.constants import AssociationsSearchMode
|
from hotpocket_common.constants import AssociationsSearchMode
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,9 +35,10 @@ def test_ok(authenticated_client: Client,
|
||||||
fetch_redirect_response=False,
|
fetch_redirect_response=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
association_object = Association.objects.get(pk=association_out.pk)
|
AssociationsTestingService().assert_deleted(
|
||||||
assert association_object.updated_at > association_out.updated_at
|
pk=association_out.pk,
|
||||||
assert association_object.deleted_at is not None
|
reference=association_out,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -65,6 +66,34 @@ def test_ok_htmx(authenticated_client: Client,
|
||||||
assert result.json() == expected_payload
|
assert result.json() == expected_payload
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_ok_archived(authenticated_client: Client,
|
||||||
|
archived_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.delete', args=(archived_association_out.pk,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
asserts.assertRedirects(
|
||||||
|
result,
|
||||||
|
reverse(
|
||||||
|
'ui.associations.browse',
|
||||||
|
query=[('mode', AssociationsSearchMode.ARCHIVED.value)],
|
||||||
|
),
|
||||||
|
fetch_redirect_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
AssociationsTestingService().assert_deleted(
|
||||||
|
pk=archived_association_out.pk,
|
||||||
|
reference=archived_association_out,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_invalid_all_missing(authenticated_client: Client,
|
def test_invalid_all_missing(authenticated_client: Client,
|
||||||
association_out,
|
association_out,
|
||||||
|
@ -78,13 +107,13 @@ def test_invalid_all_missing(authenticated_client: Client,
|
||||||
|
|
||||||
# Then
|
# Then
|
||||||
assert result.status_code == http.HTTPStatus.OK
|
assert result.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
association_object = Association.objects.get(pk=association_out.pk)
|
|
||||||
assert association_object.updated_at == association_out.updated_at
|
|
||||||
assert association_object.deleted_at is None
|
|
||||||
|
|
||||||
assert 'canhazconfirm' in result.context['form'].errors
|
assert 'canhazconfirm' in result.context['form'].errors
|
||||||
|
|
||||||
|
AssociationsTestingService().assert_not_deleted(
|
||||||
|
pk=association_out.pk,
|
||||||
|
reference=association_out,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_invalid_all_empty(authenticated_client: Client,
|
def test_invalid_all_empty(authenticated_client: Client,
|
||||||
|
@ -100,13 +129,45 @@ def test_invalid_all_empty(authenticated_client: Client,
|
||||||
|
|
||||||
# Then
|
# Then
|
||||||
assert result.status_code == http.HTTPStatus.OK
|
assert result.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
association_object = Association.objects.get(pk=association_out.pk)
|
|
||||||
assert association_object.updated_at == association_out.updated_at
|
|
||||||
assert association_object.deleted_at is None
|
|
||||||
|
|
||||||
assert 'canhazconfirm' in result.context['form'].errors
|
assert 'canhazconfirm' in result.context['form'].errors
|
||||||
|
|
||||||
|
AssociationsTestingService().assert_not_deleted(
|
||||||
|
pk=association_out.pk,
|
||||||
|
reference=association_out,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deleted(authenticated_client: Client,
|
||||||
|
deleted_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.delete', args=(deleted_association_out.pk,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_found(authenticated_client: Client,
|
||||||
|
null_uuid,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.delete', args=(null_uuid,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_other_account_association(authenticated_client: Client,
|
def test_other_account_association(authenticated_client: Client,
|
||||||
|
|
|
@ -47,10 +47,10 @@ def test_ok(authenticated_client: Client,
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_invalid_all_empty(authenticated_client: Client,
|
def test_all_empty(authenticated_client: Client,
|
||||||
association_out,
|
association_out,
|
||||||
payload,
|
payload,
|
||||||
):
|
):
|
||||||
# Given
|
# Given
|
||||||
effective_payload = {
|
effective_payload = {
|
||||||
key: ''
|
key: ''
|
||||||
|
@ -79,9 +79,9 @@ def test_invalid_all_empty(authenticated_client: Client,
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_invalid_all_missing(authenticated_client: Client,
|
def test_all_missing(authenticated_client: Client,
|
||||||
association_out,
|
association_out,
|
||||||
):
|
):
|
||||||
# Given
|
# Given
|
||||||
effective_payload = {}
|
effective_payload = {}
|
||||||
|
|
||||||
|
@ -105,6 +105,45 @@ def test_invalid_all_missing(authenticated_client: Client,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_archived(authenticated_client: Client,
|
||||||
|
archived_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.edit', args=(archived_association_out.pk,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deleted(authenticated_client: Client,
|
||||||
|
deleted_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.edit', args=(deleted_association_out.pk,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_found(authenticated_client: Client,
|
||||||
|
null_uuid,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.edit', args=(null_uuid,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_other_account_association(authenticated_client: Client,
|
def test_other_account_association(authenticated_client: Client,
|
||||||
other_account_association_out,
|
other_account_association_out,
|
||||||
|
|
|
@ -128,6 +128,54 @@ def test_invalid_all_empty(authenticated_client: Client,
|
||||||
assert 'canhazconfirm' in result.context['form'].errors
|
assert 'canhazconfirm' in result.context['form'].errors
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_archived(authenticated_client: Client,
|
||||||
|
archived_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.refresh', args=(archived_association_out.pk,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deleted(authenticated_client: Client,
|
||||||
|
deleted_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.refresh', args=(deleted_association_out.pk,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_found(authenticated_client: Client,
|
||||||
|
null_uuid,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.associations.refresh', args=(null_uuid,)),
|
||||||
|
data={
|
||||||
|
'canhazconfirm': 'hai',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_other_account_association(authenticated_client: Client,
|
def test_other_account_association(authenticated_client: Client,
|
||||||
other_account_association_out,
|
other_account_association_out,
|
||||||
|
|
|
@ -54,6 +54,45 @@ def test_ok_htmx(authenticated_client: Client,
|
||||||
assert result.context['association'].target.pk == association_out.target.pk
|
assert result.context['association'].target.pk == association_out.target.pk
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_archived(authenticated_client: Client,
|
||||||
|
archived_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse('ui.associations.star', args=(archived_association_out.pk,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deleted(authenticated_client: Client,
|
||||||
|
deleted_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse('ui.associations.star', args=(deleted_association_out.pk,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_found(authenticated_client: Client,
|
||||||
|
null_uuid,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse('ui.associations.star', args=(null_uuid,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_other_account_association(authenticated_client: Client,
|
def test_other_account_association(authenticated_client: Client,
|
||||||
other_account_association_out,
|
other_account_association_out,
|
||||||
|
|
|
@ -54,6 +54,45 @@ def test_ok_htmx(authenticated_client: Client,
|
||||||
assert result.context['association'].target.pk == starred_association_out.target.pk
|
assert result.context['association'].target.pk == starred_association_out.target.pk
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_archived(authenticated_client: Client,
|
||||||
|
archived_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse('ui.associations.unstar', args=(archived_association_out.pk,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deleted(authenticated_client: Client,
|
||||||
|
deleted_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse('ui.associations.unstar', args=(deleted_association_out.pk,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_found(authenticated_client: Client,
|
||||||
|
null_uuid,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse('ui.associations.unstar', args=(null_uuid,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_other_account_association(authenticated_client: Client,
|
def test_other_account_association(authenticated_client: Client,
|
||||||
other_account_starred_association_out,
|
other_account_starred_association_out,
|
||||||
|
|
|
@ -66,6 +66,19 @@ def test_authenticated_deleted(authenticated_client: Client,
|
||||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_authenticated_deleted_save(authenticated_client: Client,
|
||||||
|
deleted_save_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse('ui.associations.view', args=(deleted_save_association_out.pk,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_authenticated_not_found(authenticated_client: Client,
|
def test_authenticated_not_found(authenticated_client: Client,
|
||||||
null_uuid,
|
null_uuid,
|
||||||
|
@ -169,6 +182,23 @@ def test_authenticated_share_deleted(authenticated_client: Client,
|
||||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_authenticated_share_deleted_save(authenticated_client: Client,
|
||||||
|
other_account_deleted_save_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.get(
|
||||||
|
reverse(
|
||||||
|
'ui.associations.view',
|
||||||
|
args=(other_account_deleted_save_association_out.pk,),
|
||||||
|
query=[('share', 'true')],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_authenticated_share_not_found(authenticated_client: Client,
|
def test_authenticated_share_not_found(authenticated_client: Client,
|
||||||
null_uuid,
|
null_uuid,
|
||||||
|
@ -240,6 +270,23 @@ def test_anonymous_share_deleted(client: Client,
|
||||||
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_anonymous_share_deleted_save(client: Client,
|
||||||
|
deleted_save_association_out,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = client.get(
|
||||||
|
reverse(
|
||||||
|
'ui.associations.view',
|
||||||
|
args=(deleted_save_association_out.pk,),
|
||||||
|
query=[('share', 'true')],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_anonymous_share_not_found(client: Client,
|
def test_anonymous_share_not_found(client: Client,
|
||||||
null_uuid,
|
null_uuid,
|
||||||
|
|
|
@ -76,6 +76,7 @@ def test_ok(override_settings_upload_path,
|
||||||
|
|
||||||
mock_ui_import_from_pocket_task_apply_async.assert_called_once_with(
|
mock_ui_import_from_pocket_task_apply_async.assert_called_once_with(
|
||||||
kwargs={
|
kwargs={
|
||||||
|
'job': mock.ANY,
|
||||||
'account_uuid': account.pk,
|
'account_uuid': account.pk,
|
||||||
'csv_path': str(uploaded_file_path),
|
'csv_path': str(uploaded_file_path),
|
||||||
},
|
},
|
||||||
|
|
|
@ -77,10 +77,11 @@ def test_auth_key_not_found(null_uuid,
|
||||||
|
|
||||||
call_result = result.json()
|
call_result = result.json()
|
||||||
assert 'error' in call_result
|
assert 'error' in call_result
|
||||||
assert call_result['error']['data'].startswith(
|
assert call_result['error']['code'] == -32001
|
||||||
|
assert call_result['error']['message'].startswith(
|
||||||
'Auth Key not found',
|
'Auth Key not found',
|
||||||
)
|
)
|
||||||
assert call_auth_key in call_result['error']['data']
|
assert call_auth_key in call_result['error']['message']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -108,10 +109,11 @@ def test_deleted_auth_key(deleted_auth_key_out,
|
||||||
|
|
||||||
call_result = result.json()
|
call_result = result.json()
|
||||||
assert 'error' in call_result
|
assert 'error' in call_result
|
||||||
assert call_result['error']['data'].startswith(
|
assert call_result['error']['code'] == -32001
|
||||||
|
assert call_result['error']['message'].startswith(
|
||||||
'Auth Key not found',
|
'Auth Key not found',
|
||||||
)
|
)
|
||||||
assert call_auth_key in call_result['error']['data']
|
assert call_auth_key in call_result['error']['message']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -139,10 +141,11 @@ def test_expired_auth_key(expired_auth_key_out,
|
||||||
|
|
||||||
call_result = result.json()
|
call_result = result.json()
|
||||||
assert 'error' in call_result
|
assert 'error' in call_result
|
||||||
assert call_result['error']['data'].startswith(
|
assert call_result['error']['code'] == -32000
|
||||||
|
assert call_result['error']['message'].startswith(
|
||||||
'Auth Key expired',
|
'Auth Key expired',
|
||||||
)
|
)
|
||||||
assert call_auth_key in call_result['error']['data']
|
assert call_auth_key in call_result['error']['message']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -170,10 +173,11 @@ def test_consumed_auth_key(consumed_auth_key,
|
||||||
|
|
||||||
call_result = result.json()
|
call_result = result.json()
|
||||||
assert 'error' in call_result
|
assert 'error' in call_result
|
||||||
assert call_result['error']['data'].startswith(
|
assert call_result['error']['code'] == -32000
|
||||||
|
assert call_result['error']['message'].startswith(
|
||||||
'Auth Key already consumed',
|
'Auth Key already consumed',
|
||||||
)
|
)
|
||||||
assert call_auth_key in call_result['error']['data']
|
assert call_auth_key in call_result['error']['message']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -201,4 +205,5 @@ def test_inactive_account(inactive_account_auth_key,
|
||||||
|
|
||||||
call_result = result.json()
|
call_result = result.json()
|
||||||
assert 'error' in call_result
|
assert 'error' in call_result
|
||||||
assert str(inactive_account.pk) in call_result['error']['data']
|
assert call_result['error']['code'] == -32001
|
||||||
|
assert str(inactive_account.pk) in call_result['error']['message']
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
run:
|
|
||||||
echo: true
|
|
||||||
pty: true
|
|
|
@ -1,14 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from invoke import task
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def publish(ctx):
|
|
||||||
ctx.run((
|
|
||||||
'rsync '
|
|
||||||
'-rv '
|
|
||||||
'webroot/ '
|
|
||||||
'snakeweb.net:/srv/sites/bilbo/hotpocket.app/dotcom/'
|
|
||||||
))
|
|
|
@ -1,26 +0,0 @@
|
||||||
BTHLabsHotPocketBot
|
|
||||||
===================
|
|
||||||
|
|
||||||
BTHLabsHotPocketBot is the metadata bot software that BTHLabs HotPocket uses to
|
|
||||||
discover and collect metadata of links saved by its users.
|
|
||||||
|
|
||||||
BTHLabsHotPocketBot collects the following data from HTML documents:
|
|
||||||
|
|
||||||
* Page title extracted from ``meta`` and ``title`` tags.
|
|
||||||
* Description extract from ``meta`` tags.
|
|
||||||
|
|
||||||
Metadata collected by BTHLabsHotPocketBot is stored in HotPocket database and
|
|
||||||
displayed to users who saved the link.
|
|
||||||
|
|
||||||
How BTHLabsHotPocketBot accesses your site
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
BTHLabsHotPocketBot makes an HTTP request to the site when the link is saved
|
|
||||||
or a user requests metadata refresh through the UI. The response content is
|
|
||||||
then processed. The response content **is not** stored in any database.
|
|
||||||
|
|
||||||
Getting Support
|
|
||||||
----------------
|
|
||||||
|
|
||||||
If you have any questions about BTHLabsHotPocketBot, please contact us at
|
|
||||||
contact@bthlabs.pl and we will respond as soon as possible.
|
|
|
@ -1,12 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>hotpocket.app</title>
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>SOON</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>hotpocket.app</title>
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>SOON</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -8,12 +8,7 @@ ARG APP_USER_UID
|
||||||
ARG APP_USER_GID
|
ARG APP_USER_GID
|
||||||
ARG IMAGE_ID
|
ARG IMAGE_ID
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
# COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ops/bin/*.sh /srv/bin/
|
# COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ops/bin/*.sh /srv/bin/
|
||||||
RUN chown -R ${APP_USER_UID}:${APP_USER_GID} /srv
|
|
||||||
|
|
||||||
USER app
|
|
||||||
|
|
||||||
VOLUME ["/srv/node_modules", "/srv/venv"]
|
VOLUME ["/srv/node_modules", "/srv/venv"]
|
||||||
|
|
||||||
|
@ -22,5 +17,3 @@ FROM development AS ci
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ /srv/app/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID extension/ /srv/app/
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/packages/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/packages/
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
|
||||||
|
|
||||||
RUN chown -R $APP_USER_UID:$APP_USER_GID /srv
|
|
||||||
|
|
0
services/extension/ops/bin/.placeholder
Normal file
0
services/extension/ops/bin/.placeholder
Normal file
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hotpocket-extension",
|
"name": "hotpocket-extension",
|
||||||
"version": "25.10.4",
|
"version": "25.10.13",
|
||||||
"description": "HotPocket Extension",
|
"description": "HotPocket Extension",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",
|
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hotpocket-extension"
|
name = "hotpocket-extension"
|
||||||
version = "25.10.4"
|
version = "25.10.13"
|
||||||
description = "HotPocket Extension"
|
description = "HotPocket Extension"
|
||||||
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
|
@ -164,7 +164,7 @@ const doHandleAuthFlow = (authTab) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedSessionTabQuery = `?authSessionToken=${authSessionToken}`;
|
const expectedSessionTabQuery = `?authSessionToken=${authSessionToken}`;
|
||||||
if (tabId !== currentAuthTabId && changedURL.includes(expectedSessionTabQuery)) {
|
if (tabId !== currentAuthTabId && changedURL && changedURL.includes(expectedSessionTabQuery)) {
|
||||||
// When redirecting from the preauth page to the HotPocket instance,
|
// When redirecting from the preauth page to the HotPocket instance,
|
||||||
// Safari "replaces" the auth tab with a new one. This nasty hack will
|
// Safari "replaces" the auth tab with a new one. This nasty hack will
|
||||||
// allow the extension to keep track of it.
|
// allow the extension to keep track of it.
|
||||||
|
@ -268,7 +268,7 @@ const doSetupRPC = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const doSendTabMessage = (tab, message) => {
|
const doSendTabMessage = (tab, message) => {
|
||||||
HotPocketExtension.api.tabs.sendMessage(tab.id, message).
|
return HotPocketExtension.api.tabs.sendMessage(tab.id, message).
|
||||||
then((result) => {
|
then((result) => {
|
||||||
HotPocketExtension.LOGGER.debug(
|
HotPocketExtension.LOGGER.debug(
|
||||||
'HotPocketExtension.background.doSendTabMessage(): message sent',
|
'HotPocketExtension.background.doSendTabMessage(): message sent',
|
||||||
|
@ -327,6 +327,10 @@ const onBrowserActionClicked = async (tab) => {
|
||||||
let result = false;
|
let result = false;
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
await doSendTabMessage(tab, {
|
||||||
|
type: 'HotPocket:Extension:browserActionClicked',
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let accessToken = await doSetupRPC();
|
let accessToken = await doSetupRPC();
|
||||||
|
|
||||||
|
@ -348,7 +352,7 @@ const onBrowserActionClicked = async (tab) => {
|
||||||
error: error,
|
error: error,
|
||||||
};
|
};
|
||||||
|
|
||||||
doSendTabMessage(tab, message);
|
return await doSendTabMessage(tab, message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMessage = (message, sender, sendResponse) => {
|
const onMessage = (message, sender, sendResponse) => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import HotPocketExtension from '../common';
|
import HotPocketExtension from '../common';
|
||||||
|
|
||||||
import POPUP from './templates/popup.html';
|
import POPUP from './templates/popup.html';
|
||||||
|
import POPUP_CONTENT_SAVING from './templates/popup_content_saving.html';
|
||||||
import POPUP_CONTENT_SUCCESS from './templates/popup_content_success.html';
|
import POPUP_CONTENT_SUCCESS from './templates/popup_content_success.html';
|
||||||
import POPUP_CONTENT_ERROR from './templates/popup_content_error.html';
|
import POPUP_CONTENT_ERROR from './templates/popup_content_error.html';
|
||||||
|
|
||||||
|
@ -18,6 +19,19 @@ class Popup {
|
||||||
this.timeout = null;
|
this.timeout = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
setContent = (content) => {
|
||||||
|
const shadow = this.container.shadowRoot;
|
||||||
|
|
||||||
|
const body = shadow.querySelector('.hotpocket-extension-popup-body');
|
||||||
|
body.innerHTML = content;
|
||||||
|
|
||||||
|
const i18nElements = shadow.querySelectorAll('[data-message]');
|
||||||
|
for (let i18nElement of i18nElements) {
|
||||||
|
i18nElement.innerHTML = HotPocketExtension.api.i18n.getMessage(
|
||||||
|
i18nElement.dataset.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
close = () => {
|
close = () => {
|
||||||
this.clearCloseTimeout();
|
this.clearCloseTimeout();
|
||||||
|
|
||||||
|
@ -36,15 +50,7 @@ class Popup {
|
||||||
const shadow = this.container.attachShadow({mode: 'open'});
|
const shadow = this.container.attachShadow({mode: 'open'});
|
||||||
shadow.innerHTML = POPUP;
|
shadow.innerHTML = POPUP;
|
||||||
|
|
||||||
const body = shadow.querySelector('.hotpocket-extension-popup-body');
|
this.setContent(content);
|
||||||
body.innerHTML = content;
|
|
||||||
|
|
||||||
const i18nElements = shadow.querySelectorAll('[data-message]');
|
|
||||||
for (let i18nElement of i18nElements) {
|
|
||||||
i18nElement.innerHTML = HotPocketExtension.api.i18n.getMessage(
|
|
||||||
i18nElement.dataset.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close');
|
const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close');
|
||||||
for (const closeElement of closeElements) {
|
for (const closeElement of closeElements) {
|
||||||
|
@ -53,6 +59,9 @@ class Popup {
|
||||||
|
|
||||||
document.body.appendChild(this.container);
|
document.body.appendChild(this.container);
|
||||||
};
|
};
|
||||||
|
update = (content) => {
|
||||||
|
this.setContent(content);
|
||||||
|
};
|
||||||
onCloseClick = (event) => {
|
onCloseClick = (event) => {
|
||||||
this.close();
|
this.close();
|
||||||
};
|
};
|
||||||
|
@ -60,15 +69,27 @@ class Popup {
|
||||||
|
|
||||||
let currentPopup = null;
|
let currentPopup = null;
|
||||||
|
|
||||||
const doHandleSaveMessage = (message) => {
|
const doHandleBrowserActionClickedMessage = (message) => {
|
||||||
if (currentPopup !== null) {
|
if (currentPopup !== null) {
|
||||||
currentPopup.close();
|
currentPopup.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPopup = new Popup();
|
currentPopup = new Popup();
|
||||||
currentPopup.show(
|
currentPopup.show(POPUP_CONTENT_SAVING);
|
||||||
(message.result === true) ? POPUP_CONTENT_SUCCESS : POPUP_CONTENT_ERROR,
|
};
|
||||||
);
|
|
||||||
|
const doHandleSaveMessage = (message) => {
|
||||||
|
let content = POPUP_CONTENT_ERROR;
|
||||||
|
if (message.result === true) {
|
||||||
|
content = POPUP_CONTENT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPopup === null) {
|
||||||
|
currentPopup = new Popup();
|
||||||
|
currentPopup.show(content);
|
||||||
|
} else {
|
||||||
|
currentPopup.update(content);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const doSendMessage = (message) => {
|
const doSendMessage = (message) => {
|
||||||
|
@ -93,7 +114,9 @@ export default ({...configuration}) => {
|
||||||
|
|
||||||
let response = {ok: true};
|
let response = {ok: true};
|
||||||
try {
|
try {
|
||||||
if (message.type === 'HotPocket:Extension:save') {
|
if (message.type === 'HotPocket:Extension:browserActionClicked') {
|
||||||
|
doHandleBrowserActionClickedMessage(message);
|
||||||
|
} else if (message.type === 'HotPocket:Extension:save') {
|
||||||
doHandleSaveMessage(message);
|
doHandleSaveMessage(message);
|
||||||
}
|
}
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
|
||||||
<meta name="generator" content="pl.bthlabs.HotPocket.Extension@v25.10.4">
|
<meta name="generator" content="pl.bthlabs.HotPocket.Extension@v25.10.13">
|
||||||
<meta name="theme-color" content="#2b3035"/>
|
<meta name="theme-color" content="#2b3035"/>
|
||||||
<title>HotPocket by BTHLabs</title>
|
<title>HotPocket by BTHLabs</title>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAAH/0lEQVRYCbVXWWxcVxn+zp07i2e8p3Y8WRw3dtI4sZSmxC52ixBLIG5LS0JfqEQKvMETNJEqKlEhkJAQLUtf2QLKA6CkoVKa1CGtWlUNkFRpQ9PGJM7i2MR27LHH9tiz3nv5/v/O2OOO+wLlyPf6rP/3/eu5Y1DWuh9+vA8uDsGgj9NNfKyy5f+l6/LwJDycpcTnzr989GxJmGHHdPT3h+pN9MfGw3e8jw+0hLHiPwFdz+AXSW/xmaFTp3K2rAo42T3l8SV/0rTvdz+Wt6FZpRHc4uspYsrwkPnE3n0PGmPe8DzPerfnh7ALGVn4v7WCHcG9554FMV1iftomm4PU1vJcj+BpdJ77EWkWzfDf0pDzxtd4hQjOXe75PgSLdhBLHLSNhV6Z8DxXre85DnLhWsyv6dKztdOXEczOIFW3BdmoxKXfjOegcfycumw63sOzgeKKh5BJI1QbhJdMYsH4Z2qmLyGUnfMxiGU8C8YyvbQAmoQwzaEChMh84w7M3rUT1cmrcA3DhEtOIAzHVr/pPiEgoS3NsavYD3CbB6u9HbnuXhQWZhH8659RyFZhoX6rrq25/ZbuFyw1ErElCJlqykAX5SVUqpNX0HTrjPiK5gTqEpcAPgJSCijdS0l1+WHkdvTCTI8j09OHyMDvEX77NM9aqBJPtMnOsqbKqsKWLQI/3GqnP2BUBBVchEhzaxqQX9uK0NC7HPln3Eg1vGAY8weehX31ApwdDwABG5kvHoDTtBGxV37LvQZ1d96hyR0Rs6KpxVbM6MDQ50mEMwnVgC8NqLmvPg07OVncbpBftwXp+x9CvnUbrNkEqv74U4QGDiMwehWRYy8gv3sPnLVtqkQ4M0X/J3lWzLGyaR0onxKTZ2JxeHYYsdQIlwzcUAiFxriCBSIx2CNX4DRvQnbXZ2EW5xFIjKqI0MU3II+cyT40C7exGWZqBOm6TbAKeUQWx8uhtL9qqV2o34xUw9biZvo8m4E9+i+kvvAk5p74HhxxwfwUPJJxG1tgJm+rbuIufSxmWFUMJp1SGamGexiImyvAZWJVAuU7xduSGdGTvwOohUeQ7KYu5Dp2LW0L3PzABw4EYPHx6u9iLAQRmJ9Z2vNRnQoCflD6QaZvTRmm151hVA38gdY1SD/xNPI0vzRDkCAD0FBri4+xAnDbd8KaS8CamfBxVQtRxJfrT/rvCgIa4Nynm4sH3Jo1yN3TjcA/TsKaGC4/j+CF1zimmywmpxKg/+/bg9Dgec5XBt2KwxxUBKFsiCyMwQ1EdK8QSfd9CU7jOgQnRxmcIZ2Xl2FWhN487puflpEAznY9yADdgOqjz+tY9kXmRxFwVr9jKgmQdGz2hppazC3aea6LQtt2FL79M5HnNxILHvslTD4No6SYmuu3IrX3m4ieOQIrNesToIjamUE9w5uwolW4QKqcE4zqI/VSLBC8/s+Kg/bpI7CvXyIIRdD86b5HNUMibw8gcuFVBVeXcN0JxVReeQUtCay0AFeSTbvgsr43jZzxb67ZqdJ+IJ+DfeLXCJ47BYQiKHTej9wDj8JpaWPlO4zwxdd98KJL5OB0vA9WfhGlu2BZ2EfEAOsmXIv3lFrAZZ1niWUqBhhY1tkT9FEt8o99C05XLxCOIvj+WcRefAGBVHIZnAHJgXiQ8RRkISqHXe5XWEDSMDZzDcM7vo4Cb79GZwh5BqFo7mzrhkONwQIj5o+cOozQtXdgOYUisBQiPxsE3KMSI+37eZV3oO29X0k0LSMXe7b4pTTtWiHMN3SihpfRlvM/oSvuhZUZh3X6GOyZEZi5CZiZOyww08x5Fh3b9nNfKyAlicbU3LXCWKzZgChLeSx5DesH/0QzFHjNb+daWRYRO7Cuo/MHtLSyq04MYuLufsy09KCKqRObHoThvT5nr8dEfA9s1vxoYkirncVbzwpI4Vl+OIHplk9ieNsB2Pl5VM/wYkr9G5loHLe2fw2LdZvRevkIglwrBajZvXc/Xe3C5ZeQy3STlJtr7MR4+2M8PIrmK3+BnU4gF2nA5NZ9ek23vvcbFeBXPt/XkmE3O59k+BQQv/EygpkZ5EJ1GLv7YaSr1yN+7SXUJC4vnZOSrffG7v6vEN8Hdl1eMySjn2gUKNpMtu1F7fh5NN14hb5mMaHmhjflra5voGV4APyYxMTGPdg4dBTGYU2gjAJdcKf180g234fmm6fROPY3BiSrpcQH3SXuW7IcCQiqpVYQIhL5/M+OknH4YTLZ+jkk471Yc+tVTSV6GYu8LW+3f1kvp/jNk6hNvK/9Kabc5IbPoGH872gafo0Ec/T0cnwICbWc1A9+GZvu/v3j5LbWB/RBlz5Si9aQCMkHaxgfj2ChsQMt10+gfvKin2bFaJ5t2klzP6J+X0sXSAwIMEGWNNf7ohiwOg9vggQef5EA+/xMEAISkWKFIhm1RHGO/WysBWOMj0KwGuuGeJTtdsd+AqYQv/4SwgvjHwIWAhKoy2TISqixmeNGfw96eJMjfkUQVOaLoKVCVD4u9RfqOzDWsU92Iz50nOk2pJqqZkWt/X6xNnBOx3JA+kxMsviU9kjiOSIflFlpagW/o/1KIn6s+Gy5UWWLVuxIWiqB0n9dFGk6rx0d4Hn+SD2klXAxap6JLoqZ8V2uWVpQxAoqiAd5jXn6+MHJBUY7SSh9xS8D5tqSn30k3eZrLROi+c8F01+Vd7Gt+vO85BayW4qPoosqLCAgSlog1R465oDMV/95/h8tMX6C+Qt1JgAAAABJRU5ErkJggg==">
|
<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAAH/0lEQVRYCbVXWWxcVxn+zp07i2e8p3Y8WRw3dtI4sZSmxC52ixBLIG5LS0JfqEQKvMETNJEqKlEhkJAQLUtf2QLKA6CkoVKa1CGtWlUNkFRpQ9PGJM7i2MR27LHH9tiz3nv5/v/O2OOO+wLlyPf6rP/3/eu5Y1DWuh9+vA8uDsGgj9NNfKyy5f+l6/LwJDycpcTnzr989GxJmGHHdPT3h+pN9MfGw3e8jw+0hLHiPwFdz+AXSW/xmaFTp3K2rAo42T3l8SV/0rTvdz+Wt6FZpRHc4uspYsrwkPnE3n0PGmPe8DzPerfnh7ALGVn4v7WCHcG9554FMV1iftomm4PU1vJcj+BpdJ77EWkWzfDf0pDzxtd4hQjOXe75PgSLdhBLHLSNhV6Z8DxXre85DnLhWsyv6dKztdOXEczOIFW3BdmoxKXfjOegcfycumw63sOzgeKKh5BJI1QbhJdMYsH4Z2qmLyGUnfMxiGU8C8YyvbQAmoQwzaEChMh84w7M3rUT1cmrcA3DhEtOIAzHVr/pPiEgoS3NsavYD3CbB6u9HbnuXhQWZhH8659RyFZhoX6rrq25/ZbuFyw1ErElCJlqykAX5SVUqpNX0HTrjPiK5gTqEpcAPgJSCijdS0l1+WHkdvTCTI8j09OHyMDvEX77NM9aqBJPtMnOsqbKqsKWLQI/3GqnP2BUBBVchEhzaxqQX9uK0NC7HPln3Eg1vGAY8weehX31ApwdDwABG5kvHoDTtBGxV37LvQZ1d96hyR0Rs6KpxVbM6MDQ50mEMwnVgC8NqLmvPg07OVncbpBftwXp+x9CvnUbrNkEqv74U4QGDiMwehWRYy8gv3sPnLVtqkQ4M0X/J3lWzLGyaR0onxKTZ2JxeHYYsdQIlwzcUAiFxriCBSIx2CNX4DRvQnbXZ2EW5xFIjKqI0MU3II+cyT40C7exGWZqBOm6TbAKeUQWx8uhtL9qqV2o34xUw9biZvo8m4E9+i+kvvAk5p74HhxxwfwUPJJxG1tgJm+rbuIufSxmWFUMJp1SGamGexiImyvAZWJVAuU7xduSGdGTvwOohUeQ7KYu5Dp2LW0L3PzABw4EYPHx6u9iLAQRmJ9Z2vNRnQoCflD6QaZvTRmm151hVA38gdY1SD/xNPI0vzRDkCAD0FBri4+xAnDbd8KaS8CamfBxVQtRxJfrT/rvCgIa4Nynm4sH3Jo1yN3TjcA/TsKaGC4/j+CF1zimmywmpxKg/+/bg9Dgec5XBt2KwxxUBKFsiCyMwQ1EdK8QSfd9CU7jOgQnRxmcIZ2Xl2FWhN487puflpEAznY9yADdgOqjz+tY9kXmRxFwVr9jKgmQdGz2hppazC3aea6LQtt2FL79M5HnNxILHvslTD4No6SYmuu3IrX3m4ieOQIrNesToIjamUE9w5uwolW4QKqcE4zqI/VSLBC8/s+Kg/bpI7CvXyIIRdD86b5HNUMibw8gcuFVBVeXcN0JxVReeQUtCay0AFeSTbvgsr43jZzxb67ZqdJ+IJ+DfeLXCJ47BYQiKHTej9wDj8JpaWPlO4zwxdd98KJL5OB0vA9WfhGlu2BZ2EfEAOsmXIv3lFrAZZ1niWUqBhhY1tkT9FEt8o99C05XLxCOIvj+WcRefAGBVHIZnAHJgXiQ8RRkISqHXe5XWEDSMDZzDcM7vo4Cb79GZwh5BqFo7mzrhkONwQIj5o+cOozQtXdgOYUisBQiPxsE3KMSI+37eZV3oO29X0k0LSMXe7b4pTTtWiHMN3SihpfRlvM/oSvuhZUZh3X6GOyZEZi5CZiZOyww08x5Fh3b9nNfKyAlicbU3LXCWKzZgChLeSx5DesH/0QzFHjNb+daWRYRO7Cuo/MHtLSyq04MYuLufsy09KCKqRObHoThvT5nr8dEfA9s1vxoYkirncVbzwpI4Vl+OIHplk9ieNsB2Pl5VM/wYkr9G5loHLe2fw2LdZvRevkIglwrBajZvXc/Xe3C5ZeQy3STlJtr7MR4+2M8PIrmK3+BnU4gF2nA5NZ9ek23vvcbFeBXPt/XkmE3O59k+BQQv/EygpkZ5EJ1GLv7YaSr1yN+7SXUJC4vnZOSrffG7v6vEN8Hdl1eMySjn2gUKNpMtu1F7fh5NN14hb5mMaHmhjflra5voGV4APyYxMTGPdg4dBTGYU2gjAJdcKf180g234fmm6fROPY3BiSrpcQH3SXuW7IcCQiqpVYQIhL5/M+OknH4YTLZ+jkk471Yc+tVTSV6GYu8LW+3f1kvp/jNk6hNvK/9Kabc5IbPoGH872gafo0Ec/T0cnwICbWc1A9+GZvu/v3j5LbWB/RBlz5Si9aQCMkHaxgfj2ChsQMt10+gfvKin2bFaJ5t2klzP6J+X0sXSAwIMEGWNNf7ohiwOg9vggQef5EA+/xMEAISkWKFIhm1RHGO/WysBWOMj0KwGuuGeJTtdsd+AqYQv/4SwgvjHwIWAhKoy2TISqixmeNGfw96eJMjfkUQVOaLoKVCVD4u9RfqOzDWsU92Iz50nOk2pJqqZkWt/X6xNnBOx3JA+kxMsviU9kjiOSIflFlpagW/o/1KIn6s+Gy5UWWLVuxIWiqB0n9dFGk6rx0d4Hn+SD2klXAxap6JLoqZ8V2uWVpQxAoqiAd5jXn6+MHJBUY7SSh9xS8D5tqSn30k3eZrLROi+c8F01+Vd7Gt+vO85BayW4qPoosqLCAgSlog1R465oDMV/95/h8tMX6C+Qt1JgAAAABJRU5ErkJggg==">
|
||||||
|
@ -79,7 +79,7 @@ body, html {
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-0 mt-2 text-center text-muted ui-uname">
|
<p class="mb-0 mt-2 text-center text-muted ui-uname">
|
||||||
<span>
|
<span>
|
||||||
<a href="https://hotpocket.app/" target="_blank" rel="noopener noreferer">HotPocket by BTHLabs</a> v25.10.4
|
<a href="https://hotpocket.app/" target="_blank" rel="noopener noreferer">HotPocket by BTHLabs</a> v25.10.13
|
||||||
</span>
|
</span>
|
||||||
<br>
|
<br>
|
||||||
<span>Copyright © 2025-present by BTHLabs. All rights reserved.</span>
|
<span>Copyright © 2025-present by BTHLabs. All rights reserved.</span>
|
||||||
|
|
|
@ -58,6 +58,9 @@
|
||||||
.hotpocket-extension-popup .hotpocket-extension-popup-body > * {
|
.hotpocket-extension-popup .hotpocket-extension-popup-body > * {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
.hotpocket-extension-popup .hotpocket-extension-popup-body > .hotpocket-extension-popup-loader {
|
||||||
|
margin: 0px auto;
|
||||||
|
}
|
||||||
.hotpocket-extension-popup .hotpocket-extension-popup-body strong {
|
.hotpocket-extension-popup .hotpocket-extension-popup-body strong {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
@ -67,6 +70,33 @@
|
||||||
.hotpocket-extension-popup .hotpocket-extension-popup-message-error {
|
.hotpocket-extension-popup .hotpocket-extension-popup-message-error {
|
||||||
color: #EE6476;
|
color: #EE6476;
|
||||||
}
|
}
|
||||||
|
.hotpocket-extension-popup-loader {
|
||||||
|
animation: hotpocket-extension-popup-loader-animation 1s infinite steps(12);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background:
|
||||||
|
linear-gradient(0deg ,rgb(240 240 240/50%) 30%,#0000 0 70%,rgb(240 240 240/100%) 0) 50%/8% 100%,
|
||||||
|
linear-gradient(90deg,rgb(240 240 240/25%) 30%,#0000 0 70%,rgb(240 240 240/75% ) 0) 50%/100% 8%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: grid;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
.hotpocket-extension-popup-loader::before,
|
||||||
|
.hotpocket-extension-popup-loader::after {
|
||||||
|
background: inherit;
|
||||||
|
border-radius: 50%;
|
||||||
|
content: "";
|
||||||
|
grid-area: 1/1;
|
||||||
|
opacity: 0.915;
|
||||||
|
transform: rotate(30deg);
|
||||||
|
}
|
||||||
|
.hotpocket-extension-popup-loader::after {
|
||||||
|
opacity: 0.83;
|
||||||
|
transform: rotate(60deg);
|
||||||
|
}
|
||||||
|
@keyframes hotpocket-extension-popup-loader-animation {
|
||||||
|
100% {transform: rotate(1turn)}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="hotpocket-extension-popup">
|
<div class="hotpocket-extension-popup">
|
||||||
<div class="hotpocket-extension-popup-header">
|
<div class="hotpocket-extension-popup-header">
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<div class="hotpocket-extension-popup-loader"></div>
|
|
@ -3,7 +3,7 @@
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"name": "__MSG_extension_name__",
|
"name": "__MSG_extension_name__",
|
||||||
"description": "__MSG_extension_description__",
|
"description": "__MSG_extension_description__",
|
||||||
"version": "25.10.4",
|
"version": "25.10.13",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "images/icon-16.png",
|
"16": "images/icon-16.png",
|
||||||
"32": "images/icon-32.png",
|
"32": "images/icon-32.png",
|
||||||
|
|
|
@ -8,12 +8,7 @@ ARG APP_USER_UID
|
||||||
ARG APP_USER_GID
|
ARG APP_USER_GID
|
||||||
ARG IMAGE_ID
|
ARG IMAGE_ID
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
# COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ops/bin/*.sh /srv/bin/
|
# COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ops/bin/*.sh /srv/bin/
|
||||||
RUN chown -R ${APP_USER_UID}:${APP_USER_GID} /srv
|
|
||||||
|
|
||||||
USER app
|
|
||||||
|
|
||||||
VOLUME ["/srv/node_modules", "/srv/venv"]
|
VOLUME ["/srv/node_modules", "/srv/venv"]
|
||||||
|
|
||||||
|
@ -21,5 +16,3 @@ FROM development AS ci
|
||||||
|
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/app/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/app/
|
||||||
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
|
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/
|
||||||
|
|
||||||
RUN chown -R $APP_USER_UID:$APP_USER_GID /srv
|
|
||||||
|
|
0
services/packages/ops/bin/.placeholder
Normal file
0
services/packages/ops/bin/.placeholder
Normal file
11
services/packages/soa/hotpocket_soa/constants.py
Normal file
11
services/packages/soa/hotpocket_soa/constants.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class BackendServiceErrorCode(enum.Enum):
|
||||||
|
INTERNAL = -32000
|
||||||
|
NOT_FOUND = -32001
|
||||||
|
ACCESS_DENIED = -32002
|
||||||
|
INVALID = -32003
|
89
services/packages/soa/hotpocket_soa/exceptions/backend.py
Normal file
89
services/packages/soa/hotpocket_soa/exceptions/backend.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from hotpocket_soa.constants import BackendServiceErrorCode
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
VALIDATION_CODE_INVALID = 'invalid'
|
||||||
|
|
||||||
|
|
||||||
|
def get_validation_error_data(validation_error: typing.Any) -> typing.Any: # Heh
|
||||||
|
if hasattr(validation_error, 'error_dict') is True:
|
||||||
|
return {
|
||||||
|
field: [
|
||||||
|
get_validation_error_data(inner_error)
|
||||||
|
for inner_error
|
||||||
|
in inner_errors
|
||||||
|
]
|
||||||
|
for field, inner_errors in validation_error.error_dict.items()
|
||||||
|
}
|
||||||
|
elif hasattr(validation_error, 'error_list') is True and len(validation_error.error_list) > 1:
|
||||||
|
return [
|
||||||
|
get_validation_error_data(inner_error)
|
||||||
|
for inner_error
|
||||||
|
in validation_error.error_list
|
||||||
|
]
|
||||||
|
elif hasattr(validation_error, 'code') is True:
|
||||||
|
return validation_error.code
|
||||||
|
elif isinstance(validation_error, (tuple, list)) is True:
|
||||||
|
return [
|
||||||
|
get_validation_error_data(inner_error)
|
||||||
|
for inner_error
|
||||||
|
in validation_error
|
||||||
|
]
|
||||||
|
elif isinstance(validation_error, dict) is True:
|
||||||
|
return {
|
||||||
|
field: [
|
||||||
|
get_validation_error_data(inner_error)
|
||||||
|
for inner_error
|
||||||
|
in inner_errors
|
||||||
|
]
|
||||||
|
for field, inner_errors in validation_error.items()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return VALIDATION_CODE_INVALID
|
||||||
|
|
||||||
|
|
||||||
|
class BackendServiceError(Exception):
|
||||||
|
CODE = BackendServiceErrorCode.INTERNAL
|
||||||
|
|
||||||
|
def __init__(self, message: str, *args):
|
||||||
|
super().__init__(message, *args)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
self.data: typing.Any = None
|
||||||
|
if len(args) > 0:
|
||||||
|
self.data = args[0]
|
||||||
|
|
||||||
|
|
||||||
|
class InternalError(BackendServiceError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(BackendServiceError):
|
||||||
|
CODE = BackendServiceErrorCode.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
class AccessDenied(BackendServiceError):
|
||||||
|
CODE = BackendServiceErrorCode.ACCESS_DENIED
|
||||||
|
|
||||||
|
|
||||||
|
class Invalid(BackendServiceError):
|
||||||
|
CODE = BackendServiceErrorCode.INVALID
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_django_validation_error(cls: type[typing.Self],
|
||||||
|
exception: ValidationError,
|
||||||
|
message: str | None = None,
|
||||||
|
) -> typing.Self:
|
||||||
|
data = get_validation_error_data(exception)
|
||||||
|
|
||||||
|
result = cls(message or 'Invalid', data)
|
||||||
|
result.__cause__ = exception
|
||||||
|
|
||||||
|
return result
|
28
services/packages/soa/hotpocket_soa/exceptions/frontend.py
Normal file
28
services/packages/soa/hotpocket_soa/exceptions/frontend.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from .backend import BackendServiceError
|
||||||
|
|
||||||
|
|
||||||
|
class SOAError(Exception):
|
||||||
|
def __init__(self, code: int, message: str, *args):
|
||||||
|
super().__init__(code, message, *args)
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
self.data: typing.Any = None
|
||||||
|
if len(args) > 0:
|
||||||
|
self.data = args[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_backend_error(cls: type[typing.Self], exception: BackendServiceError) -> typing.Self:
|
||||||
|
result = cls(
|
||||||
|
exception.CODE.value,
|
||||||
|
exception.message,
|
||||||
|
exception.data,
|
||||||
|
)
|
||||||
|
result.__cause__ = exception
|
||||||
|
|
||||||
|
return result
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import http
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from hotpocket_backend.apps.accounts.services import (
|
from hotpocket_backend.apps.accounts.services import (
|
||||||
|
@ -11,6 +12,7 @@ from hotpocket_soa.dto.accounts import (
|
||||||
AccessTokenOut,
|
AccessTokenOut,
|
||||||
AccessTokensQuery,
|
AccessTokensQuery,
|
||||||
)
|
)
|
||||||
|
from hotpocket_soa.exceptions.backend import NotFound
|
||||||
|
|
||||||
from .base import ProxyService, SOAError
|
from .base import ProxyService, SOAError
|
||||||
|
|
||||||
|
@ -19,22 +21,18 @@ class AccessTokensService(ProxyService):
|
||||||
class AccessTokensServiceError(SOAError):
|
class AccessTokensServiceError(SOAError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AccessTokenNotFound(AccessTokensServiceError):
|
class NotFound(AccessTokensServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AccessTokenAccessDenied(AccessTokensServiceError):
|
class AccessDenied(AccessTokensServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.backend_access_tokens_service = BackendAccessTokensService()
|
self.backend_access_tokens_service = BackendAccessTokensService()
|
||||||
|
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
def get_error_class(self) -> type[SOAError]:
|
||||||
new_exception_args = []
|
return self.AccessTokensServiceError
|
||||||
if len(exception.args) > 0:
|
|
||||||
new_exception_args = [exception.args[0]]
|
|
||||||
|
|
||||||
return self.AccessTokensServiceError(*new_exception_args)
|
|
||||||
|
|
||||||
def create(self,
|
def create(self,
|
||||||
*,
|
*,
|
||||||
|
@ -69,16 +67,14 @@ class AccessTokensService(ProxyService):
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.account_uuid != account_uuid:
|
if result.account_uuid != account_uuid:
|
||||||
raise self.AccessTokenAccessDenied(
|
raise self.AccessDenied(
|
||||||
|
http.HTTPStatus.FORBIDDEN.value,
|
||||||
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except SOAError as exception:
|
except NotFound as exception:
|
||||||
if isinstance(exception.__cause__, BackendAccessTokensService.AccessTokenNotFound) is True:
|
raise self.NotFound.from_backend_error(exception)
|
||||||
raise self.AccessTokenNotFound(*exception.args) from exception
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_by_key(self,
|
def get_by_key(self,
|
||||||
*,
|
*,
|
||||||
|
@ -96,16 +92,14 @@ class AccessTokensService(ProxyService):
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.account_uuid != account_uuid:
|
if result.account_uuid != account_uuid:
|
||||||
raise self.AccessTokenAccessDenied(
|
raise self.AccessDenied(
|
||||||
|
http.HTTPStatus.FORBIDDEN.value,
|
||||||
f'account_uuid=`{account_uuid}` key=`{key}`',
|
f'account_uuid=`{account_uuid}` key=`{key}`',
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except SOAError as exception:
|
except NotFound as exception:
|
||||||
if isinstance(exception.__cause__, BackendAccessTokensService.AccessTokenNotFound) is True:
|
raise self.NotFound.from_backend_error(exception)
|
||||||
raise self.AccessTokenNotFound(f'account_uuid=`{account_uuid}` pk=`{key}`') from exception
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def search(self,
|
def search(self,
|
||||||
*,
|
*,
|
||||||
|
@ -124,23 +118,29 @@ class AccessTokensService(ProxyService):
|
||||||
]
|
]
|
||||||
|
|
||||||
def delete(self, *, access_token: AccessTokenOut) -> bool:
|
def delete(self, *, access_token: AccessTokenOut) -> bool:
|
||||||
return self.call(
|
try:
|
||||||
self.backend_access_tokens_service,
|
return self.call(
|
||||||
'delete',
|
self.backend_access_tokens_service,
|
||||||
pk=access_token.pk,
|
'delete',
|
||||||
)
|
pk=access_token.pk,
|
||||||
|
)
|
||||||
|
except NotFound as exception:
|
||||||
|
raise self.NotFound.from_backend_error(exception)
|
||||||
|
|
||||||
def update_meta(self,
|
def update_meta(self,
|
||||||
*,
|
*,
|
||||||
access_token: AccessTokenOut,
|
access_token: AccessTokenOut,
|
||||||
update: AccessTokenMetaUpdateIn,
|
update: AccessTokenMetaUpdateIn,
|
||||||
) -> AccessTokenOut:
|
) -> AccessTokenOut:
|
||||||
return AccessTokenOut.model_validate(
|
try:
|
||||||
self.call(
|
return AccessTokenOut.model_validate(
|
||||||
self.backend_access_tokens_service,
|
self.call(
|
||||||
'update_meta',
|
self.backend_access_tokens_service,
|
||||||
pk=access_token.pk,
|
'update_meta',
|
||||||
update=update,
|
pk=access_token.pk,
|
||||||
),
|
update=update,
|
||||||
from_attributes=True,
|
),
|
||||||
)
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
except NotFound as exception:
|
||||||
|
raise self.NotFound.from_backend_error(exception)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from hotpocket_backend.apps.accounts.services import (
|
||||||
AccountsService as BackendAccountsService,
|
AccountsService as BackendAccountsService,
|
||||||
)
|
)
|
||||||
from hotpocket_soa.dto.accounts import AccountOut
|
from hotpocket_soa.dto.accounts import AccountOut
|
||||||
|
from hotpocket_soa.exceptions.backend import NotFound
|
||||||
|
|
||||||
from .base import ProxyService, SOAError
|
from .base import ProxyService, SOAError
|
||||||
|
|
||||||
|
@ -15,19 +16,15 @@ class AccountsService(ProxyService):
|
||||||
class AccountsServiceError(SOAError):
|
class AccountsServiceError(SOAError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AccountNotFound(AccountsServiceError):
|
class NotFound(AccountsServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.backend_accounts_service = BackendAccountsService()
|
self.backend_accounts_service = BackendAccountsService()
|
||||||
|
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
def get_error_class(self) -> type[SOAError]:
|
||||||
new_exception_args = []
|
return self.AccountsServiceError
|
||||||
if len(exception.args) > 0:
|
|
||||||
new_exception_args = [exception.args[0]]
|
|
||||||
|
|
||||||
return self.AccountsServiceError(*new_exception_args)
|
|
||||||
|
|
||||||
def get(self, *, pk: uuid.UUID) -> AccountOut:
|
def get(self, *, pk: uuid.UUID) -> AccountOut:
|
||||||
try:
|
try:
|
||||||
|
@ -41,8 +38,5 @@ class AccountsService(ProxyService):
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except SOAError as exception:
|
except NotFound as exception:
|
||||||
if isinstance(exception.__cause__, BackendAccountsService.AccountNotFound) is True:
|
raise self.NotFound.from_backend_error(exception)
|
||||||
raise self.AccountNotFound(*exception.args) from exception
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import http
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from hotpocket_backend.apps.saves.services import (
|
from hotpocket_backend.apps.saves.services import (
|
||||||
|
@ -14,6 +15,7 @@ from hotpocket_soa.dto.associations import (
|
||||||
AssociationWithTargetOut,
|
AssociationWithTargetOut,
|
||||||
)
|
)
|
||||||
from hotpocket_soa.dto.saves import SaveOut
|
from hotpocket_soa.dto.saves import SaveOut
|
||||||
|
from hotpocket_soa.exceptions.backend import NotFound
|
||||||
|
|
||||||
from .base import ProxyService, SOAError
|
from .base import ProxyService, SOAError
|
||||||
|
|
||||||
|
@ -22,22 +24,18 @@ class AssociationsService(ProxyService):
|
||||||
class AssociationsServiceError(SOAError):
|
class AssociationsServiceError(SOAError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AssociationNotFound(AssociationsServiceError):
|
class NotFound(AssociationsServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AssociationAccessDenied(AssociationsServiceError):
|
class AccessDenied(AssociationsServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.backend_associations_service = BackendAssociationsService()
|
self.backend_associations_service = BackendAssociationsService()
|
||||||
|
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
def get_error_class(self) -> type[SOAError]:
|
||||||
new_exception_args = []
|
return self.AssociationsServiceError
|
||||||
if len(exception.args) > 0:
|
|
||||||
new_exception_args = [exception.args[0]]
|
|
||||||
|
|
||||||
return self.AssociationsServiceError(*new_exception_args)
|
|
||||||
|
|
||||||
def create(self,
|
def create(self,
|
||||||
*,
|
*,
|
||||||
|
@ -81,19 +79,19 @@ class AssociationsService(ProxyService):
|
||||||
)
|
)
|
||||||
|
|
||||||
if allow_archived is False and result.archived_at is not None:
|
if allow_archived is False and result.archived_at is not None:
|
||||||
raise self.AssociationNotFound(f'pk=`{pk}`')
|
raise self.NotFound(
|
||||||
|
http.HTTPStatus.NOT_FOUND.value, f'pk=`{pk}`',
|
||||||
|
)
|
||||||
|
|
||||||
if account_uuid is not None and result.account_uuid != account_uuid:
|
if account_uuid is not None and result.account_uuid != account_uuid:
|
||||||
raise self.AssociationAccessDenied(
|
raise self.AccessDenied(
|
||||||
|
http.HTTPStatus.FORBIDDEN.value,
|
||||||
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except SOAError as exception:
|
except NotFound as exception:
|
||||||
if isinstance(exception.__cause__, BackendAssociationsService.AssociationNotFound) is True:
|
raise self.NotFound.from_backend_error(exception)
|
||||||
raise self.AssociationNotFound(*exception.args) from exception
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def search(self,
|
def search(self,
|
||||||
*,
|
*,
|
||||||
|
@ -112,50 +110,65 @@ class AssociationsService(ProxyService):
|
||||||
]
|
]
|
||||||
|
|
||||||
def archive(self, *, association: AssociationOut) -> bool:
|
def archive(self, *, association: AssociationOut) -> bool:
|
||||||
return self.call(
|
try:
|
||||||
self.backend_associations_service,
|
return self.call(
|
||||||
'archive',
|
self.backend_associations_service,
|
||||||
pk=association.pk,
|
'archive',
|
||||||
)
|
pk=association.pk,
|
||||||
|
)
|
||||||
|
except NotFound as exception:
|
||||||
|
raise self.NotFound.from_backend_error(exception)
|
||||||
|
|
||||||
def star(self, *, association: AssociationOut) -> AssociationOut:
|
def star(self, *, association: AssociationOut) -> AssociationOut:
|
||||||
return AssociationOut.model_validate(
|
try:
|
||||||
self.call(
|
return AssociationOut.model_validate(
|
||||||
self.backend_associations_service,
|
self.call(
|
||||||
'star',
|
self.backend_associations_service,
|
||||||
pk=association.pk,
|
'star',
|
||||||
),
|
pk=association.pk,
|
||||||
from_attributes=True,
|
),
|
||||||
)
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
except NotFound as exception:
|
||||||
|
raise self.NotFound.from_backend_error(exception)
|
||||||
|
|
||||||
def unstar(self, *, association: AssociationOut) -> AssociationOut:
|
def unstar(self, *, association: AssociationOut) -> AssociationOut:
|
||||||
return AssociationOut.model_validate(
|
try:
|
||||||
self.call(
|
return AssociationOut.model_validate(
|
||||||
self.backend_associations_service,
|
self.call(
|
||||||
'unstar',
|
self.backend_associations_service,
|
||||||
pk=association.pk,
|
'unstar',
|
||||||
),
|
pk=association.pk,
|
||||||
from_attributes=True,
|
),
|
||||||
)
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
except NotFound as exception:
|
||||||
|
raise self.NotFound.from_backend_error(exception)
|
||||||
|
|
||||||
def update(self,
|
def update(self,
|
||||||
*,
|
*,
|
||||||
association: AssociationOut,
|
association: AssociationOut,
|
||||||
update: AssociationUpdateIn,
|
update: AssociationUpdateIn,
|
||||||
) -> AssociationOut:
|
) -> AssociationOut:
|
||||||
return AssociationOut.model_validate(
|
try:
|
||||||
self.call(
|
return AssociationOut.model_validate(
|
||||||
self.backend_associations_service,
|
self.call(
|
||||||
'update',
|
self.backend_associations_service,
|
||||||
pk=association.pk,
|
'update',
|
||||||
update=update,
|
pk=association.pk,
|
||||||
),
|
update=update,
|
||||||
from_attributes=True,
|
),
|
||||||
)
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
except NotFound as exception:
|
||||||
|
raise self.NotFound.from_backend_error(exception)
|
||||||
|
|
||||||
def delete(self, *, association: AssociationOut) -> bool:
|
def delete(self, *, association: AssociationOut) -> bool:
|
||||||
return self.call(
|
try:
|
||||||
self.backend_associations_service,
|
return self.call(
|
||||||
'delete',
|
self.backend_associations_service,
|
||||||
pk=association.pk,
|
'delete',
|
||||||
)
|
pk=association.pk,
|
||||||
|
)
|
||||||
|
except NotFound as exception:
|
||||||
|
raise self.NotFound.from_backend_error(exception)
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import http
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from hotpocket_backend.apps.accounts.services import (
|
from hotpocket_backend.apps.accounts.services import (
|
||||||
AuthKeysService as BackendAuthKeysService,
|
AuthKeysService as BackendAuthKeysService,
|
||||||
)
|
)
|
||||||
from hotpocket_soa.dto.accounts import AuthKeyOut
|
from hotpocket_soa.dto.accounts import AuthKeyOut
|
||||||
|
from hotpocket_soa.exceptions.backend import NotFound
|
||||||
|
|
||||||
from .base import ProxyService, SOAError
|
from .base import ProxyService, SOAError
|
||||||
|
|
||||||
|
@ -15,23 +17,16 @@ class AuthKeysService(ProxyService):
|
||||||
class AuthKeysServiceError(SOAError):
|
class AuthKeysServiceError(SOAError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AuthKeyNotFound(AuthKeysServiceError):
|
class NotFound(AuthKeysServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AuthKeyAccessDenied(AuthKeysServiceError):
|
class AccessDenied(AuthKeysServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.backend_auth_keys_service = BackendAuthKeysService()
|
self.backend_auth_keys_service = BackendAuthKeysService()
|
||||||
|
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
|
||||||
new_exception_args = []
|
|
||||||
if len(exception.args) > 0:
|
|
||||||
new_exception_args = [exception.args[0]]
|
|
||||||
|
|
||||||
return self.AuthKeysServiceError(*new_exception_args)
|
|
||||||
|
|
||||||
def _check_auth_key_access(self,
|
def _check_auth_key_access(self,
|
||||||
auth_key: AuthKeyOut,
|
auth_key: AuthKeyOut,
|
||||||
account_uuid: uuid.UUID | None,
|
account_uuid: uuid.UUID | None,
|
||||||
|
@ -70,16 +65,14 @@ class AuthKeysService(ProxyService):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._check_auth_key_access(result, account_uuid) is False:
|
if self._check_auth_key_access(result, account_uuid) is False:
|
||||||
raise self.AuthKeyAccessDenied(
|
raise self.AccessDenied(
|
||||||
|
http.HTTPStatus.FORBIDDEN.value,
|
||||||
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
f'account_uuid=`{account_uuid}` pk=`{pk}`',
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except SOAError as exception:
|
except NotFound as exception:
|
||||||
if isinstance(exception.__cause__, BackendAuthKeysService.AuthKeyNotFound) is True:
|
raise self.NotFound.from_backend_error(exception)
|
||||||
raise self.AuthKeyNotFound(*exception.args) from exception
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_by_key(self,
|
def get_by_key(self,
|
||||||
*,
|
*,
|
||||||
|
@ -97,13 +90,11 @@ class AuthKeysService(ProxyService):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._check_auth_key_access(result, account_uuid) is False:
|
if self._check_auth_key_access(result, account_uuid) is False:
|
||||||
raise self.AuthKeyAccessDenied(
|
raise self.AccessDenied(
|
||||||
|
http.HTTPStatus.FORBIDDEN.value,
|
||||||
f'account_uuid=`{account_uuid}` key=`{key}`',
|
f'account_uuid=`{account_uuid}` key=`{key}`',
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except SOAError as exception:
|
except NotFound as exception:
|
||||||
if isinstance(exception.__cause__, BackendAuthKeysService.AuthKeyNotFound) is True:
|
raise self.NotFound.from_backend_error(exception)
|
||||||
raise self.AuthKeyNotFound(*exception.args) from exception
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
|
@ -1,16 +1,66 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import http
|
||||||
|
import types
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from hotpocket_soa.exceptions.backend import BackendServiceError
|
||||||
class SOAError(Exception):
|
from hotpocket_soa.exceptions.frontend import SOAError
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
def __getattribute__(self, name: str) -> typing.Any:
|
||||||
return SOAError(exception.args[0])
|
result = super().__getattribute__(name)
|
||||||
|
|
||||||
|
is_service_method = all((
|
||||||
|
name.startswith('_') is False,
|
||||||
|
hasattr(Service, name) is False,
|
||||||
|
isinstance(result, types.MethodType),
|
||||||
|
getattr(result, '__self__', None) is self,
|
||||||
|
))
|
||||||
|
if is_service_method is True:
|
||||||
|
@functools.wraps(result)
|
||||||
|
def wrapped_result(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return result(*args, **kwargs)
|
||||||
|
except Exception as exception:
|
||||||
|
raise self.wrap_exception(exception) from exception
|
||||||
|
|
||||||
|
return wrapped_result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_error_class(self) -> type[SOAError]:
|
||||||
|
return SOAError
|
||||||
|
|
||||||
|
def wrap_exception(self, exception: Exception) -> SOAError:
|
||||||
|
error_class = self.get_error_class()
|
||||||
|
|
||||||
|
if isinstance(exception, error_class) is True:
|
||||||
|
return typing.cast(SOAError, exception)
|
||||||
|
|
||||||
|
result = error_class(
|
||||||
|
http.HTTPStatus.IM_A_TEAPOT.value,
|
||||||
|
'SOA Error',
|
||||||
|
*exception.args,
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(exception, BackendServiceError) is True:
|
||||||
|
baskend_error = typing.cast(BackendServiceError, exception)
|
||||||
|
result = error_class(
|
||||||
|
baskend_error.CODE.value,
|
||||||
|
baskend_error.message,
|
||||||
|
baskend_error.data,
|
||||||
|
*baskend_error.args,
|
||||||
|
)
|
||||||
|
|
||||||
|
result.__cause__ = exception
|
||||||
|
return result
|
||||||
|
|
||||||
|
def call(self, *args, **kwargs) -> typing.Any:
|
||||||
|
raise NotImplementedError('TODO')
|
||||||
|
|
||||||
|
|
||||||
class ProxyService(Service):
|
class ProxyService(Service):
|
||||||
|
@ -18,7 +68,4 @@ class ProxyService(Service):
|
||||||
handler = getattr(service, method, None)
|
handler = getattr(service, method, None)
|
||||||
assert handler is not None, f'Unknown method: method=`{method}`'
|
assert handler is not None, f'Unknown method: method=`{method}`'
|
||||||
|
|
||||||
try:
|
return handler(*args, **kwargs)
|
||||||
return handler(*args, **kwargs)
|
|
||||||
except Exception as exception:
|
|
||||||
raise self.wrap_exception(exception) from exception
|
|
||||||
|
|
|
@ -15,12 +15,8 @@ class BotService(ProxyService):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.backend_associations_service = BackendBotService()
|
self.backend_associations_service = BackendBotService()
|
||||||
|
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
def get_error_class(self) -> type[SOAError]:
|
||||||
new_exception_args = []
|
return self.BotServiceError
|
||||||
if len(exception.args) > 0:
|
|
||||||
new_exception_args = [exception.args[0]]
|
|
||||||
|
|
||||||
return self.BotServiceError(*new_exception_args)
|
|
||||||
|
|
||||||
def is_netloc_banned(self, *, url: str) -> bool:
|
def is_netloc_banned(self, *, url: str) -> bool:
|
||||||
return self.call(
|
return self.call(
|
||||||
|
|
|
@ -18,17 +18,13 @@ class SaveProcessorService(ProxyService):
|
||||||
class SaveProcessorServiceError(SOAError):
|
class SaveProcessorServiceError(SOAError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
|
||||||
new_exception_args = []
|
|
||||||
if len(exception.args) > 0:
|
|
||||||
new_exception_args = [exception.args[0]]
|
|
||||||
|
|
||||||
return self.SaveProcessorServiceError(*new_exception_args)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.backend_save_processor_service = BackendSaveProcessorService()
|
self.backend_save_processor_service = BackendSaveProcessorService()
|
||||||
|
|
||||||
|
def get_error_class(self) -> type[SOAError]:
|
||||||
|
return self.SaveProcessorServiceError
|
||||||
|
|
||||||
def schedule_process_save(self, *, save: SaveOut) -> AsyncResultOut:
|
def schedule_process_save(self, *, save: SaveOut) -> AsyncResultOut:
|
||||||
result = AsyncResultOut.model_validate(
|
result = AsyncResultOut.model_validate(
|
||||||
self.call(
|
self.call(
|
||||||
|
|
|
@ -7,6 +7,7 @@ from hotpocket_backend.apps.saves.services import (
|
||||||
SavesService as BackendSavesService,
|
SavesService as BackendSavesService,
|
||||||
)
|
)
|
||||||
from hotpocket_soa.dto.saves import SaveIn, SaveOut
|
from hotpocket_soa.dto.saves import SaveIn, SaveOut
|
||||||
|
from hotpocket_soa.exceptions.backend import Invalid, NotFound
|
||||||
|
|
||||||
from .base import ProxyService, SOAError
|
from .base import ProxyService, SOAError
|
||||||
|
|
||||||
|
@ -15,30 +16,34 @@ class SavesService(ProxyService):
|
||||||
class SavesServiceError(SOAError):
|
class SavesServiceError(SOAError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class SaveNotFound(SavesServiceError):
|
class NotFound(SavesServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def wrap_exception(self, exception: Exception) -> Exception:
|
class Invalid(SavesServiceError):
|
||||||
new_exception_args = []
|
pass
|
||||||
if len(exception.args) > 0:
|
|
||||||
new_exception_args = [exception.args[0]]
|
|
||||||
|
|
||||||
return self.SavesServiceError(*new_exception_args)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.backend_saves_service = BackendSavesService()
|
self.backend_saves_service = BackendSavesService()
|
||||||
|
|
||||||
|
def get_error_class(self) -> type[SOAError]:
|
||||||
|
return self.SavesServiceError
|
||||||
|
|
||||||
def create(self, *, account_uuid: uuid.UUID, save: SaveIn) -> SaveOut:
|
def create(self, *, account_uuid: uuid.UUID, save: SaveIn) -> SaveOut:
|
||||||
return SaveOut.model_validate(
|
try:
|
||||||
self.call(
|
return SaveOut.model_validate(
|
||||||
self.backend_saves_service,
|
self.call(
|
||||||
'create',
|
self.backend_saves_service,
|
||||||
account_uuid=account_uuid,
|
'create',
|
||||||
save=save,
|
account_uuid=account_uuid,
|
||||||
),
|
save=save,
|
||||||
from_attributes=True,
|
),
|
||||||
)
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
except Invalid as exception:
|
||||||
|
raise self.Invalid(
|
||||||
|
exception.CODE.value, exception.message, exception.data,
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, *, pk: uuid.UUID) -> SaveOut:
|
def get(self, *, pk: uuid.UUID) -> SaveOut:
|
||||||
try:
|
try:
|
||||||
|
@ -52,8 +57,5 @@ class SavesService(ProxyService):
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
except SOAError as exception:
|
except NotFound as exception:
|
||||||
if isinstance(exception.__cause__, BackendSavesService.SaveNotFound) is True:
|
raise self.NotFound.from_backend_error(exception)
|
||||||
raise self.SaveNotFound(*exception.args) from exception
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ server {
|
||||||
listen *:443 ssl;
|
listen *:443 ssl;
|
||||||
server_name app.hotpocket.work.bthlabs.net;
|
server_name app.hotpocket.work.bthlabs.net;
|
||||||
|
|
||||||
ssl_certificate /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
ssl_certificate /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
||||||
ssl_certificate_key /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
ssl_certificate_key /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# proxy_cache_bypass $http_upgrade;
|
# proxy_cache_bypass $http_upgrade;
|
||||||
|
@ -71,8 +71,8 @@ server {
|
||||||
listen *:443 ssl;
|
listen *:443 ssl;
|
||||||
server_name admin.hotpocket.work.bthlabs.net;
|
server_name admin.hotpocket.work.bthlabs.net;
|
||||||
|
|
||||||
ssl_certificate /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
ssl_certificate /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.crt;
|
||||||
ssl_certificate_key /Users/bilbo/Projects/PLAYG/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
ssl_certificate_key /Users/bilbo/Projects/HOTPOCKET/hotpocket/services/tls/app.hotpocket.work.bthlabs.net.key;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# proxy_cache_bypass $http_upgrade;
|
# proxy_cache_bypass $http_upgrade;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user