This commit is contained in:
commit
b4338e2769
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
services/keycloak/fixtures/*.json filter=git-crypt diff=git-crypt
|
||||||
|
services/tls/*.crt filter=git-crypt diff=git-crypt
|
||||||
|
services/tls/*.key filter=git-crypt diff=git-crypt
|
70
.gitea/workflows/ci.yaml
Normal file
70
.gitea/workflows/ci.yaml
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
name: "CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-checks:
|
||||||
|
name: "Checks"
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- name: "Checkout the code"
|
||||||
|
uses: "actions/checkout@v2"
|
||||||
|
- name: "Set up Docker Buildx"
|
||||||
|
uses: "docker/setup-buildx-action@v3"
|
||||||
|
- name: "Build `postgres` image"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: "services/postgres/Dockerfile"
|
||||||
|
context: "services/"
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-local"
|
||||||
|
- name: "Build `keycloak` image"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: "services/keycloak/Dockerfile"
|
||||||
|
context: "services/"
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-local"
|
||||||
|
- name: "Build `rabbitmq` image"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: "services/rabbitmq/Dockerfile"
|
||||||
|
context: "services/"
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-local"
|
||||||
|
- name: "Build `backend-ci` image"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: "services/backend/Dockerfile"
|
||||||
|
context: "services/"
|
||||||
|
target: "ci"
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-local"
|
||||||
|
- name: "Build `packages-ci` image"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: "services/packages/Dockerfile"
|
||||||
|
context: "services/"
|
||||||
|
target: "ci"
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-local"
|
||||||
|
- name: "Run `backend` checks"
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm backend-ci inv ci
|
||||||
|
- name: "Run `packages` checks"
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm packages-ci inv ci
|
||||||
|
- name: "Clean up"
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml down --volumes
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.envrc*
|
||||||
|
.ipythonhome/
|
202
LICENSE
Normal file
202
LICENSE
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
88
NOTICE.txt
Normal file
88
NOTICE.txt
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
HotPocket by BTHLabs
|
||||||
|
Copyright 2025-present BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl/)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
HotPocket by BTHLabs includes the following third party software
|
||||||
|
|
||||||
|
Bootstrap
|
||||||
|
Copyright 2011-2025 The Bootstrap Authors
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
Bootstrap Icons
|
||||||
|
Copyright 2019-2024 The Bootstrap Authors
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
htmx.org
|
||||||
|
Licensed under terms of the Zero-Clause BSD License
|
||||||
|
|
||||||
|
celery
|
||||||
|
Copyright (c) 2017-2026 Asif Saif Uddin, core team & contributors. All rights reserved.
|
||||||
|
Copyright (c) 2015-2016 Ask Solem & contributors. All rights reserved.
|
||||||
|
Copyright (c) 2012-2014 GoPivotal, Inc. All rights reserved.
|
||||||
|
Copyright (c) 2009, 2010, 2011, 2012 Ask Solem, and individual contributors. All rights reserved.
|
||||||
|
Licensed under terms of The BSD License (3 Clause, also known as the new BSD license)
|
||||||
|
|
||||||
|
crispy-bootstrap5
|
||||||
|
Copyright (c) 2020 David Smith and contributors.
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
django
|
||||||
|
Copyright (c) Django Software Foundation and individual contributors.
|
||||||
|
Licensed under terms of The BSD License (3 Clause, also known as the new BSD license)
|
||||||
|
|
||||||
|
django-crispy-form
|
||||||
|
Copyright (c) 2009-2021 Miguel Araujo, Daniel Feldroy and contributors.
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
django-hmtx
|
||||||
|
Copyright (c) Adam Johnson
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
psycopg
|
||||||
|
Copyright (c) Daniele Varrazzo
|
||||||
|
Licensed under terms of the GNU Lesser General Public License v3.0
|
||||||
|
|
||||||
|
pydantic
|
||||||
|
Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors.
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
pyquery
|
||||||
|
Copyright (C) 2008 - Olivier Lauzanne <olauzanne@gmail.com>
|
||||||
|
Licensed under terms of The BSD License (3 Clause, also known as the new BSD license)
|
||||||
|
|
||||||
|
requests
|
||||||
|
Copyright 2019 Kenneth Reitz
|
||||||
|
Licensed under terms of the Apache License 2.0
|
||||||
|
|
||||||
|
social-auth-app-django
|
||||||
|
Copyright (c) 2012-2016, Matías Aguirre
|
||||||
|
Licensed under terms of The BSD License (3 Clause, also known as the new BSD license)
|
||||||
|
|
||||||
|
social-auth-core
|
||||||
|
Copyright (c) 2012-2016, Matías Aguirre
|
||||||
|
Licensed under terms of The BSD License (3 Clause, also known as the new BSD license)
|
||||||
|
|
||||||
|
sqlalchemy
|
||||||
|
Copyright 2005-2025 SQLAlchemy authors and contributors
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
uuid6
|
||||||
|
Copyright (c) 2021 oittaa
|
||||||
|
Licensed under terms of the MIT License
|
||||||
|
|
||||||
|
Pepper Hot Solid icon
|
||||||
|
Copyright (c) Icons8
|
||||||
|
Licensed under terms of the MIT License
|
167
README.md
Normal file
167
README.md
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
# HotPocket by BTHLabs
|
||||||
|
|
||||||
|
This repository contains the _HotPocket_ project.
|
||||||
|
|
||||||
|
## Development setup
|
||||||
|
|
||||||
|
### Requirements:
|
||||||
|
|
||||||
|
* Python 3.12,
|
||||||
|
* Poetry 1.8.3,
|
||||||
|
* `git-crypt`,
|
||||||
|
* Docker with Docker Compose and Buildx.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. `$ git-crypt unlock KEYFILE`
|
||||||
|
1. `$ poetry install`
|
||||||
|
1. `$ docker buildx bake`
|
||||||
|
1. `$ poetry run inv setup`
|
||||||
|
|
||||||
|
### Running local development stack
|
||||||
|
|
||||||
|
1. `$ docker compose up`
|
||||||
|
|
||||||
|
**Exported services:**
|
||||||
|
|
||||||
|
* The app: https://app.hotpocket.work.bthlabs.net:8000/
|
||||||
|
* The admin: https://admin.hotpocket.work.bthlabs.net:8000/
|
||||||
|
* Keycloak: https://auth.hotpocket.work.bthlabs.net:8443/
|
||||||
|
* Postgres: postgres://postgres.hotpocket.work.bthlabs.net:5432/
|
||||||
|
* RabbitMQ: amqp://rabbitmq.hotpocket.work.bthlabs.net:5672/
|
||||||
|
* RabbitMQ Management: amqp://rabbitmq.hotpocket.work.bthlabs.net:15672/
|
||||||
|
|
||||||
|
**Default credentials:**
|
||||||
|
|
||||||
|
The default credentials across most of the services are:
|
||||||
|
`hotpocket:hotpocketm4st3r`. This applies to the initial app and admin account,
|
||||||
|
Keycloak master realm, Postgres and RabbitMQ.
|
||||||
|
|
||||||
|
The Keycloak `hotpocket-development` realm user's credentials are:
|
||||||
|
`hotpocket@bthlabs.net:hotpocketm4st3r`. You can use these to log in to the app
|
||||||
|
and admin using OIDC.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
There are two deployment images - `aio` and `deployment`.
|
||||||
|
|
||||||
|
### The AIO image
|
||||||
|
|
||||||
|
The `aio` image is pre-configured for running small instances in a single
|
||||||
|
container:
|
||||||
|
|
||||||
|
* It defaults to SQLite database.
|
||||||
|
* It defaults to running all background tasks in the foreground.
|
||||||
|
* It defaults to accepting traffic with any `Host` HTTP header.
|
||||||
|
|
||||||
|
The `aio` image is recommended for self-hosting with minimal use, e.g. by a
|
||||||
|
single user.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker run --rm -it \
|
||||||
|
-v `realpath run/`:/srv/run \
|
||||||
|
-e HOTPOCKET_BACKEND_SECRET_KEY=thisisntright \
|
||||||
|
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \
|
||||||
|
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \
|
||||||
|
-p 8000:8000 \
|
||||||
|
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v1.0.0-01
|
||||||
|
```
|
||||||
|
|
||||||
|
The command above will set up and start the application. The SQLite file will
|
||||||
|
be placed in `run/hotpocket-backend-aio.sqlite` and database migrations will
|
||||||
|
be ran. The initial superuser account will be created with the specified
|
||||||
|
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 `DJANGO_SETTINGS_MODULE` environment variable defaults to
|
||||||
|
`hotpocket_backend.settings.deployment.webapp`. This should be set to
|
||||||
|
`hotpocket_backend.settings.deployment.admin` in the Admin container.
|
||||||
|
|
||||||
|
**NOTE:** The command above specifies wildly insecure `SECRET_KEY` which is
|
||||||
|
used among other things to secure the session cookie. Please *please*
|
||||||
|
**please** don't run it like this. Not even in your homelab :).
|
||||||
|
|
||||||
|
The `deployment/aio/docker-compose.yaml` file can be used as a starting
|
||||||
|
point for AIO deployments.
|
||||||
|
|
||||||
|
### The Deployment image
|
||||||
|
|
||||||
|
The `deployment` image doesn't make any assumptions about the env and in turn
|
||||||
|
will require the operator to configure database, Celery broker and result
|
||||||
|
backend etc. The final deployment will require services for at least the Web
|
||||||
|
app, the Celery worker and Celery Beat. Admin is optional.
|
||||||
|
|
||||||
|
The `DJANGO_SETTINGS_MODULE` environment variable defaults to
|
||||||
|
`hotpocket_backend.settings.deployment.aio`.
|
||||||
|
|
||||||
|
The `deployment/fullstack/docker-compose.yaml` file can be used as a
|
||||||
|
starting point for full-stack deployments.
|
||||||
|
|
||||||
|
### Configuration environment variables
|
||||||
|
|
||||||
|
HotPocket deployment images provide extensive set of environment variables
|
||||||
|
that can be used to configure the services.
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------|
|
||||||
|
| `HOTPOCKET_BACKEND_ENV` | `deployment` or `aio` | The environment name. See below. |
|
||||||
|
| `HOTPOCKET_BACKEND_APP` | `webapp` | The app name. See below. |
|
||||||
|
| `HOTPOCKET_BACKEND_DEBUG` | `false` | Django `DEBUG` setting. **Do not enable in production**. Only effective in the AIO image. |
|
||||||
|
| `HOTPOCKET_BACKEND_ALLOWED_HOSTS` | N/A or `*` | Django `ALLOWED_HOSTS` setting. **Required in the Deployment image.** |
|
||||||
|
| `HOTPOCKET_BACKEND_SECRET_KEY` | N/A | Django `SECRET_KEY` setting. Recommended different for the Web app and Admin. **Required**. |
|
||||||
|
| `HOTPOCKET_BACKEND_DATABASE_ENGINE` | `django.db.backends.postgresql` or `django.db.backends.sqlite3` | The database configuration engine. |
|
||||||
|
| `HOTPOCKET_BACKEND_DATABASE_NAME` | N/A or `/srv/run/hotpocket-backend-aio.sqlite` | The database name. |
|
||||||
|
| `HOTPOCKET_BACKEND_DATABASE_USER` | N/A or N/A | The database user. |
|
||||||
|
| `HOTPOCKET_BACKEND_DATABASE_PASSWORD` | N/A | The database password. |
|
||||||
|
| `HOTPOCKET_BACKEND_DATABASE_HOST` | N/A | The database host. |
|
||||||
|
| `HOTPOCKET_BACKEND_DATABASE_PORT` | `5432` or N/A | The database port. |
|
||||||
|
| `HOTPOCKET_BACKEND_MODEL_AUTH_IS_DISABLED` | `false` | Set to `true` to disable username and password login. |
|
||||||
|
| `HOTPOCKET_BACKEND_OIDC_PAYLOAD` | N/A | The OIDC configuration payload. |
|
||||||
|
| `HOTPOCKET_BACKEND_CELERY_BROKER_URL` | N/A | The Celery broker URL. |
|
||||||
|
| `HOTPOCKET_BACKEND_CELERY_RESULT_BACKEND` | N/A | The Celery result backend URL. |
|
||||||
|
| `HOTPOCKET_BACKEND_CELERY_IGNORE_RESULT` | `false` | Set to `true` to prevent Celery from saving task results. |
|
||||||
|
| `HOTPOCKET_BACKEND_CELERY_ALWAYS_EAGER` | `false` | Set to `true` to run Celery tasks in the foreground. |
|
||||||
|
| `HOTPOCKET_BACKEND_UPLOADS_PATH` | `/srv/uploads` or `/srv/run/uploads` | The absolute path to user-uploaded files. |
|
||||||
|
| `HOTPOCKET_BACKEND_GUNICORN_WORKERS` | `4` or `2` | The number of Gunicorn workers to run for Web servers. |
|
||||||
|
| `HOTPOCKET_BACKEND_RUN_MIGRATIONS` | `false` or `true` | Set to `true` to run database muigrations when the container starts. |
|
||||||
|
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME` | N/A | Username for the initial account. |
|
||||||
|
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD` | N/A | Password for the initial account. |
|
||||||
|
|
||||||
|
**Env and App settings**
|
||||||
|
|
||||||
|
The `HOTPOCKET_BACKEND_ENV` and `HOTPOCKET_BACKEND_APP` variables are used
|
||||||
|
internally to resolve other settings and identify the running app.
|
||||||
|
`HOTPOCKET_BACKEND_ENV` should only be changed if when creating heavily
|
||||||
|
customized version of the project. `HOTPOCKET_BACKEND_APP` should generally be
|
||||||
|
set to `admin` only in the Admin container.
|
||||||
|
|
||||||
|
**OIDC login configuration**
|
||||||
|
|
||||||
|
The `HOTPOCKET_BACKEND_OIDC_PAYLOAD` can be used to enable OIDC login. It must
|
||||||
|
be a JSON string that deserializes to the following object:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"endpoint": "https://some.oidc.host/some-realm/",
|
||||||
|
"key": "client-key",
|
||||||
|
"secret": "client-secret",
|
||||||
|
"scope": ["roles"],
|
||||||
|
"display_name": "My OIDC server"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `scope` field specified additional scopes to request from IdP. It can be
|
||||||
|
ommited and defaults to `["roles"]`. The `display_name` field specifies the
|
||||||
|
method's name in the UI and defaults to `OIDC`.
|
||||||
|
|
||||||
|
**NOTE:** Currently, only Keycloak has been tested with this login method.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
_HotPocket_ is developed by [BTHLabs](https://www.bthlabs.pl/).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
_HotPocket_ is licensed under the Apache 2.0 License.
|
2
deployment/aio/.gitignore
vendored
Normal file
2
deployment/aio/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
run/*.sqlite
|
||||||
|
uploads/
|
12
deployment/aio/docker-compose.yaml
Normal file
12
deployment/aio/docker-compose.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v1.0.0-01"
|
||||||
|
environment:
|
||||||
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
|
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"
|
||||||
|
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD: "hotpocketm4st3r"
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- "./run:/srv/run"
|
||||||
|
restart: "unless-stopped"
|
0
deployment/aio/run/.placeholder
Normal file
0
deployment/aio/run/.placeholder
Normal file
2
deployment/fullstack/.gitignore
vendored
Normal file
2
deployment/fullstack/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
run/celery-beat-schedule
|
||||||
|
uploads/*.csv
|
77
deployment/fullstack/docker-compose.yaml
Normal file
77
deployment/fullstack/docker-compose.yaml
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
x-backend-environment: &x-backend-environment
|
||||||
|
HOTPOCKET_BACKEND_DATABASE_NAME: "hotpocket_backend_staging"
|
||||||
|
HOTPOCKET_BACKEND_DATABASE_USER: "hotpocket"
|
||||||
|
HOTPOCKET_BACKEND_DATABASE_PASSWORD: "hotpocketm4st3r"
|
||||||
|
HOTPOCKET_BACKEND_DATABASE_HOST: "databases.bthlab.bthlabs.net"
|
||||||
|
HOTPOCKET_BACKEND_CELERY_BROKER_URL: "amqp://hotpocket:hotpocketm4st3r@databases.bthlab.bthlabs.net/hotpocket_backend_staging"
|
||||||
|
HOTPOCKET_BACKEND_CELERY_RESULT_BACKEND: "db+postgresql+psycopg://hotpocket:hotpocketm4st3r@databases.bthlab.bthlabs.net/hotpocket_backend_staging"
|
||||||
|
|
||||||
|
services:
|
||||||
|
webapp:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.0-01"
|
||||||
|
environment:
|
||||||
|
<<: *x-backend-environment
|
||||||
|
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
||||||
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- "./run:/srv/run"
|
||||||
|
- "./uploads:/srv/uploads"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
|
||||||
|
admin:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.0-01"
|
||||||
|
environment:
|
||||||
|
<<: *x-backend-environment
|
||||||
|
HOTPOCKET_BACKEND_APP: "admin"
|
||||||
|
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
||||||
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
volumes:
|
||||||
|
- "./run:/srv/run"
|
||||||
|
- "./uploads:/srv/uploads"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
|
||||||
|
celery-worker:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.0-01"
|
||||||
|
command:
|
||||||
|
- "/srv/venv/bin/celery"
|
||||||
|
- "-A"
|
||||||
|
- "hotpocket_backend.celery:app"
|
||||||
|
- "worker"
|
||||||
|
- "-l"
|
||||||
|
- "INFO"
|
||||||
|
- "-Q"
|
||||||
|
- "celery,webapp"
|
||||||
|
- "-c"
|
||||||
|
- "2"
|
||||||
|
environment:
|
||||||
|
<<: *x-backend-environment
|
||||||
|
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
||||||
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
|
volumes:
|
||||||
|
- "./run:/srv/run"
|
||||||
|
- "./uploads:/srv/uploads"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
|
||||||
|
celery-beat:
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.0-01"
|
||||||
|
command:
|
||||||
|
- "/srv/venv/bin/celery"
|
||||||
|
- "-A"
|
||||||
|
- "hotpocket_backend.celery:app"
|
||||||
|
- "beat"
|
||||||
|
- "-l"
|
||||||
|
- "INFO"
|
||||||
|
- "-s"
|
||||||
|
- "/srv/run/celery-beat-schedule"
|
||||||
|
environment:
|
||||||
|
<<: *x-backend-environment
|
||||||
|
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
||||||
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
|
volumes:
|
||||||
|
- "./run:/srv/run"
|
||||||
|
- "./uploads:/srv/uploads"
|
||||||
|
restart: "unless-stopped"
|
0
deployment/fullstack/run/.placeholder
Normal file
0
deployment/fullstack/run/.placeholder
Normal file
0
deployment/fullstack/uploads/.placeholder
Normal file
0
deployment/fullstack/uploads/.placeholder
Normal file
111
docker-bake.json
Normal file
111
docker-bake.json
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"group": {
|
||||||
|
"default": {
|
||||||
|
"targets": [
|
||||||
|
"backend-management",
|
||||||
|
"caddy",
|
||||||
|
"keycloak",
|
||||||
|
"packages-management",
|
||||||
|
"postgres",
|
||||||
|
"rabbitmq"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"backend-management": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "backend/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/backend:local"
|
||||||
|
],
|
||||||
|
"target": "development",
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"backend-aio-webapp": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "backend/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-local"
|
||||||
|
],
|
||||||
|
"target": "aio",
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"backend-ci": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "backend/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-local"
|
||||||
|
],
|
||||||
|
"target": "ci",
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packages-management": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "packages/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/packages:local"
|
||||||
|
],
|
||||||
|
"target": "development",
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packages-ci": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "packages/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-local"
|
||||||
|
],
|
||||||
|
"target": "ci",
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"caddy": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "caddy/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/caddy:2.9.1-local"
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "postgres/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-local"
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keycloak": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "keycloak/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-local"
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rabbitmq": {
|
||||||
|
"context": "services/",
|
||||||
|
"dockerfile": "rabbitmq/Dockerfile",
|
||||||
|
"tags": [
|
||||||
|
"docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-local"
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
"type=docker,load=true,push=false"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
docker-compose-aio.yaml
Normal file
10
docker-compose-aio.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
services: {}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- path: "./docker-compose-caddy.yaml"
|
||||||
|
- path: "./services/backend/docker-compose-aio.yaml"
|
||||||
|
|
||||||
|
volumes: {}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
24
docker-compose-caddy.yaml
Normal file
24
docker-compose-caddy.yaml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
build:
|
||||||
|
context: "services/"
|
||||||
|
dockerfile: "caddy/Dockerfile"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/caddy:2.9.1-local"
|
||||||
|
command:
|
||||||
|
- "caddy"
|
||||||
|
- "run"
|
||||||
|
- "--config"
|
||||||
|
- "/etc/caddy/Caddyfile"
|
||||||
|
- "--adapter"
|
||||||
|
- "caddyfile"
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- "./services/caddy/conf:/etc/caddy"
|
||||||
|
- "./services/tls:/opt/tls"
|
||||||
|
- "caddy_data:/data"
|
||||||
|
- "caddy_srv:/srv"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
caddy_srv:
|
15
docker-compose-ci.yaml
Normal file
15
docker-compose-ci.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
ports: []
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
command: "echo 'NOOP'"
|
||||||
|
ports: []
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
ports: []
|
||||||
|
|
||||||
|
include:
|
||||||
|
- path: "./services/backend/docker-compose-ci.yaml"
|
||||||
|
- path: "./services/packages/docker-compose-ci.yaml"
|
69
docker-compose-cloud.yaml
Normal file
69
docker-compose-cloud.yaml
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
build:
|
||||||
|
context: "services/"
|
||||||
|
dockerfile: "postgres/Dockerfile"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-local"
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
- "POSTGRES_PASSWORD=hotpocketm4st3r"
|
||||||
|
- "POSTGRES_USER=hotpocket"
|
||||||
|
- "POSTGRES_DB=hotpocket"
|
||||||
|
volumes:
|
||||||
|
- "postgres_data:/var/lib/postgresql/data"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- "postgres.hotpocket.work.bthlabs.net"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
build:
|
||||||
|
context: "services/"
|
||||||
|
dockerfile: "keycloak/Dockerfile"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-local"
|
||||||
|
command: "-v start-dev --https-certificate-file=/opt/tls/app.hotpocket.work.bthlabs.net.crt --https-certificate-key-file=/opt/tls/app.hotpocket.work.bthlabs.net.key"
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
- "8443:8443"
|
||||||
|
environment:
|
||||||
|
- "KEYCLOAK_ADMIN=hotpocket"
|
||||||
|
- "KEYCLOAK_ADMIN_PASSWORD=hotpocketm4st3r"
|
||||||
|
- "KEYCLOAK_HOSTNAME=auth.hotpocket.work.bthlabs.net"
|
||||||
|
volumes:
|
||||||
|
- "./services/keycloak/fixtures:/opt/import"
|
||||||
|
- "./services/tls:/opt/tls"
|
||||||
|
- "keycloak_data:/opt/keycloak/data"
|
||||||
|
- "keycloak_var:/opt/kcsetup"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- "auth.hotpocket.work.bthlabs.net"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
build:
|
||||||
|
context: "services/"
|
||||||
|
dockerfile: "rabbitmq/Dockerfile"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-local"
|
||||||
|
ports:
|
||||||
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: "hotpocket"
|
||||||
|
RABBITMQ_DEFAULT_PASS: "hotpocketm4st3r"
|
||||||
|
RABBITMQ_DEFAULT_VHOST: "hotpocket"
|
||||||
|
volumes:
|
||||||
|
- "rabbitmq_data:/var/lib/rabbitmq"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- "rabbitmq.hotpocket.work.bthlabs.net"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
keycloak_data:
|
||||||
|
keycloak_var:
|
||||||
|
rabbitmq_data:
|
12
docker-compose.yaml
Normal file
12
docker-compose.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
services: {}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- path: "./docker-compose-caddy.yaml"
|
||||||
|
- path: "./docker-compose-cloud.yaml"
|
||||||
|
- path: "./services/backend/docker-compose.yaml"
|
||||||
|
- path: "./services/packages/docker-compose.yaml"
|
||||||
|
|
||||||
|
volumes: {}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
6
invoke.json
Normal file
6
invoke.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"run": {
|
||||||
|
"echo": true,
|
||||||
|
"pty": true
|
||||||
|
}
|
||||||
|
}
|
30
poetry.lock
generated
Normal file
30
poetry.lock
generated
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hotpocket-workspace-tools"
|
||||||
|
version = "1.0.0.dev0"
|
||||||
|
description = "HotPocket Workspace Tools"
|
||||||
|
optional = false
|
||||||
|
python-versions = "^3.12"
|
||||||
|
files = []
|
||||||
|
develop = true
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "directory"
|
||||||
|
url = "services/packages/workspace_tools"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "invoke"
|
||||||
|
version = "2.2.0"
|
||||||
|
description = "Pythonic task execution"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"},
|
||||||
|
{file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.12"
|
||||||
|
content-hash = "ec33c3b3ec0f988e333872bdd134c1adce0782e98512dd2484cb85009b3da6cb"
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "hotpocket-workspace"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "HotPocket Workspace"
|
||||||
|
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.12"
|
||||||
|
hotpocket-workspace-tools = {path = "services/packages/workspace_tools", develop = true}
|
||||||
|
invoke = "2.2.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
10
services/.dockerignore
Normal file
10
services/.dockerignore
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
_tmp/
|
||||||
|
backend/node_modules/
|
||||||
|
backend/ops/metal/
|
||||||
|
backend/hotpocket_backend/playground.py
|
||||||
|
backend/hotpocket_backend/secrets/docker/
|
||||||
|
backend/hotpocket_backend/settings/docker/
|
||||||
|
backend/hotpocket_backend/secrets/metal/
|
||||||
|
backend/hotpocket_backend/settings/metal/
|
||||||
|
backend/hotpocket_backend/static/
|
||||||
|
.envrc*
|
10
services/backend/.gitignore
vendored
Normal file
10
services/backend/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
hotpocket_backend/playground.py
|
||||||
|
hotpocket_backend/secrets/docker
|
||||||
|
hotpocket_backend/secrets/metal
|
||||||
|
hotpocket_backend/settings/docker
|
||||||
|
hotpocket_backend/settings/metal
|
||||||
|
hotpocket_backend/static/
|
||||||
|
node_modules/
|
||||||
|
run/celery-beat-schedule*
|
||||||
|
run/*.sqlite
|
||||||
|
run/uploads/
|
113
services/backend/Dockerfile
Normal file
113
services/backend/Dockerfile
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
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"]
|
||||||
|
|
||||||
|
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-python-20250819-01 AS deployment-build
|
||||||
|
|
||||||
|
ARG APP_USER_UID
|
||||||
|
ARG APP_USER_GID
|
||||||
|
ARG IMAGE_ID
|
||||||
|
|
||||||
|
RUN mkdir /srv/app/hotpocket_backend /srv/packages/common /srv/packages/soa
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/hotpocket_backend/ /srv/app/hotpocket_backend/
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/poetry.lock backend/pyproject.toml backend/manage.py backend/README.md /srv/app/
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/common/ /srv/packages/common/
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/soa/ /srv/packages/soa/
|
||||||
|
|
||||||
|
RUN poetry install --only main,deployment && \
|
||||||
|
minify -i --css-precision 0 --js-precision 0 --js-version 2022 hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend*.css hotpocket_backend/apps/ui/static/ui/js/hotpocket*.js && \
|
||||||
|
./manage.py collectstatic --settings hotpocket_backend.settings.deployment.build --noinput && \
|
||||||
|
find hotpocket_backend/static/ -name "*.map*" -delete && \
|
||||||
|
rm -f hotpocket_backend/settings/deployment/build.py && \
|
||||||
|
rm -rf node_modules/
|
||||||
|
|
||||||
|
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:base-20250819-01 AS deployment-base
|
||||||
|
|
||||||
|
ARG APP_USER_UID
|
||||||
|
ARG APP_USER_GID
|
||||||
|
ARG IMAGE_ID
|
||||||
|
|
||||||
|
ENV HOTPOCKET_BACKEND_IMAGE_ID=${IMAGE_ID}
|
||||||
|
ENV PYTHONPATH="/srv/local"
|
||||||
|
|
||||||
|
COPY --from=deployment-build /srv/app /srv/app
|
||||||
|
COPY --from=deployment-build /srv/packages /srv/packages
|
||||||
|
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/deployment/gunicorn.conf.py backend/ops/deployment/gunicorn.logging.conf /srv/lib/
|
||||||
|
RUN chown -R $APP_USER_UID:$APP_USER_GID /srv
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y libpq5 dumb-init && \
|
||||||
|
apt-get clean autoclean && \
|
||||||
|
apt-get autoremove --yes && \
|
||||||
|
rm -rf /var/lib/apt /var/lib/dpkg && \
|
||||||
|
rm -rf /home/app/.cache
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
ENTRYPOINT ["/srv/bin/entrypoint-deployment.sh"]
|
||||||
|
CMD ["/srv/venv/bin/gunicorn", "-c", "/srv/lib/gunicorn.conf.py", "hotpocket_backend.wsgi:application"]
|
||||||
|
|
||||||
|
FROM deployment-base AS deployment
|
||||||
|
|
||||||
|
ARG APP_USER_UID
|
||||||
|
ARG APP_USER_GID
|
||||||
|
ARG IMAGE_ID
|
||||||
|
|
||||||
|
ENV DJANGO_SETTINGS_MODULE=hotpocket_backend.settings.deployment.webapp
|
||||||
|
ENV HOTPOCKET_BACKEND_ENV=deployment
|
||||||
|
ENV HOTPOCKET_BACKEND_APP=webapp
|
||||||
|
|
||||||
|
VOLUME ["/srv/run", "/srv/uploads"]
|
||||||
|
|
||||||
|
FROM deployment-base AS aio
|
||||||
|
|
||||||
|
ARG APP_USER_UID
|
||||||
|
ARG APP_USER_GID
|
||||||
|
ARG IMAGE_ID
|
||||||
|
|
||||||
|
ENV DJANGO_SETTINGS_MODULE=hotpocket_backend.settings.aio
|
||||||
|
ENV HOTPOCKET_BACKEND_ENV=aio
|
||||||
|
ENV HOTPOCKET_BACKEND_APP=webapp
|
||||||
|
ENV HOTPOCKET_BACKEND_DEBUG=false
|
||||||
|
ENV HOTPOCKET_BACKEND_DATABASE_ENGINE=django.db.backends.sqlite3
|
||||||
|
ENV HOTPOCKET_BACKEND_DATABASE_NAME=/srv/run/hotpocket-backend-aio.sqlite
|
||||||
|
ENV HOTPOCKET_BACKEND_DATABASE_USER=
|
||||||
|
ENV HOTPOCKET_BACKEND_DATABASE_PASSWORD=
|
||||||
|
ENV HOTPOCKET_BACKEND_DATABASE_HOST=
|
||||||
|
ENV HOTPOCKET_BACKEND_DATABASE_PORT=
|
||||||
|
ENV HOTPOCKET_BACKEND_CELERY_IGNORE_RESULT=true
|
||||||
|
ENV HOTPOCKET_BACKEND_CELERY_ALWAYS_EAGER=true
|
||||||
|
ENV HOTPOCKET_BACKEND_GUNICORN_WORKERS=2
|
||||||
|
ENV HOTPOCKET_BACKEND_RUN_MIGRATIONS=true
|
||||||
|
ENV HOTPOCKET_BACKEND_UPLOADS_PATH=/srv/run/uploads
|
||||||
|
|
||||||
|
VOLUME ["/srv/run"]
|
||||||
|
|
||||||
|
FROM development AS ci
|
||||||
|
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ /srv/app/
|
||||||
|
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/packages/
|
||||||
|
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 && \
|
||||||
|
ln -s /srv/app/ops/docker/secrets /srv/app/hotpocket_backend/secrets/docker && \
|
||||||
|
chown -R $APP_USER_UID:$APP_USER_GID /srv
|
3
services/backend/README.md
Normal file
3
services/backend/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# HotPocket by BTHLabs
|
||||||
|
|
||||||
|
This repository contains the _HotPocket Backend_ project.
|
30
services/backend/docker-compose-aio.yaml
Normal file
30
services/backend/docker-compose-aio.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
services:
|
||||||
|
webapp:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "backend/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-local"
|
||||||
|
environment:
|
||||||
|
DJANGO_SETTINGS_MODULE: "hotpocket_backend.settings.aio"
|
||||||
|
HOTPOCKET_BACKEND_ENV: "${HOTPOCKET_BACKEND_ENV:-aio}"
|
||||||
|
HOTPOCKET_BACKEND_APP: "webapp"
|
||||||
|
HOTPOCKET_BACKEND_DEBUG: "false"
|
||||||
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
|
HOTPOCKET_BACKEND_DATABASE_PAYLOAD: '{"engine":"django.db.backends.sqlite3","name":"/srv/run/hotpocket-backend-aio.sqlite"}'
|
||||||
|
HOTPOCKET_BACKEND_CELERY_IGNORE_RESULT: "true"
|
||||||
|
HOTPOCKET_BACKEND_CELERY_ALWAYS_EAGER: "true"
|
||||||
|
HOTPOCKET_BACKEND_GUNICORN_WORKERS: "2"
|
||||||
|
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"
|
||||||
|
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD: "hotpocketm4st3r"
|
||||||
|
HOTPOCKET_BACKEND_RUN_MIGRATIONS: "true"
|
||||||
|
HOTPOCKET_BACKEND_UPLOADS_PATH: "/srv/run/uploads"
|
||||||
|
volumes:
|
||||||
|
- "./run:/srv/run"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- "backend-webapp.hotpocket.work.bthlabs.net"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
31
services/backend/docker-compose-ci.yaml
Normal file
31
services/backend/docker-compose-ci.yaml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
services:
|
||||||
|
backend-ci:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "backend/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-local"
|
||||||
|
command: "echo 'NOOP'"
|
||||||
|
environment:
|
||||||
|
PYTHONBREAKPOINT: "ipdb.set_trace"
|
||||||
|
PYTHONPATH: "/srv/packages/common"
|
||||||
|
DJANGO_SETTINGS_MODULE: "hotpocket_backend.settings.docker.webapp"
|
||||||
|
DJANGO_TESTING_SETTINGS_MODULE: "hotpocket_backend.settings.docker.testing"
|
||||||
|
HOTPOCKET_BACKEND_ENV: "docker"
|
||||||
|
HOTPOCKET_BACKEND_APP: "webapp"
|
||||||
|
POSTGRES_HOSTPORT: "${POSTGRES_HOST:-postgres.hotpocket.work.bthlabs.net}:${POSTGRES_PORT:-5432}"
|
||||||
|
RABBITMQ_HOSTPORT: "${RABBITMQ_HOST:-rabbitmq.hotpocket.work.bthlabs.net}:${RABBITMQ_PORT:-5672}"
|
||||||
|
# REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
|
||||||
|
RUN_POETRY_INSTALL: "true"
|
||||||
|
RUN_YARN_INSTALL: "true"
|
||||||
|
SETUP_BACKEND: "true"
|
||||||
|
SETUP_FRONTEND: "true"
|
||||||
|
volumes:
|
||||||
|
- "backend_venv:/srv/venv"
|
||||||
|
- "backend_node_modules:/srv/node_modules"
|
||||||
|
restart: "no"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
depends_on:
|
||||||
|
- "postgres"
|
||||||
|
- "rabbitmq"
|
182
services/backend/docker-compose.yaml
Normal file
182
services/backend/docker-compose.yaml
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
services:
|
||||||
|
backend-management:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "backend/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:local"
|
||||||
|
command: "echo 'NOOP'"
|
||||||
|
environment: &backend-env
|
||||||
|
PYTHONBREAKPOINT: "ipdb.set_trace"
|
||||||
|
PYTHONPATH: "/srv/packages/common"
|
||||||
|
DJANGO_SETTINGS_MODULE: "hotpocket_backend.settings.docker.webapp"
|
||||||
|
DJANGO_TESTING_SETTINGS_MODULE: "hotpocket_backend.settings.docker.testing"
|
||||||
|
HOTPOCKET_BACKEND_ENV: "${HOTPOCKET_BACKEND_ENV:-docker}"
|
||||||
|
HOTPOCKET_BACKEND_APP: "webapp"
|
||||||
|
POSTGRES_HOSTPORT: "${POSTGRES_HOST:-postgres.hotpocket.work.bthlabs.net}:${POSTGRES_PORT:-5432}"
|
||||||
|
RABBITMQ_HOSTPORT: "${RABBITMQ_HOST:-rabbitmq.hotpocket.work.bthlabs.net}:${RABBITMQ_PORT:-5672}"
|
||||||
|
KEYCLOAK_HOSTPORT: "${KEYCLOAK_HOST:-auth.hotpocket.work.bthlabs.net}:${KEYCLOAK_PORT:-8080}"
|
||||||
|
REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
|
||||||
|
RUN_POETRY_INSTALL: "true"
|
||||||
|
RUN_YARN_INSTALL: "true"
|
||||||
|
SETUP_BACKEND: "true"
|
||||||
|
SETUP_FRONTEND: "true"
|
||||||
|
volumes:
|
||||||
|
- "backend_venv:/srv/venv"
|
||||||
|
- "backend_node_modules:/srv/node_modules"
|
||||||
|
- ".:/srv/app"
|
||||||
|
- "./ops/docker/settings:/srv/app/hotpocket_backend/settings/docker"
|
||||||
|
- "./ops/docker/secrets:/srv/app/hotpocket_backend/secrets/docker"
|
||||||
|
- "../packages:/srv/packages"
|
||||||
|
- "../tls:/srv/tls"
|
||||||
|
restart: "no"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
depends_on:
|
||||||
|
- "postgres"
|
||||||
|
- "keycloak"
|
||||||
|
- "rabbitmq"
|
||||||
|
|
||||||
|
backend-webapp:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "backend/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:local"
|
||||||
|
command:
|
||||||
|
- "./manage.py"
|
||||||
|
- "runserver"
|
||||||
|
- "0.0.0.0:8000"
|
||||||
|
environment:
|
||||||
|
<<: *backend-env
|
||||||
|
RUN_POETRY_INSTALL: "nope"
|
||||||
|
RUN_YARN_INSTALL: "nope"
|
||||||
|
volumes:
|
||||||
|
- "backend_venv:/srv/venv"
|
||||||
|
- "backend_node_modules:/srv/node_modules"
|
||||||
|
- ".:/srv/app"
|
||||||
|
- "./ops/docker/settings:/srv/app/hotpocket_backend/settings/docker"
|
||||||
|
- "./ops/docker/secrets:/srv/app/hotpocket_backend/secrets/docker"
|
||||||
|
- "../packages:/srv/packages"
|
||||||
|
- "../tls:/srv/tls"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- "backend-webapp.hotpocket.work.bthlabs.net"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
depends_on:
|
||||||
|
- "postgres"
|
||||||
|
- "keycloak"
|
||||||
|
- "rabbitmq"
|
||||||
|
|
||||||
|
backend-admin:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "backend/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:local"
|
||||||
|
command:
|
||||||
|
- "./manage.py"
|
||||||
|
- "runserver"
|
||||||
|
- "0.0.0.0:8000"
|
||||||
|
environment:
|
||||||
|
<<: *backend-env
|
||||||
|
DJANGO_SETTINGS_MODULE: "hotpocket_backend.settings.docker.admin"
|
||||||
|
HOTPOCKET_BACKEND_APP: "admin"
|
||||||
|
RUN_POETRY_INSTALL: "nope"
|
||||||
|
RUN_YARN_INSTALL: "nope"
|
||||||
|
volumes:
|
||||||
|
- "backend_venv:/srv/venv"
|
||||||
|
- "backend_node_modules:/srv/node_modules"
|
||||||
|
- ".:/srv/app"
|
||||||
|
- "./ops/docker/settings:/srv/app/hotpocket_backend/settings/docker"
|
||||||
|
- "./ops/docker/secrets:/srv/app/hotpocket_backend/secrets/docker"
|
||||||
|
- "../packages:/srv/packages"
|
||||||
|
- "../tls:/srv/tls"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- "backend-admin.hotpocket.work.bthlabs.net"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
depends_on:
|
||||||
|
- "postgres"
|
||||||
|
- "keycloak"
|
||||||
|
- "rabbitmq"
|
||||||
|
|
||||||
|
backend-celery-worker:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "backend/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:local"
|
||||||
|
command:
|
||||||
|
- "celery"
|
||||||
|
- "-A"
|
||||||
|
- "hotpocket_backend.celery:app"
|
||||||
|
- "worker"
|
||||||
|
- "--loglevel=INFO"
|
||||||
|
- "-Q"
|
||||||
|
- "celery,webapp"
|
||||||
|
- "-c"
|
||||||
|
- "4"
|
||||||
|
environment:
|
||||||
|
<<: *backend-env
|
||||||
|
RUN_POETRY_INSTALL: "nope"
|
||||||
|
RUN_YARN_INSTALL: "nope"
|
||||||
|
volumes:
|
||||||
|
- "backend_venv:/srv/venv"
|
||||||
|
- "backend_node_modules:/srv/node_modules"
|
||||||
|
- ".:/srv/app"
|
||||||
|
- "./ops/docker/settings:/srv/app/hotpocket_backend/settings/docker"
|
||||||
|
- "./ops/docker/secrets:/srv/app/hotpocket_backend/secrets/docker"
|
||||||
|
- "../packages:/srv/packages"
|
||||||
|
- "../tls:/srv/tls"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
depends_on:
|
||||||
|
- "postgres"
|
||||||
|
- "keycloak"
|
||||||
|
- "rabbitmq"
|
||||||
|
|
||||||
|
backend-celery-beat:
|
||||||
|
build:
|
||||||
|
context: ".."
|
||||||
|
dockerfile: "backend/Dockerfile"
|
||||||
|
target: "development"
|
||||||
|
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:local"
|
||||||
|
command:
|
||||||
|
- "celery"
|
||||||
|
- "-A"
|
||||||
|
- "hotpocket_backend.celery:app"
|
||||||
|
- "beat"
|
||||||
|
- "--loglevel=INFO"
|
||||||
|
- "-s"
|
||||||
|
- "/srv/app/run/celery-beat-schedule-docker"
|
||||||
|
environment:
|
||||||
|
<<: *backend-env
|
||||||
|
RUN_POETRY_INSTALL: "nope"
|
||||||
|
RUN_YARN_INSTALL: "nope"
|
||||||
|
volumes:
|
||||||
|
- "backend_venv:/srv/venv"
|
||||||
|
- "backend_node_modules:/srv/node_modules"
|
||||||
|
- ".:/srv/app"
|
||||||
|
- "./ops/docker/settings:/srv/app/hotpocket_backend/settings/docker"
|
||||||
|
- "./ops/docker/secrets:/srv/app/hotpocket_backend/secrets/docker"
|
||||||
|
- "../packages:/srv/packages"
|
||||||
|
- "../tls:/srv/tls"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
depends_on:
|
||||||
|
- "postgres"
|
||||||
|
- "keycloak"
|
||||||
|
- "rabbitmq"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backend_venv:
|
||||||
|
backend_node_modules:
|
67
services/backend/eslint.config.js
Normal file
67
services/backend/eslint.config.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// eslint.config.js
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import {defineConfig} from 'eslint/config';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'eslint.config.js',
|
||||||
|
'hotpocket_backend/apps/ui/static/ui/js/hotpocket.*.js',
|
||||||
|
],
|
||||||
|
plugins: {
|
||||||
|
js,
|
||||||
|
},
|
||||||
|
extends: ['js/recommended'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'error',
|
||||||
|
'quotes': [
|
||||||
|
'error',
|
||||||
|
'single',
|
||||||
|
{'avoidEscape': true, 'allowTemplateLiterals': true},
|
||||||
|
],
|
||||||
|
'no-unused-vars': ['error', {'args': 'none'}],
|
||||||
|
'no-console': ['error', {'allow': ['warn', 'error']}],
|
||||||
|
'no-empty': ['error', {'allowEmptyCatch': true}],
|
||||||
|
'array-bracket-spacing': ['error', 'never'],
|
||||||
|
'block-spacing': ['error', 'always'],
|
||||||
|
'brace-style': ['error', '1tbs', {'allowSingleLine': true}],
|
||||||
|
'camelcase': ['error', {'properties': 'never'}],
|
||||||
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
|
'comma-spacing': ['error', {'before': false, 'after': true}],
|
||||||
|
'comma-style': ['error', 'last'],
|
||||||
|
'computed-property-spacing': ['error', 'never'],
|
||||||
|
'key-spacing': [
|
||||||
|
'error', {'beforeColon': false, 'afterColon': true, 'mode': 'strict'},
|
||||||
|
],
|
||||||
|
'keyword-spacing': ['error', { 'before': true, 'after': true }],
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
|
'max-len': ['error', 120],
|
||||||
|
'no-multiple-empty-lines': 'error',
|
||||||
|
'no-spaced-func': 'error',
|
||||||
|
'no-trailing-spaces': 'error',
|
||||||
|
'no-unreachable': 'warn',
|
||||||
|
'no-whitespace-before-property': 'error',
|
||||||
|
'object-curly-spacing': 'off',
|
||||||
|
'one-var-declaration-per-line': ['error', 'always'],
|
||||||
|
'one-var': ['error', 'never'],
|
||||||
|
'semi-spacing': ['error', {'before': false, 'after': true}],
|
||||||
|
'semi': ['error', 'always'],
|
||||||
|
'space-before-function-paren': ['error', 'always'],
|
||||||
|
'space-before-blocks': ['error', 'always'],
|
||||||
|
'space-in-parens': ['error', 'never'],
|
||||||
|
'space-infix-ops': 'error',
|
||||||
|
'unicode-bom': ['error', 'never'],
|
||||||
|
'no-useless-escape': 'off',
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'no-invalid-this': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
8
services/backend/hotpocket_backend/__init__.py
Normal file
8
services/backend/hotpocket_backend/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import _meta
|
||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ('celery_app',)
|
||||||
|
__version__ = _meta.version
|
4
services/backend/hotpocket_backend/_meta.py
Normal file
4
services/backend/hotpocket_backend/_meta.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
version = '1.0.0'
|
0
services/backend/hotpocket_backend/apps/__init__.py
Normal file
0
services/backend/hotpocket_backend/apps/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .account import AccountAdmin # noqa: F401
|
|
@ -0,0 +1,17 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.accounts.models import Account
|
||||||
|
|
||||||
|
|
||||||
|
class AccountAdmin(UserAdmin):
|
||||||
|
list_display = (*UserAdmin.list_display, 'is_active')
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Account, AccountAdmin)
|
12
services/backend/hotpocket_backend/apps/accounts/apps.py
Normal file
12
services/backend/hotpocket_backend/apps/accounts/apps.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
label = 'accounts'
|
||||||
|
name = 'hotpocket_backend.apps.accounts'
|
||||||
|
verbose_name = _('Accounts')
|
11
services/backend/hotpocket_backend/apps/accounts/checks.py
Normal file
11
services/backend/hotpocket_backend/apps/accounts/checks.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.accounts.types import PAccount
|
||||||
|
|
||||||
|
|
||||||
|
def is_authenticated_and_active_account(account: PAccount) -> bool:
|
||||||
|
return all((
|
||||||
|
account.is_authenticated,
|
||||||
|
account.is_active,
|
||||||
|
))
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.core.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def hotpocket_oidc(request: HttpRequest) -> dict:
|
||||||
|
return {
|
||||||
|
'HOTPOCKET_OIDC_IS_ENABLED': settings.SECRETS.OIDC.is_enabled,
|
||||||
|
'HOTPOCKET_OIDC_DISPLAY_NAME': settings.SECRETS.OIDC.display_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def auth_settings(request: HttpRequest) -> dict:
|
||||||
|
return {
|
||||||
|
'MODEL_AUTH_IS_DISABLED': settings.MODEL_AUTH_IS_DISABLED,
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
|
|
||||||
|
from .checks import is_authenticated_and_active_account
|
||||||
|
|
||||||
|
|
||||||
|
def account_required(function=None,
|
||||||
|
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||||
|
login_url=None,
|
||||||
|
):
|
||||||
|
actual_decorator = user_passes_test(
|
||||||
|
is_authenticated_and_active_account,
|
||||||
|
login_url=login_url,
|
||||||
|
redirect_field_name=redirect_field_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if function:
|
||||||
|
return actual_decorator(function)
|
||||||
|
|
||||||
|
return actual_decorator
|
|
@ -0,0 +1,60 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
import django.db
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.accounts.models import Account
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Create initial Account'
|
||||||
|
|
||||||
|
def add_arguments(self, parser: ArgumentParser):
|
||||||
|
parser.add_argument(
|
||||||
|
'username',
|
||||||
|
help='Username for the Account',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'password',
|
||||||
|
help='Password for the Account',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-d', '--dry-run', action='store_true', default=False,
|
||||||
|
help='Dry run',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
LOGGER.debug('args=`%s` options=`%s`', args, options)
|
||||||
|
username = options.get('username')
|
||||||
|
password = options.get('password')
|
||||||
|
dry_run = options.get('dry_run', False)
|
||||||
|
|
||||||
|
with django.db.transaction.atomic():
|
||||||
|
current_account = Account.objects.filter(username=username).first()
|
||||||
|
if current_account is not None:
|
||||||
|
LOGGER.info(
|
||||||
|
'Account already exists: account=`%s`', current_account,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
account = Account.objects.create(
|
||||||
|
username=username,
|
||||||
|
first_name=username,
|
||||||
|
is_superuser=True,
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
account.set_password(password)
|
||||||
|
account.save()
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
'Account created: account=`%s`', account,
|
||||||
|
)
|
||||||
|
|
||||||
|
if dry_run is True:
|
||||||
|
raise RuntimeError('DRY RUN')
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-30 20:37
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid6
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Account',
|
||||||
|
fields=[
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('id', models.UUIDField(default=uuid6.uuid7, primary_key=True, serialize=False)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='account_set', related_query_name='account', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='account_set', related_query_name='account', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Account',
|
||||||
|
'verbose_name_plural': 'Accounts',
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-07-10 19:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='account',
|
||||||
|
name='settings',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-08-02 19:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_account_settings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='account',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-08-08 19:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0003_account_updated_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='settings',
|
||||||
|
field=models.JSONField(db_column='settings', default=dict),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='account',
|
||||||
|
old_name='settings',
|
||||||
|
new_name='raw_settings',
|
||||||
|
),
|
||||||
|
]
|
14
services/backend/hotpocket_backend/apps/accounts/mixins.py
Normal file
14
services/backend/hotpocket_backend/apps/accounts/mixins.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
|
|
||||||
|
from .checks import is_authenticated_and_active_account
|
||||||
|
|
||||||
|
|
||||||
|
class AccountRequiredMixin(AccessMixin):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if is_authenticated_and_active_account(self.request.user) is False:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
|
@ -0,0 +1 @@
|
||||||
|
from .account import Account # noqa: F401
|
|
@ -0,0 +1,71 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib.auth.models import (
|
||||||
|
AbstractUser,
|
||||||
|
Group,
|
||||||
|
Permission,
|
||||||
|
UserManager,
|
||||||
|
)
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
import uuid6
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveAccountsManager(models.Manager):
|
||||||
|
def get_queryset(self) -> models.QuerySet:
|
||||||
|
return super().get_queryset().filter(
|
||||||
|
is_active=True,
|
||||||
|
is_staff=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Account(AbstractUser):
|
||||||
|
id = models.UUIDField(
|
||||||
|
null=False, blank=False, default=uuid6.uuid7, primary_key=True,
|
||||||
|
)
|
||||||
|
groups = models.ManyToManyField(
|
||||||
|
Group,
|
||||||
|
verbose_name=_('groups'),
|
||||||
|
blank=True,
|
||||||
|
help_text=_(
|
||||||
|
(
|
||||||
|
'The groups this user belongs to. A user will get all '
|
||||||
|
'permissions granted to each of their groups.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
related_name='account_set',
|
||||||
|
related_query_name='account',
|
||||||
|
)
|
||||||
|
user_permissions = models.ManyToManyField(
|
||||||
|
Permission,
|
||||||
|
verbose_name=_('user permissions'),
|
||||||
|
blank=True,
|
||||||
|
help_text=_('Specific permissions for this user.'),
|
||||||
|
related_name='account_set',
|
||||||
|
related_query_name='account',
|
||||||
|
)
|
||||||
|
raw_settings = models.JSONField(default=dict, db_column='settings')
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
objects = UserManager()
|
||||||
|
active_accounts = ActiveAccountsManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Account')
|
||||||
|
verbose_name_plural = _('Accounts')
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'<Account pk={self.pk} username={self.username}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def settings(self) -> dict:
|
||||||
|
result = {**self.raw_settings}
|
||||||
|
|
||||||
|
auto_load_embeds = result.get('auto_load_embeds', None)
|
||||||
|
if isinstance(auto_load_embeds, str) is True:
|
||||||
|
result['auto_load_embeds'] = (auto_load_embeds == 'True')
|
||||||
|
else:
|
||||||
|
result['auto_load_embeds'] = auto_load_embeds
|
||||||
|
|
||||||
|
return result
|
40
services/backend/hotpocket_backend/apps/accounts/social.py
Normal file
40
services/backend/hotpocket_backend/apps/accounts/social.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from social_core.backends.open_id_connect import OpenIdConnectAuth
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HotPocketOpenIdConnectAuth(OpenIdConnectAuth):
|
||||||
|
name = 'hotpocket_oidc'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_roles_from_response(response) -> list[str]:
|
||||||
|
from hotpocket_backend.apps.core.conf import settings
|
||||||
|
return response.\
|
||||||
|
get('resource_access', {}).\
|
||||||
|
get(settings.SECRETS.OIDC.key, {}).\
|
||||||
|
get('roles', [])
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_is_staff(strategy, details, response, user=None, *args, **kwargs):
|
||||||
|
if user is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
roles = _get_roles_from_response(response)
|
||||||
|
user.is_staff = 'staff' in roles
|
||||||
|
|
||||||
|
strategy.storage.user.changed(user)
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_is_superuser(strategy, details, response, user=None, *args, **kwargs):
|
||||||
|
if user is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
roles = _get_roles_from_response(response)
|
||||||
|
user.is_superuser = 'superuser' in roles
|
||||||
|
|
||||||
|
strategy.storage.user.changed(user)
|
18
services/backend/hotpocket_backend/apps/accounts/types.py
Normal file
18
services/backend/hotpocket_backend/apps/accounts/types.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class PAccount(typing.Protocol):
|
||||||
|
id: uuid.UUID
|
||||||
|
username: str
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
email: str
|
||||||
|
|
||||||
|
is_active: bool
|
||||||
|
is_anonymous: bool
|
||||||
|
is_authenticated: bool
|
||||||
|
pk: uuid.UUID
|
17
services/backend/hotpocket_backend/apps/admin.py
Normal file
17
services/backend/hotpocket_backend/apps/admin.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.admin.apps import AdminConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class HotPocketAdminSite(admin.AdminSite):
|
||||||
|
site_header = 'HotPocket'
|
||||||
|
site_title = 'HotPocket by BTHLabs'
|
||||||
|
index_title = _('Home')
|
||||||
|
login_template = 'core/admin/login.html'
|
||||||
|
|
||||||
|
|
||||||
|
class HotPocketAdminConfig(AdminConfig):
|
||||||
|
default_site = 'hotpocket_backend.apps.admin.HotPocketAdminSite'
|
22
services/backend/hotpocket_backend/apps/bot/apps.py
Normal file
22
services/backend/hotpocket_backend/apps/bot/apps.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class BotConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
label = 'bot'
|
||||||
|
name = 'hotpocket_backend.apps.bot'
|
||||||
|
verbose_name = _('Bot')
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
super().ready()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hotpocket_backend.apps.bot import conf
|
||||||
|
conf.bot_settings = conf.from_django_settings()
|
||||||
|
except Exception as exception:
|
||||||
|
raise ImproperlyConfigured('Invalid bot settings') from exception
|
54
services/backend/hotpocket_backend/apps/bot/conf.py
Normal file
54
services/backend/hotpocket_backend/apps/bot/conf.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from .types import PStrategy
|
||||||
|
|
||||||
|
DEFAULT_STRATEGY = 'hotpocket_backend.apps.bot.strategy.basic:BasicStrategy'
|
||||||
|
DEFAULT_BANNED_HOSTNAMES = [
|
||||||
|
# YT returns dummy data when I try to fetch the page and extract
|
||||||
|
# metadata. I'd have to use Google APIs for that and it's 11:30 PM...
|
||||||
|
'youtube.com',
|
||||||
|
'youtu.be',
|
||||||
|
# Reddit's response is too generic to pull any useful info from it.
|
||||||
|
# Since they forced Apollo to shut down, I refuse to even think about
|
||||||
|
# interacting with their API :P.
|
||||||
|
'reddit.com',
|
||||||
|
# Twitter, amirite?
|
||||||
|
'twitter.com',
|
||||||
|
't.co',
|
||||||
|
'x.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(kw_only=True)
|
||||||
|
class Settings:
|
||||||
|
STRATEGY: str
|
||||||
|
BANNED_HOSTNAMES: list[str]
|
||||||
|
|
||||||
|
def get_strategy(self, *, url: str) -> PStrategy:
|
||||||
|
from hotpocket_common.loader import load_module_attribute
|
||||||
|
|
||||||
|
strategy = load_module_attribute(self.STRATEGY)
|
||||||
|
return strategy(url)
|
||||||
|
|
||||||
|
|
||||||
|
def from_django_settings() -> Settings:
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
return Settings(
|
||||||
|
STRATEGY=getattr(
|
||||||
|
settings,
|
||||||
|
'HOTPOCKET_BOT_STRATEGY',
|
||||||
|
DEFAULT_STRATEGY,
|
||||||
|
),
|
||||||
|
BANNED_HOSTNAMES=getattr(
|
||||||
|
settings,
|
||||||
|
'HOTPOCKET_BOT_BANNED_HOSTNAMES',
|
||||||
|
DEFAULT_BANNED_HOSTNAMES,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
bot_settings: Settings = None # type: ignore[assignment]
|
11
services/backend/hotpocket_backend/apps/bot/dto/strategy.py
Normal file
11
services/backend/hotpocket_backend/apps/bot/dto/strategy.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
|
||||||
|
class FetchResult(pydantic.BaseModel):
|
||||||
|
status_code: int
|
||||||
|
content: bytes
|
||||||
|
content_type: str | None
|
||||||
|
encoding: str
|
|
@ -0,0 +1 @@
|
||||||
|
from .bot import BotService # noqa: F401
|
16
services/backend/hotpocket_backend/apps/bot/services/bot.py
Normal file
16
services/backend/hotpocket_backend/apps/bot/services/bot.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.bot.conf import bot_settings
|
||||||
|
from hotpocket_backend.apps.bot.types import PStrategy
|
||||||
|
from hotpocket_soa.dto import BotResultOut
|
||||||
|
|
||||||
|
|
||||||
|
class BotService:
|
||||||
|
def is_netloc_banned(self, *, url: str) -> bool:
|
||||||
|
strategy: PStrategy = bot_settings.get_strategy(url=url)
|
||||||
|
return strategy.is_netloc_banned()
|
||||||
|
|
||||||
|
def handle(self, *, url: str) -> BotResultOut:
|
||||||
|
strategy: PStrategy = bot_settings.get_strategy(url=url)
|
||||||
|
return strategy.run()
|
|
@ -0,0 +1 @@
|
||||||
|
from .basic import BasicStrategy # noqa: F401
|
174
services/backend/hotpocket_backend/apps/bot/strategy/base.py
Normal file
174
services/backend/hotpocket_backend/apps/bot/strategy/base.py
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyquery import PyQuery
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from hotpocket_backend._meta import version as backend_version
|
||||||
|
from hotpocket_backend.apps.bot.conf import bot_settings
|
||||||
|
from hotpocket_backend.apps.bot.dto.strategy import FetchResult
|
||||||
|
from hotpocket_common.url import URL
|
||||||
|
from hotpocket_soa.dto import BotResultOut
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Strategy(abc.ABC):
|
||||||
|
class StrategyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FetchError(StrategyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RuntimeError(StrategyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
USER_AGENT = (
|
||||||
|
'Mozilla/5.0 '
|
||||||
|
'('
|
||||||
|
'compatible; '
|
||||||
|
f'BTHLabsHotPocketBot/{backend_version}; '
|
||||||
|
'+https://hotpocket.app/bot.txt'
|
||||||
|
')'
|
||||||
|
)
|
||||||
|
TITLE_TAG_SELECTORS = [
|
||||||
|
'head > meta[name=title]',
|
||||||
|
'head > meta[property="og:title"]',
|
||||||
|
'head > title',
|
||||||
|
]
|
||||||
|
DESCRIPTION_TAG_SELECTORS = [
|
||||||
|
'head > meta[property="og:description"]',
|
||||||
|
'head > meta[name=description]',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, url: str):
|
||||||
|
super().__init__()
|
||||||
|
self.url = url
|
||||||
|
self.parsed_url = URL(self.url)
|
||||||
|
|
||||||
|
self.logger = self.get_logger()
|
||||||
|
|
||||||
|
def get_logger(self) -> logging.Logger:
|
||||||
|
return LOGGER.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
|
def is_netloc_banned(self) -> bool:
|
||||||
|
result = False
|
||||||
|
|
||||||
|
for banned_netloc in bot_settings.BANNED_HOSTNAMES:
|
||||||
|
hostname = self.parsed_url.hostname
|
||||||
|
if hostname is not None and hostname.endswith(banned_netloc) is True:
|
||||||
|
result = True
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def fetch(self, url: str) -> FetchResult:
|
||||||
|
try:
|
||||||
|
response = requests.request(
|
||||||
|
'GET',
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
'User-Agent': self.USER_AGENT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return FetchResult.model_validate(dict(
|
||||||
|
status_code=response.status_code,
|
||||||
|
content=response.content,
|
||||||
|
content_type=response.headers.get('Content-Type', None),
|
||||||
|
encoding=response.encoding or response.apparent_encoding,
|
||||||
|
))
|
||||||
|
except Exception as exception:
|
||||||
|
self.logger.error(
|
||||||
|
'Fetch error: %s', exception, exc_info=True,
|
||||||
|
)
|
||||||
|
raise self.FetchError() from exception
|
||||||
|
|
||||||
|
def extract_title_and_description_from_html(self, content: str) -> tuple[str | None, str | None]:
|
||||||
|
dom = PyQuery(content)
|
||||||
|
|
||||||
|
title: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
for selector in self.TITLE_TAG_SELECTORS:
|
||||||
|
title_tags = dom.find(selector)
|
||||||
|
if len(title_tags) > 0:
|
||||||
|
title_tag = PyQuery(title_tags[0])
|
||||||
|
if title_tag.is_('meta'):
|
||||||
|
title = title_tag.attr('content')
|
||||||
|
else:
|
||||||
|
title = title_tag.text()
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
for selector in self.DESCRIPTION_TAG_SELECTORS:
|
||||||
|
description_tags = dom.find(selector)
|
||||||
|
if len(description_tags) > 0:
|
||||||
|
description = PyQuery(description_tags[0]).attr('content')
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
if description is None:
|
||||||
|
try:
|
||||||
|
description = PyQuery(dom.find('p')[0]).text()
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return (
|
||||||
|
title.strip() or None
|
||||||
|
if title is not None
|
||||||
|
else None,
|
||||||
|
description.strip() or None
|
||||||
|
if description is not None
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self) -> BotResultOut:
|
||||||
|
result = BotResultOut.model_validate(dict(
|
||||||
|
title=None,
|
||||||
|
description=None,
|
||||||
|
is_netloc_banned=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
result.is_netloc_banned = self.is_netloc_banned()
|
||||||
|
|
||||||
|
if result.is_netloc_banned is False:
|
||||||
|
fetch_result = self.fetch(self.url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert fetch_result.content is not None, (
|
||||||
|
'Received empty content'
|
||||||
|
)
|
||||||
|
assert fetch_result.content_type is not None, (
|
||||||
|
'Unable to determine the content type'
|
||||||
|
)
|
||||||
|
assert fetch_result.content_type.startswith('text/html') is True, (
|
||||||
|
f'Unsupported content type: `{fetch_result.content_type}`'
|
||||||
|
)
|
||||||
|
except AssertionError as exception:
|
||||||
|
self.logger.error(
|
||||||
|
'Unprocessable fetch result: %s', exception, exc_info=exception,
|
||||||
|
)
|
||||||
|
raise self.RuntimeError(exception.args[0]) from exception
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded_content = fetch_result.content.decode(fetch_result.encoding)
|
||||||
|
|
||||||
|
title, description = self.extract_title_and_description_from_html(
|
||||||
|
decoded_content,
|
||||||
|
)
|
||||||
|
result.title = title
|
||||||
|
result.description = description
|
||||||
|
except Exception as exception:
|
||||||
|
self.logger.error(
|
||||||
|
'Processing error: %s', exception, exc_info=exception,
|
||||||
|
)
|
||||||
|
raise self.RuntimeError() from exception
|
||||||
|
else:
|
||||||
|
self.logger.debug('Skipping banned netloc: url=`%s`', self.url)
|
||||||
|
|
||||||
|
return result
|
|
@ -0,0 +1,8 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .base import Strategy
|
||||||
|
|
||||||
|
|
||||||
|
class BasicStrategy(Strategy):
|
||||||
|
pass
|
17
services/backend/hotpocket_backend/apps/bot/types.py
Normal file
17
services/backend/hotpocket_backend/apps/bot/types.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from hotpocket_soa.dto import BotResultOut
|
||||||
|
|
||||||
|
|
||||||
|
class PStrategy(typing.Protocol):
|
||||||
|
def __init__(self, url: str):
|
||||||
|
...
|
||||||
|
|
||||||
|
def is_netloc_banned(self) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
def run(self) -> BotResultOut:
|
||||||
|
...
|
12
services/backend/hotpocket_backend/apps/core/apps.py
Normal file
12
services/backend/hotpocket_backend/apps/core/apps.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
label = 'core'
|
||||||
|
name = 'hotpocket_backend.apps.core'
|
||||||
|
verbose_name = _('Core')
|
8
services/backend/hotpocket_backend/apps/core/conf.py
Normal file
8
services/backend/hotpocket_backend/apps/core/conf.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
from .types import PSettings
|
||||||
|
|
||||||
|
settings: PSettings = django_settings
|
14
services/backend/hotpocket_backend/apps/core/context.py
Normal file
14
services/backend/hotpocket_backend/apps/core/context.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextvars
|
||||||
|
|
||||||
|
from hotpocket_common.constants import NULL_UUID
|
||||||
|
|
||||||
|
REQUEST_ID: contextvars.ContextVar[str] = contextvars.ContextVar(
|
||||||
|
'request_id', default=str(NULL_UUID),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_id() -> str:
|
||||||
|
return REQUEST_ID.get()
|
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.core.context import get_request_id
|
||||||
|
|
||||||
|
|
||||||
|
class RequestIDFilter(logging.Filter):
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
record.request_id = get_request_id()
|
||||||
|
return True
|
|
@ -0,0 +1 @@
|
||||||
|
from .request_id import RequestIDMiddleware # noqa: F401
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.http import HttpHeaders, HttpRequest
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.core.context import REQUEST_ID
|
||||||
|
|
||||||
|
|
||||||
|
class RequestIDMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request: HttpRequest) -> HttpHeaders:
|
||||||
|
request_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
REQUEST_ID.set(request_id)
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
response["X-RequestID"] = REQUEST_ID.get()
|
||||||
|
|
||||||
|
return response
|
|
@ -0,0 +1 @@
|
||||||
|
from .base import Model # noqa: F401
|
54
services/backend/hotpocket_backend/apps/core/models/base.py
Normal file
54
services/backend/hotpocket_backend/apps/core/models/base.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
import uuid6
|
||||||
|
|
||||||
|
|
||||||
|
class Model(models.Model):
|
||||||
|
id = models.UUIDField(
|
||||||
|
primary_key=True,
|
||||||
|
null=False,
|
||||||
|
default=uuid6.uuid7,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
account_uuid = models.UUIDField(
|
||||||
|
blank=False, null=False, default=None, db_index=True,
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
deleted_at = models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
db_index=True,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
return self.deleted_at is None
|
||||||
|
|
||||||
|
def save(self,
|
||||||
|
force_insert=False,
|
||||||
|
force_update=False,
|
||||||
|
using=None,
|
||||||
|
update_fields=None,
|
||||||
|
):
|
||||||
|
self.full_clean()
|
||||||
|
super().save(
|
||||||
|
force_insert=force_insert,
|
||||||
|
force_update=force_update,
|
||||||
|
using=using,
|
||||||
|
update_fields=update_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
def soft_delete(self, save=True) -> None:
|
||||||
|
self.deleted_at = now()
|
||||||
|
|
||||||
|
if save is True:
|
||||||
|
self.save()
|
13
services/backend/hotpocket_backend/apps/core/services.py
Normal file
13
services/backend/hotpocket_backend/apps/core/services.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from hotpocket_common.loader import load_module_attribute
|
||||||
|
|
||||||
|
from .conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_adapter(setting: str, default: str) -> typing.Any:
|
||||||
|
import_path = getattr(settings, setting, default)
|
||||||
|
return load_module_attribute(import_path)
|
22
services/backend/hotpocket_backend/apps/core/tasks.py
Normal file
22
services/backend/hotpocket_backend/apps/core/tasks.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def ping():
|
||||||
|
LOGGER.info('PONG')
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(bind=True)
|
||||||
|
def debug_request(self):
|
||||||
|
LOGGER.warning(
|
||||||
|
'request.id=`%s` request.properties=`%s`',
|
||||||
|
self.request.id,
|
||||||
|
self.request.properties,
|
||||||
|
)
|
|
@ -0,0 +1,83 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block title %}{% if form.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
|
||||||
|
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/login.css" %}">
|
||||||
|
{{ form.media }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bodyclass %}{{ block.super }} login{% endblock %}
|
||||||
|
|
||||||
|
{% block usertools %}{% endblock %}
|
||||||
|
|
||||||
|
{% block nav-global %}{% endblock %}
|
||||||
|
|
||||||
|
{% block nav-sidebar %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}{% endblock %}
|
||||||
|
|
||||||
|
{% block nav-breadcrumbs %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if form.errors and not form.non_field_errors %}
|
||||||
|
<p class="errornote">
|
||||||
|
{% blocktranslate count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
<p class="errornote">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="content-main">
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<p class="errornote">
|
||||||
|
{% blocktranslate trimmed %}
|
||||||
|
You are authenticated as {{ username }}, but are not authorized to
|
||||||
|
access this page. Would you like to login to a different account?
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
|
||||||
|
{% if not MODEL_AUTH_IS_DISABLED %}
|
||||||
|
<div class="form-row">
|
||||||
|
{{ form.username.errors }}
|
||||||
|
{{ form.username.label_tag }} {{ form.username }}
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
{{ form.password.errors }}
|
||||||
|
{{ form.password.label_tag }} {{ form.password }}
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
</div>
|
||||||
|
{% url 'admin_password_reset' as password_reset_url %}
|
||||||
|
{% if password_reset_url %}
|
||||||
|
<div class="password-reset-link">
|
||||||
|
<a href="{{ password_reset_url }}">{% translate 'Forgotten your login credentials?' %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="submit-row">
|
||||||
|
<input type="submit" value="{% translate 'Log in' %}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if HOTPOCKET_OIDC_IS_ENABLED %}
|
||||||
|
<div class="submit-row">
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
href="{% url 'social:begin' 'hotpocket_oidc' %}"
|
||||||
|
style="display: block;"
|
||||||
|
>
|
||||||
|
{% blocktranslate %}Log in with {{ HOTPOCKET_OIDC_DISPLAY_NAME }}{% endblocktranslate %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
32
services/backend/hotpocket_backend/apps/core/types.py
Normal file
32
services/backend/hotpocket_backend/apps/core/types.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from hotpocket_backend.secrets.admin import AdminSecrets
|
||||||
|
from hotpocket_backend.secrets.webapp import WebAppSecrets
|
||||||
|
from hotpocket_common.constants import App, Env
|
||||||
|
|
||||||
|
|
||||||
|
class PSettings(typing.Protocol):
|
||||||
|
DEBUG: bool
|
||||||
|
TESTING: bool
|
||||||
|
ALLOWED_HOSTS: list[str]
|
||||||
|
SECRET_KEY: str
|
||||||
|
|
||||||
|
APP: App
|
||||||
|
ENV: Env
|
||||||
|
|
||||||
|
SECRETS: AdminSecrets | WebAppSecrets
|
||||||
|
|
||||||
|
MODEL_AUTH_IS_DISABLED: bool
|
||||||
|
|
||||||
|
SITE_TITLE: str
|
||||||
|
SITE_SHORT_TITLE: str
|
||||||
|
IMAGE_ID: str
|
||||||
|
|
||||||
|
SAVES_SAVE_ADAPTER: str
|
||||||
|
SAVES_ASSOCIATION_ADAPTER: str
|
||||||
|
|
||||||
|
UPLOADS_PATH: pathlib.Path
|
12
services/backend/hotpocket_backend/apps/htmx/apps.py
Normal file
12
services/backend/hotpocket_backend/apps/htmx/apps.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class HTMXConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
label = 'htmx'
|
||||||
|
name = 'hotpocket_backend.apps.htmx'
|
||||||
|
verbose_name = _('HTMX')
|
40
services/backend/hotpocket_backend/apps/htmx/messages.py
Normal file
40
services/backend/hotpocket_backend/apps/htmx/messages.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib.messages.constants import ( # noqa: F401
|
||||||
|
DEBUG,
|
||||||
|
DEFAULT_TAGS,
|
||||||
|
ERROR,
|
||||||
|
INFO,
|
||||||
|
SUCCESS,
|
||||||
|
WARNING,
|
||||||
|
)
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django_htmx.http import trigger_client_event
|
||||||
|
|
||||||
|
|
||||||
|
def add_htmx_message(*,
|
||||||
|
request: HttpRequest,
|
||||||
|
response: HttpResponse,
|
||||||
|
level: str,
|
||||||
|
message: str,
|
||||||
|
extra_tags: str = '',
|
||||||
|
fail_silently: bool = False,
|
||||||
|
):
|
||||||
|
if not request.htmx:
|
||||||
|
if fail_silently is False:
|
||||||
|
raise RuntimeError(
|
||||||
|
"This doesn't look like an HTMX request: request=`%s`",
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
trigger_client_event(
|
||||||
|
response,
|
||||||
|
'HotPocket:UI:Messages:addMessage',
|
||||||
|
{
|
||||||
|
'level': DEFAULT_TAGS.get(level, DEFAULT_TAGS[INFO]),
|
||||||
|
'message': message,
|
||||||
|
'extra_tags': extra_tags,
|
||||||
|
},
|
||||||
|
after='swap',
|
||||||
|
)
|
|
@ -0,0 +1,61 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.saves.models import Save
|
||||||
|
from hotpocket_common.constants import AssociationsSearchMode
|
||||||
|
from hotpocket_soa.dto.associations import AssociationsQuery
|
||||||
|
|
||||||
|
|
||||||
|
class SaveAdapter:
|
||||||
|
def get_for_processing(self, *, pk: uuid.UUID) -> Save | None:
|
||||||
|
return Save.active_objects.get(pk=pk)
|
||||||
|
|
||||||
|
|
||||||
|
class AssociationAdapter:
|
||||||
|
def get_search_term_filters(self, *, query: AssociationsQuery) -> list[models.Q]:
|
||||||
|
# I suck at naming things, LOL.
|
||||||
|
result = [
|
||||||
|
(
|
||||||
|
models.Q(target__url__icontains=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target_title__icontains=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target__title__icontains=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target_description__icontains=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target__description__icontains=query.search)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_search_filters(self, *, query: AssociationsQuery) -> list[models.Q]:
|
||||||
|
result = [
|
||||||
|
models.Q(account_uuid=query.account_uuid),
|
||||||
|
models.Q(target__deleted_at__isnull=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
if query.mode == AssociationsSearchMode.ARCHIVED:
|
||||||
|
result.append(models.Q(archived_at__isnull=False))
|
||||||
|
else:
|
||||||
|
result.append(models.Q(archived_at__isnull=True))
|
||||||
|
|
||||||
|
match query.mode:
|
||||||
|
case AssociationsSearchMode.STARRED:
|
||||||
|
result.append(models.Q(starred_at__isnull=False))
|
||||||
|
|
||||||
|
if query.before is not None:
|
||||||
|
result.append(models.Q(pk__lt=query.before))
|
||||||
|
|
||||||
|
if query.after is not None:
|
||||||
|
result.append(models.Q(pk__gte=query.after))
|
||||||
|
|
||||||
|
if query.search is not None:
|
||||||
|
result.extend(self.get_search_term_filters(query=query))
|
||||||
|
|
||||||
|
return result
|
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .base import AssociationAdapter, SaveAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class BasicSaveAdapter(SaveAdapter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAssociationAdapter(AssociationAdapter):
|
||||||
|
pass
|
|
@ -0,0 +1,49 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.saves.models import Save
|
||||||
|
from hotpocket_common.db import postgres # noqa: F401
|
||||||
|
from hotpocket_soa.dto.associations import AssociationsQuery
|
||||||
|
|
||||||
|
from .base import AssociationAdapter, SaveAdapter
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresSaveAdapter(SaveAdapter):
|
||||||
|
ROW_LOCKED_MESSAGE = 'could not obtain lock on row in relation "saves_save"'
|
||||||
|
|
||||||
|
def get_for_processing(self, *, pk: uuid.UUID) -> Save | None:
|
||||||
|
try:
|
||||||
|
return Save.active_objects.select_for_update(nowait=True).get(pk=pk)
|
||||||
|
except django.db.utils.OperationalError as exception:
|
||||||
|
if exception.args[0].startswith(self.ROW_LOCKED_MESSAGE) is True:
|
||||||
|
LOGGER.info('Trying to process a locked save: pk=`%s`', pk)
|
||||||
|
return None
|
||||||
|
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresAssociationAdapter(AssociationAdapter):
|
||||||
|
def get_search_term_filters(self, *, query: AssociationsQuery) -> list[models.Q]:
|
||||||
|
result = [
|
||||||
|
(
|
||||||
|
models.Q(target__url__ilike=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target_title__ilike=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target__title__ilike=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target_description__ilike=query.search)
|
||||||
|
|
|
||||||
|
models.Q(target__description__ilike=query.search)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return result
|
|
@ -0,0 +1 @@
|
||||||
|
from . import save # noqa: F401
|
29
services/backend/hotpocket_backend/apps/saves/admin/save.py
Normal file
29
services/backend/hotpocket_backend/apps/saves/admin/save.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from hotpocket_backend.apps.saves.models import Save
|
||||||
|
|
||||||
|
|
||||||
|
class SaveAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'pk', 'key', 'account_uuid', 'created_at', 'render_is_active',
|
||||||
|
)
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
@admin.display(
|
||||||
|
description=_('Is Active?'), boolean=True, ordering='-deleted_at',
|
||||||
|
)
|
||||||
|
def render_is_active(self, obj: Save | None = None) -> bool | None:
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return obj.is_active
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Save, SaveAdmin)
|
12
services/backend/hotpocket_backend/apps/saves/apps.py
Normal file
12
services/backend/hotpocket_backend/apps/saves/apps.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
label = 'saves'
|
||||||
|
name = 'hotpocket_backend.apps.saves'
|
||||||
|
verbose_name = _('Saves')
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user