Compare commits
317 Commits
7ca40edfcf
...
0.11.3
| Author | SHA1 | Date | |
|---|---|---|---|
| cc699506d3 | |||
| 84da2eb489 | |||
| a9a61c98f5 | |||
| 4678022c7b | |||
| ca2d2e6587 | |||
| 912f97490c | |||
| d7f5809771 | |||
| 782be6bd38 | |||
| 996adb4bce | |||
| 6412d9f01a | |||
| 875e20304b | |||
| fed00ff2a0 | |||
| 10560a01fb | |||
| 56e7f1be53 | |||
| f5168c27bf | |||
| 4cf9cca116 | |||
| af375a2b5c | |||
| 23a55e025c | |||
| 40b1a63cf5 | |||
| 033f82eb2b | |||
| 1370bef1f1 | |||
| 1a6741ec10 | |||
| 0b9005b481 | |||
| 7ec3027410 | |||
| 405cc83ba1 | |||
| 0bbdf0eaab | |||
| 0da5d9305d | |||
| 9fc09be7de | |||
| 3f8517ec7d | |||
| e85d91d1f4 | |||
| bb139a2b95 | |||
| c06dd6c578 | |||
| 3d4034d6b1 | |||
| f825bab894 | |||
| c9d1d7416b | |||
| e25c7660b0 | |||
| 843af814fd | |||
| cb19b8b21c | |||
| 95456651a6 | |||
| 57aebb8c9e | |||
| e486b8a83d | |||
| 2f6de65e5c | |||
| 7092167001 | |||
| efdb6d50c3 | |||
| 04448e34c2 | |||
| da75b4cd50 | |||
| 4e9a3fe139 | |||
| e469db9ba6 | |||
| 64569c3505 | |||
| 15f69f533a | |||
| 5d28bf31dd | |||
| 3f3dba8759 | |||
| 8fde46c157 | |||
| d87b3e5b02 | |||
| 6b87fd6fc1 | |||
| 68940e87f9 | |||
| f0115a70f9 | |||
| a801eb1eeb | |||
| 83c94acbac | |||
| 5c2c514825 | |||
| 04689797f7 | |||
| 468235662b | |||
| f5e6076123 | |||
| 0f1dc51949 | |||
| 583f1c9d32 | |||
| af4ffb8559 | |||
| a9130bbe61 | |||
| 7015a46f94 | |||
| b96d5e4487 | |||
| 5bb0667060 | |||
| 580289d3a1 | |||
| 5c3cce1556 | |||
| f2361b94ba | |||
| f71b461e29 | |||
| 326bd1bd72 | |||
| 8f76832f69 | |||
| 309b03956f | |||
| 176ab4a194 | |||
| e43718894b | |||
| c31f1bb59d | |||
| 2014c326b1 | |||
| 112a99ffcb | |||
| 7aa96119a9 | |||
| 53ac07a9ee | |||
| 3288824383 | |||
| 9194b48eb8 | |||
| 301a83b295 | |||
| 2521c319a0 | |||
| 522ea2d976 | |||
| 91469735d2 | |||
| 7a8005c263 | |||
| fc05fda666 | |||
| fd34dd20ca | |||
| a917822fb8 | |||
| 30142f83df | |||
| 1cbfab0c2f | |||
| 46b56369c2 | |||
| 2db476ce09 | |||
| c5c1f35c08 | |||
| 3e5285225d | |||
| e58bec3da0 | |||
| cde3cba527 | |||
| c0346efcc7 | |||
| acececf899 | |||
| 8509b0567b | |||
| d619283d09 | |||
| e4238d9fdb | |||
| d7c1305dae | |||
| f6abdb5c63 | |||
| 51cea567ca | |||
| 85b769f7dd | |||
| 996f7dca16 | |||
| eb2946510a | |||
| 99a76c540f | |||
| 56177b2811 | |||
| 57fa221d47 | |||
| f65265b4a5 | |||
| 0047cfbcd1 | |||
| 33f6e0386b | |||
| 164e8373a4 | |||
| 5834aec885 | |||
| ca32ee0de4 | |||
| ee5b2d9fd9 | |||
| d4ae0fe2d8 | |||
| 39cbff2234 | |||
| 6273866324 | |||
| a78f3c0786 | |||
| 97fde3388b | |||
| 6e5229c8ba | |||
| b9df790d1f | |||
| 71213b87a0 | |||
| 5746600340 | |||
| 905a056c87 | |||
| 41cdd5ec7f | |||
| 88c3bc1b5b | |||
| 0f9f9d1b73 | |||
| 3dc897518e | |||
| 79fa32d1dd | |||
| 6ae1ee3365 | |||
| add00d488c | |||
| 0a471792e1 | |||
| e948a9e55f | |||
| 7f97d90f04 | |||
| b580bb2db3 | |||
| 9dd4ac5863 | |||
| 27d4922ce8 | |||
| c1d4fe1c9a | |||
| 02e7710676 | |||
| cd16d34fe5 | |||
| 4d38c8715e | |||
| 8edd96b671 | |||
| ea841e4d54 | |||
| 5c6c3f436e | |||
| 4dbaa27cba | |||
| c95a1c456b | |||
| 66ac834ab0 | |||
| 81ee935150 | |||
| 765c07316a | |||
| 694c8df364 | |||
| 220d892fa0 | |||
| 75127394c7 | |||
| 11745f2eb8 | |||
| fdef0c5f66 | |||
| 5a9b6be79d | |||
| 38e24a690a | |||
| 7d08a57c85 | |||
| 17e0781b14 | |||
| b06a282160 | |||
| e8d79cca19 | |||
| 047ad6ef10 | |||
| 06772ed48c | |||
| b9233136a7 | |||
| 85bfd0f3ae | |||
| 2800da1859 | |||
| 3a768146c1 | |||
| ea118a37c5 | |||
| aeefd17b10 | |||
| c0a97a1714 | |||
| b55a919944 | |||
| 3e7f7ff636 | |||
| e1390a8adc | |||
| 1c74706736 | |||
| 58c23a6380 | |||
| 779e81346b | |||
| 08a11c240d | |||
| c452a920b1 | |||
| fda5655533 | |||
| ac49c842b8 | |||
| a9c0debd6c | |||
| c2f06a8e15 | |||
| 2c9b302a69 | |||
| 224d4d400e | |||
| 5e5d1beb5e | |||
| 4e6bf7b03c | |||
| 4bd5180b87 | |||
| 90bca4c93b | |||
| 36695c1398 | |||
| fb2837e523 | |||
| 45a1bc07c6 | |||
| 4494ebdeb3 | |||
| 4bb776f801 | |||
| e0be56a7f4 | |||
| 821330eba5 | |||
| f13f5a1e08 | |||
| 0d5fd83e01 | |||
| 50f7e115d6 | |||
| f7853f96ed | |||
| a08345e676 | |||
| c09a5c758e | |||
| 43f29f9a46 | |||
| 24612879a1 | |||
| 7723cd0d70 | |||
| 9159817947 | |||
| fee79c6ec7 | |||
| 986db28494 | |||
| 37fcc5f34c | |||
| 8f24e21d5d | |||
| 5dbdd43785 | |||
| 6d959ac253 | |||
| f00d569db3 | |||
| 3174f8fa5b | |||
| 5ecb8c956c | |||
| 096a70bb5d | |||
| 4f8261bda9 | |||
| c8dd4ac165 | |||
| b70e4624cb | |||
| 5c5829c487 | |||
| 1134f5b099 | |||
| 6272f71355 | |||
| 082924a3ba | |||
| 2a1b6f8a22 | |||
| d77dcec068 | |||
| ef4b4ab39f | |||
| 552d1feb2e | |||
| 9b98370989 | |||
| 11446e00db | |||
| d4465c20a8 | |||
| 92bf70974f | |||
| 71072994b5 | |||
| b091f1bb4e | |||
| c9c0eba15b | |||
| 5c5d03e60c | |||
| 56d9b5e95b | |||
| e20af39e83 | |||
| 437a054d3b | |||
| 669a96174c | |||
| d76bee84f9 | |||
| 348a1dd601 | |||
| c0d93fe6ac | |||
| c12ccaab53 | |||
| 45b3fb0cd6 | |||
| e9b1fb6577 | |||
| 26572b740e | |||
| ca0c3ef4bd | |||
| 37cc454804 | |||
| 586337ceec | |||
| 5bb37da909 | |||
| 737da6c8d9 | |||
| 149487195b | |||
| 071b8ca2aa | |||
| 95407cc3d7 | |||
| 160794404e | |||
| 91a70a720c | |||
| 17e1c4181f | |||
| 43e1ea37d5 | |||
| 1a9d848459 | |||
| 0c745e7544 | |||
| 60582d4520 | |||
| 9dc5fbdebc | |||
| fe66775dc8 | |||
| 3e3451adda | |||
| f0a2f28f8e | |||
| dca23787e4 | |||
| a67470d616 | |||
| e5d54c499b | |||
| 65c71ced4d | |||
| 5cd470bf5a | |||
| 9860a9a36c | |||
| a987a551a1 | |||
| 7ecb3ae400 | |||
| 674596782d | |||
| a71fb202bb | |||
| 12f73c46a7 | |||
| 35f6dd4827 | |||
| 791b232c0a | |||
| 0e03681d82 | |||
| fd356fbd1c | |||
| a198fa3e0e | |||
| 029815c218 | |||
| 81a97dafae | |||
| 0aadd1f5e9 | |||
| c00fe16b94 | |||
| 8e55893abb | |||
| 084f5526a4 | |||
| b7a3059426 | |||
| cac35a53c6 | |||
| 661534692d | |||
| 517a063747 | |||
| ab4dcaa199 | |||
| 710964fe72 | |||
| e26615e7c8 | |||
| 7602a2a930 | |||
| fa76e7044c | |||
| 20d834b1dc | |||
| 47df04b078 | |||
| df0ace6e31 | |||
| 9e0e17a8cd | |||
| f546430daa | |||
| 6e62e21b0c | |||
| b0ed651abe | |||
| 1814c6669a | |||
| 4b8396a3bc | |||
| 76e542485a | |||
| 7b60927941 | |||
| 0bf66d4430 | |||
| 8832fe3061 | |||
| aee26682db |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -26,4 +26,7 @@ logs/
|
|||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
32
Dockerfile
32
Dockerfile
@@ -1,26 +1,38 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
gcc \
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
|
curl \
|
||||||
|
netcat-traditional \
|
||||||
|
dos2unix \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create a non-root user
|
||||||
|
RUN useradd -m -u 1000 celery
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy requirements first to leverage Docker cache
|
# Copy requirements first to leverage Docker cache
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy the rest of the application
|
# Copy application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Set environment variables
|
# Convert line endings and set permissions
|
||||||
ENV FLASK_APP=app.py
|
RUN dos2unix /app/entrypoint.sh && \
|
||||||
ENV FLASK_ENV=development
|
chmod +x /app/entrypoint.sh && \
|
||||||
|
mkdir -p /app/uploads/rooms /app/uploads/profile_pics /app/static/uploads && \
|
||||||
|
chown -R celery:celery /app && \
|
||||||
|
chmod -R 755 /app/uploads
|
||||||
|
|
||||||
# Expose the port the app runs on
|
# Switch to non-root user
|
||||||
EXPOSE 5000
|
USER celery
|
||||||
|
|
||||||
# Command to run the application
|
# Set entrypoint
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
||||||
274
NGINX_swagger.json
Normal file
274
NGINX_swagger.json
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"info": {
|
||||||
|
"title": "Nginx Proxy Manager API",
|
||||||
|
"version": "2.x.x"
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://127.0.0.1:81/api"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"securitySchemes": {
|
||||||
|
"bearerAuth": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "JWT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/get.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/audit-log": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/audit-log/get.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/access-lists": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/access-lists/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/access-lists/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/access-lists/{listID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/access-lists/listID/get.json"
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/nginx/access-lists/listID/put.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/nginx/access-lists/listID/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/certificates": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/certificates/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/certificates/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/certificates/validate": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/certificates/validate/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/certificates/test-http": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/certificates/test-http/get.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/certificates/{certID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/certificates/certID/get.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/nginx/certificates/certID/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/certificates/{certID}/download": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/certificates/certID/download/get.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/certificates/{certID}/renew": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/certificates/certID/renew/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/certificates/{certID}/upload": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/certificates/certID/upload/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/proxy-hosts": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/proxy-hosts/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/proxy-hosts/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/proxy-hosts/{hostID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/proxy-hosts/hostID/get.json"
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/nginx/proxy-hosts/hostID/put.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/nginx/proxy-hosts/hostID/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/proxy-hosts/{hostID}/enable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/proxy-hosts/hostID/enable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/proxy-hosts/{hostID}/disable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/proxy-hosts/hostID/disable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/redirection-hosts": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/redirection-hosts/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/redirection-hosts/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/redirection-hosts/{hostID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/redirection-hosts/hostID/get.json"
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/nginx/redirection-hosts/hostID/put.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/nginx/redirection-hosts/hostID/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/redirection-hosts/{hostID}/enable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/redirection-hosts/hostID/enable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/redirection-hosts/{hostID}/disable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/redirection-hosts/hostID/disable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/dead-hosts": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/dead-hosts/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/dead-hosts/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/dead-hosts/{hostID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/dead-hosts/hostID/get.json"
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/nginx/dead-hosts/hostID/put.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/nginx/dead-hosts/hostID/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/dead-hosts/{hostID}/enable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/dead-hosts/hostID/enable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/dead-hosts/{hostID}/disable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/dead-hosts/hostID/disable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/streams": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/streams/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/streams/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/streams/{streamID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/nginx/streams/streamID/get.json"
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/nginx/streams/streamID/put.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/nginx/streams/streamID/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/streams/{streamID}/enable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/streams/streamID/enable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/nginx/streams/{streamID}/disable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/nginx/streams/streamID/disable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/reports/hosts": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/reports/hosts/get.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/schema": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/schema/get.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/settings": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/settings/get.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/settings/{settingID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/settings/settingID/get.json"
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/settings/settingID/put.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/tokens": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/tokens/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/tokens/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/users/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/users/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/{userID}": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/users/userID/get.json"
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/users/userID/put.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/users/userID/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/{userID}/auth": {
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/users/userID/auth/put.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/{userID}/permissions": {
|
||||||
|
"put": {
|
||||||
|
"$ref": "./paths/users/userID/permissions/put.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/{userID}/login": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/users/userID/login/post.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
246
PRICING_CONFIGURATION.md
Normal file
246
PRICING_CONFIGURATION.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# Pricing Configuration Feature
|
||||||
|
|
||||||
|
This document describes the new configurable pricing feature that allows MASTER instances to manage pricing plans through the admin interface.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The pricing configuration feature allows administrators on MASTER instances to:
|
||||||
|
- Create, edit, and delete pricing plans
|
||||||
|
- Configure plan features, prices, and settings
|
||||||
|
- Set resource quotas for rooms, conversations, storage, and users
|
||||||
|
- Mark plans as "Most Popular" or "Custom"
|
||||||
|
- Control plan visibility and ordering
|
||||||
|
- Update pricing without code changes
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Pricing Plan Management
|
||||||
|
- **Plan Name**: Display name for the pricing plan
|
||||||
|
- **Description**: Optional description shown below the plan name
|
||||||
|
- **Pricing**: Monthly and annual prices (annual prices are typically 20% lower)
|
||||||
|
- **Features**: Dynamic list of features with checkmarks
|
||||||
|
- **Button Configuration**: Customizable button text and URL
|
||||||
|
- **Plan Types**: Regular plans with prices or custom plans
|
||||||
|
- **Popular Plans**: Mark one plan as "Most Popular" with special styling
|
||||||
|
- **Active/Inactive**: Toggle plan visibility
|
||||||
|
- **Ordering**: Control the display order of plans
|
||||||
|
|
||||||
|
### Resource Quotas
|
||||||
|
- **Room Quota**: Maximum number of rooms allowed (0 = unlimited)
|
||||||
|
- **Conversation Quota**: Maximum number of conversations allowed (0 = unlimited)
|
||||||
|
- **Storage Quota**: Maximum storage in gigabytes (0 = unlimited)
|
||||||
|
- **Manager Quota**: Maximum number of manager users allowed (0 = unlimited)
|
||||||
|
- **Admin Quota**: Maximum number of admin users allowed (0 = unlimited)
|
||||||
|
|
||||||
|
### Admin Interface
|
||||||
|
- **Pricing Tab**: New tab in admin settings (MASTER instances only)
|
||||||
|
- **Add/Edit Modals**: User-friendly forms for plan management
|
||||||
|
- **Real-time Updates**: Changes are reflected immediately
|
||||||
|
- **Feature Management**: Add/remove features dynamically
|
||||||
|
- **Quota Configuration**: Set resource limits for each plan
|
||||||
|
- **Status Toggles**: Quick switches for plan properties
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Database Migration
|
||||||
|
Run the migrations to create the pricing_plans table and add quota fields:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply the migrations
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Initialize Default Plans (Optional)
|
||||||
|
Run the initialization script to create default pricing plans:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set MASTER environment variable
|
||||||
|
export MASTER=true
|
||||||
|
|
||||||
|
# Run the initialization script
|
||||||
|
python init_pricing_plans.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create four default plans with quotas:
|
||||||
|
- **Starter**: 5 rooms, 10 conversations, 10GB storage, 10 managers, 1 admin
|
||||||
|
- **Professional**: 25 rooms, 50 conversations, 100GB storage, 50 managers, 3 admins
|
||||||
|
- **Enterprise**: 100 rooms, 200 conversations, 500GB storage, 200 managers, 10 admins
|
||||||
|
- **Custom**: Unlimited everything
|
||||||
|
|
||||||
|
### 3. Access Admin Interface
|
||||||
|
1. Log in as an admin user on a MASTER instance
|
||||||
|
2. Go to Settings
|
||||||
|
3. Click on the "Pricing" tab
|
||||||
|
4. Configure your pricing plans
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating a New Plan
|
||||||
|
1. Click "Add New Plan" in the pricing tab
|
||||||
|
2. Fill in the plan details:
|
||||||
|
- **Name**: Plan display name
|
||||||
|
- **Description**: Optional description
|
||||||
|
- **Monthly Price**: Price per month
|
||||||
|
- **Annual Price**: Price per month when billed annually
|
||||||
|
- **Quotas**: Set resource limits (0 = unlimited)
|
||||||
|
- **Features**: Add features using the "Add Feature" button
|
||||||
|
- **Button Text**: Text for the call-to-action button
|
||||||
|
- **Button URL**: URL the button should link to
|
||||||
|
- **Options**: Check "Most Popular", "Custom Plan", or "Active" as needed
|
||||||
|
3. Click "Create Plan"
|
||||||
|
|
||||||
|
### Editing a Plan
|
||||||
|
1. Click the "Edit" button on any plan card
|
||||||
|
2. Modify the plan details in the modal
|
||||||
|
3. Click "Update Plan"
|
||||||
|
|
||||||
|
### Managing Plan Status
|
||||||
|
- **Active/Inactive**: Use the toggle switch in the plan header
|
||||||
|
- **Most Popular**: Check the "Most Popular" checkbox (only one plan can be popular)
|
||||||
|
- **Custom Plan**: Check "Custom Plan" for plans without fixed pricing
|
||||||
|
|
||||||
|
### Deleting a Plan
|
||||||
|
1. Click the "Delete" button on a plan card
|
||||||
|
2. Confirm the deletion in the modal
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
The `pricing_plans` table includes:
|
||||||
|
- `id`: Primary key
|
||||||
|
- `name`: Plan name (required)
|
||||||
|
- `description`: Optional description
|
||||||
|
- `monthly_price`: Monthly price (float)
|
||||||
|
- `annual_price`: Annual price (float)
|
||||||
|
- `features`: JSON array of feature strings
|
||||||
|
- `button_text`: Button display text
|
||||||
|
- `button_url`: Button link URL
|
||||||
|
- `is_popular`: Boolean for "Most Popular" styling
|
||||||
|
- `is_custom`: Boolean for custom plans
|
||||||
|
- `is_active`: Boolean for plan visibility
|
||||||
|
- `order_index`: Integer for display ordering
|
||||||
|
- `room_quota`: Maximum rooms (0 = unlimited)
|
||||||
|
- `conversation_quota`: Maximum conversations (0 = unlimited)
|
||||||
|
- `storage_quota_gb`: Maximum storage in GB (0 = unlimited)
|
||||||
|
- `manager_quota`: Maximum managers (0 = unlimited)
|
||||||
|
- `admin_quota`: Maximum admins (0 = unlimited)
|
||||||
|
- `created_by`: Foreign key to user who created the plan
|
||||||
|
- `created_at`/`updated_at`: Timestamps
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
- `POST /api/admin/pricing-plans` - Create new plan
|
||||||
|
- `GET /api/admin/pricing-plans/<id>` - Get plan details
|
||||||
|
- `PUT /api/admin/pricing-plans/<id>` - Update plan
|
||||||
|
- `DELETE /api/admin/pricing-plans/<id>` - Delete plan
|
||||||
|
- `PATCH /api/admin/pricing-plans/<id>/status` - Update plan status
|
||||||
|
|
||||||
|
### Template Integration
|
||||||
|
The pricing page automatically uses configured plans:
|
||||||
|
- Falls back to hardcoded plans if no plans are configured
|
||||||
|
- Supports dynamic feature lists
|
||||||
|
- Handles custom plans without pricing
|
||||||
|
- Shows/hides billing toggle based on plan types
|
||||||
|
- Displays quota information in plan cards
|
||||||
|
|
||||||
|
### Quota Enforcement
|
||||||
|
The PricingPlan model includes utility methods for quota checking:
|
||||||
|
- `check_quota(quota_type, current_count)`: Returns True if quota allows the operation
|
||||||
|
- `get_quota_remaining(quota_type, current_count)`: Returns remaining quota
|
||||||
|
- `format_quota_display(quota_type)`: Formats quota for display
|
||||||
|
- `get_storage_quota_bytes()`: Converts GB to bytes for storage calculations
|
||||||
|
|
||||||
|
Example usage in your application:
|
||||||
|
```python
|
||||||
|
# Check if user can create a new room
|
||||||
|
plan = PricingPlan.query.get(user_plan_id)
|
||||||
|
current_rooms = Room.query.filter_by(created_by=user.id).count()
|
||||||
|
if plan.check_quota('room_quota', current_rooms):
|
||||||
|
# Allow room creation
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Show upgrade message
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Only admin users can access pricing configuration
|
||||||
|
- Only MASTER instances can configure pricing
|
||||||
|
- All API endpoints require authentication and admin privileges
|
||||||
|
- CSRF protection is enabled for all forms
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
The pricing plans use the existing CSS variables:
|
||||||
|
- `--primary-color`: Main brand color
|
||||||
|
- `--secondary-color`: Secondary brand color
|
||||||
|
- `--shadow-color`: Card shadows
|
||||||
|
|
||||||
|
### Button URLs
|
||||||
|
Configure button URLs to point to:
|
||||||
|
- Contact forms
|
||||||
|
- Payment processors
|
||||||
|
- Sales pages
|
||||||
|
- Custom landing pages
|
||||||
|
|
||||||
|
### Features
|
||||||
|
Features can include:
|
||||||
|
- Storage limits
|
||||||
|
- User limits
|
||||||
|
- Feature availability
|
||||||
|
- Support levels
|
||||||
|
- Integration options
|
||||||
|
|
||||||
|
### Quota Integration
|
||||||
|
To integrate quotas into your application:
|
||||||
|
|
||||||
|
1. **User Plan Assignment**: Associate users with pricing plans
|
||||||
|
2. **Quota Checking**: Use the `check_quota()` method before operations
|
||||||
|
3. **Upgrade Prompts**: Show upgrade messages when quotas are exceeded
|
||||||
|
4. **Usage Tracking**: Track current usage for quota calculations
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Pricing tab not visible**
|
||||||
|
- Ensure you're on a MASTER instance (`MASTER=true`)
|
||||||
|
- Ensure you're logged in as an admin user
|
||||||
|
|
||||||
|
2. **Plans not showing on pricing page**
|
||||||
|
- Check that plans are marked as "Active"
|
||||||
|
- Verify the database migration was applied
|
||||||
|
- Check for JavaScript errors in browser console
|
||||||
|
|
||||||
|
3. **Features not saving**
|
||||||
|
- Ensure at least one feature is provided
|
||||||
|
- Check that feature text is not empty
|
||||||
|
|
||||||
|
4. **Quota fields not working**
|
||||||
|
- Verify the quota migration was applied
|
||||||
|
- Check that quota values are integers
|
||||||
|
- Ensure quota fields are included in form submissions
|
||||||
|
|
||||||
|
5. **API errors**
|
||||||
|
- Verify CSRF token is included in requests
|
||||||
|
- Check that all required fields are provided
|
||||||
|
- Ensure proper JSON formatting for features
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
- Review server logs for API errors
|
||||||
|
- Verify database connectivity
|
||||||
|
- Test with default plans first
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential future improvements:
|
||||||
|
- Plan categories/tiers
|
||||||
|
- Regional pricing
|
||||||
|
- Currency support
|
||||||
|
- Promotional pricing
|
||||||
|
- Plan comparison features
|
||||||
|
- Analytics and usage tracking
|
||||||
|
- Automatic quota enforcement middleware
|
||||||
|
- Usage dashboard for quota monitoring
|
||||||
53
README.md
53
README.md
@@ -10,8 +10,9 @@ DocuPulse is a powerful document management system designed to streamline docume
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js (version 18 or higher)
|
- Python 3.11 or higher
|
||||||
- npm or yarn
|
- PostgreSQL 13 or higher
|
||||||
|
- Docker and Docker Compose (for containerized deployment)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -23,18 +24,50 @@ cd docupulse
|
|||||||
|
|
||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
```bash
|
```bash
|
||||||
npm install
|
pip install -r requirements.txt
|
||||||
# or
|
|
||||||
yarn install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start the development server:
|
3. Set up environment variables:
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
# Copy example environment file
|
||||||
# or
|
cp .env.example .env
|
||||||
yarn dev
|
|
||||||
|
# Set version information for local development
|
||||||
|
python set_version.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
4. Initialize the database:
|
||||||
|
```bash
|
||||||
|
flask db upgrade
|
||||||
|
flask create-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Start the development server:
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Tracking
|
||||||
|
|
||||||
|
DocuPulse uses a database-only approach for version tracking:
|
||||||
|
|
||||||
|
- **Environment Variables**: Version information is passed via environment variables (`APP_VERSION`, `GIT_COMMIT`, `GIT_BRANCH`, `DEPLOYED_AT`)
|
||||||
|
- **Database Storage**: Instance version information is stored in the `instances` table
|
||||||
|
- **API Endpoint**: Version information is available via `/api/version`
|
||||||
|
|
||||||
|
### Setting Version Information
|
||||||
|
|
||||||
|
For local development:
|
||||||
|
```bash
|
||||||
|
python set_version.py
|
||||||
|
```
|
||||||
|
|
||||||
|
For production deployments, set the following environment variables:
|
||||||
|
- `APP_VERSION`: Application version/tag
|
||||||
|
- `GIT_COMMIT`: Git commit hash
|
||||||
|
- `GIT_BRANCH`: Git branch name
|
||||||
|
- `DEPLOYED_AT`: Deployment timestamp
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Document upload and management
|
- Document upload and management
|
||||||
@@ -42,6 +75,8 @@ yarn dev
|
|||||||
- Secure document storage
|
- Secure document storage
|
||||||
- User authentication and authorization
|
- User authentication and authorization
|
||||||
- Document version control
|
- Document version control
|
||||||
|
- Multi-tenant instance management
|
||||||
|
- RESTful API
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
BIN
__pycache__/app.cpython-311.pyc
Normal file
BIN
__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/celery_worker.cpython-311.pyc
Normal file
BIN
__pycache__/celery_worker.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/celery_worker.cpython-313.pyc
Normal file
BIN
__pycache__/celery_worker.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/create_default_templates.cpython-313.pyc
Normal file
BIN
__pycache__/create_default_templates.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/extensions.cpython-311.pyc
Normal file
BIN
__pycache__/extensions.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/forms.cpython-311.pyc
Normal file
BIN
__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/models.cpython-311.pyc
Normal file
BIN
__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/tasks.cpython-311.pyc
Normal file
BIN
__pycache__/tasks.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
175
app.py
175
app.py
@@ -1,29 +1,45 @@
|
|||||||
from flask import Flask, send_from_directory
|
import random
|
||||||
|
from flask import Flask, send_from_directory, jsonify
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
from models import User
|
from models import User, SiteSettings
|
||||||
from flask_wtf.csrf import generate_csrf
|
from flask_wtf.csrf import generate_csrf
|
||||||
from routes.room_files import room_files_bp
|
from routes.room_files import room_files_bp
|
||||||
from routes.user import user_bp
|
|
||||||
from routes.room_members import room_members_bp
|
from routes.room_members import room_members_bp
|
||||||
from routes.trash import trash_bp
|
from routes.trash import trash_bp
|
||||||
|
from routes.admin_api import admin_api
|
||||||
|
from routes.launch_api import launch_api
|
||||||
from tasks import cleanup_trash
|
from tasks import cleanup_trash
|
||||||
import click
|
import click
|
||||||
from utils import timeago
|
from utils import timeago
|
||||||
from extensions import db, login_manager, csrf, socketio
|
from extensions import db, login_manager, csrf
|
||||||
|
from utils.email_templates import create_default_templates
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import text
|
||||||
|
from utils.asset_utils import get_asset_version
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
print("Environment variables after loading .env:")
|
||||||
|
print(f"MASTER: {os.getenv('MASTER')}")
|
||||||
|
print(f"ISMASTER: {os.getenv('ISMASTER')}")
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Configure the database
|
# Configure the database
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:1253@localhost:5432/docupulse'
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'postgresql://postgres:1253@localhost:5432/docupulse')
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secure-secret-key-here')
|
||||||
app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static', 'uploads')
|
app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static', 'uploads')
|
||||||
|
app.config['CSS_VERSION'] = os.getenv('CSS_VERSION', '1.0.3') # Add CSS version for cache busting
|
||||||
|
app.config['SERVER_NAME'] = os.getenv('SERVER_NAME', '127.0.0.1:5000')
|
||||||
|
app.config['PREFERRED_URL_SCHEME'] = os.getenv('PREFERRED_URL_SCHEME', 'http')
|
||||||
|
|
||||||
|
# Configure request timeouts for long-running operations
|
||||||
|
app.config['REQUEST_TIMEOUT'] = int(os.getenv('REQUEST_TIMEOUT', '300')) # 5 minutes default
|
||||||
|
app.config['STACK_DEPLOYMENT_TIMEOUT'] = int(os.getenv('STACK_DEPLOYMENT_TIMEOUT', '300')) # 5 minutes for stack deployment
|
||||||
|
|
||||||
# Initialize extensions
|
# Initialize extensions
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
@@ -31,24 +47,68 @@ def create_app():
|
|||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
login_manager.login_view = 'auth.login'
|
login_manager.login_view = 'auth.login'
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
socketio.init_app(app)
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_csrf_token():
|
def inject_csrf_token():
|
||||||
return dict(csrf_token=generate_csrf())
|
return dict(csrf_token=generate_csrf())
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_config():
|
||||||
|
site_settings = SiteSettings.query.first()
|
||||||
|
if not site_settings:
|
||||||
|
site_settings = SiteSettings()
|
||||||
|
db.session.add(site_settings)
|
||||||
|
db.session.commit()
|
||||||
|
return dict(config=app.config, site_settings=site_settings)
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_unread_notifications():
|
||||||
|
from flask_login import current_user
|
||||||
|
from utils import get_unread_count
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
unread_count = get_unread_count(current_user.id)
|
||||||
|
return {'unread_notifications': unread_count}
|
||||||
|
return {'unread_notifications': 0}
|
||||||
|
|
||||||
|
@app.template_filter('asset_version')
|
||||||
|
def asset_version_filter(filename):
|
||||||
|
"""Template filter to get version hash for static assets"""
|
||||||
|
return get_asset_version(filename) or ''
|
||||||
|
|
||||||
# User loader for Flask-Login
|
# User loader for Flask-Login
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(user_id))
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
@app.route('/health')
|
||||||
|
def health_check():
|
||||||
|
try:
|
||||||
|
# Check database connection with a timeout
|
||||||
|
db.session.execute(text('SELECT 1'))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy',
|
||||||
|
'database': 'connected',
|
||||||
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
|
}), 200
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Health check failed: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'status': 'unhealthy',
|
||||||
|
'error': str(e),
|
||||||
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
|
}), 500
|
||||||
|
|
||||||
# Initialize routes
|
# Initialize routes
|
||||||
from routes import init_app
|
from routes import init_app
|
||||||
init_app(app)
|
init_app(app)
|
||||||
app.register_blueprint(room_files_bp, url_prefix='/api/rooms')
|
app.register_blueprint(room_files_bp, url_prefix='/api/rooms')
|
||||||
app.register_blueprint(room_members_bp, url_prefix='/api/rooms')
|
app.register_blueprint(room_members_bp, url_prefix='/api/rooms')
|
||||||
app.register_blueprint(trash_bp, url_prefix='/api/rooms')
|
app.register_blueprint(trash_bp, url_prefix='/api/trash')
|
||||||
app.register_blueprint(user_bp)
|
app.register_blueprint(admin_api, url_prefix='/api/admin')
|
||||||
|
app.register_blueprint(launch_api, url_prefix='/api/admin')
|
||||||
|
|
||||||
@app.cli.command("cleanup-trash")
|
@app.cli.command("cleanup-trash")
|
||||||
def cleanup_trash_command():
|
def cleanup_trash_command():
|
||||||
@@ -56,16 +116,109 @@ def create_app():
|
|||||||
cleanup_trash()
|
cleanup_trash()
|
||||||
click.echo("Trash cleanup completed.")
|
click.echo("Trash cleanup completed.")
|
||||||
|
|
||||||
|
@app.cli.command("cleanup-tokens")
|
||||||
|
def cleanup_tokens_command():
|
||||||
|
"""Clean up expired password reset and setup tokens."""
|
||||||
|
from tasks import cleanup_expired_tokens
|
||||||
|
cleanup_expired_tokens()
|
||||||
|
click.echo("Token cleanup completed.")
|
||||||
|
|
||||||
|
@app.cli.command("create-admin")
|
||||||
|
def create_admin():
|
||||||
|
"""Create the default administrator user."""
|
||||||
|
admin = User.query.filter_by(email='administrator@docupulse.com').first()
|
||||||
|
if admin:
|
||||||
|
click.echo("Admin user already exists.")
|
||||||
|
return
|
||||||
|
|
||||||
|
admin = User(
|
||||||
|
username='administrator',
|
||||||
|
email='administrator@docupulse.com',
|
||||||
|
last_name='Administrator',
|
||||||
|
company='DocuPulse',
|
||||||
|
position='System Administrator',
|
||||||
|
is_admin=True,
|
||||||
|
is_active=True,
|
||||||
|
preferred_view='grid'
|
||||||
|
)
|
||||||
|
admin.set_password('changeme')
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
click.echo("Default administrator user created successfully.")
|
||||||
|
click.echo("Admin credentials:")
|
||||||
|
click.echo("Email: administrator@docupulse.com")
|
||||||
|
click.echo("Password: changeme")
|
||||||
|
|
||||||
# Register custom filters
|
# Register custom filters
|
||||||
app.jinja_env.filters['timeago'] = timeago
|
app.jinja_env.filters['timeago'] = timeago
|
||||||
|
|
||||||
|
# Create default email templates if they don't exist
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
# Ensure database tables exist
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Create admin user first
|
||||||
|
admin = User.query.filter_by(email='administrator@docupulse.com').first()
|
||||||
|
if not admin:
|
||||||
|
admin = User(
|
||||||
|
username='administrator',
|
||||||
|
email='administrator@docupulse.com',
|
||||||
|
last_name='Administrator',
|
||||||
|
company='DocuPulse',
|
||||||
|
position='System Administrator',
|
||||||
|
is_admin=True,
|
||||||
|
is_active=True,
|
||||||
|
preferred_view='grid'
|
||||||
|
)
|
||||||
|
admin.set_password('changeme')
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
print("Default administrator user created successfully.")
|
||||||
|
print("Admin credentials:")
|
||||||
|
print("Email: administrator@docupulse.com")
|
||||||
|
print("Password: changeme")
|
||||||
|
|
||||||
|
# Then create default templates
|
||||||
|
create_default_templates()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not create default templates: {e}")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
from flask import render_template
|
||||||
|
return render_template('common/404.html'), 404
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def forbidden(e):
|
||||||
|
from flask import render_template
|
||||||
|
return render_template('common/403.html'), 403
|
||||||
|
|
||||||
|
@app.errorhandler(401)
|
||||||
|
def unauthorized(e):
|
||||||
|
from flask import render_template
|
||||||
|
return render_template('common/401.html'), 401
|
||||||
|
|
||||||
|
@app.errorhandler(400)
|
||||||
|
def bad_request(e):
|
||||||
|
from flask import render_template
|
||||||
|
return render_template('common/400.html'), 400
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_server_error(e):
|
||||||
|
from flask import render_template
|
||||||
|
import traceback
|
||||||
|
error_details = f"{str(e)}\n\n{traceback.format_exc()}"
|
||||||
|
app.logger.error(f"500 error: {error_details}")
|
||||||
|
return render_template('common/500.html', error=error_details), 500
|
||||||
|
|
||||||
@app.route('/uploads/profile_pics/<filename>')
|
@app.route('/uploads/profile_pics/<filename>')
|
||||||
def profile_pic(filename):
|
def profile_pic(filename):
|
||||||
return send_from_directory(os.path.join(os.getcwd(), 'uploads', 'profile_pics'), filename)
|
return send_from_directory('/app/uploads/profile_pics', filename)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
socketio.run(app, debug=True)
|
app.run(debug=True)
|
||||||
11
create_notifs_table.py
Normal file
11
create_notifs_table.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from app import app, db
|
||||||
|
from models import Notif
|
||||||
|
|
||||||
|
def create_notifs_table():
|
||||||
|
with app.app_context():
|
||||||
|
# Create the table
|
||||||
|
Notif.__table__.create(db.engine)
|
||||||
|
print("Notifications table created successfully!")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
create_notifs_table()
|
||||||
@@ -1,28 +1,75 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
docupulse_network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build: .
|
build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
context: https://git.kobeamerijckx.com/Kobe/docupulse.git
|
||||||
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "10335:5000"
|
- "${PORT:-10335}:5000"
|
||||||
volumes:
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
environment:
|
environment:
|
||||||
- FLASK_APP=app.py
|
- FLASK_APP=app.py
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=production
|
||||||
- UPLOAD_FOLDER=/app/uploads
|
- DATABASE_URL=postgresql://docupulse_${PORT:-10335}:docupulse_${PORT:-10335}@db:5432/docupulse_${PORT:-10335}
|
||||||
|
- POSTGRES_USER=docupulse_${PORT:-10335}
|
||||||
|
- POSTGRES_PASSWORD=docupulse_${PORT:-10335}
|
||||||
|
- POSTGRES_DB=docupulse_${PORT:-10335}
|
||||||
|
- MASTER=${ISMASTER:-false}
|
||||||
|
- APP_VERSION=${APP_VERSION:-unknown}
|
||||||
|
- GIT_COMMIT=${GIT_COMMIT:-unknown}
|
||||||
|
- GIT_BRANCH=${GIT_BRANCH:-unknown}
|
||||||
|
- DEPLOYED_AT=${DEPLOYED_AT:-unknown}
|
||||||
|
- PRICING_TIER_ID=${PRICING_TIER_ID:-0}
|
||||||
|
- PRICING_TIER_NAME=${PRICING_TIER_NAME:-Unknown}
|
||||||
|
- ROOM_QUOTA=${ROOM_QUOTA:-0}
|
||||||
|
- CONVERSATION_QUOTA=${CONVERSATION_QUOTA:-0}
|
||||||
|
- STORAGE_QUOTA_GB=${STORAGE_QUOTA_GB:-0}
|
||||||
|
- MANAGER_QUOTA=${MANAGER_QUOTA:-0}
|
||||||
|
- ADMIN_QUOTA=${ADMIN_QUOTA:-0}
|
||||||
|
volumes:
|
||||||
|
- docupulse_uploads:/app/uploads
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
||||||
|
interval: 60s
|
||||||
|
timeout: 30s
|
||||||
|
retries: 3
|
||||||
|
start_period: 120s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
networks:
|
||||||
|
- docupulse_network
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:13
|
image: postgres:13
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=postgres
|
- POSTGRES_USER=docupulse_${PORT:-10335}
|
||||||
- POSTGRES_PASSWORD=postgres
|
- POSTGRES_PASSWORD=docupulse_${PORT:-10335}
|
||||||
- POSTGRES_DB=docupulse
|
- POSTGRES_DB=docupulse_${PORT:-10335}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- docupulse_postgres_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U docupulse_${PORT:-10335}"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- docupulse_network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
docupulse_postgres_data:
|
||||||
uploads:
|
name: docupulse_${PORT:-10335}_postgres_data
|
||||||
|
docupulse_uploads:
|
||||||
|
name: docupulse_${PORT:-10335}_uploads
|
||||||
151
entrypoint.sh
Normal file
151
entrypoint.sh
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Print environment variables for debugging
|
||||||
|
echo "Environment variables:"
|
||||||
|
echo "POSTGRES_USER: $POSTGRES_USER"
|
||||||
|
echo "POSTGRES_PASSWORD: $POSTGRES_PASSWORD"
|
||||||
|
echo "POSTGRES_DB: $POSTGRES_DB"
|
||||||
|
echo "DATABASE_URL: $DATABASE_URL"
|
||||||
|
|
||||||
|
# Function to wait for database
|
||||||
|
wait_for_db() {
|
||||||
|
echo "Waiting for database..."
|
||||||
|
while ! nc -z db 5432; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "Database is ready!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create database if it doesn't exist
|
||||||
|
create_database() {
|
||||||
|
echo "Creating database if it doesn't exist..."
|
||||||
|
PGPASSWORD=$POSTGRES_PASSWORD psql -h db -U $POSTGRES_USER -tc "SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB'" | grep -q 1 || \
|
||||||
|
PGPASSWORD=$POSTGRES_PASSWORD psql -h db -U $POSTGRES_USER -c "CREATE DATABASE $POSTGRES_DB"
|
||||||
|
echo "Database check/creation complete!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for database to be ready
|
||||||
|
wait_for_db
|
||||||
|
|
||||||
|
# Create database if it doesn't exist
|
||||||
|
create_database
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready to accept connections
|
||||||
|
echo "Waiting for PostgreSQL to accept connections..."
|
||||||
|
until PGPASSWORD=$POSTGRES_PASSWORD psql -h db -U $POSTGRES_USER -d $POSTGRES_DB -c '\q'; do
|
||||||
|
echo "PostgreSQL is unavailable - sleeping"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "PostgreSQL is up - executing command"
|
||||||
|
|
||||||
|
# Run all initialization in a single Python script to avoid multiple Flask instances
|
||||||
|
echo "Running initialization..."
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
from app import create_app
|
||||||
|
from models import SiteSettings, db, User
|
||||||
|
from utils.email_templates import create_default_templates
|
||||||
|
|
||||||
|
def log_error(message, error=None):
|
||||||
|
print(f'ERROR: {message}', file=sys.stderr)
|
||||||
|
if error:
|
||||||
|
print(f'Error details: {str(error)}', file=sys.stderr)
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
# Run migrations
|
||||||
|
print('Running database migrations...')
|
||||||
|
from flask_migrate import upgrade
|
||||||
|
upgrade()
|
||||||
|
print('Database migrations completed successfully')
|
||||||
|
|
||||||
|
# Create default site settings
|
||||||
|
print('Creating default site settings...')
|
||||||
|
try:
|
||||||
|
settings = SiteSettings.get_settings()
|
||||||
|
print('Default site settings created successfully')
|
||||||
|
except Exception as e:
|
||||||
|
log_error('Error creating site settings', e)
|
||||||
|
|
||||||
|
# Create admin user if it doesn't exist
|
||||||
|
print('Creating admin user...')
|
||||||
|
try:
|
||||||
|
# Check for admin user by both username and email to avoid constraint violations
|
||||||
|
admin_by_username = User.query.filter_by(username='administrator').first()
|
||||||
|
admin_by_email = User.query.filter_by(email='administrator@docupulse.com').first()
|
||||||
|
|
||||||
|
if admin_by_username and admin_by_email and admin_by_username.id == admin_by_email.id:
|
||||||
|
print('Admin user already exists (found by both username and email).')
|
||||||
|
print('Admin credentials:')
|
||||||
|
print('Email: administrator@docupulse.com')
|
||||||
|
print('Password: changeme')
|
||||||
|
elif admin_by_username or admin_by_email:
|
||||||
|
print('WARNING: Found partial admin user data:')
|
||||||
|
if admin_by_username:
|
||||||
|
print(f' - Found user with username "administrator" (ID: {admin_by_username.id})')
|
||||||
|
if admin_by_email:
|
||||||
|
print(f' - Found user with email "administrator@docupulse.com" (ID: {admin_by_email.id})')
|
||||||
|
print('Admin credentials:')
|
||||||
|
print('Email: administrator@docupulse.com')
|
||||||
|
print('Password: changeme')
|
||||||
|
else:
|
||||||
|
print('Admin user not found, creating new admin user...')
|
||||||
|
admin = User(
|
||||||
|
username='administrator',
|
||||||
|
email='administrator@docupulse.com',
|
||||||
|
last_name='Administrator',
|
||||||
|
company='DocuPulse',
|
||||||
|
position='System Administrator',
|
||||||
|
is_admin=True,
|
||||||
|
is_active=True,
|
||||||
|
preferred_view='grid'
|
||||||
|
)
|
||||||
|
admin.set_password('changeme')
|
||||||
|
print('Admin user object created, attempting to add to database...')
|
||||||
|
db.session.add(admin)
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
print('Default administrator user created successfully.')
|
||||||
|
print('Admin credentials:')
|
||||||
|
print('Email: administrator@docupulse.com')
|
||||||
|
print('Password: changeme')
|
||||||
|
except Exception as commit_error:
|
||||||
|
db.session.rollback()
|
||||||
|
if 'duplicate key value violates unique constraint' in str(commit_error):
|
||||||
|
print('WARNING: Admin user creation failed due to duplicate key constraint.')
|
||||||
|
print('This might indicate a race condition or the user was created by another process.')
|
||||||
|
print('Checking for existing admin user again...')
|
||||||
|
# Check again after the failed commit
|
||||||
|
admin_by_username = User.query.filter_by(username='administrator').first()
|
||||||
|
admin_by_email = User.query.filter_by(email='administrator@docupulse.com').first()
|
||||||
|
if admin_by_username or admin_by_email:
|
||||||
|
print('Admin user now exists (likely created by another process).')
|
||||||
|
print('Admin credentials:')
|
||||||
|
print('Email: administrator@docupulse.com')
|
||||||
|
print('Password: changeme')
|
||||||
|
else:
|
||||||
|
log_error('Admin user creation failed and user still not found', commit_error)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
log_error('Failed to commit admin user creation', commit_error)
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log_error('Error during admin user creation/check', e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Create default templates
|
||||||
|
print('Creating default templates...')
|
||||||
|
try:
|
||||||
|
create_default_templates()
|
||||||
|
print('Default templates created successfully')
|
||||||
|
except Exception as e:
|
||||||
|
log_error('Error creating default templates', e)
|
||||||
|
except Exception as e:
|
||||||
|
log_error('Fatal error during initialization', e)
|
||||||
|
sys.exit(1)
|
||||||
|
"
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
echo "Starting application..."
|
||||||
|
exec gunicorn --bind 0.0.0.0:5000 app:app
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
from flask_socketio import SocketIO
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_wtf.csrf import CSRFProtect
|
from flask_wtf.csrf import CSRFProtect
|
||||||
@@ -6,5 +5,4 @@ from flask_wtf.csrf import CSRFProtect
|
|||||||
# Initialize extensions
|
# Initialize extensions
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
socketio = SocketIO(cors_allowed_origins="*")
|
|
||||||
30
forms.py
30
forms.py
@@ -1,5 +1,5 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, TextAreaField, BooleanField, SubmitField, PasswordField, SelectMultipleField
|
from wtforms import StringField, TextAreaField, BooleanField, SubmitField, PasswordField, SelectMultipleField, SelectField
|
||||||
from wtforms.validators import DataRequired, Email, Length, Optional, ValidationError
|
from wtforms.validators import DataRequired, Email, Length, Optional, ValidationError
|
||||||
from models import User
|
from models import User
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
@@ -13,8 +13,11 @@ class UserForm(FlaskForm):
|
|||||||
company = StringField('Company (Optional)', validators=[Optional(), Length(max=100)])
|
company = StringField('Company (Optional)', validators=[Optional(), Length(max=100)])
|
||||||
position = StringField('Position (Optional)', validators=[Optional(), Length(max=100)])
|
position = StringField('Position (Optional)', validators=[Optional(), Length(max=100)])
|
||||||
notes = TextAreaField('Notes (Optional)', validators=[Optional()])
|
notes = TextAreaField('Notes (Optional)', validators=[Optional()])
|
||||||
is_active = BooleanField('Active', default=True)
|
role = SelectField('Role', choices=[
|
||||||
is_admin = BooleanField('Admin Role', default=False)
|
('user', 'Standard User'),
|
||||||
|
('manager', 'Manager'),
|
||||||
|
('admin', 'Administrator')
|
||||||
|
], validators=[DataRequired()])
|
||||||
new_password = PasswordField('New Password (Optional)')
|
new_password = PasswordField('New Password (Optional)')
|
||||||
confirm_password = PasswordField('Confirm Password (Optional)')
|
confirm_password = PasswordField('Confirm Password (Optional)')
|
||||||
profile_picture = FileField('Profile Picture (Optional)', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')])
|
profile_picture = FileField('Profile Picture (Optional)', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')])
|
||||||
@@ -31,6 +34,11 @@ class UserForm(FlaskForm):
|
|||||||
if total_admins <= 1:
|
if total_admins <= 1:
|
||||||
raise ValidationError('There must be at least one admin user in the system.')
|
raise ValidationError('There must be at least one admin user in the system.')
|
||||||
|
|
||||||
|
def validate_is_manager(self, field):
|
||||||
|
# Prevent setting both admin and manager roles
|
||||||
|
if field.data and self.is_admin.data:
|
||||||
|
raise ValidationError('A user cannot be both an admin and a manager.')
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self, extra_validators=None):
|
||||||
rv = super().validate(extra_validators=extra_validators)
|
rv = super().validate(extra_validators=extra_validators)
|
||||||
if not rv:
|
if not rv:
|
||||||
@@ -57,4 +65,18 @@ class ConversationForm(FlaskForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ConversationForm, self).__init__(*args, **kwargs)
|
super(ConversationForm, self).__init__(*args, **kwargs)
|
||||||
self.members.choices = [(u.id, f"{u.username} {u.last_name}") for u in User.query.filter_by(is_active=True).all()]
|
self.members.choices = [(u.id, f"{u.username} {u.last_name}") for u in User.query.filter_by(is_active=True).all()]
|
||||||
|
|
||||||
|
class CompanySettingsForm(FlaskForm):
|
||||||
|
company_name = StringField('Company Name', validators=[Optional(), Length(max=100)])
|
||||||
|
company_website = StringField('Website', validators=[Optional(), Length(max=200)])
|
||||||
|
company_email = StringField('Email', validators=[Optional(), Email(), Length(max=100)])
|
||||||
|
company_phone = StringField('Phone', validators=[Optional(), Length(max=20)])
|
||||||
|
company_address = StringField('Address', validators=[Optional(), Length(max=200)])
|
||||||
|
company_city = StringField('City', validators=[Optional(), Length(max=100)])
|
||||||
|
company_state = StringField('State', validators=[Optional(), Length(max=100)])
|
||||||
|
company_zip = StringField('ZIP Code', validators=[Optional(), Length(max=20)])
|
||||||
|
company_country = StringField('Country', validators=[Optional(), Length(max=100)])
|
||||||
|
company_description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
company_industry = StringField('Industry', validators=[Optional(), Length(max=100)])
|
||||||
|
company_logo = FileField('Company Logo', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')])
|
||||||
24
init_admin.py
Normal file
24
init_admin.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from app import app, db
|
||||||
|
from models import User
|
||||||
|
|
||||||
|
def init_admin():
|
||||||
|
with app.app_context():
|
||||||
|
admin = User.query.filter_by(email='administrator@docupulse.com').first()
|
||||||
|
if not admin:
|
||||||
|
admin = User(
|
||||||
|
username='administrator',
|
||||||
|
email='administrator@docupulse.com',
|
||||||
|
last_name='None',
|
||||||
|
company='docupulse',
|
||||||
|
is_admin=True,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
admin.set_password('q]H488h[8?.A')
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
print("Default administrator user created successfully.")
|
||||||
|
else:
|
||||||
|
print("Admin user already exists.")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_admin()
|
||||||
170
init_pricing_plans.py
Normal file
170
init_pricing_plans.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to initialize default pricing plans in the database.
|
||||||
|
This should be run on a MASTER instance only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from app import app, db
|
||||||
|
from models import PricingPlan, User
|
||||||
|
|
||||||
|
def init_pricing_plans():
|
||||||
|
"""Initialize default pricing plans"""
|
||||||
|
|
||||||
|
# Check if this is a MASTER instance
|
||||||
|
if os.environ.get('MASTER', 'false').lower() != 'true':
|
||||||
|
print("Error: This script should only be run on a MASTER instance.")
|
||||||
|
print("Set MASTER=true environment variable to run this script.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# Check if pricing plans already exist
|
||||||
|
existing_plans = PricingPlan.query.count()
|
||||||
|
if existing_plans > 0:
|
||||||
|
print(f"Found {existing_plans} existing pricing plans. Skipping initialization.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the first admin user
|
||||||
|
admin_user = User.query.filter_by(is_admin=True).first()
|
||||||
|
if not admin_user:
|
||||||
|
print("Error: No admin user found. Please create an admin user first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Default pricing plans
|
||||||
|
default_plans = [
|
||||||
|
{
|
||||||
|
'name': 'Starter',
|
||||||
|
'description': 'Perfect for small teams getting started',
|
||||||
|
'monthly_price': 29.0,
|
||||||
|
'annual_price': 23.0,
|
||||||
|
'features': [
|
||||||
|
'Up to 5 rooms',
|
||||||
|
'Up to 10 conversations',
|
||||||
|
'10GB storage',
|
||||||
|
'Up to 10 managers',
|
||||||
|
'Email support'
|
||||||
|
],
|
||||||
|
'button_text': 'Get Started',
|
||||||
|
'button_url': '#',
|
||||||
|
'is_popular': False,
|
||||||
|
'is_custom': False,
|
||||||
|
'is_active': True,
|
||||||
|
'order_index': 1,
|
||||||
|
'room_quota': 5,
|
||||||
|
'conversation_quota': 10,
|
||||||
|
'storage_quota_gb': 10,
|
||||||
|
'manager_quota': 10,
|
||||||
|
'admin_quota': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Professional',
|
||||||
|
'description': 'Ideal for growing businesses',
|
||||||
|
'monthly_price': 99.0,
|
||||||
|
'annual_price': 79.0,
|
||||||
|
'features': [
|
||||||
|
'Up to 25 rooms',
|
||||||
|
'Up to 50 conversations',
|
||||||
|
'100GB storage',
|
||||||
|
'Up to 50 managers',
|
||||||
|
'Priority support'
|
||||||
|
],
|
||||||
|
'button_text': 'Get Started',
|
||||||
|
'button_url': '#',
|
||||||
|
'is_popular': True,
|
||||||
|
'is_custom': False,
|
||||||
|
'is_active': True,
|
||||||
|
'order_index': 2,
|
||||||
|
'room_quota': 25,
|
||||||
|
'conversation_quota': 50,
|
||||||
|
'storage_quota_gb': 100,
|
||||||
|
'manager_quota': 50,
|
||||||
|
'admin_quota': 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Enterprise',
|
||||||
|
'description': 'For large organizations with advanced needs',
|
||||||
|
'monthly_price': 299.0,
|
||||||
|
'annual_price': 239.0,
|
||||||
|
'features': [
|
||||||
|
'Up to 100 rooms',
|
||||||
|
'Up to 200 conversations',
|
||||||
|
'500GB storage',
|
||||||
|
'Up to 200 managers',
|
||||||
|
'24/7 dedicated support'
|
||||||
|
],
|
||||||
|
'button_text': 'Get Started',
|
||||||
|
'button_url': '#',
|
||||||
|
'is_popular': False,
|
||||||
|
'is_custom': False,
|
||||||
|
'is_active': True,
|
||||||
|
'order_index': 3,
|
||||||
|
'room_quota': 100,
|
||||||
|
'conversation_quota': 200,
|
||||||
|
'storage_quota_gb': 500,
|
||||||
|
'manager_quota': 200,
|
||||||
|
'admin_quota': 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Custom',
|
||||||
|
'description': 'Tailored solutions for enterprise customers',
|
||||||
|
'monthly_price': 0.0,
|
||||||
|
'annual_price': 0.0,
|
||||||
|
'features': [
|
||||||
|
'Unlimited rooms',
|
||||||
|
'Unlimited conversations',
|
||||||
|
'Unlimited storage',
|
||||||
|
'Unlimited users',
|
||||||
|
'Custom integrations',
|
||||||
|
'Dedicated account manager'
|
||||||
|
],
|
||||||
|
'button_text': 'Contact Sales',
|
||||||
|
'button_url': '#',
|
||||||
|
'is_popular': False,
|
||||||
|
'is_custom': True,
|
||||||
|
'is_active': True,
|
||||||
|
'order_index': 4,
|
||||||
|
'room_quota': 0,
|
||||||
|
'conversation_quota': 0,
|
||||||
|
'storage_quota_gb': 0,
|
||||||
|
'manager_quota': 0,
|
||||||
|
'admin_quota': 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create pricing plans
|
||||||
|
for plan_data in default_plans:
|
||||||
|
plan = PricingPlan(
|
||||||
|
name=plan_data['name'],
|
||||||
|
description=plan_data['description'],
|
||||||
|
monthly_price=plan_data['monthly_price'],
|
||||||
|
annual_price=plan_data['annual_price'],
|
||||||
|
features=plan_data['features'],
|
||||||
|
button_text=plan_data['button_text'],
|
||||||
|
button_url=plan_data['button_url'],
|
||||||
|
is_popular=plan_data['is_popular'],
|
||||||
|
is_custom=plan_data['is_custom'],
|
||||||
|
is_active=plan_data['is_active'],
|
||||||
|
order_index=plan_data['order_index'],
|
||||||
|
room_quota=plan_data['room_quota'],
|
||||||
|
conversation_quota=plan_data['conversation_quota'],
|
||||||
|
storage_quota_gb=plan_data['storage_quota_gb'],
|
||||||
|
manager_quota=plan_data['manager_quota'],
|
||||||
|
admin_quota=plan_data['admin_quota'],
|
||||||
|
created_by=admin_user.id
|
||||||
|
)
|
||||||
|
db.session.add(plan)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
print("Successfully created default pricing plans:")
|
||||||
|
for plan_data in default_plans:
|
||||||
|
print(f" - {plan_data['name']}: €{plan_data['monthly_price']}/month")
|
||||||
|
print("\nYou can now configure these plans in the admin settings.")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Error creating pricing plans: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_pricing_plans()
|
||||||
BIN
migrations/__pycache__/env.cpython-311.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,41 @@
|
|||||||
|
"""add key value settings table
|
||||||
|
|
||||||
|
Revision ID: 0a8006bd1732
|
||||||
|
Revises: 20519a2437c2
|
||||||
|
Create Date: 2025-06-02 14:10:54.033943
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0a8006bd1732'
|
||||||
|
down_revision = '20519a2437c2'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
|
if 'key_value_settings' not in tables:
|
||||||
|
op.create_table('key_value_settings',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('key', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('value', sa.Text(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('key')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('key_value_settings')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-26 14:00:05.521776
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,23 +19,41 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('message')]
|
||||||
|
|
||||||
with op.batch_alter_table('message', schema=None) as batch_op:
|
with op.batch_alter_table('message', schema=None) as batch_op:
|
||||||
batch_op.add_column(sa.Column('has_attachment', sa.Boolean(), nullable=True))
|
if 'has_attachment' not in columns:
|
||||||
batch_op.add_column(sa.Column('attachment_name', sa.String(length=255), nullable=True))
|
batch_op.add_column(sa.Column('has_attachment', sa.Boolean(), nullable=True))
|
||||||
batch_op.add_column(sa.Column('attachment_path', sa.String(length=512), nullable=True))
|
if 'attachment_name' not in columns:
|
||||||
batch_op.add_column(sa.Column('attachment_type', sa.String(length=100), nullable=True))
|
batch_op.add_column(sa.Column('attachment_name', sa.String(length=255), nullable=True))
|
||||||
batch_op.add_column(sa.Column('attachment_size', sa.Integer(), nullable=True))
|
if 'attachment_path' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('attachment_path', sa.String(length=512), nullable=True))
|
||||||
|
if 'attachment_type' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('attachment_type', sa.String(length=100), nullable=True))
|
||||||
|
if 'attachment_size' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('attachment_size', sa.Integer(), nullable=True))
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('message')]
|
||||||
|
|
||||||
with op.batch_alter_table('message', schema=None) as batch_op:
|
with op.batch_alter_table('message', schema=None) as batch_op:
|
||||||
batch_op.drop_column('attachment_size')
|
if 'attachment_size' in columns:
|
||||||
batch_op.drop_column('attachment_type')
|
batch_op.drop_column('attachment_size')
|
||||||
batch_op.drop_column('attachment_path')
|
if 'attachment_type' in columns:
|
||||||
batch_op.drop_column('attachment_name')
|
batch_op.drop_column('attachment_type')
|
||||||
batch_op.drop_column('has_attachment')
|
if 'attachment_path' in columns:
|
||||||
|
batch_op.drop_column('attachment_path')
|
||||||
|
if 'attachment_name' in columns:
|
||||||
|
batch_op.drop_column('attachment_name')
|
||||||
|
if 'has_attachment' in columns:
|
||||||
|
batch_op.drop_column('has_attachment')
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
Revision ID: 1c297825e3a9
|
Revision ID: 1c297825e3a9
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2025-05-23 08:39:40.494853
|
Create Date: 2025-06-02 13:26:30.353000
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -17,20 +18,27 @@ depends_on = None
|
|||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# Check if the table exists before creating it
|
||||||
op.create_table('user',
|
conn = op.get_bind()
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
inspector = sa.inspect(conn)
|
||||||
sa.Column('username', sa.String(length=150), nullable=False),
|
if 'user' not in inspector.get_table_names():
|
||||||
sa.Column('email', sa.String(length=150), nullable=False),
|
conn = op.get_bind()
|
||||||
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
inspector = inspect(conn)
|
||||||
sa.PrimaryKeyConstraint('id'),
|
tables = inspector.get_table_names()
|
||||||
sa.UniqueConstraint('email'),
|
|
||||||
sa.UniqueConstraint('username')
|
if 'user' not in tables:
|
||||||
)
|
op.create_table('user',
|
||||||
# ### end Alembic commands ###
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=150), nullable=False),
|
||||||
|
sa.Column('email', sa.String(length=150), nullable=False),
|
||||||
|
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('email'),
|
||||||
|
sa.UniqueConstraint('username')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('user')
|
op.drop_table('user')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
47
migrations/versions/20519a2437c2_add_mails_table.py
Normal file
47
migrations/versions/20519a2437c2_add_mails_table.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""add_mails_table
|
||||||
|
|
||||||
|
Revision ID: 20519a2437c2
|
||||||
|
Revises: 444d76da74ba
|
||||||
|
Create Date: 2025-06-02 09:04:39.972021
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '20519a2437c2'
|
||||||
|
down_revision = '444d76da74ba'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
|
if 'mails' not in tables:
|
||||||
|
op.create_table('mails',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('recipient', sa.String(length=150), nullable=False),
|
||||||
|
sa.Column('subject', sa.String(length=200), nullable=False),
|
||||||
|
sa.Column('body', sa.Text(), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('sent_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('template_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('notif_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['notif_id'], ['notifs.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['template_id'], ['email_templates.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('mails')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-23 16:10:53.731035
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,15 +19,25 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('contact')]
|
||||||
|
|
||||||
with op.batch_alter_table('contact', schema=None) as batch_op:
|
with op.batch_alter_table('contact', schema=None) as batch_op:
|
||||||
batch_op.add_column(sa.Column('is_admin', sa.Boolean(), nullable=True))
|
if 'is_admin' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('is_admin', sa.Boolean(), nullable=True))
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('contact')]
|
||||||
|
|
||||||
with op.batch_alter_table('contact', schema=None) as batch_op:
|
with op.batch_alter_table('contact', schema=None) as batch_op:
|
||||||
batch_op.drop_column('is_admin')
|
if 'is_admin' in columns:
|
||||||
|
batch_op.drop_column('is_admin')
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-23 21:44:58.832286
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,17 +19,22 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('room_member_permissions',
|
conn = op.get_bind()
|
||||||
sa.Column('room_id', sa.Integer(), nullable=False),
|
inspector = inspect(conn)
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
tables = inspector.get_table_names()
|
||||||
sa.Column('can_view', sa.Boolean(), nullable=False),
|
|
||||||
sa.Column('can_upload', sa.Boolean(), nullable=False),
|
if 'room_member_permissions' not in tables:
|
||||||
sa.Column('can_delete', sa.Boolean(), nullable=False),
|
op.create_table('room_member_permissions',
|
||||||
sa.Column('can_share', sa.Boolean(), nullable=False),
|
sa.Column('room_id', sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['room_id'], ['room.id'], ),
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
sa.Column('can_view', sa.Boolean(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('room_id', 'user_id')
|
sa.Column('can_upload', sa.Boolean(), nullable=False),
|
||||||
)
|
sa.Column('can_delete', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('can_share', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['room_id'], ['room.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('room_id', 'user_id')
|
||||||
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-23 21:27:17.497481
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,15 +19,24 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('room_members',
|
conn = op.get_bind()
|
||||||
sa.Column('room_id', sa.Integer(), nullable=False),
|
inspector = inspect(conn)
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
tables = inspector.get_table_names()
|
||||||
sa.ForeignKeyConstraint(['room_id'], ['room.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
if 'room_members' not in tables:
|
||||||
sa.PrimaryKeyConstraint('room_id', 'user_id')
|
op.create_table('room_members',
|
||||||
)
|
sa.Column('room_id', sa.Integer(), nullable=False),
|
||||||
with op.batch_alter_table('room', schema=None) as batch_op:
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
batch_op.drop_column('is_private')
|
sa.ForeignKeyConstraint(['room_id'], ['room.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('room_id', 'user_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if is_private column exists before dropping it
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('room')]
|
||||||
|
if 'is_private' in columns:
|
||||||
|
with op.batch_alter_table('room', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('is_private')
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-23 21:25:27.880150
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,16 +19,21 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('room',
|
conn = op.get_bind()
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
inspector = inspect(conn)
|
||||||
sa.Column('name', sa.String(length=100), nullable=False),
|
tables = inspector.get_table_names()
|
||||||
sa.Column('description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
if 'room' not in tables:
|
||||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
op.create_table('room',
|
||||||
sa.Column('is_private', sa.Boolean(), nullable=True),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
|
sa.Column('name', sa.String(length=100), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
)
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('is_private', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-23 09:24:23.926302
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,12 +19,21 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('user')]
|
||||||
|
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
batch_op.add_column(sa.Column('phone', sa.String(length=20), nullable=True))
|
if 'phone' not in columns:
|
||||||
batch_op.add_column(sa.Column('company', sa.String(length=100), nullable=True))
|
batch_op.add_column(sa.Column('phone', sa.String(length=20), nullable=True))
|
||||||
batch_op.add_column(sa.Column('position', sa.String(length=100), nullable=True))
|
if 'company' not in columns:
|
||||||
batch_op.add_column(sa.Column('notes', sa.Text(), nullable=True))
|
batch_op.add_column(sa.Column('company', sa.String(length=100), nullable=True))
|
||||||
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=True))
|
if 'position' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('position', sa.String(length=100), nullable=True))
|
||||||
|
if 'notes' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('notes', sa.Text(), nullable=True))
|
||||||
|
if 'is_active' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=True))
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|||||||
63
migrations/versions/444d76da74ba_add_notifications_table.py
Normal file
63
migrations/versions/444d76da74ba_add_notifications_table.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""add_notifications_table
|
||||||
|
|
||||||
|
Revision ID: 444d76da74ba
|
||||||
|
Revises: c770e08966b4
|
||||||
|
Create Date: 2025-06-02 08:25:48.241102
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '444d76da74ba'
|
||||||
|
down_revision = 'c770e08966b4'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
|
if 'template_variables' in tables:
|
||||||
|
op.drop_table('template_variables')
|
||||||
|
|
||||||
|
op.create_table('notification',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=200), nullable=False),
|
||||||
|
sa.Column('message', sa.Text(), nullable=False),
|
||||||
|
sa.Column('type', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('read', sa.Boolean(), nullable=False, server_default='false'),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
|
if 'notification' in tables:
|
||||||
|
op.drop_table('notification')
|
||||||
|
|
||||||
|
if 'template_variables' not in tables:
|
||||||
|
op.create_table('template_variables',
|
||||||
|
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('notification_type', sa.VARCHAR(length=50), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('variable_name', sa.VARCHAR(length=50), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('description', sa.VARCHAR(length=200), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('example_value', sa.VARCHAR(length=200), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('template_variables_pkey'))
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
24
migrations/versions/4ee23cb29001_merge_heads.py
Normal file
24
migrations/versions/4ee23cb29001_merge_heads.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""merge heads
|
||||||
|
|
||||||
|
Revision ID: 4ee23cb29001
|
||||||
|
Revises: 72ab6c4c6a5f, add_status_details
|
||||||
|
Create Date: 2025-06-09 10:04:48.708415
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4ee23cb29001'
|
||||||
|
down_revision = ('72ab6c4c6a5f', 'add_status_details')
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-24 10:07:02.159730
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,22 +19,31 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('room_file',
|
conn = op.get_bind()
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
inspector = inspect(conn)
|
||||||
sa.Column('room_id', sa.Integer(), nullable=False),
|
tables = inspector.get_table_names()
|
||||||
sa.Column('name', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('path', sa.String(length=1024), nullable=False),
|
if 'room_file' not in tables:
|
||||||
sa.Column('type', sa.String(length=10), nullable=False),
|
op.create_table('room_file',
|
||||||
sa.Column('size', sa.BigInteger(), nullable=True),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('modified', sa.Float(), nullable=True),
|
sa.Column('room_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('uploaded_by', sa.Integer(), nullable=False),
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
sa.Column('uploaded_at', sa.DateTime(), nullable=False),
|
sa.Column('path', sa.String(length=1024), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['room_id'], ['room.id'], ),
|
sa.Column('type', sa.String(length=10), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['uploaded_by'], ['user.id'], ),
|
sa.Column('size', sa.BigInteger(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column('modified', sa.Float(), nullable=True),
|
||||||
)
|
sa.Column('uploaded_by', sa.Integer(), nullable=False),
|
||||||
with op.batch_alter_table('room_member_permissions', schema=None) as batch_op:
|
sa.Column('uploaded_at', sa.DateTime(), nullable=False),
|
||||||
batch_op.drop_column('preferred_view')
|
sa.ForeignKeyConstraint(['room_id'], ['room.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['uploaded_by'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if preferred_view column exists before trying to drop it
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('room_member_permissions')]
|
||||||
|
if 'preferred_view' in columns:
|
||||||
|
with op.batch_alter_table('room_member_permissions', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('preferred_view')
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
@@ -44,4 +54,4 @@ def downgrade():
|
|||||||
batch_op.add_column(sa.Column('preferred_view', sa.VARCHAR(length=10), autoincrement=False, nullable=False))
|
batch_op.add_column(sa.Column('preferred_view', sa.VARCHAR(length=10), autoincrement=False, nullable=False))
|
||||||
|
|
||||||
op.drop_table('room_file')
|
op.drop_table('room_file')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-24 18:14:38.320999
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,43 +19,63 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('room_file')]
|
||||||
|
|
||||||
with op.batch_alter_table('room_file', schema=None) as batch_op:
|
with op.batch_alter_table('room_file', schema=None) as batch_op:
|
||||||
batch_op.add_column(sa.Column('starred', sa.Boolean(), nullable=True))
|
if 'starred' not in columns:
|
||||||
batch_op.alter_column('path',
|
batch_op.add_column(sa.Column('starred', sa.Boolean(), nullable=True))
|
||||||
existing_type=sa.VARCHAR(length=1024),
|
|
||||||
type_=sa.String(length=255),
|
# Only alter columns if they exist
|
||||||
existing_nullable=False)
|
if 'path' in columns:
|
||||||
batch_op.alter_column('size',
|
batch_op.alter_column('path',
|
||||||
existing_type=sa.BIGINT(),
|
existing_type=sa.VARCHAR(length=1024),
|
||||||
type_=sa.Integer(),
|
type_=sa.String(length=255),
|
||||||
existing_nullable=True)
|
existing_nullable=False)
|
||||||
batch_op.alter_column('uploaded_by',
|
if 'size' in columns:
|
||||||
existing_type=sa.INTEGER(),
|
batch_op.alter_column('size',
|
||||||
nullable=True)
|
existing_type=sa.BIGINT(),
|
||||||
batch_op.alter_column('uploaded_at',
|
type_=sa.Integer(),
|
||||||
existing_type=postgresql.TIMESTAMP(),
|
existing_nullable=True)
|
||||||
nullable=True)
|
if 'uploaded_by' in columns:
|
||||||
|
batch_op.alter_column('uploaded_by',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
nullable=True)
|
||||||
|
if 'uploaded_at' in columns:
|
||||||
|
batch_op.alter_column('uploaded_at',
|
||||||
|
existing_type=postgresql.TIMESTAMP(),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('room_file')]
|
||||||
|
|
||||||
with op.batch_alter_table('room_file', schema=None) as batch_op:
|
with op.batch_alter_table('room_file', schema=None) as batch_op:
|
||||||
batch_op.alter_column('uploaded_at',
|
if 'uploaded_at' in columns:
|
||||||
existing_type=postgresql.TIMESTAMP(),
|
batch_op.alter_column('uploaded_at',
|
||||||
nullable=False)
|
existing_type=postgresql.TIMESTAMP(),
|
||||||
batch_op.alter_column('uploaded_by',
|
nullable=False)
|
||||||
existing_type=sa.INTEGER(),
|
if 'uploaded_by' in columns:
|
||||||
nullable=False)
|
batch_op.alter_column('uploaded_by',
|
||||||
batch_op.alter_column('size',
|
existing_type=sa.INTEGER(),
|
||||||
existing_type=sa.Integer(),
|
nullable=False)
|
||||||
type_=sa.BIGINT(),
|
if 'size' in columns:
|
||||||
existing_nullable=True)
|
batch_op.alter_column('size',
|
||||||
batch_op.alter_column('path',
|
existing_type=sa.Integer(),
|
||||||
existing_type=sa.String(length=255),
|
type_=sa.BIGINT(),
|
||||||
type_=sa.VARCHAR(length=1024),
|
existing_nullable=True)
|
||||||
existing_nullable=False)
|
if 'path' in columns:
|
||||||
batch_op.drop_column('starred')
|
batch_op.alter_column('path',
|
||||||
|
existing_type=sa.String(length=255),
|
||||||
|
type_=sa.VARCHAR(length=1024),
|
||||||
|
existing_nullable=False)
|
||||||
|
if 'starred' in columns:
|
||||||
|
batch_op.drop_column('starred')
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
32
migrations/versions/72ab6c4c6a5f_merge_heads.py
Normal file
32
migrations/versions/72ab6c4c6a5f_merge_heads.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""merge heads
|
||||||
|
|
||||||
|
Revision ID: 72ab6c4c6a5f
|
||||||
|
Revises: 0a8006bd1732, add_docupulse_settings, add_manager_role, make_events_user_id_nullable
|
||||||
|
Create Date: 2025-06-05 14:21:46.046125
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '72ab6c4c6a5f'
|
||||||
|
down_revision = ('0a8006bd1732', 'add_docupulse_settings', 'add_manager_role', 'make_events_user_id_nullable')
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Ensure is_manager column exists
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('user')]
|
||||||
|
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
if 'is_manager' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('is_manager', sa.Boolean(), nullable=True, server_default='false'))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2024-03-19 10:05:00.000000
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
24
migrations/versions/761908f0cacf_merge_heads.py
Normal file
24
migrations/versions/761908f0cacf_merge_heads.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""merge heads
|
||||||
|
|
||||||
|
Revision ID: 761908f0cacf
|
||||||
|
Revises: 4ee23cb29001, add_connection_token
|
||||||
|
Create Date: 2025-06-09 13:57:17.650231
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '761908f0cacf'
|
||||||
|
down_revision = ('4ee23cb29001', 'add_connection_token')
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-25 10:03:03.423064
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Create Date: 2025-05-26 10:42:17.287566
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -18,17 +19,31 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = inspect(conn)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('site_settings')]
|
||||||
|
|
||||||
with op.batch_alter_table('site_settings', schema=None) as batch_op:
|
with op.batch_alter_table('site_settings', schema=None) as batch_op:
|
||||||
batch_op.add_column(sa.Column('company_website', sa.String(length=200), nullable=True))
|
if 'company_website' not in columns:
|
||||||
batch_op.add_column(sa.Column('company_email', sa.String(length=100), nullable=True))
|
batch_op.add_column(sa.Column('company_website', sa.String(length=200), nullable=True))
|
||||||
batch_op.add_column(sa.Column('company_phone', sa.String(length=20), nullable=True))
|
if 'company_email' not in columns:
|
||||||
batch_op.add_column(sa.Column('company_address', sa.String(length=200), nullable=True))
|
batch_op.add_column(sa.Column('company_email', sa.String(length=100), nullable=True))
|
||||||
batch_op.add_column(sa.Column('company_city', sa.String(length=100), nullable=True))
|
if 'company_phone' not in columns:
|
||||||
batch_op.add_column(sa.Column('company_state', sa.String(length=100), nullable=True))
|
batch_op.add_column(sa.Column('company_phone', sa.String(length=20), nullable=True))
|
||||||
batch_op.add_column(sa.Column('company_zip', sa.String(length=20), nullable=True))
|
if 'company_address' not in columns:
|
||||||
batch_op.add_column(sa.Column('company_country', sa.String(length=100), nullable=True))
|
batch_op.add_column(sa.Column('company_address', sa.String(length=200), nullable=True))
|
||||||
batch_op.add_column(sa.Column('company_description', sa.Text(), nullable=True))
|
if 'company_city' not in columns:
|
||||||
batch_op.add_column(sa.Column('company_industry', sa.String(length=100), nullable=True))
|
batch_op.add_column(sa.Column('company_city', sa.String(length=100), nullable=True))
|
||||||
|
if 'company_state' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('company_state', sa.String(length=100), nullable=True))
|
||||||
|
if 'company_zip' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('company_zip', sa.String(length=20), nullable=True))
|
||||||
|
if 'company_country' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('company_country', sa.String(length=100), nullable=True))
|
||||||
|
if 'company_description' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('company_description', sa.Text(), nullable=True))
|
||||||
|
if 'company_industry' not in columns:
|
||||||
|
batch_op.add_column(sa.Column('company_industry', sa.String(length=100), nullable=True))
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Create Date: 2024-03-19 10:15:00.000000
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""add portainer stack fields to instances
|
||||||
|
|
||||||
|
Revision ID: 9206bf87bb8e
|
||||||
|
Revises: add_quota_fields
|
||||||
|
Create Date: 2025-06-24 14:02:17.375785
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9206bf87bb8e'
|
||||||
|
down_revision = 'add_quota_fields'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
# Check if columns already exist
|
||||||
|
result = conn.execute(text("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'instances'
|
||||||
|
AND column_name IN ('portainer_stack_id', 'portainer_stack_name')
|
||||||
|
"""))
|
||||||
|
existing_columns = [row[0] for row in result.fetchall()]
|
||||||
|
|
||||||
|
# Add portainer stack columns if they don't exist
|
||||||
|
with op.batch_alter_table('instances', schema=None) as batch_op:
|
||||||
|
if 'portainer_stack_id' not in existing_columns:
|
||||||
|
batch_op.add_column(sa.Column('portainer_stack_id', sa.String(length=100), nullable=True))
|
||||||
|
if 'portainer_stack_name' not in existing_columns:
|
||||||
|
batch_op.add_column(sa.Column('portainer_stack_name', sa.String(length=100), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('instances', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('portainer_stack_name')
|
||||||
|
batch_op.drop_column('portainer_stack_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -7,8 +7,10 @@ Create Date: 2025-05-25 21:16:39.683736
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '9faab7ef6036'
|
revision = '9faab7ef6036'
|
||||||
down_revision = 'ca9026520dad'
|
down_revision = 'ca9026520dad'
|
||||||
@@ -18,25 +20,35 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('site_settings',
|
conn = op.get_bind()
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
inspector = inspect(conn)
|
||||||
sa.Column('primary_color', sa.String(length=7), nullable=True),
|
tables = inspector.get_table_names()
|
||||||
sa.Column('secondary_color', sa.String(length=7), nullable=True),
|
|
||||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
if 'site_settings' not in tables:
|
||||||
sa.PrimaryKeyConstraint('id')
|
op.create_table('site_settings',
|
||||||
)
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('primary_color', sa.String(length=7), nullable=True),
|
||||||
|
sa.Column('secondary_color', sa.String(length=7), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
op.drop_table('color_settings')
|
op.drop_table('color_settings')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('color_settings',
|
conn = op.get_bind()
|
||||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
inspector = inspect(conn)
|
||||||
sa.Column('primary_color', sa.VARCHAR(length=7), autoincrement=False, nullable=True),
|
tables = inspector.get_table_names()
|
||||||
sa.Column('secondary_color', sa.VARCHAR(length=7), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
if 'color_settings' not in tables:
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('color_settings_pkey'))
|
op.create_table('color_settings',
|
||||||
)
|
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('primary_color', sa.VARCHAR(length=7), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('secondary_color', sa.VARCHAR(length=7), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('color_settings_pkey'))
|
||||||
|
)
|
||||||
op.drop_table('site_settings')
|
op.drop_table('site_settings')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user