Compare commits
434 Commits
v0.0.1-bet
...
@papra/cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6eae043fa | ||
|
|
e1b0555202 | ||
|
|
93517d0f13 | ||
|
|
d967fa6cef | ||
|
|
9b43bafe33 | ||
|
|
334fcbdee4 | ||
|
|
981731bbe5 | ||
|
|
96403c0047 | ||
|
|
08f4a1cd05 | ||
|
|
ca808064fa | ||
|
|
dc6ee5b228 | ||
|
|
14071b0bc9 | ||
|
|
ae3abe9ec7 | ||
|
|
479a603001 | ||
|
|
19f96a1625 | ||
|
|
a03eae79a0 | ||
|
|
4bcfb878f1 | ||
|
|
d2676052c3 | ||
|
|
ec33ae6294 | ||
|
|
432a192b94 | ||
|
|
98d272fb60 | ||
|
|
1d20c0cfe3 | ||
|
|
07a42da57a | ||
|
|
9dee142948 | ||
|
|
5ccdf446f0 | ||
|
|
11ad13058e | ||
|
|
ee9eff4914 | ||
|
|
499b2cdba7 | ||
|
|
b0877645a8 | ||
|
|
8308e93fdf | ||
|
|
1dce0ace41 | ||
|
|
868281bcff | ||
|
|
5b5ce85061 | ||
|
|
157a5cadd1 | ||
|
|
1922f24c0a | ||
|
|
7ac06a0649 | ||
|
|
c150e231aa | ||
|
|
0c235031d2 | ||
|
|
8a7c1c8368 | ||
|
|
cb1f1b5b01 | ||
|
|
abc463f751 | ||
|
|
8edfd48ceb | ||
|
|
3903eed170 | ||
|
|
c70d7e419a | ||
|
|
2240f58f04 | ||
|
|
79e9bb1b61 | ||
|
|
6e18162435 | ||
|
|
16ae4617df | ||
|
|
1c46071e00 | ||
|
|
377c11c185 | ||
|
|
28c3c15cef | ||
|
|
0391a3bcd5 | ||
|
|
2c75eec862 | ||
|
|
ccf7602f19 | ||
|
|
b8a515a313 | ||
|
|
0aad88471b | ||
|
|
efd2ae1c73 | ||
|
|
e9a719d06a | ||
|
|
68714267ad | ||
|
|
75a13da526 | ||
|
|
59d5819018 | ||
|
|
a857370343 | ||
|
|
f4740ba59a | ||
|
|
b0abf7f78a | ||
|
|
182ccbb30b | ||
|
|
75340f0ce7 | ||
|
|
1228486f28 | ||
|
|
655a1c5475 | ||
|
|
d1797eb9be | ||
|
|
bd3e321eb7 | ||
|
|
be25de7721 | ||
|
|
e85403f9a1 | ||
|
|
7de5d0956b | ||
|
|
b1a88230cd | ||
|
|
55bb29582e | ||
|
|
d9263dc703 | ||
|
|
c3ffa8387e | ||
|
|
d40514c043 | ||
|
|
d7df2f095b | ||
|
|
afdcc1c5ba | ||
|
|
92daaa35bb | ||
|
|
e4295e14ab | ||
|
|
ae37d1db36 | ||
|
|
a7464f8b89 | ||
|
|
2dd9ca9835 | ||
|
|
54cc14052c | ||
|
|
f930e46dde | ||
|
|
df75e5accb | ||
|
|
f66a9f5d1b | ||
|
|
c5b337f3bb | ||
|
|
bb1ba3e15e | ||
|
|
ce839c4127 | ||
|
|
8aabd28168 | ||
|
|
1a7a14b3ed | ||
|
|
17cebde051 | ||
|
|
12ead3d017 | ||
|
|
f6c0221858 | ||
|
|
1aaf2c96cd | ||
|
|
9c6f14fc13 | ||
|
|
3d49962ca5 | ||
|
|
c434d873bc | ||
|
|
60982da847 | ||
|
|
73ab9e8ab5 | ||
|
|
c4a9b9b088 | ||
|
|
9a6e822e71 | ||
|
|
e52bc261db | ||
|
|
624ad62c53 | ||
|
|
630f9cc328 | ||
|
|
9f5be458fe | ||
|
|
1bfdb8aa66 | ||
|
|
2e2bb6fbbd | ||
|
|
d09b9ed70d | ||
|
|
e1571d2b87 | ||
|
|
c9a66e4aa8 | ||
|
|
9fa2df4235 | ||
|
|
c84a921988 | ||
|
|
9b5f3993c3 | ||
|
|
b28772317c | ||
|
|
a3f9f05c66 | ||
|
|
0616635cd6 | ||
|
|
9e7a3ba70b | ||
|
|
04990b986e | ||
|
|
097b6bf2b7 | ||
|
|
cb3ce6b1d8 | ||
|
|
405ba645f6 | ||
|
|
ab6fd6ad10 | ||
|
|
782f70ff66 | ||
|
|
1abbf18e94 | ||
|
|
6bcb2a71e9 | ||
|
|
936bc2bd0a | ||
|
|
2efe7321cd | ||
|
|
947bdf8385 | ||
|
|
b5bf0cca4b | ||
|
|
208a561668 | ||
|
|
40cb1d71d5 | ||
|
|
3da13f7591 | ||
|
|
2a444aad31 | ||
|
|
47d8bbd356 | ||
|
|
ed4d7e4a00 | ||
|
|
f382397c0e | ||
|
|
54514e15db | ||
|
|
bb9d5556d3 | ||
|
|
83e943c5b4 | ||
|
|
40b0557553 | ||
|
|
b5a0317d24 | ||
|
|
9730a06468 | ||
|
|
ec0a437d86 | ||
|
|
1606310745 | ||
|
|
0a03f42231 | ||
|
|
a62d376772 | ||
|
|
ea9d90d6cf | ||
|
|
27a6b0b53d | ||
|
|
94c4d76b86 | ||
|
|
b08241f20f | ||
|
|
e77a42fbf1 | ||
|
|
d488efe2cc | ||
|
|
14c3587de0 | ||
|
|
7400a3a6ec | ||
|
|
14bc2b8f8d | ||
|
|
e48745331f | ||
|
|
38dcc765f9 | ||
|
|
c085b9d676 | ||
|
|
a64a93544d | ||
|
|
f20559e95d | ||
|
|
7d755d0981 | ||
|
|
5382019721 | ||
|
|
b33fde35d3 | ||
|
|
fd6f83f538 | ||
|
|
7f7e5bffcb | ||
|
|
5868800bce | ||
|
|
b5ccc135ba | ||
|
|
5e46bb9e6a | ||
|
|
41a113334a | ||
|
|
6723baf98a | ||
|
|
bbe5fe74e2 | ||
|
|
a8cff8cedc | ||
|
|
67b3b14cdf | ||
|
|
ffdae8db56 | ||
|
|
7768840aa4 | ||
|
|
dd3862e50c | ||
|
|
a82ff3a755 | ||
|
|
d5b00307da | ||
|
|
5ce21981a9 | ||
|
|
3401cfbfdc | ||
|
|
26015666de | ||
|
|
09e3bc5e15 | ||
|
|
1711ef866d | ||
|
|
1d23f40894 | ||
|
|
40a1f91b67 | ||
|
|
47b69b15f4 | ||
|
|
a188af1f88 | ||
|
|
f28d8245bf | ||
|
|
aad36f3252 | ||
|
|
21a5ccce6d | ||
|
|
42bc3c6698 | ||
|
|
a9f474dc2d | ||
|
|
ed5a93cb47 | ||
|
|
52df988c02 | ||
|
|
73b8d08076 | ||
|
|
9b2a4b2ae9 | ||
|
|
2a8b88e48a | ||
|
|
be1b70a26a | ||
|
|
1755849483 | ||
|
|
b3693fd9c9 | ||
|
|
2149b50270 | ||
|
|
0b276ee0d5 | ||
|
|
56fb9ec2c4 | ||
|
|
6cedc30716 | ||
|
|
f1e1b4037b | ||
|
|
205c6cfd46 | ||
|
|
c54a71d2c5 | ||
|
|
62b7f0382c | ||
|
|
57c6a26657 | ||
|
|
b8c2bd70e3 | ||
|
|
0c2cf698d1 | ||
|
|
585c53cd9d | ||
|
|
f035458e16 | ||
|
|
556fd8b167 | ||
|
|
81e85295ba | ||
|
|
1c574b8305 | ||
|
|
ff830c234a | ||
|
|
451564f354 | ||
|
|
ecd6af45c8 | ||
|
|
cb652c7166 | ||
|
|
17ca8f8f81 | ||
|
|
f54b8e162a | ||
|
|
6b435bba79 | ||
|
|
8ccdb74834 | ||
|
|
60059c895c | ||
|
|
6e22a93dff | ||
|
|
79c1d3206b | ||
|
|
48a953a584 | ||
|
|
fdb90fa164 | ||
|
|
e9a205c0a3 | ||
|
|
278db63fc8 | ||
|
|
e5ef40f36c | ||
|
|
27c9e39422 | ||
|
|
91d2e236d0 | ||
|
|
d4f72e889a | ||
|
|
759a3ff713 | ||
|
|
34862991fb | ||
|
|
f0876fdc63 | ||
|
|
cb38d66485 | ||
|
|
c28af1407f | ||
|
|
b62ddf2bc4 | ||
|
|
fa7909c62d | ||
|
|
1996b51b4d | ||
|
|
734027f00c | ||
|
|
557cde940c | ||
|
|
26a83052bd | ||
|
|
5aac3f7ba6 | ||
|
|
0ddc2340f0 | ||
|
|
438a31171c | ||
|
|
53bf93f128 | ||
|
|
b400b3f18d | ||
|
|
0627ec25a4 | ||
|
|
72e5a9a4de | ||
|
|
268ac8e358 | ||
|
|
249b3bcfd2 | ||
|
|
d7838b5d57 | ||
|
|
f170ddd817 | ||
|
|
4f53c70854 | ||
|
|
85fa5c4342 | ||
|
|
c5d984a3a0 | ||
|
|
565bd8d7fd | ||
|
|
9b72aa886c | ||
|
|
7410455093 | ||
|
|
dd8f194fd0 | ||
|
|
803c39cbc8 | ||
|
|
096331a4ee | ||
|
|
59ba9465f6 | ||
|
|
a1056702af | ||
|
|
fd44897bca | ||
|
|
332d836d11 | ||
|
|
f613198cbd | ||
|
|
80491a5a58 | ||
|
|
605e21a410 | ||
|
|
dec589b6ed | ||
|
|
c0bd6e2ae4 | ||
|
|
6287aaa973 | ||
|
|
cc2edc59b0 | ||
|
|
9cba84e38b | ||
|
|
5fe401778d | ||
|
|
38aa1ea7f1 | ||
|
|
ab98c1b255 | ||
|
|
265f06f8b7 | ||
|
|
a787b7915c | ||
|
|
0ba6a09923 | ||
|
|
6880bfd41c | ||
|
|
21a2c95e56 | ||
|
|
19e2083a71 | ||
|
|
e6b2d9fb2d | ||
|
|
5140a64c40 | ||
|
|
9ddb7d545d | ||
|
|
2a73551ca4 | ||
|
|
7be56455b0 | ||
|
|
1085bf079c | ||
|
|
b13986e1e3 | ||
|
|
d4462f942b | ||
|
|
2f2ad90fd3 | ||
|
|
2bbb68aa17 | ||
|
|
2b2827cdb3 | ||
|
|
4b4621e4d0 | ||
|
|
fd0f79feb0 | ||
|
|
b9c2448805 | ||
|
|
542225fabc | ||
|
|
e4af2653ea | ||
|
|
4dd15527c0 | ||
|
|
ae0f69043d | ||
|
|
79eafdb3ee | ||
|
|
979df5dad8 | ||
|
|
76c50dce6c | ||
|
|
8acd7de79e | ||
|
|
a3bd2024c6 | ||
|
|
25c26e8dc0 | ||
|
|
07563dce5d | ||
|
|
a0797beb14 | ||
|
|
0701a84973 | ||
|
|
0789ad3e9a | ||
|
|
cb3c9c3194 | ||
|
|
f9b02c4439 | ||
|
|
9afca3fd84 | ||
|
|
faca409604 | ||
|
|
fc973d20fe | ||
|
|
400541f0ce | ||
|
|
f98b810bd4 | ||
|
|
091bd26fbc | ||
|
|
6541a83e72 | ||
|
|
77cf75a08b | ||
|
|
c0785e5e7a | ||
|
|
14489457b2 | ||
|
|
feb8378227 | ||
|
|
8622751c22 | ||
|
|
b17f93b5e3 | ||
|
|
51109c39f8 | ||
|
|
3a1410f554 | ||
|
|
180a9c234f | ||
|
|
25379b5be5 | ||
|
|
300e8918d6 | ||
|
|
6f5ea9f9de | ||
|
|
7b6c37fd4c | ||
|
|
0f9f7831c9 | ||
|
|
51228dc157 | ||
|
|
0786b81e75 | ||
|
|
e92456bc6b | ||
|
|
3e7b4ea2db | ||
|
|
2fd681b8a4 | ||
|
|
73788ceb58 | ||
|
|
24b80eb785 | ||
|
|
ae69eb2b33 | ||
|
|
f78d42ca25 | ||
|
|
4be13d0742 | ||
|
|
416b9d50b6 | ||
|
|
84d2af5df5 | ||
|
|
0d1be0d3a5 | ||
|
|
c1f8507891 | ||
|
|
5422d3e2f6 | ||
|
|
b16b557733 | ||
|
|
e981785a03 | ||
|
|
9395df746e | ||
|
|
998dd2c667 | ||
|
|
b99d72d23b | ||
|
|
b2d1848226 | ||
|
|
d51f19d5e3 | ||
|
|
b0d9cfc67a | ||
|
|
e77b49832f | ||
|
|
947c09eff9 | ||
|
|
85cbd547f5 | ||
|
|
2484932f96 | ||
|
|
97d835f240 | ||
|
|
72414d8122 | ||
|
|
7f4fb3b0e7 | ||
|
|
918ae55ebc | ||
|
|
6a0dd40e1e | ||
|
|
e5d95e3ffe | ||
|
|
08886fd754 | ||
|
|
5a8d30a34f | ||
|
|
ee81d2e6c1 | ||
|
|
d84ad00e95 | ||
|
|
7c1aecd5fa | ||
|
|
e412507f30 | ||
|
|
5521d67a68 | ||
|
|
4962215093 | ||
|
|
274fb7d72e | ||
|
|
a491987c1b | ||
|
|
f37c7dd8f7 | ||
|
|
a7fbf21a9f | ||
|
|
f97e5f863e | ||
|
|
8dcd6bc5ed | ||
|
|
87cb325369 | ||
|
|
e1743954d2 | ||
|
|
44b5b9fd5a | ||
|
|
68c5a3e2b7 | ||
|
|
684138c3fd | ||
|
|
0aa3241712 | ||
|
|
ad6358195e | ||
|
|
0e99669206 | ||
|
|
a91d98fb44 | ||
|
|
f3466e4bfd | ||
|
|
c2dc8bfdfb | ||
|
|
510e8622b5 | ||
|
|
7860ea49a0 | ||
|
|
b319a86934 | ||
|
|
b15bc2a087 | ||
|
|
0c811e3fc4 | ||
|
|
8b3372a2bd | ||
|
|
753a07a008 | ||
|
|
c4943f8de7 | ||
|
|
538b490583 | ||
|
|
79542bab7b | ||
|
|
5cae1fdf7e | ||
|
|
9452c4be92 | ||
|
|
cd5b609427 | ||
|
|
d0a9842e7d | ||
|
|
82ecba25e0 | ||
|
|
904f2c091a | ||
|
|
77eb6dc476 | ||
|
|
1fc6182a09 | ||
|
|
3e1bae897e | ||
|
|
a049479fb5 | ||
|
|
c8cae4842e | ||
|
|
181e59ac87 | ||
|
|
f6960eafea | ||
|
|
e1ab9481e0 | ||
|
|
2a4731c0d7 | ||
|
|
32564fe5ee | ||
|
|
02b7f70393 | ||
|
|
1ff8902bd0 | ||
|
|
912daeaea8 | ||
|
|
81b0cd74d4 | ||
|
|
36cb2b1829 | ||
|
|
bba6cba60e | ||
|
|
0f20b9fd16 | ||
|
|
cad6ff4e51 |
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
17
.changeset/config.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "papra-hq/papra"}
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@papra/app-client", "@papra/app-server", "@papra/docs"],
|
||||
"privatePackages": {
|
||||
"tag": true,
|
||||
"version": true
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.*
|
||||
*.log
|
||||
dist
|
||||
*.local
|
||||
.env
|
||||
.git
|
||||
db.sqlite
|
||||
local-documents
|
||||
1
.dockerignore
Symbolic link
@@ -0,0 +1 @@
|
||||
packages/docker/.dockerignore
|
||||
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
github:
|
||||
- CorentinTh
|
||||
|
||||
buy_me_a_coffee: cthmsst
|
||||
48
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: 🐞 Bug Report
|
||||
description: File a bug report.
|
||||
labels: ['bug', 'triage']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
|
||||
placeholder: Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen? If you have a screenshot, you can paste it here.
|
||||
placeholder: Tell us what you see!
|
||||
value: 'A bug happened!'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: System information
|
||||
description: What is you environment? You can use the `npx envinfo --system --browsers` command to get this information.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: app-type
|
||||
attributes:
|
||||
label: Where did you encounter the bug?
|
||||
options:
|
||||
- Demo app (demo.papra.app)
|
||||
- Public app (dashboard.papra.app
|
||||
- Documentation website (docs.papra.ap)
|
||||
- A self hosted instance
|
||||
- Other (installations, docker, etc.)
|
||||
validations:
|
||||
required: true
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💬 Discord Community
|
||||
url: https://papra.app/discord
|
||||
about: Join the Papra Discord community to get help, share your feedback, and stay updated on the project.
|
||||
52
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: 🚀 New feature proposal
|
||||
description: Propose a new feature/enhancement
|
||||
labels: ['enhancement', 'triage']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||
|
||||
- type: dropdown
|
||||
id: request-type
|
||||
attributes:
|
||||
label: What type of request is this?
|
||||
options:
|
||||
- New feature idea
|
||||
- Enhancement of an existing feature
|
||||
- Deployment or CI/CD improvement
|
||||
- Hosting or Self-hosting improvement
|
||||
- Related to documentation
|
||||
- Related to the community
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Clear and concise description of the feature you are proposing
|
||||
description: A clear and concise description of what the feature is.
|
||||
placeholder: 'Example: I would like to see...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any other context or screenshots about the feature request here.
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Check the feature is not already implemented in the project.
|
||||
required: true
|
||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||
required: true
|
||||
- label: Check that the feature is technically feasible and aligns with the project's goals.
|
||||
required: true
|
||||
BIN
.github/papra-screenshot.png
vendored
Normal file
|
After Width: | Height: | Size: 151 KiB |
40
.github/workflows/ci-apps-docs.yaml
vendored
@@ -1,40 +0,0 @@
|
||||
name: CI - Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci-apps-docs:
|
||||
name: CI - Docs
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
corepack: true
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./
|
||||
|
||||
- name: Run linters
|
||||
run: pnpm lint
|
||||
|
||||
# - name: Type check
|
||||
# run: pnpm typecheck
|
||||
|
||||
# - name: Run unit test
|
||||
# run: pnpm test
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
40
.github/workflows/ci-apps-papra-client.yaml
vendored
@@ -1,40 +0,0 @@
|
||||
name: CI - App Client
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci-apps-papra-client:
|
||||
name: CI - Papra Client
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/papra-client
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
corepack: true
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./
|
||||
|
||||
- name: Run linters
|
||||
run: pnpm lint
|
||||
|
||||
- name: Type check
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Run unit test
|
||||
run: pnpm test
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
39
.github/workflows/ci-apps-papra-server.yaml
vendored
@@ -1,39 +0,0 @@
|
||||
name: CI - App Server
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci-apps-papra-server:
|
||||
name: CI - Papra Server
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/papra-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
corepack: true
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Run linters
|
||||
run: pnpm lint
|
||||
|
||||
- name: Type check
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Run unit test
|
||||
run: pnpm test
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
47
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: CI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
# Build only the packages first to properly run the lint and test first to get faster feedback
|
||||
- name: Build packages
|
||||
run: pnpm -r --parallel -F "./packages/*" build
|
||||
|
||||
- name: Run linters
|
||||
run: pnpm -r --parallel lint
|
||||
|
||||
- name: Type check
|
||||
# Exclude docs as their are some typing issues we are ok with for now
|
||||
run: pnpm -r --parallel -F "!@papra/docs" typecheck
|
||||
|
||||
# Tests are run using vitest projects
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
# Now build the apps, the longer step, so we do it last as they are more unlikely to fail if the previous steps works
|
||||
- name: Build the apps
|
||||
run: pnpm -r --parallel -F "./apps/*" build
|
||||
|
||||
- name: Ensure no non-excluded files are changed for the whole repo
|
||||
run: git diff --exit-code > /dev/null || (echo "After running the CI, some un-committed changes were detected. Please ensure cleanness before merging." && exit 1)
|
||||
39
.github/workflows/release-docker.yaml
vendored
@@ -1,9 +1,12 @@
|
||||
name: Release new versions
|
||||
name: Build and publish Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g. 0.8.2)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -11,17 +14,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
docker-release:
|
||||
name: Release Docker images
|
||||
name: Build and publish Docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get release version from tag
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Get release version from input
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
run: echo "RELEASE_VERSION=${{ github.event.inputs.release_version }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
@@ -48,24 +43,26 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./packages/docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
corentinth/papra:latest-root
|
||||
corentinth/papra:${{ env.RELEASE_VERSION }}-root
|
||||
corentinth/papra:${{ inputs.version }}-root
|
||||
ghcr.io/papra-hq/papra:latest-root
|
||||
ghcr.io/papra-hq/papra:${{ env.RELEASE_VERSION }}-root
|
||||
ghcr.io/papra-hq/papra:${{ inputs.version }}-root
|
||||
|
||||
- name: Build and push rootless Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile.rootless
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./packages/docker/Dockerfile.rootless
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
corentinth/papra:latest
|
||||
corentinth/papra:latest-rootless
|
||||
corentinth/papra:${{ env.RELEASE_VERSION }}-rootless
|
||||
corentinth/papra:${{ inputs.version }}-rootless
|
||||
ghcr.io/papra-hq/papra:latest
|
||||
ghcr.io/papra-hq/papra:latest-rootless
|
||||
ghcr.io/papra-hq/papra:${{ env.RELEASE_VERSION }}-rootless
|
||||
ghcr.io/papra-hq/papra:${{ inputs.version }}-rootless
|
||||
57
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'papra-hq/papra'
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Create Release Pull Request
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
# Note: pnpm install after versioning is necessary to refresh lockfile
|
||||
version: pnpm run version
|
||||
publish: pnpm exec changeset publish
|
||||
commit: "chore(release): update versions"
|
||||
title: "chore(release): update versions"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger Docker build
|
||||
if: steps.changesets.outputs.published == 'true' && contains(fromJson(steps.changesets.outputs.publishedPackages).*.name, '@papra/docker')
|
||||
run: |
|
||||
VERSION=$(echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[] | select(.name=="@papra/docker") | .version')
|
||||
echo "VERSION: $VERSION"
|
||||
gh workflow run release-docker.yaml -f version="$VERSION"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
9
.gitignore
vendored
@@ -35,6 +35,13 @@ cache
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
|
||||
local-documents
|
||||
.cursorrules
|
||||
ingestion
|
||||
.cursorrules
|
||||
*.traineddata
|
||||
|
||||
.eslintcache
|
||||
.claude
|
||||
222
CLAUDE.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Papra is a minimalistic document management and archiving platform built as a monorepo using PNPM workspaces. The project includes a SolidJS frontend, HonoJS backend, CLI tools, and supporting packages.
|
||||
It's open-source and designed for easy self-hosting using Docker, and also offers a cloud-hosted SaaS version.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
- **apps/papra-server**: Backend API server (HonoJS + Drizzle ORM + Better Auth)
|
||||
- **apps/papra-client**: Frontend application (SolidJS + UnoCSS + Shadcn Solid)
|
||||
- **apps/docs**: Documentation site (Astro + Starlight)
|
||||
- **packages/lecture**: Text extraction library for documents
|
||||
- **packages/api-sdk**: API client SDK
|
||||
- **packages/cli**: Command-line interface
|
||||
- **packages/webhooks**: Webhook types and utilities
|
||||
|
||||
### Backend Architecture (apps/papra-server)
|
||||
|
||||
The backend follows a modular architecture with feature-based modules:
|
||||
|
||||
- **Module pattern**: Each feature lives in `src/modules/<feature>/` with:
|
||||
- `*.repository.ts`: Database access layer (Drizzle ORM queries)
|
||||
- `*.usecases.ts`: Business logic and orchestration
|
||||
- `*.routes.ts`: HTTP route handlers (Hono)
|
||||
- `*.services.ts`: Service layer for external integrations
|
||||
- `*.table.ts`: Drizzle schema definitions
|
||||
- `*.types.ts`: TypeScript type definitions
|
||||
- `*.errors.ts`: Error definitions
|
||||
|
||||
- **Core modules**: `app`, `shared`, `config`, `tasks`
|
||||
- **Feature modules**: `documents`, `organizations`, `users`, `tags`, `tagging-rules`, `intake-emails`, `ingestion-folders`, `webhooks`, `api-keys`, `subscriptions`, etc.
|
||||
|
||||
- **Database**: Uses Drizzle ORM with SQLite/Turso (libsql). Schema is in `*.table.ts` files, migrations in `src/migrations/`
|
||||
|
||||
- **Authentication**: Better Auth library for user auth
|
||||
|
||||
- **Task system**: Background job processing using Cadence MQ, a custom made queue system (papra-hq/cadence-mq)
|
||||
|
||||
- **Document storage**: Abstracted storage supporting local filesystem, S3, and Azure Blob
|
||||
|
||||
### Frontend Architecture (apps/papra-client)
|
||||
|
||||
- **SolidJS** for reactivity with router (`@solidjs/router`)
|
||||
- **Module pattern**: Features in `src/modules/<feature>/` with:
|
||||
- `components/`: UI components
|
||||
- `pages/`: Route components
|
||||
- `*.services.ts`: API client calls
|
||||
- `*.provider.tsx`: Context providers
|
||||
- `*.types.ts`: Type definitions
|
||||
|
||||
- **Routing**: Defined in `src/routes.tsx`
|
||||
- **Styling**: UnoCSS for atomic CSS with Shadcn Solid components
|
||||
- **State**: TanStack Query for server state, local storage for client state
|
||||
- **i18n**: TypeScript-based translations in `src/locales/*.dictionary.ts`
|
||||
|
||||
### Dependency Injection Pattern
|
||||
|
||||
The server uses a dependency injection pattern with `@corentinth/chisels/injectArguments` to create testable services that accept dependencies as parameters.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build all packages (required before running apps)
|
||||
pnpm build:packages
|
||||
```
|
||||
|
||||
### Backend Development
|
||||
|
||||
```bash
|
||||
cd apps/papra-server
|
||||
|
||||
# Run database migrations
|
||||
pnpm migrate:up
|
||||
|
||||
# Start development server (localhost:1221)
|
||||
pnpm dev
|
||||
|
||||
# Run tests
|
||||
pnpm test # All tests
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:unit # Unit tests only
|
||||
pnpm test:int # Integration tests only
|
||||
|
||||
# Lint and typecheck
|
||||
pnpm lint
|
||||
pnpm typecheck
|
||||
|
||||
# Database management
|
||||
pnpm db:studio # Open Drizzle Studio
|
||||
pnpm migrate:create "migration_name" # Create new migration
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
|
||||
```bash
|
||||
cd apps/papra-client
|
||||
|
||||
# Start development server (localhost:3000)
|
||||
pnpm dev
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
pnpm test:watch
|
||||
pnpm test:e2e # Playwright E2E tests
|
||||
|
||||
# Lint and typecheck
|
||||
pnpm lint
|
||||
pnpm typecheck
|
||||
|
||||
# i18n key synchronization
|
||||
pnpm script:sync-i18n-key-order
|
||||
```
|
||||
|
||||
### Package Development
|
||||
|
||||
```bash
|
||||
cd packages/<package-name>
|
||||
|
||||
# Build package
|
||||
pnpm build
|
||||
pnpm build:watch # Watch mode (or pnpm dev)
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
### Root-level Commands
|
||||
|
||||
```bash
|
||||
# Run tests across all packages
|
||||
pnpm test
|
||||
pnpm test:watch
|
||||
|
||||
# Build all packages
|
||||
pnpm build:packages
|
||||
|
||||
# Version management (changesets)
|
||||
pnpm changeset # Create changeset
|
||||
pnpm version # Apply changesets and bump versions
|
||||
|
||||
# Docker builds
|
||||
pnpm docker:build:root
|
||||
pnpm docker:build:root:amd64
|
||||
pnpm docker:build:root:arm64
|
||||
```
|
||||
|
||||
### Documentation Development
|
||||
|
||||
```bash
|
||||
cd apps/docs
|
||||
pnpm dev # localhost:4321
|
||||
```
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Use **Vitest** for all testing
|
||||
- Test files: `*.test.ts` for unit tests, `*.int.test.ts` for integration tests
|
||||
- Integration tests may use Testcontainers (Azurite, LocalStack)
|
||||
- All new features require test coverage
|
||||
|
||||
### Writing Good Test Names
|
||||
|
||||
Test names should explain the **why** (business logic, user scenario, or expected behavior), not the **how** (implementation details or return values).
|
||||
|
||||
**Key principles:**
|
||||
- **Describe blocks** should explain the business goal or rule being tested
|
||||
- **Test names** should explain the scenario, context, and reason for the behavior
|
||||
- Avoid implementation details like "returns X", "should be Y", "calls Z method"
|
||||
- Focus on user scenarios and business rules
|
||||
- Make tests readable as documentation - someone unfamiliar with the code should understand what's being tested and why
|
||||
|
||||
## Code Style
|
||||
|
||||
- **ESLint config**: `@antfu/eslint-config` (auto-fix on save recommended)
|
||||
- **Conventions**:
|
||||
- Use functional programming where possible
|
||||
- Prefer clarity and maintainability over performance
|
||||
- Use meaningful names for variables, functions, and components
|
||||
- Follow Conventional Commits for commit messages
|
||||
- **Type safety**: Strict TypeScript throughout
|
||||
|
||||
## i18n
|
||||
|
||||
- Language files in `apps/papra-client/src/locales/*.dictionary.ts`
|
||||
- Reference `en.dictionary.ts` for all keys (English is fallback)
|
||||
- Fully type-safe with TypeScript
|
||||
- Update `i18n.constants.ts` when adding new languages
|
||||
- Use `pnpm script:sync-i18n-key-order` to sync key order
|
||||
- **Branchlet/core**: Uses `@branchlet/core` for pluralization and conditional i18n string templates (variant of ICU message format)
|
||||
- Basic interpolation: `'Hello {{ name }}!'` with `{ name: 'World' }`
|
||||
- Conditionals: `'{{ count, =0:no items, =1:one item, many items }}'`
|
||||
- Pluralization with variables: `'{{ count, =0:no items, =1:{count} item, {count} items }}'`
|
||||
- Range conditions: `'{{ score, [0-50]:bad, [51-75]:good, [76-100]:excellent }}'`
|
||||
- See [branchlet documentation](https://github.com/CorentinTh/branchlet) for more details
|
||||
|
||||
## Contributing Flow
|
||||
|
||||
1. Open an issue before submitting PRs for features/bugs
|
||||
2. Target the `main` branch (continuously deployed to production)
|
||||
3. Keep PRs small and atomic
|
||||
4. Ensure CI is green (linting, type checking, testing, building)
|
||||
5. PRs are squashed on merge
|
||||
|
||||
## Key Technologies
|
||||
|
||||
- **Frontend**: SolidJS, UnoCSS, Shadcn Solid, TanStack Query, Vite
|
||||
- **Backend**: HonoJS, Drizzle ORM, Better Auth, Zod, Cadence MQ
|
||||
- **Database**: SQLite/Turso (libsql)
|
||||
- **Testing**: Vitest, Playwright, Testcontainers
|
||||
- **Monorepo**: PNPM workspaces with catalog for shared dependencies
|
||||
- **Build**: esbuild (backend), Vite (frontend), tsdown (packages)
|
||||
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @papra-hq/papra-maintainers
|
||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<corentinth@proton.me>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||
<https://www.contributor-covenant.org/translations>.
|
||||
236
CONTRIBUTING.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Contributing to Papra
|
||||
|
||||
First off, thanks for taking the time to contribute to Papra! We welcome contributions of all types and encourage you to help make this project better for everyone.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project adheres to the [Contributor Covenant](https://www.contributor-covenant.org/). By participating, you are expected to uphold this code. Please report unacceptable behavior to <corentinth@proton.me>
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Issues
|
||||
|
||||
If you find a bug, have a feature request, or need help, feel free to open an issue in the [GitHub Issue Tracker](https://github.com/papra-hq/papra/issues). You're also welcome to comment on existing issues.
|
||||
|
||||
### Submitting Pull Requests
|
||||
|
||||
Please refrain from submitting pull requests that implement new features or fix bugs without first opening an issue. This will help us avoid duplicate work and ensure that your contribution is in line with the project's goals and prevents wasted effort on your part.
|
||||
|
||||
We follow a **GitHub Flow** model where all PRs should target the `main` branch, which is continuously deployed to production.
|
||||
|
||||
**Guidelines for submitting PRs:**
|
||||
|
||||
- Each PR should be small and atomic. Please avoid solving multiple unrelated issues in a single PR.
|
||||
- Ensure that the **CI is green** before submitting. Some of the following checks are automatically run for each package: linting, type checking, testing, and building.
|
||||
- If your PR fixes an issue, please reference the issue number in the PR description.
|
||||
- If your PR adds a new feature, please include tests and update the documentation if necessary.
|
||||
- Be prepared to address feedback and iterate on your PR.
|
||||
- Resolving merge conflicts is part of the PR author's responsibility.
|
||||
- Draft PRs are welcome to get feedback early on your work but only when requested, they'll not be reviewed.
|
||||
|
||||
### Branching
|
||||
|
||||
- **Main branch**: This is the production branch. All pull requests must target this branch.
|
||||
- **Feature branches**: Create a new branch for your feature (e.g., `my-new-feature`), make your changes, and then open a PR targeting `main`.
|
||||
|
||||
### Commit Guidelines
|
||||
|
||||
We use **[Conventional Commits](https://www.conventionalcommits.org/)** to keep commit messages consistent and meaningful. Please follow these guidelines when writing commit messages. While you can structure commits however you like, PRs will be squashed on merge.
|
||||
|
||||
## i18n
|
||||
|
||||
We welcome contributions to improve and expand the app's internationalization (i18n) support. Below are the guidelines for adding a new language or updating an existing translation.
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
1. **Create a Language File**: To add a new language, create a TypeScript file named with the appropriate [ISO language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) followed by `.dictionary.ts` (e.g., `fr.dictionary.ts` for French) in the [`apps/papra-client/src/locales`](./apps/papra-client/src/locales) directory.
|
||||
|
||||
2. **Use the Reference File**: Refer to the [`en.dictionary.ts`](./apps/papra-client/src/locales/en.dictionary.ts) file, which contains all keys used in the app. Use it as a base to ensure consistency when creating your new language file. The English translations act as a fallback if a key is missing in the new language file.
|
||||
|
||||
3. **Update the Locale List**: After adding the new language file, include the language code in the `locales` array found in the [`apps/papra-client/src/modules/i18n/i18n.constants.ts`](./apps/papra-client/src/modules/i18n/i18n.constants.ts) file.
|
||||
|
||||
4. **Submit a Pull Request**: Once you've added the file and updated `i18n.constants.ts`, create a pull request (PR) with your changes. Ensure that your PR is clearly titled with the language being added (e.g., "Add French translations").
|
||||
|
||||
### Updating an Existing Language
|
||||
|
||||
If you want to update an existing language file, you can do so directly in the corresponding TypeScript file in the [`apps/papra-client/src/locales`](./apps/papra-client/src/locales) directory. The translation keys are now fully type-safe with TypeScript, so you'll get immediate feedback if you add invalid keys or have syntax errors.
|
||||
|
||||
> [!TIP]
|
||||
> You can use the command `pnpm script:sync-i18n-key-order` to sync the order of the keys in the TypeScript i18n files, it'll also add the missing keys as comments.
|
||||
|
||||
### Using Branchlet for Pluralization and Conditionals
|
||||
|
||||
Papra uses [`@branchlet/core`](https://github.com/CorentinTh/branchlet) for pluralization and conditional i18n string templates (a variant of ICU message format). Here are some common patterns:
|
||||
|
||||
- **Basic interpolation**: `'Hello {{ name }}!'` with `{ name: 'World' }`
|
||||
- **Conditionals**: `'{{ count, =0:no items, =1:one item, many items }}'`
|
||||
- **Pluralization with variables**: `'{{ count, =0:no items, =1:{count} item, {count} items }}'`
|
||||
- **Range conditions**: `'{{ score, [0-50]:bad, [51-75]:good, [76-100]:excellent }}'`
|
||||
|
||||
See the [branchlet documentation](https://github.com/CorentinTh/branchlet) for more details on syntax and advanced usage.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Local Environment Setup
|
||||
|
||||
We recommend running the app locally for development. Follow these steps:
|
||||
|
||||
1. Clone the repository and navigate inside the project directory.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/papra-hq/papra.git
|
||||
cd papra
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Build the monorepo packages:
|
||||
|
||||
As the apps rely on internal packages, you need to build them first.
|
||||
|
||||
```bash
|
||||
pnpm build:packages
|
||||
```
|
||||
|
||||
4. Start the development server for the backend:
|
||||
|
||||
```bash
|
||||
cd apps/papra-server
|
||||
# Run the migration script to create the database schema
|
||||
pnpm migrate:up
|
||||
# Start the server
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
5. Start the frontend:
|
||||
|
||||
```bash
|
||||
cd apps/papra-client
|
||||
# Start the client
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
6. Open your browser and navigate to `http://localhost:3000`.
|
||||
|
||||
### IDE Setup
|
||||
|
||||
#### ESLint Extension
|
||||
|
||||
We recommend installing the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for VS Code to get real-time linting feedback and automatic code fixing.
|
||||
The linting configuration is based on [@antfu/eslint-config](https://github.com/antfu/eslint-config), you can find specific IDE configurations in their repository.
|
||||
|
||||
<details>
|
||||
<summary>Recommended VS Code Settings</summary>
|
||||
|
||||
Create or update your `.vscode/settings.json` file with the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in your IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Testing
|
||||
|
||||
We use **Vitest** for testing. Each package comes with its own testing commands.
|
||||
|
||||
- To run the tests for any package:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
- To run tests in watch mode:
|
||||
|
||||
```bash
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
All new features must be covered by unit or integration tests. Be sure to use business-oriented test names (avoid vague descriptions like `it('should return true')`).
|
||||
|
||||
## Writing Documentation
|
||||
|
||||
If your code changes affect the documentation, you must update the docs. The documentation is powered by [**Astro Starlight**](https://starlight.astro.build/).
|
||||
|
||||
To start the documentation server for local development:
|
||||
|
||||
1. Navigate to the `packages/docs` directory:
|
||||
|
||||
```bash
|
||||
cd apps/docs
|
||||
```
|
||||
|
||||
2. Start the documentation server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
3. Open your browser and navigate to `http://localhost:4321`.
|
||||
|
||||
## Coding Style
|
||||
|
||||
- Use functional programming where possible.
|
||||
- Focus on clarity and maintainability over performance.
|
||||
- Choose meaningful, relevant names for variables, functions, and components.
|
||||
|
||||
## Issue Labels
|
||||
|
||||
Look out for issues tagged as [**good first issue**](https://github.com/papra-hq/papra/issues?q=sort%3Aupdated-desc%20is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) or [**PR welcome**](https://github.com/papra-hq/papra/issues?q=sort%3Aupdated-desc+is%3Aissue+state%3Aopen+label%3A%22PR+welcome%22) for tasks that are well-suited for new contributors. Feel free to comment on existing issues or create new ones.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the [AGPL3](./LICENSE), the same as the project itself.
|
||||
83
README.md
@@ -17,24 +17,31 @@
|
||||
<a href="https://demo.papra.app">Demo</a>
|
||||
<span> • </span>
|
||||
<a href="https://docs.papra.app">Docs</a>
|
||||
<!-- <span> • </span>
|
||||
<a href="https://docs.papra.app/self-hosting/docker">Self-hosting</a> -->
|
||||
<span> • </span>
|
||||
<a href="https://docs.papra.app/self-hosting/using-docker">Self-hosting</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/orgs/papra-hq/projects/2">Roadmap</a>
|
||||
<span> • </span>
|
||||
<a href="https://dashboard.papra.app">Managed instance</a>
|
||||
<a href="https://papra.app/discord">Discord</a>
|
||||
<!-- <span> • </span>
|
||||
<a href="https://dashboard.papra.app">Managed instance</a> -->
|
||||
</p>
|
||||
|
||||
## Introduction
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Papra** is currently in active development and is not yet ready for production use or self-hosting.
|
||||
|
||||
**Papra** is a minimalistic document management and archiving platform. It is designed to be simple to use and accessible to everyone. Papra is a plateform for long-term document storage and management, like a digital archive for your documents.
|
||||
**Papra** is a minimalistic document management and archiving platform. It is designed to be simple to use and accessible to everyone. Papra is a platform for long-term document storage and management, like a digital archive for your documents.
|
||||
|
||||
Forget about that receipt of that gift you bought for your friend last year, or that warranty for your new phone. With Papra, you can easily store, forget, and retrieve your documents whenever you need them.
|
||||
|
||||
A live demo of the platform is available at [demo.papra.cc](https://demo.papra.cc) (no backend, client-side local storage only).
|
||||
A live demo of the platform is available at [demo.papra.app](https://demo.papra.app) (no backend, client-side local storage only).
|
||||
|
||||
[](https://demo.papra.app)
|
||||
|
||||
## Project Status
|
||||
|
||||
Papra is under active development, the core functionalities are stable and usable. With lots of features and improvements added regularly.
|
||||
|
||||
Feedback and bug reports are highly appreciated to help us improve the platform.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -45,28 +52,51 @@ A live demo of the platform is available at [demo.papra.cc](https://demo.papra.c
|
||||
- **Dark Mode**: A dark theme for those late-night document management sessions.
|
||||
- **Responsive Design**: Works on all devices, from desktops to mobile phones.
|
||||
- **Open Source**: The project is open-source and free to use.
|
||||
- *Coming soon:* **Self-hosting**: Host your own instance of Papra using Docker or other methods.
|
||||
- *Coming soon:* **Tags**: Organize your documents with tags.
|
||||
- *Coming soon:* **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||
- *Coming soon:* **OCR**: Automatically extract text from images or scanned documents for search.
|
||||
- *Coming soon:* **i18n**: Support for multiple languages.
|
||||
- *Coming soon:* **Email ingestion**: Forward emails to automatically import documents.
|
||||
- *Coming soon:* **SDK and API**: Build your own applications on top of Papra.
|
||||
- *Coming soon:* **CLI**: Manage your documents from the command line.
|
||||
- **Self-hosting**: Host your own instance of Papra using Docker or other methods.
|
||||
- **Tags**: Organize your documents with tags.
|
||||
- **Email ingestion**: Send/forward emails to a generated address to automatically import documents.
|
||||
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
||||
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||
- **Folder ingestion**: Automatically import documents from a folder.
|
||||
- **CLI**: Manage your documents from the command line.
|
||||
- **API, SDK and webhooks**: Build your own applications on top of Papra.
|
||||
- **i18n**: Support for multiple languages.
|
||||
- *Coming soon:* **Document sharing**: Share documents with others.
|
||||
- *Coming soon:* **Folder ingestion**: Automatically import documents from a folder.
|
||||
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.
|
||||
- *Coming maybe one day:* **Mobile app**: Access and upload documents on the go.
|
||||
- *Coming maybe one day:* **Desktop app**: Access and upload documents from your computer.
|
||||
- *Coming maybe one day:* **Browser extension**: Upload documents from your browser.
|
||||
- *Coming maybe one day:* **AI**: Use AI to help you manage or tag your documents.
|
||||
|
||||
## Sponsors
|
||||
|
||||
Papra is a free and open-source project, but it is not free to run and develop. If you want to support the project, you can become a sponsor on [GitHub Sponsors](https://github.com/sponsors/corentinth) or [Buy me a coffee](https://buymeacoffee.com/cthmsst). If you are a company, you can also contact me to discuss a sponsorship.
|
||||
|
||||
## Self-hosting
|
||||
|
||||
Papra is dedicated to providing a simple yet highly configurable self-hosting experience. Our lightweight Docker image (<200MB) is compatible with multiple architectures including x86, ARM64, and ARMv7.
|
||||
|
||||
For a quick start, simply run the following command:
|
||||
|
||||
```bash
|
||||
docker run -d --name papra -p 1221:1221 ghcr.io/papra-hq/papra:latest
|
||||
```
|
||||
|
||||
Please refer to the [self-hosting documentation](https://docs.papra.app/self-hosting/using-docker) for more information and configuration options.
|
||||
|
||||
## Contributing
|
||||
|
||||
*Coming soon*
|
||||
Currently, the project is in heavy development and is not yet ready for contributions as changes are frequent and the architecture is not yet finalized. However, you can star the project to follow its progress.
|
||||
Contributions are welcome! Please refer to the [`CONTRIBUTING.md`](./CONTRIBUTING.md) file for guidelines on how to get started, report issues, and submit pull requests.
|
||||
You can find easy-to-pick-up tasks with the [`good first issue`](https://github.com/papra-hq/papra/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22) or [`PR welcome`](https://github.com/papra-hq/papra/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22) labels.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPL-3.0 License - see the [LICENSE](./LICENSE) file for details.
|
||||
|
||||
## Community
|
||||
|
||||
Join the community on [Papra's Discord server](https://papra.app/discord) to discuss the project, ask questions, or get help.
|
||||
|
||||
## Credits
|
||||
|
||||
This project is crafted with ❤️ by [Corentin Thomasset](https://corentin.tech).
|
||||
@@ -76,7 +106,7 @@ If you find this project helpful, please consider [supporting my work](https://b
|
||||
|
||||
### Stack
|
||||
|
||||
Enclosed would not have been possible without the following open-source projects:
|
||||
Papra would not have been possible without the following open-source projects:
|
||||
|
||||
- **Frontend**
|
||||
- **[SolidJS](https://www.solidjs.com)**: A declarative JavaScript library for building user interfaces.
|
||||
@@ -87,7 +117,20 @@ Enclosed would not have been possible without the following open-source projects
|
||||
- **Backend**
|
||||
- **[HonoJS](https://hono.dev/)**: A small, fast, and lightweight web framework for building APIs.
|
||||
- **[Drizzle](https://orm.drizzle.team/)**: A simple and lightweight ORM for Node.js.
|
||||
- **[Better Auth](https://better-auth.com/)**: A simple and lightweight authentication library for Node.js.
|
||||
- **[CadenceMQ](https://github.com/papra-hq/cadence-mq)**: A self-hosted-friendly job queue for Node.js, made by Papra.
|
||||
- And other dependencies listed in the **[server package.json](./apps/papra-server/package.json)**
|
||||
- **Documentation**
|
||||
- **[Astro](https://astro.build)**: A great static site generator.
|
||||
- **[Starlight](https://starlight.astro.build)**: A module for Astro that provides a starting point for building documentation websites.
|
||||
- **[HiDeoo/starlight-theme-rapide](https://github.com/HiDeoo/starlight-theme-rapide)**: A theme for Starlight.
|
||||
- **Project**
|
||||
- **[PNPM Workspaces](https://pnpm.io/workspaces)**: A monorepo management tool.
|
||||
- **[Github Actions](https://github.com/features/actions)**: For CI/CD.
|
||||
- **Infrastructure**
|
||||
- **[Cloudflare Pages](https://pages.cloudflare.com/)**: For static site hosting.
|
||||
- **[Fly.io](https://fly.io/)**: For backend hosting.
|
||||
- **[Turso](https://turso.tech/)**: For production database.
|
||||
|
||||
### Inspiration
|
||||
|
||||
|
||||
35
SECURITY.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Security Policy
|
||||
|
||||
Security is critically important to Papra. We actively welcome responsible disclosure of any vulnerabilities found in our platform.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security issue within Papra, please email us directly at **security@papra.app** with the following details:
|
||||
|
||||
- Clear description of the vulnerability.
|
||||
- Steps or proof-of-concept to reproduce the vulnerability.
|
||||
- Potential impact or implications of the vulnerability.
|
||||
|
||||
We ask you **not to publicly disclose the vulnerability** until we have had a reasonable opportunity to address it.
|
||||
|
||||
## Response and Communication
|
||||
|
||||
We will:
|
||||
|
||||
- Acknowledge receipt of your report within **48 hours**.
|
||||
- Investigate and provide initial feedback within **5 business days**.
|
||||
- Work diligently to fix validated vulnerabilities.
|
||||
- Keep you updated throughout the process until the issue is resolved.
|
||||
|
||||
## Security Practices at Papra
|
||||
|
||||
Papra follows industry-standard security practices:
|
||||
|
||||
- Secure hosting infrastructure provided by trusted services (Render, Cloudflare, Turso).
|
||||
- Regular security and dependency updates.
|
||||
- Strict access controls to production environments.
|
||||
- Encryption of data in transit and at rest.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We greatly appreciate and acknowledge all researchers who responsibly report vulnerabilities, helping us keep Papra secure.
|
||||
73
apps/docs/CHANGELOG.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# @papra/docs
|
||||
|
||||
## 0.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#512](https://github.com/papra-hq/papra/pull/512) [`cb3ce6b`](https://github.com/papra-hq/papra/commit/cb3ce6b1d8d5dba09cbf0d2964f14b1c93220571) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added organizations permissions for api keys
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#480](https://github.com/papra-hq/papra/pull/480) [`0a03f42`](https://github.com/papra-hq/papra/commit/0a03f42231f691d339c7ab5a5916c52385e31bd2) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added documents encryption layer
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#455](https://github.com/papra-hq/papra/pull/455) [`b33fde3`](https://github.com/papra-hq/papra/commit/b33fde35d3e8622e31b51aadfe56875d8e48a2ef) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Improved feedback message in case of invalid origin configuration
|
||||
|
||||
## 0.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#405](https://github.com/papra-hq/papra/pull/405) [`3401cfb`](https://github.com/papra-hq/papra/commit/3401cfbfdc7e280d2f0f3166ceddcbf55486f574) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Introduce APP_BASE_URL to mutualize server and client base url
|
||||
|
||||
- [#379](https://github.com/papra-hq/papra/pull/379) [`6cedc30`](https://github.com/papra-hq/papra/commit/6cedc30716e320946f79a0a9fd8d3b26e834f4db) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Updated dependencies
|
||||
|
||||
- [#390](https://github.com/papra-hq/papra/pull/390) [`42bc3c6`](https://github.com/papra-hq/papra/commit/42bc3c669840eb778d251dcfb0dd96b45bf6e277) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added API endpoints documentation
|
||||
|
||||
- [#402](https://github.com/papra-hq/papra/pull/402) [`1d23f40`](https://github.com/papra-hq/papra/commit/1d23f4089479387d5b87dbcf6d3819f5ee14d580) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix invalid domain in json schema urls
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#357](https://github.com/papra-hq/papra/pull/357) [`585c53c`](https://github.com/papra-hq/papra/commit/585c53cd9d0d7dbd517dbb1adddfd9e7b70f9fe5) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added a /llms.txt on main website
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#337](https://github.com/papra-hq/papra/pull/337) [`1c574b8`](https://github.com/papra-hq/papra/commit/1c574b8305eb7bde4f1b75ac38a610ca0120a613) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added troubleshooting page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#337](https://github.com/papra-hq/papra/pull/337) [`1c574b8`](https://github.com/papra-hq/papra/commit/1c574b8305eb7bde4f1b75ac38a610ca0120a613) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added `docker compose up` command in dc generator
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#322](https://github.com/papra-hq/papra/pull/322) [`f54b8e1`](https://github.com/papra-hq/papra/commit/f54b8e162acd6cfe92241aaa649847fc03ca5852) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Auto computes urls from the provided port
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#320](https://github.com/papra-hq/papra/pull/320) [`8ccdb74`](https://github.com/papra-hq/papra/commit/8ccdb748349a3cacf38f032fd4d3beebce202487) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added base url configuration in docker compose generator
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#295](https://github.com/papra-hq/papra/pull/295) [`438a311`](https://github.com/papra-hq/papra/commit/438a31171c606138c4b7fa299fdd58dcbeaaf298) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added support for custom oauth2 providers
|
||||
|
||||
- [#293](https://github.com/papra-hq/papra/pull/293) [`53bf93f`](https://github.com/papra-hq/papra/commit/53bf93f128b54ad1d3553e18680c87ab23155f8d) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added a papra docker-compose.yml generator
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix broken lint and added auto link check
|
||||
@@ -1,54 +1,22 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
# Papra - Docs website
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
This is the documentation website for [Papra](https://papra.app).
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
This website is built using [Astro](https://astro.build), and based on the [Starlight](https://starlight.astro.build) module styled with [HiDeoo/starlight-theme-rapide](https://github.com/HiDeoo/starlight-theme-rapide) theme.
|
||||
|
||||
## Development
|
||||
|
||||
To start the development server, run:
|
||||
|
||||
```bash
|
||||
# Navigate to the docs directory
|
||||
cd apps/docs
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start the development server
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ ├── docs/
|
||||
│ └── content.config.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||
The development server will start at [http://localhost:4321](http://localhost:4321).
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import { env } from 'node:process';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlightLinksValidator from 'starlight-links-validator';
|
||||
import starlightThemeRapide from 'starlight-theme-rapide';
|
||||
import UnoCSS from 'unocss/astro';
|
||||
import { sidebar } from './src/content/navigation';
|
||||
|
||||
import posthogRawScript from './src/scripts/posthog.script.js?raw';
|
||||
|
||||
const posthogApiKey = env.POSTHOG_API_KEY;
|
||||
const posthogApiHost = env.POSTHOG_API_HOST ?? 'https://eu.i.posthog.com';
|
||||
const isPosthogEnabled = Boolean(posthogApiKey);
|
||||
|
||||
const posthogScript = posthogRawScript.replace('[POSTHOG-API-KEY]', posthogApiKey ?? '').replace('[POSTHOG-API-HOST]', posthogApiHost);
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://docs.papra.app',
|
||||
integrations: [
|
||||
UnoCSS(),
|
||||
starlight({
|
||||
plugins: [starlightThemeRapide()],
|
||||
plugins: [starlightThemeRapide(), starlightLinksValidator({ exclude: ['http://localhost:1221'] })],
|
||||
title: 'Papra Docs',
|
||||
logo: {
|
||||
dark: './src/assets/logo-dark.svg',
|
||||
light: './src/assets/logo-light.svg',
|
||||
alt: 'Papra Logo',
|
||||
},
|
||||
social: {
|
||||
blueSky: 'https://bsky.app/profile/papra.app',
|
||||
github: 'https://github.com/papra-hq/papra',
|
||||
social: [
|
||||
{ href: 'https://github.com/papra-hq/papra', icon: 'github', label: 'GitHub' },
|
||||
{ href: 'https://bsky.app/profile/papra.app', icon: 'blueSky', label: 'BlueSky' },
|
||||
{ href: 'https://papra.app/discord', icon: 'discord', label: 'Discord' },
|
||||
],
|
||||
expressiveCode: {
|
||||
themes: ['vitesse-black', 'vitesse-light'],
|
||||
},
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/papra-hq/papra/edit/main/apps/docs/',
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{ label: 'Introduction', slug: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Self Hosting',
|
||||
items: [
|
||||
{ label: 'Using Docker', slug: 'self-hosting/using-docker' },
|
||||
{ label: 'Using Docker Compose', slug: 'self-hosting/using-docker-compose' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Configuration',
|
||||
items: [
|
||||
{ label: 'Environment variables', slug: 'configuration/environment-variables' },
|
||||
],
|
||||
},
|
||||
],
|
||||
sidebar,
|
||||
favicon: '/favicon.svg',
|
||||
head: [
|
||||
// Add ICO favicon fallback for Safari.
|
||||
// Add ICO favicon fallback for Safari.
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
@@ -53,6 +50,14 @@ export default defineConfig({
|
||||
sizes: '32x32',
|
||||
},
|
||||
},
|
||||
...(isPosthogEnabled
|
||||
? [
|
||||
{
|
||||
tag: 'script',
|
||||
content: posthogScript,
|
||||
} as const,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
customCss: ['./src/assets/app.css'],
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,10 @@ export default antfu({
|
||||
semi: true,
|
||||
},
|
||||
|
||||
ignores: [
|
||||
'src/scripts/posthog.script.js',
|
||||
],
|
||||
|
||||
rules: {
|
||||
// To allow export on top of files
|
||||
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"name": "@papra/docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1-beta.2",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"description": "Papra documentation website",
|
||||
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
@@ -13,17 +17,31 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.30.5",
|
||||
"astro": "^5.0.2",
|
||||
"@astrojs/solid-js": "^5.1.0",
|
||||
"@astrojs/starlight": "^0.34.3",
|
||||
"astro": "^5.8.0",
|
||||
"sharp": "^0.32.5",
|
||||
"starlight-theme-rapide": "^0.3.0"
|
||||
"shiki": "^3.4.2",
|
||||
"starlight-links-validator": "^0.16.0",
|
||||
"starlight-theme-rapide": "^0.5.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"unocss-preset-animations": "^1.2.1",
|
||||
"yaml": "^2.8.0",
|
||||
"zod": "^3.25.67",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.13.0",
|
||||
"@antfu/eslint-config": "catalog:",
|
||||
"@iconify-json/tabler": "^1.1.120",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@unocss/reset": "^0.64.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
"typescript": "^5.7.3"
|
||||
"figue": "^3.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.6",
|
||||
"typescript": "catalog:",
|
||||
"unocss": "0.65.0-beta.2",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/docs/public/_headers
Normal file
@@ -0,0 +1,3 @@
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
BIN
apps/docs/src/assets/api-key-creation-1.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
apps/docs/src/assets/api-key-creation-2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
@@ -1,8 +1,96 @@
|
||||
:root[data-theme='dark'] {
|
||||
--background: 240 4% 10%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 4% 8%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 4% 8%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 77 100% 74%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--warning: 31 98% 50%;
|
||||
--warning-foreground: 0 0% 98%;
|
||||
|
||||
--border: 345 4% 17%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
|
||||
--background-color: #0c0d0f!important;
|
||||
--accent-color: #fff!important;
|
||||
--foreground-color: #9ea3a2!important;
|
||||
--sl-color-white: #f3f7f6!important;
|
||||
|
||||
--surface: #0a0b0d!important;
|
||||
|
||||
--sl-color-text: var(--foreground-color)!important;
|
||||
|
||||
--sl-color-text-accent: var(--accent-color)!important;
|
||||
--sl-color-accent-high: var(--accent-color)!important;
|
||||
|
||||
--sl-color-bg: var(--background-color)!important;
|
||||
--sl-rapide-ui-header-bg-color: var(--background-color)!important;
|
||||
--sl-color-bg-sidebar: var(--background-color)!important;
|
||||
}
|
||||
|
||||
.sl-link-card {
|
||||
background-color: var(--surface)!important;
|
||||
}
|
||||
|
||||
.hero .sl-link-button {
|
||||
background-color: var(--sl-color-text)!important;
|
||||
border-color: transparent!important;
|
||||
color: var(--sl-color-bg)!important;
|
||||
border-radius: 0.8rem!important;
|
||||
transition: opacity 0.2s ease-in-out!important;
|
||||
font-weight: 500!important;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .hero .sl-link-button {
|
||||
background-color: var(--accent-color)!important;
|
||||
color: var(--background-color)!important;
|
||||
}
|
||||
|
||||
.hero .sl-link-button:hover {
|
||||
opacity: 0.8!important;
|
||||
}
|
||||
|
||||
#_top {
|
||||
padding-top: 10px!important;
|
||||
padding-bottom: 30px!important;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
color:inherit !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .site-title {
|
||||
color:#fff !important;
|
||||
}
|
||||
|
||||
|
||||
.site-title img {
|
||||
width: 1.8rem !important;
|
||||
}
|
||||
|
||||
pre.shiki {
|
||||
border-radius: 0.5rem!important;
|
||||
}
|
||||
BIN
apps/docs/src/assets/cf-catchall-config.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
apps/docs/src/assets/cf-intake-email.dark.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
apps/docs/src/assets/cf-intake-email.light.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
apps/docs/src/assets/docs/encryption-schema-dark.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
apps/docs/src/assets/docs/encryption-schema-light.png
Normal file
|
After Width: | Height: | Size: 460 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#c4bdbd" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2zM9 9h1m-1 4h6m-6 4h6"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2zM9 9h1m-1 4h6m-6 4h6"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 318 B |
BIN
apps/docs/src/assets/owlrelay-api-keys.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
apps/docs/src/assets/owlrelay-intake-email.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
48
apps/docs/src/changelog-parser.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
const linesToRemove = [
|
||||
/^# (.*)$/gm, // Remove main title
|
||||
/^### (.*)$/gm, // Remove section titles
|
||||
];
|
||||
|
||||
export function parseChangelog(changelog: string) {
|
||||
const logs: { entries: {
|
||||
pr: { number: number; url: string };
|
||||
commit: { hash: string; url: string };
|
||||
contributor: { username: string; url: string };
|
||||
content: string;
|
||||
}[]; version: string; }[] = [];
|
||||
|
||||
for (const lineToRemove of linesToRemove) {
|
||||
changelog = changelog.replace(lineToRemove, '');
|
||||
}
|
||||
|
||||
const sections = changelog.match(/## (.*)\n([\s\S]*?)(?=\n## |$)/g) ?? [];
|
||||
|
||||
for (const section of sections) {
|
||||
const version = section.match(/## (.*)\n/)?.[1].trim() ?? 'unknown version';
|
||||
|
||||
const entries = section.split('\n- ').slice(1).map((entry) => {
|
||||
// Example entry:
|
||||
// [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Maybe multiline content
|
||||
|
||||
// Thanks copilot! :sweat-smile:
|
||||
const prMatch = entry.match(/\[#(\d+)\]\((https:\/\/github\.com\/papra-hq\/papra\/pull\/\d+)\)/);
|
||||
const commitMatch = entry.match(/\[`([a-f0-9]{7,40})`\]\((https:\/\/github\.com\/papra-hq\/papra\/commit\/[a-f0-9]{7,40})\)/);
|
||||
const contributorMatch = entry.match(/Thanks \[@([\w-]+)\]\((https:\/\/github\.com\/[\w-]+)\)/);
|
||||
const contentMatch = entry.match(/\)! - (.*)$/s);
|
||||
|
||||
return {
|
||||
pr: prMatch ? { number: Number.parseInt(prMatch[1], 10), url: prMatch[2] } : { number: 0, url: '' },
|
||||
commit: commitMatch ? { hash: commitMatch[1], url: commitMatch[2] } : { hash: '', url: '' },
|
||||
contributor: contributorMatch ? { username: contributorMatch[1], url: contributorMatch[2] } : { username: 'unknown', url: '' },
|
||||
content: contentMatch ? contentMatch[1].trim() : entry.trim(),
|
||||
};
|
||||
});
|
||||
|
||||
logs.push({
|
||||
version,
|
||||
entries,
|
||||
});
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
154
apps/docs/src/components/encryption-key-generator.astro
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
const iconSize = '20';
|
||||
const refreshIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/></svg>`;
|
||||
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z"/><path d="M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1"/></g></svg>`;
|
||||
const copiedIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path fill="currentColor" d="M18.333 6A3.667 3.667 0 0 1 22 9.667v8.666A3.667 3.667 0 0 1 18.333 22H9.667A3.667 3.667 0 0 1 6 18.333V9.667A3.667 3.667 0 0 1 9.667 6zM15 2c1.094 0 1.828.533 2.374 1.514a1 1 0 1 1-1.748.972C15.405 4.088 15.284 4 15 4H5c-.548 0-1 .452-1 1v9.998c0 .32.154.618.407.805l.1.065a1 1 0 1 1-.99 1.738A3 3 0 0 1 2 15V5c0-1.652 1.348-3 3-3zm1.293 9.293L13 14.585l-1.293-1.292a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414"/></svg>`;
|
||||
---
|
||||
|
||||
<div class="key-generator">
|
||||
<div class="key-row">
|
||||
<input type="text" class="key-input" readonly />
|
||||
<button class="cbtn btn-refresh" title="Generate new key">
|
||||
<span set:html={refreshIcon} aria-label="Refresh" />
|
||||
</button>
|
||||
<button class="cbtn btn-copy" title="Copy to clipboard">
|
||||
<span set:html={copyIcon} aria-label="Copy" class="icon-copy" />
|
||||
<span set:html={copiedIcon} aria-label="Copied" class="icon-copied hidden" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
Generated locally in your browser - no network or server involved
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function generateKey({ keyInputElement }: { keyInputElement: HTMLInputElement }) {
|
||||
// Generate a 32-byte (256-bit) encryption key
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
|
||||
// Convert to hex format
|
||||
const key = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
keyInputElement.value = key;
|
||||
}
|
||||
|
||||
function copyToClipboard({ keyInputElement, copyButtonElement, iconCopyElement, iconCopiedElement }: { keyInputElement: HTMLInputElement; copyButtonElement: HTMLButtonElement; iconCopyElement: HTMLSpanElement; iconCopiedElement: HTMLSpanElement }) {
|
||||
keyInputElement.select();
|
||||
keyInputElement.setSelectionRange(0, 64); // For mobile devices
|
||||
|
||||
navigator.clipboard.writeText(keyInputElement.value).then(() => {
|
||||
iconCopyElement.classList.add('hidden');
|
||||
iconCopiedElement.classList.remove('hidden');
|
||||
copyButtonElement.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
iconCopyElement.classList.remove('hidden');
|
||||
iconCopiedElement.classList.add('hidden');
|
||||
copyButtonElement.disabled = false;
|
||||
}, 1_000);
|
||||
}).catch(() => {
|
||||
// Fallback for older browsers
|
||||
document.execCommand('copy');
|
||||
});
|
||||
}
|
||||
|
||||
const keyGenerators = document.querySelectorAll('.key-generator');
|
||||
|
||||
keyGenerators.forEach((keyGenerator) => {
|
||||
const refreshButtonElement = keyGenerator.querySelector('.btn-refresh')!;
|
||||
const copyButtonElement = keyGenerator.querySelector<HTMLButtonElement>('.btn-copy')!;
|
||||
const keyInputElement = keyGenerator.querySelector<HTMLInputElement>('.key-input')!;
|
||||
const iconCopyElement = keyGenerator.querySelector<HTMLSpanElement>('.icon-copy')!;
|
||||
const iconCopiedElement = keyGenerator.querySelector<HTMLSpanElement>('.icon-copied')!;
|
||||
|
||||
generateKey({ keyInputElement });
|
||||
|
||||
refreshButtonElement.addEventListener('click', () => generateKey({ keyInputElement }));
|
||||
copyButtonElement.addEventListener('click', () => copyToClipboard({ copyButtonElement, keyInputElement, iconCopyElement, iconCopiedElement }));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.key-generator {
|
||||
/* background-color: var(--ec-frm-trmBg);
|
||||
border-radius: var(--ec-brdRad);
|
||||
border: 1px solid var(--ec-brdCol);
|
||||
font-family: monospace;
|
||||
max-width: 100%; */
|
||||
}
|
||||
|
||||
.key-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.key-input {
|
||||
flex: 1;
|
||||
background-color: var(--sl-color-black);
|
||||
border: 1px solid var(--sl-color-gray-5);
|
||||
border-radius: 4px 0 0 4px;
|
||||
padding: 8px 12px;
|
||||
font-family: var(--__sl-font-mono, monospace);
|
||||
font-size: 14px;
|
||||
color: var(--sl-color-gray-2);
|
||||
min-width: 0; /* Allow input to shrink */
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.key-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--ec-frm-inpBrd, #4a9eff);
|
||||
box-shadow: 0 0 0 2px var(--ec-frm-inpBrd, #4a9eff)33;
|
||||
}
|
||||
|
||||
.cbtn {
|
||||
background-color: var(--ec-frm-btnBg);
|
||||
border: 1px solid var(--ec-brdCol);
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.cbtn.btn-copy {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.cbtn.btn-refresh {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.cbtn:hover {
|
||||
background-color: var(--sl-color-gray-6)!important;
|
||||
}
|
||||
|
||||
.cbtn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-refresh:hover:not(:disabled) {
|
||||
background-color: var(--ec-frm-btnBgHover, #3a3a3a);
|
||||
}
|
||||
|
||||
.btn-copy:hover:not(:disabled) {
|
||||
background-color: var(--ec-frm-btnBgHover, #3a3a3a);
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: var(--ec-frm-txtSecondary, #888888);
|
||||
font-style: italic;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
99
apps/docs/src/config.data.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { ConfigDefinition, ConfigDefinitionElement } from 'figue';
|
||||
import { castArray, isArray, isEmpty, isNil } from 'lodash-es';
|
||||
|
||||
import { configDefinition } from '../../papra-server/src/modules/config/config';
|
||||
import { renderMarkdown } from './markdown';
|
||||
|
||||
function walk(configDefinition: ConfigDefinition, path: string[] = []): (ConfigDefinitionElement & { path: string[] })[] {
|
||||
return Object
|
||||
.entries(configDefinition)
|
||||
.flatMap(([key, value]) => {
|
||||
if ('schema' in value) {
|
||||
return [{ ...value, path: [...path, key] }] as (ConfigDefinitionElement & { path: string[] })[];
|
||||
}
|
||||
|
||||
return walk(value, [...path, key]);
|
||||
});
|
||||
}
|
||||
|
||||
const configDetails = walk(configDefinition);
|
||||
|
||||
function formatDoc(doc: string | undefined): string {
|
||||
const coerced = (doc ?? '').trim();
|
||||
|
||||
if (coerced.endsWith('.')) {
|
||||
return coerced;
|
||||
}
|
||||
|
||||
return `${coerced}.`;
|
||||
}
|
||||
|
||||
const rows = configDetails
|
||||
.filter(({ path }) => path[0] !== 'env')
|
||||
.map(({ doc, default: defaultValue, env, path }) => {
|
||||
const isEmptyDefaultValue = isNil(defaultValue) || (isArray(defaultValue) && isEmpty(defaultValue)) || defaultValue === '';
|
||||
|
||||
const rawDocumentation = formatDoc(doc);
|
||||
|
||||
// The client baseUrl default value is overridden in the Dockerfiles
|
||||
const defaultOverride = path.join('.') === 'client.baseUrl' ? 'http://localhost:1221' : undefined;
|
||||
|
||||
return {
|
||||
path,
|
||||
env,
|
||||
documentation: rawDocumentation,
|
||||
defaultValue: defaultOverride ?? (isEmptyDefaultValue ? undefined : defaultValue),
|
||||
};
|
||||
});
|
||||
|
||||
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => {
|
||||
const envs = castArray(env);
|
||||
const [firstEnv, ...restEnvs] = envs;
|
||||
|
||||
return `
|
||||
### ${firstEnv}
|
||||
${documentation}
|
||||
|
||||
- Path: \`${path.join('.')}\`
|
||||
- Environment variable: \`${firstEnv}\` ${restEnvs.length ? `, with fallback to: ${restEnvs.map(e => `\`${e}\``).join(', ')}` : ''}
|
||||
- Default value: \`${defaultValue}\`
|
||||
|
||||
|
||||
`.trim();
|
||||
}).join('\n\n---\n\n');
|
||||
|
||||
function wrapText(text: string, maxLength = 75) {
|
||||
const words = text.split(' ');
|
||||
const lines: string[] = [];
|
||||
let currentLine = '';
|
||||
|
||||
words.forEach((word) => {
|
||||
if ((currentLine + word).length + 1 <= maxLength) {
|
||||
currentLine += (currentLine ? ' ' : '') + word;
|
||||
} else {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
}
|
||||
});
|
||||
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines.map(line => `# ${line}`);
|
||||
}
|
||||
|
||||
const fullDotEnv = rows.map(({ env, defaultValue, documentation }) => {
|
||||
const isEmptyDefaultValue = isNil(defaultValue) || (isArray(defaultValue) && isEmpty(defaultValue)) || defaultValue === '';
|
||||
const envs = castArray(env);
|
||||
const [firstEnv] = envs;
|
||||
|
||||
return [
|
||||
...wrapText(documentation),
|
||||
`# ${firstEnv}=${isEmptyDefaultValue ? '' : defaultValue}`,
|
||||
].join('\n');
|
||||
}).join('\n\n');
|
||||
|
||||
const sectionsHtml = renderMarkdown(mdSections);
|
||||
|
||||
export { fullDotEnv, mdSections, sectionsHtml };
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
title: Installing Papra using Docker
|
||||
description: Self-host Papra using Docker.
|
||||
slug: self-hosting/using-docker
|
||||
---
|
||||
|
||||
Papra can be easily installed and run using Docker. This method is recommended for users who want a quick and straightforward way to deploy their own instance of Papra with minimal setup.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure that you have Docker installed on your system. You can download and install Docker from the official Docker website.
|
||||
|
||||
## Root and Rootless installation
|
||||
|
||||
Papra can be installed in two different ways:
|
||||
|
||||
- **Root**: This is the default installation method. It requires root privileges to run. The images are suffixed with `-root` like `corentinth/papra:latest-root` or `corentinth/papra:1.0.0-root`.
|
||||
- **Rootless**: This method does not require root privileges to run. The images are suffixed with `-rootless` like `corentinth/papra:latest-rootless` or `corentinth/papra:1.0.0-rootless`.
|
||||
|
||||
## Image Sources
|
||||
|
||||
Papra Docker images are available on both **Docker Hub** and **GitHub Container Registry** (GHCR). You can choose the source that best suits your needs.
|
||||
|
||||
```bash frame="none"
|
||||
# Using Docker Hub
|
||||
docker pull corentinth/papra:latest-root
|
||||
docker pull corentinth/papra:latest-rootless
|
||||
|
||||
# Using GitHub Container Registry
|
||||
docker pull ghcr.io/papra-hq/papra:latest-root
|
||||
docker pull ghcr.io/papra-hq/papra:latest-rootless
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash frame="none"
|
||||
docker run -d --name papra --restart unless-stopped -p 1221:1221 ghcr.io/papra-hq/papra:latest-root
|
||||
```
|
||||
113
apps/docs/src/content/docs/02-self-hosting/01-using-docker.mdx
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: Installing Papra using Docker
|
||||
description: Self-host Papra using Docker.
|
||||
slug: self-hosting/using-docker
|
||||
---
|
||||
import { Steps } from '@astrojs/starlight/components';
|
||||
|
||||
Papra provides optimized Docker images for streamlined deployment. This method is recommended for users seeking a production-ready setup with minimal maintenance overhead.
|
||||
|
||||
- **Simplified management**: Single container handles all components
|
||||
- **Lightweight**: Optimized image sizes across architectures
|
||||
- **Cross-platform support**: Compatible with `arm64`, `arm/v7`, and `x86_64` systems
|
||||
- **Security options**: Supports both rootless (recommended) and rootful configurations
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure Docker is installed on your host system. Official installation guides are available at:
|
||||
[docker.com/get-started](https://www.docker.com/get-started)
|
||||
|
||||
Verify Docker installation with:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
```
|
||||
|
||||
## Quick Deployment
|
||||
|
||||
Launch Papra with default configuration using:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name papra \
|
||||
--restart unless-stopped \
|
||||
--env APP_BASE_URL=http://localhost:1221 \
|
||||
-p 1221:1221 \
|
||||
ghcr.io/papra-hq/papra:latest
|
||||
```
|
||||
|
||||
This command will:
|
||||
1. Pull the latest rootless image from GitHub Container Registry
|
||||
2. Expose the web interface on [http://localhost:1221](http://localhost:1221)
|
||||
3. Configure automatic restarts for service continuity
|
||||
|
||||
## Image Variants
|
||||
|
||||
Choose between two security models based on your requirements:
|
||||
|
||||
- **Rootless**: Tagged as `latest`, `latest-rootless` or `<version>-rootless` (like `0.2.1-rootless`). Recommended for most users.
|
||||
- **Root**: Tagged as `latest-root` or `<version>-root` (like `0.2.1-root`). Only use if you need to run Papra as the root user.
|
||||
|
||||
The `:latest` tag always references the latest rootless build.
|
||||
|
||||
## Persistent Data Configuration
|
||||
|
||||
For production deployments, mount host directories to preserve application data between container updates.
|
||||
|
||||
<Steps>
|
||||
|
||||
1. Create Storage Directories
|
||||
|
||||
Create a directory for Papra data `./papra-data`, with `./papra-data/db` and `./papra-data/documents` subdirectories:
|
||||
|
||||
```bash
|
||||
mkdir -p ./papra-data/{db,documents}
|
||||
```
|
||||
|
||||
2. Launch Container with Volume Binding
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name papra \
|
||||
--restart unless-stopped \
|
||||
--env APP_BASE_URL=http://localhost:1221 \
|
||||
-p 1221:1221 \
|
||||
-v $(pwd)/papra-data:/app/app-data \
|
||||
--user $(id -u):$(id -g) \
|
||||
ghcr.io/papra-hq/papra:latest
|
||||
```
|
||||
|
||||
This configuration:
|
||||
- Maintains data integrity across container lifecycle events
|
||||
- Enforces proper file ownership without manual permission adjustments
|
||||
- Stores both database files and document assets persistently
|
||||
|
||||
</Steps>
|
||||
|
||||
## Image Registries
|
||||
|
||||
Papra images are distributed through multiple channels:
|
||||
|
||||
**Primary Source (GHCR):**
|
||||
```bash
|
||||
docker pull ghcr.io/papra-hq/papra:latest
|
||||
docker pull ghcr.io/papra-hq/papra:latest-rootless
|
||||
docker pull ghcr.io/papra-hq/papra:latest-root
|
||||
```
|
||||
|
||||
**Community Mirror (Docker Hub):**
|
||||
```bash
|
||||
docker pull corentinth/papra:latest
|
||||
docker pull corentinth/papra:latest-rootless
|
||||
docker pull corentinth/papra:latest-root
|
||||
```
|
||||
|
||||
## Updating Papra
|
||||
|
||||
Regularly pull updated images and recreate containers to receive security patches and feature updates.
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/papra-hq/papra:latest
|
||||
# Or
|
||||
docker pull corentinth/papra:latest
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: Using Docker Compose
|
||||
slug: self-hosting/using-docker-compose
|
||||
---
|
||||
|
||||
Coming soon.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Using Docker Compose
|
||||
slug: self-hosting/using-docker-compose
|
||||
description: Self-host Papra using Docker Compose.
|
||||
---
|
||||
|
||||
import { Steps } from '@astrojs/starlight/components';
|
||||
|
||||
This guide covers how to deploy Papra using Docker Compose, ideal for users who prefer declarative configurations or plan to integrate Papra into a broader service stack.
|
||||
|
||||
Using Docker Compose provides:
|
||||
- A single, versioned configuration file
|
||||
- Easy integration with volumes, networks, and service dependencies
|
||||
- Simplified updates and re-deployments
|
||||
|
||||
This method supports both `rootless` and `rootful` Papra images, please refer to the [Docker](/self-hosting/using-docker) guide for more information about the difference between the two. The following example uses the recommended `rootless` setup.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure Docker and Docker Compose are installed on your host system. Official installation guides are available at: [docker.com/get-started](https://www.docker.com/get-started)
|
||||
|
||||
Verify Docker installation with:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
|
||||
<Steps>
|
||||
|
||||
1. Initialize Project Structure
|
||||
|
||||
Create working directory and persistent storage subdirectories:
|
||||
|
||||
```bash
|
||||
mkdir -p papra/app-data/{db,documents} && cd papra
|
||||
```
|
||||
|
||||
2. Create Docker Compose file
|
||||
|
||||
Create a file named `docker-compose.yml` with the following content:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
papra:
|
||||
container_name: papra
|
||||
image: ghcr.io/papra-hq/papra:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1221:1221"
|
||||
volumes:
|
||||
- ./app-data:/app/app-data
|
||||
user: "${UID}:${GID}"
|
||||
```
|
||||
|
||||
3. Start Papra
|
||||
|
||||
From the directory containing your `docker-compose.yml` file, run:
|
||||
|
||||
```bash
|
||||
UID=$(id -u) GID=$(id -g) docker compose up -d
|
||||
```
|
||||
|
||||
This command downloads the latest Papra image, sets up the container, and starts the Papra service. The `UID` and `GID` variables are used to set the user and group for the container, ensuring proper file ownership. If you don't want to use the `UID` and `GID` variables, you can replace the image with the rootful variant.
|
||||
|
||||
4. Access Papra
|
||||
|
||||
Once your container is running, access Papra via your browser at:
|
||||
|
||||
```
|
||||
http://localhost:1221
|
||||
```
|
||||
|
||||
Your Papra instance is now ready for use!
|
||||
|
||||
5. To go further
|
||||
|
||||
Check the [configuration](/self-hosting/configuration) page for more information on how to configure your Papra instance.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Maintenance
|
||||
|
||||
Check logs
|
||||
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
Stop the service
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Update Papra
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
You're all set! Enjoy managing your documents with Papra.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Configuration
|
||||
slug: self-hosting/configuration
|
||||
description: Configure your self-hosted Papra instance.
|
||||
---
|
||||
|
||||
import { sectionsHtml, fullDotEnv } from '../../../config.data.ts';
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import { Code } from '@astrojs/starlight/components';
|
||||
|
||||
Configuring your self hosted Papra allows you to customize the application to better suit your environment and requirements. This guide covers the key environment variables you can set to control various aspects of the application, including port settings, security options, and storage configurations.
|
||||
|
||||
## Complete .env
|
||||
|
||||
Here is the full configuration file that you can use to configure Papra. The variables values are the default values.
|
||||
|
||||
<Code code={fullDotEnv} language="env" title=".env" />
|
||||
|
||||
## Configuration variables
|
||||
|
||||
Here is the complete list of configuration variables that you can use to configure Papra. You can set these variables in the `.env` file or pass them as environment variables when running the Docker container.
|
||||
|
||||
<Fragment set:html={sectionsHtml} />
|
||||
|
||||
|
||||
## Configuration files
|
||||
|
||||
You can configure Papra using standard environment variables or use some configuration files.
|
||||
Papra uses [c12](https://github.com/unjs/c12) to load configuration files and [figue](https://github.com/CorentinTh/figue) to validate and merge environment variables and configuration files.
|
||||
|
||||
The [c12](https://github.com/unjs/c12) allows you to use the file format you want. The configuration file should be named `papra.config.[ext]` and should be located in the root of the project or in `/app/app-data/` directory in docker container (it can be changed using `PAPRA_CONFIG_DIR` environment variable).
|
||||
|
||||
The supported formats are: `json`, `jsonc`, `json5`, `yaml`, `yml`, `toml`, `js`, `ts`, `cjs`, `mjs`.
|
||||
|
||||
Example of configuration files:
|
||||
<Tabs>
|
||||
<TabItem label="papra.config.yaml">
|
||||
```yaml
|
||||
server:
|
||||
baseUrl: https://papra.example.com
|
||||
corsOrigins: *
|
||||
|
||||
client:
|
||||
baseUrl: https://papra.example.com
|
||||
|
||||
auth:
|
||||
secret: your-secret-key
|
||||
isRegistrationEnabled: true
|
||||
# ...
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="papra.config.json">
|
||||
```json
|
||||
{
|
||||
"$schema": "https://docs.papra.app/papra-config-schema.json",
|
||||
"server": {
|
||||
"baseUrl": "https://papra.example.com"
|
||||
},
|
||||
"client": {
|
||||
"baseUrl": "https://papra.example.com"
|
||||
},
|
||||
"auth": {
|
||||
"secret": "your-secret-key",
|
||||
"isRegistrationEnabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Aside type="tip">
|
||||
When using an IDE, you can use the [papra-config-schema.json](/papra-config-schema.json) file to get autocompletion for the configuration file. Just add a `$schema` property to your configuration file and point it to the schema file.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://docs.papra.app/papra-config-schema.json",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</Aside>
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
You'll find the complete list of configuration variables with their environment variables equivalents and path for files in the previous section.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: Environment variables
|
||||
slug: configuration/environment-variables
|
||||
---
|
||||
|
||||
Coming soon.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Setup Intake Emails with OwlRelay
|
||||
description: Step-by-step guide to setup OwlRelay to receive emails in your Papra instance.
|
||||
slug: guides/intake-emails-with-owlrelay
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import { Steps } from '@astrojs/starlight/components';
|
||||
|
||||
This guide will show you how to setup Papra to receive emails using [OwlRelay](https://owlrelay.email).
|
||||
|
||||
[OwlRelay](https://owlrelay.email) is an open-source agnostic email-to-http service developed by the Papra team. It's a free service that allows you to receive emails and forward them to your self-hosted Papra instance.
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
In order to follow this guide, your Papra instance needs to be running and accessible from the internet.
|
||||
|
||||
## How it works
|
||||
|
||||
By integrating Papra with OwlRelay, your instance will generate email addresses on OwlRelay through it's API (specifying a webhook on your Papra instance). And OwlRelay will forward the emails to your Papra instance through a webhook.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Setup
|
||||
<Steps>
|
||||
|
||||
1. **Create an account on OwlRelay**
|
||||
|
||||
Go to [owlrelay.email](https://owlrelay.email) and create an account.
|
||||
|
||||
2. **Create an OwlRelay API key**
|
||||
|
||||
Once you have created your account, you can create an API key by going to the [API keys page](https://app.owlrelay.email/api-keys).
|
||||
|
||||

|
||||
|
||||
3. **Configure your Papra instance**
|
||||
|
||||
Once you have created your API key, you can configure your Papra instance to receive emails by setting the `OWLRELAY_API_KEY` and `INTAKE_EMAILS_WEBHOOK_SECRET` environment variables.
|
||||
|
||||
```bash
|
||||
# Enable intake emails
|
||||
INTAKE_EMAILS_IS_ENABLED=true
|
||||
|
||||
# Tell your Papra instance to use OwlRelay
|
||||
INTAKE_EMAILS_DRIVER=owlrelay
|
||||
|
||||
# This is your OwlRelay API key
|
||||
OWLRELAY_API_KEY=owrl_*****
|
||||
|
||||
# Set a random key that will be transmitted to OwlRelay to sign the requests,
|
||||
# this is to authenticate that the emails are coming from OwlRelay
|
||||
INTAKE_EMAILS_WEBHOOK_SECRET=a-random-key
|
||||
|
||||
# [Optional]
|
||||
# This is the URL that OwlRelay will send the emails to,
|
||||
# if not provided, the webhook will be inferred from the server URL.
|
||||
# Can be relevant if you have multiple urls pointing to your Papra instance
|
||||
# or when using tunnel services
|
||||
OWLRELAY_WEBHOOK_URL=https://your-instance.com/api/intake-emails/ingest
|
||||
```
|
||||
|
||||
4. **That's it!**
|
||||
|
||||
You can now generate intake emails in your Papra instance and send emails with attachments to the email address generated.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues, you can:
|
||||
- Check the logs of your Papra instance to see if there are any errors.
|
||||
- Connect to your OwlRelay account to see if the emails address are generated and emails are received.
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Setup Intake Emails with CF Email Workers
|
||||
description: Step-by-step guide to setup a Cloudflare Email worker to receive emails in your Papra instance.
|
||||
slug: guides/intake-emails-with-cloudflare-email-workers
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import { Steps } from '@astrojs/starlight/components';
|
||||
|
||||
This guide will show you how to setup a Cloudflare Email worker to receive emails in your Papra instance.
|
||||
|
||||
<Aside type="note">
|
||||
Setting up a Papra Intake Emails with a Cloudflare Email worker requires bit of knowledge about how to setup a CF Email worker and how to configure your Papra instance to receive emails.
|
||||
|
||||
For a simpler solution, you can use the official [OwlRelay integration](/guides/intake-emails-with-owlrelay) guide.
|
||||
</Aside>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
In order to follow this guide, you need:
|
||||
- a [Cloudflare](https://www.cloudflare.com/) account
|
||||
- a custom domain name available on Cloudflare
|
||||
- a publicly accessible Papra instance
|
||||
- basic development skills (git and node.js to setup the Email Worker)
|
||||
|
||||
If you prefer a simpler solution, you can use the official [OwlRelay integration](/guides/intake-emails-with-owlrelay) guide.
|
||||
|
||||
## How it works
|
||||
|
||||
In order to receive emails in your Papra instance, we need to convert the email to an HTTP request. This is currently done by setting up a Cloudflare Email Worker that will forward the email to your Papra instance, basically acting as a bridge between the email and your Papra instance.
|
||||
|
||||
The code for the Email Worker proxy is available in the [papra-hq/email-proxy](https://github.com/papra-hq/email-proxy) repository.
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Create the Email Worker**
|
||||
|
||||
There are two ways to create an Email Worker, either from the Cloudflare dashboard or by cloning and deploying the code from the [papra-hq/email-proxy](https://github.com/papra-hq/email-proxy) repository.
|
||||
|
||||
- **Option 1**: From the Cloudflare dashboard (easier).
|
||||
|
||||
- Go to the [Cloudflare dashboard](https://dash.cloudflare.com/).
|
||||
- Select your domain.
|
||||
- Go to the `Compute (Workers)` tab.
|
||||
- Click on the `Create Worker` button.
|
||||
- Name your worker (e.g. `email-proxy`).
|
||||
- Copy the code from the [index.js](https://github.com/papra-hq/email-proxy/releases/latest/download/index.js) file (from the [papra-hq/email-proxy](https://github.com/papra-hq/email-proxy) repository) and paste it in the editor.
|
||||
|
||||
- **Option 2**: Build and deploy the Email Worker
|
||||
|
||||
Clone the [papra-hq/email-proxy](https://github.com/papra-hq/email-proxy) repository and deploy the worker using Wrangler cli. You will need to have Node.js v24 and pnpm installed.
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/papra-hq/email-proxy.git
|
||||
|
||||
# Change directory
|
||||
cd email-proxy
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build the worker
|
||||
pnpm build
|
||||
|
||||
# Deploy the worker (you will be prompted to login to Cloudflare through wrangler)
|
||||
pnpm deploy
|
||||
```
|
||||
|
||||
2. **Configure the Email Worker**
|
||||
|
||||
Add the following environment variables to the worker:
|
||||
- `WEBHOOK_URL`: The email intake endpoint in your Papra instance (basically. `https://<your-papra-instance.com>/api/intake-emails/ingest`).
|
||||
- `WEBHOOK_SECRET`: The secret key to authenticate the webhook requests, set the same as the `INTAKE_EMAILS_WEBHOOK_SECRET` environment variable in your Papra instance.
|
||||
|
||||
3. **Configure your Papra instance**
|
||||
|
||||
In your Papra instance, add the following environment variables:
|
||||
|
||||
```bash
|
||||
# Enable intake emails
|
||||
INTAKE_EMAILS_IS_ENABLED=true
|
||||
|
||||
# Tell your Papra instance that it can generate any email address from the
|
||||
# domain you setup in the Email Worker as it's a wildcard redirection
|
||||
INTAKE_EMAILS_DRIVER=random-username
|
||||
|
||||
# This is the domain from which the intake email will be generated
|
||||
# eg. `domain.com`
|
||||
INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN=papra.email
|
||||
|
||||
# This is the secret key to authenticate the webhook requests
|
||||
# set the same as the `WEBHOOK_SECRET` variable in the Email Worker
|
||||
INTAKE_EMAILS_WEBHOOK_SECRET=a-random-key
|
||||
```
|
||||
|
||||
4. **Configure the Email Routing**
|
||||
|
||||
- Go to the `Email Routing` tab in the Cloudflare dashboard.
|
||||
- Follow the email onboarding process.
|
||||
- Add a catch-all rule to forward all emails to the Email Worker you created.
|
||||
|
||||

|
||||
|
||||
5. **Test the setup**
|
||||
|
||||
In your Papra instance, go to the `Integrations` page in your organization and generates an intake email URL, setup an allowed sender (basically your email address), and copy the generated email address. Send an email to the generated address with a file attached, and check if the file is uploaded to your Papra instance.
|
||||
</Steps>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Email Worker not receiving emails
|
||||
|
||||
If the Email Worker is not receiving emails, make sure that the email routing is correctly configured in the Cloudflare dashboard.
|
||||
Also, check the [logs in the Email Worker dashboard](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/) for any errors.
|
||||
|
||||
### Papra instance returning 403 Forbidden
|
||||
|
||||
If your Papra instance is returning a `403 Forbidden` error, make sure that the `INTAKE_EMAILS_IS_ENABLED` environment variable is set to `true` in your Papra instance.
|
||||
|
||||
### Papra instance is returning 401 Unauthorized
|
||||
|
||||
If your Papra instance is returning a `401 Unauthorized` error, make sure that the `INTAKE_EMAILS_WEBHOOK_SECRET` environment variable is correctly set in your Papra instance.
|
||||
|
||||
### The worker is not forwarding the email to the Papra instance
|
||||
|
||||
Make sure that the `WEBHOOK_URL` and `WEBHOOK_SECRET` environment variables are correctly set in the Email Worker, and the `INTAKE_EMAILS_WEBHOOK_SECRET` environment variable is correctly set in your Papra instance.
|
||||
Also, check the [logs in the Email Worker dashboard](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/) or the logs in your Papra instance for any errors.
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Setup Ingestion Folder
|
||||
description: Step-by-step guide to setup an ingestion folder to automatically ingest documents into your Papra instance.
|
||||
slug: guides/setup-ingestion-folder
|
||||
---
|
||||
import { Steps } from '@astrojs/starlight/components';
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import { FileTree } from '@astrojs/starlight/components';
|
||||
|
||||
The ingestion folder is a special folder that is watched by Papra for new files. When a new file is added to the ingestion folder, Papra will automatically import it.
|
||||
|
||||
## Multi-Organization Structure
|
||||
|
||||
Papra supports multiple organizations within a single instance, each requiring a dedicated ingestion folder. The ingestion system uses a hierarchical structure where:
|
||||
|
||||
<FileTree>
|
||||
- ingestion-folder
|
||||
- org_abc123
|
||||
- document.pdf
|
||||
- report.docx
|
||||
- org_def456
|
||||
- file.txt
|
||||
- foo.txt # Ignored as it's not in an organization
|
||||
</FileTree>
|
||||
|
||||
|
||||
This allows you to have a single instance of Papra watching multiple organizations' ingestion folders.
|
||||
|
||||
<Aside>
|
||||
Files and folders that are within the `ingestion-root-folder` but not within an organization folder are ignored.
|
||||
</Aside>
|
||||
|
||||
## Setup
|
||||
|
||||
Add the following to your `docker-compose.yml` file:
|
||||
|
||||
```yaml title="docker-compose.yml" ins={9,12}
|
||||
services:
|
||||
papra:
|
||||
container_name: papra
|
||||
image: ghcr.io/papra-hq/papra:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1221:1221"
|
||||
environment:
|
||||
- INGESTION_FOLDER_IS_ENABLED=true
|
||||
volumes:
|
||||
- ./app-data:/app/app-data
|
||||
- <your-ingestion-folder>:/app/ingestion
|
||||
user: "${UID}:${GID}"
|
||||
```
|
||||
|
||||
Then add files to a folder named with the organization id (available in Papra URL, e.g. `https://papra.example.com/organizations/<organization-id>`, the format is `org_<random>`).
|
||||
|
||||
```bash
|
||||
mkdir -p <your-ingestion-folder>/<org_id>
|
||||
touch <your-ingestion-folder>/<org_id>/hello.txt
|
||||
```
|
||||
|
||||
## Post-processing
|
||||
|
||||
Once a file has been ingested in your Papra organization, you can configure what happens to it by setting the `INGESTION_FOLDER_POST_PROCESSING_STRATEGY` environment variable. There are two strategies:
|
||||
|
||||
- `delete`: The file is deleted from the ingestion folder (default strategy)
|
||||
- `move`: The file is moved to the `INGESTION_FOLDER_POST_PROCESSING_MOVE_FOLDER_PATH` folder (default: `./ingestion-done`)
|
||||
|
||||
Note that the `INGESTION_FOLDER_POST_PROCESSING_MOVE_FOLDER_PATH` path is relative to the organization ingestion folder.
|
||||
|
||||
So with `INGESTION_FOLDER_POST_PROCESSING_MOVE_FOLDER_PATH=ingestion-done`, the file `<ingestion-folder>/<org_id>/file.pdf` will be moved to `<ingestion-folder>/<org_id>/ingestion-done/file.pdf` once ingested.
|
||||
|
||||
## Safeguards
|
||||
|
||||
To avoid accidental data loss, if for some reason the ingestion fails, the file is moved to the `INGESTION_FOLDER_ERROR_FOLDER_PATH` folder (default: `./ingestion-error`).
|
||||
|
||||
<Aside>
|
||||
As for the `INGESTION_FOLDER_POST_PROCESSING_MOVE_FOLDER_PATH`, the `INGESTION_FOLDER_ERROR_FOLDER_PATH` path is relative to the organization ingestion folder.
|
||||
</Aside>
|
||||
|
||||
## Polling
|
||||
|
||||
By default, Papra uses native file watchers to detect changes in the ingestion folder. On some OS (like Windows), this can be flaky with Docker. To avoid this issue, you can enable polling by setting the `INGESTION_FOLDER_WATCHER_USE_POLLING` environment variable to `true`.
|
||||
|
||||
The default polling interval is 2 seconds, you can change it by setting the `INGESTION_FOLDER_WATCHER_POLLING_INTERVAL_MS` environment variable.
|
||||
|
||||
|
||||
```yaml title="docker-compose.yml" ins={2-3}
|
||||
environment:
|
||||
- INGESTION_FOLDER_WATCHER_USE_POLLING=true
|
||||
- INGESTION_FOLDER_WATCHER_POLLING_INTERVAL_MS=2000
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
You can find the list of all configuration options in the [configuration reference](/self-hosting/configuration), the related variables are prefixed with `INGESTION_FOLDER_`.
|
||||
|
||||
|
||||
## Edge cases and behaviors
|
||||
|
||||
- The ingestion folder is watched recursively.
|
||||
- Files in the ingestion folder `done` and `error` folders are ignored.
|
||||
- When a file from the ingestion folder is already present (and not in the trash) in the organization, no ingestion is done, but the file is post-processed (deleted or moved) as successfully ingested.
|
||||
- When a file is moved to "done" or "error" folder
|
||||
- If a file with the same name and same content is present in the destination folder, the original file is deleted
|
||||
- If a file with the same name but different content is present in the destination folder, the original file is moved and a timestamp is added to the filename
|
||||
- Some files are ignored by default (`.DS_Store`, `Thumbs.db`, `desktop.ini`, etc.) see [ingestion-folders.constants.ts](https://github.com/papra-hq/papra/blob/main/apps/papra-server/src/modules/ingestion-folders/ingestion-folders.constants.ts) for the list of ignored files and patterns. You can change this by setting the `INGESTION_FOLDER_IGNORED_PATTERNS` environment variable.
|
||||
181
apps/docs/src/content/docs/03-guides/04-setup-custom-oauth2.mdx
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
title: Setup Custom OAuth2 Providers
|
||||
description: Step-by-step guide to setup custom OAuth2 providers for authentication in your Papra instance.
|
||||
slug: guides/setup-custom-oauth2-providers
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import { Steps } from '@astrojs/starlight/components';
|
||||
|
||||
This guide will show you how to configure custom OAuth2 providers for authentication in your Papra instance.
|
||||
|
||||
<Aside type="note">
|
||||
Papra's OAuth2 implementation is based on the [Better Auth Generic OAuth plugin](https://www.better-auth.com/docs/plugins/generic-oauth). For more detailed information about the configuration options and advanced usage, please refer to their documentation.
|
||||
</Aside>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
In order to follow this guide, you need:
|
||||
- A custom OAuth2 provider
|
||||
- An accessible Papra instance
|
||||
- Basic understanding of OAuth2 flows
|
||||
|
||||
## Configuration
|
||||
|
||||
To set up custom OAuth2 providers, you'll need to configure the `AUTH_PROVIDERS_CUSTOMS` environment variable with an array of provider configurations. Here's an example:
|
||||
|
||||
```bash
|
||||
AUTH_PROVIDERS_CUSTOMS='[
|
||||
{
|
||||
"providerId": "custom-oauth2",
|
||||
"providerName": "Custom OAuth2",
|
||||
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
|
||||
"clientId": "your-client-id",
|
||||
"clientSecret": "your-client-secret",
|
||||
"type": "oidc",
|
||||
"discoveryUrl": "https://your-provider.tld/.well-known/openid-configuration",
|
||||
"scopes": ["openid", "profile", "email"]
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
Each provider configuration supports the following fields:
|
||||
|
||||
- `providerId`: A unique identifier for the OAuth provider
|
||||
- `providerName`: The display name of the provider
|
||||
- `providerIconUrl`: URL of the icon to display (optional) you can use a base64 encoded image or an url to a remote image.
|
||||
- `clientId`: OAuth client ID
|
||||
- `clientSecret`: OAuth client secret
|
||||
- `type`: Type of OAuth flow ("oauth2" or "oidc")
|
||||
- `discoveryUrl`: URL to fetch OAuth 2.0 configuration (recommended for OIDC providers)
|
||||
- `authorizationUrl`: URL for the authorization endpoint (required for OAuth2 if not using discoveryUrl)
|
||||
- `tokenUrl`: URL for the token endpoint (required for OAuth2 if not using discoveryUrl)
|
||||
- `userInfoUrl`: URL for the user info endpoint (required for OAuth2 if not using discoveryUrl)
|
||||
- `scopes`: Array of OAuth scopes to request
|
||||
- `redirectURI`: Custom redirect URI (optional)
|
||||
- `responseType`: OAuth response type (defaults to "code")
|
||||
- `prompt`: Controls the authentication experience ("select_account", "consent", "login", "none")
|
||||
- `pkce`: Whether to use PKCE (Proof Key for Code Exchange)
|
||||
- `accessType`: Access type for the authorization request
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Configure your OAuth2 Provider**
|
||||
|
||||
First, you'll need to register your application with your OAuth2 provider. This typically involves:
|
||||
- Creating a new application in your provider's dashboard
|
||||
- Setting up the redirect URI (usually `https://<your-papra-instance>/api/auth/oauth2/callback/:providerId`)
|
||||
- Obtaining the client ID and client secret
|
||||
- Configuring the required scopes
|
||||
|
||||
2. **Configure Papra**
|
||||
|
||||
Add the `AUTH_PROVIDERS_CUSTOMS` environment variable to your Papra instance. Here are some examples:
|
||||
|
||||
For OIDC providers:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"providerId": "custom-oauth2",
|
||||
"providerName": "Custom OAuth2",
|
||||
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
|
||||
"clientId": "your-client-id",
|
||||
"clientSecret": "your-client-secret",
|
||||
"type": "oidc",
|
||||
"discoveryUrl": "https://your-provider.tld/.well-known/openid-configuration",
|
||||
"scopes": ["openid", "profile", "email"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
For standard OAuth2 providers:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"providerId": "custom-oauth2",
|
||||
"providerName": "Custom OAuth2",
|
||||
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
|
||||
"clientId": "your-client-id",
|
||||
"clientSecret": "your-client-secret",
|
||||
"type": "oauth2",
|
||||
"authorizationUrl": "https://your-provider.tld/oauth2/authorize",
|
||||
"tokenUrl": "https://your-provider.tld/oauth2/token",
|
||||
"userInfoUrl": "https://your-provider.tld/oauth2/userinfo",
|
||||
"scopes": ["profile", "email"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
The `discoveryUrl` is recommended for OIDC providers as it automatically configures all the necessary endpoints.
|
||||
For standard OAuth2 providers, you'll need to specify the endpoints manually.
|
||||
</Aside>
|
||||
|
||||
3. **Test the Configuration**
|
||||
|
||||
- Restart your Papra instance to apply the changes
|
||||
- Go to the login page
|
||||
- You should see your custom OAuth2 providers as login options
|
||||
- Try logging in with a test account
|
||||
|
||||
</Steps>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Providers Not Showing Up
|
||||
|
||||
If your OAuth2 providers are not showing up on the login page:
|
||||
- Check that the JSON configuration in `AUTH_PROVIDERS_CUSTOMS` is valid
|
||||
- Ensure all required fields are provided
|
||||
- Verify that the provider IDs are unique
|
||||
|
||||
### Authentication Fails
|
||||
|
||||
If authentication fails:
|
||||
- Verify that the redirect URI is correctly configured in your OAuth2 provider
|
||||
- Check that the client ID and client secret are correct
|
||||
- Ensure the required scopes are properly configured
|
||||
- Check the Papra logs for any error messages
|
||||
|
||||
### OIDC Discovery Issues
|
||||
|
||||
If you're using OIDC and experiencing issues:
|
||||
- Verify that the `discoveryUrl` is accessible
|
||||
- Check that the provider supports OIDC discovery
|
||||
- Ensure the provider's configuration is properly exposed through the discovery endpoint
|
||||
|
||||
## Security Considerations
|
||||
|
||||
<Aside type="caution">
|
||||
Always use HTTPS for your OAuth2 endpoints and ensure your client secret is kept secure.
|
||||
Consider using PKCE (Proof Key for Code Exchange) for additional security by setting `pkce: true` in your configuration.
|
||||
</Aside>
|
||||
|
||||
## Multiple Providers
|
||||
|
||||
You can configure multiple custom OAuth2 providers by adding them to the array:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"providerId": "custom-oauth2-1",
|
||||
"providerName": "Custom OAuth2 Provider 1",
|
||||
"type": "oidc",
|
||||
"discoveryUrl": "https://provider1.tld/.well-known/openid-configuration",
|
||||
"clientId": "client-id-1",
|
||||
"clientSecret": "client-secret-1",
|
||||
"scopes": ["openid", "profile", "email"]
|
||||
},
|
||||
{
|
||||
"providerId": "custom-oauth2-2",
|
||||
"providerName": "Custom OAuth2 Provider 2",
|
||||
"type": "oidc",
|
||||
"discoveryUrl": "https://provider2.tld/.well-known/openid-configuration",
|
||||
"clientId": "client-id-2",
|
||||
"clientSecret": "client-secret-2",
|
||||
"scopes": ["openid", "profile", "email"]
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,448 @@
|
||||
---
|
||||
title: Setup Document Encryption
|
||||
description: Step-by-step guide to enable and configure document encryption in Papra for enhanced data security.
|
||||
slug: guides/document-encryption
|
||||
---
|
||||
import { Steps } from '@astrojs/starlight/components';
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import { Code } from '@astrojs/starlight/components';
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
import EncryptionKeyGenerator from '../../../components/encryption-key-generator.astro';
|
||||
|
||||
<Aside type="note">
|
||||
Document encryption is available in Papra v0.9.0 and above.
|
||||
</Aside>
|
||||
|
||||
Document encryption in Papra provides end-to-end protection for your stored documents using industry-standard AES-256-GCM encryption. This guide will walk you through enabling encryption, understanding how it works, and managing encryption keys.
|
||||
|
||||
## How Encryption Works
|
||||
|
||||
Papra uses a two-layer encryption approach that provides both security and flexibility:
|
||||
|
||||
### Key Encryption Architecture
|
||||
|
||||
1. **Key Encryption Key (KEK)**: A master key that you provide, used to encrypt document-specific keys
|
||||
2. **Document Encryption Key (DEK)**: Unique per-document keys that actually encrypt your files
|
||||
3. **File Encryption**: Each document gets its own random 256-bit encryption key for maximum security
|
||||
|
||||
<div class="dark:block hidden">
|
||||

|
||||
</div>
|
||||
<div class="dark:hidden block">
|
||||

|
||||
</div>
|
||||
|
||||
### Encryption Flow
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Document Upload**: When you upload a document, Papra generates a unique 256-bit encryption key (DEK)
|
||||
|
||||
2. **File Encryption**: The document is encrypted using AES-256-GCM with the DEK
|
||||
|
||||
3. **Key Wrapping**: The DEK is encrypted (wrapped) using your Key Encryption Key (KEK)
|
||||
|
||||
4. **Storage**: The encrypted document and wrapped DEK are stored separately - the file in your storage backend, the wrapped key in the database along with the document metadata
|
||||
|
||||
5. **Retrieval**: When accessing a document, Papra unwraps the DEK using your KEK, then decrypts the file stream
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="note">
|
||||
This architecture means that even if someone gains access to your file storage, they cannot decrypt documents without access to both your document records and your KEK, in other words, without your database and your environment variables.
|
||||
</Aside>
|
||||
|
||||
## Quick Setup
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Generate an encryption key**
|
||||
|
||||
Generate a secure random 256-bit key in hex format, using this generator or OpenSSL command.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Key generator">
|
||||
<EncryptionKeyGenerator />
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="OpenSSL command">
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
This will output something like: `0deba5534bd70548de92d1fd4ae37cf901cca3dc20589b7e022ddb680c98e50c`
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
|
||||
|
||||
2. **Enable encryption in your configuration**
|
||||
|
||||
Add the following environment variables to your `.env` file or Docker configuration:
|
||||
|
||||
```bash
|
||||
DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true
|
||||
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-encryption-key>
|
||||
```
|
||||
|
||||
3. **Restart Papra**
|
||||
|
||||
Restart your Papra instance to apply the encryption settings.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED` | Enable/disable document encryption | No |
|
||||
| `DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS` | Key encryption keys for document encryption | Yes (if encryption enabled) |
|
||||
|
||||
### Key Formats
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Single Key">
|
||||
For simple setups, provide a single 32-byte hex string:
|
||||
|
||||
```bash
|
||||
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-encryption-key>
|
||||
```
|
||||
|
||||
This key will automatically be assigned version `1`.
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Multiple Keys">
|
||||
For key rotation and advanced setups, provide versioned keys:
|
||||
|
||||
```bash
|
||||
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=1:<your-encryption-key-1>,2:<your-encryption-key-2>
|
||||
```
|
||||
|
||||
- The highest version key encrypts new documents
|
||||
- All keys can decrypt existing documents
|
||||
- Versions can be any alphabetically sortable string
|
||||
- Order in the list doesn't matter
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Docker Compose Setup
|
||||
|
||||
Add encryption configuration to your Docker Compose file:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Environment Variables">
|
||||
```yaml title="docker-compose.yml" ins={8-9}
|
||||
services:
|
||||
papra:
|
||||
container_name: papra
|
||||
image: ghcr.io/papra-hq/papra:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# ... other environment variables ...
|
||||
- DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true
|
||||
- DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-encryption-key>
|
||||
volumes:
|
||||
- ./app-data:/app/app-data
|
||||
ports:
|
||||
- "1221:1221"
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Config File">
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
papra:
|
||||
container_name: papra
|
||||
image: ghcr.io/papra-hq/papra:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./app-data:/app/app-data
|
||||
- ./papra.config.yaml:/app/app-data/papra.config.yaml
|
||||
ports:
|
||||
- "1221:1221"
|
||||
```
|
||||
|
||||
```yaml title="./papra.config.yaml"
|
||||
documentsStorage:
|
||||
encryption:
|
||||
isEncryptionEnabled: true
|
||||
documentKeyEncryptionKeys: "<your-encryption-key>"
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Key Management
|
||||
|
||||
### Key Rotation
|
||||
|
||||
Key rotation allows you to replace encryption keys without losing access to existing documents:
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Generate a new key**
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
2. **Add the new key with a higher version**
|
||||
|
||||
```bash
|
||||
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=1:old_key_here,2:new_key_here
|
||||
```
|
||||
|
||||
3. **Restart Papra**
|
||||
|
||||
New documents will use the highest version key (version 2), while existing documents remain accessible with the old key.
|
||||
|
||||
4. **Optional: Remove old keys**
|
||||
|
||||
Once you're confident all documents are using the new key, you can remove old keys. However, this will make any documents encrypted with old keys inaccessible.
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="caution">
|
||||
Never remove a key version if there are still documents encrypted with that key, unless you're certain you no longer need access to those documents.
|
||||
</Aside>
|
||||
|
||||
### Key Security Best Practices
|
||||
|
||||
1. **Store keys securely**: Use a secrets management system in production
|
||||
2. **Use different keys per environment**: Development, staging, and production should have separate keys
|
||||
3. **Backup your keys**: Loss of encryption keys means permanent loss of document access
|
||||
4. **Rotate keys periodically**: Consider rotating keys annually or after security incidents
|
||||
5. **Limit key access**: Only authorized personnel should have access to encryption keys
|
||||
|
||||
### Docker Secrets Example
|
||||
|
||||
For production environments, store your encryption keys securely using external secret management systems or secure file systems, and reference them via environment variables.
|
||||
|
||||
## Compatibility and Migration
|
||||
|
||||
### Enabling Encryption on Existing Instances
|
||||
|
||||
When you enable encryption on a Papra instance that already has documents:
|
||||
|
||||
- **Existing documents**: Remain unencrypted but accessible
|
||||
- **New documents**: Are encrypted using the current KEK
|
||||
- **Mixed storage**: Papra automatically handles both encrypted and unencrypted documents
|
||||
|
||||
### Migrating Existing Documents to Encrypted Format
|
||||
|
||||
If you want to encrypt all existing unencrypted documents after enabling encryption, Papra provides a maintenance command to handle this migration automatically.
|
||||
|
||||
<Aside type="caution">
|
||||
It's advised to make a backup of your documents and database before running the migration.
|
||||
</Aside>
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Verify encryption is properly configured**
|
||||
|
||||
Ensure encryption is enabled and working for new documents before migrating existing ones:
|
||||
|
||||
```bash
|
||||
# Check that your configuration includes:
|
||||
DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true
|
||||
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-key>
|
||||
```
|
||||
|
||||
2. **Run dry-run to preview changes**
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Docker Compose">
|
||||
```bash
|
||||
# Run dry-run inside the Docker container
|
||||
docker compose exec papra pnpm maintenance:encrypt-all-documents --dry-run
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Docker">
|
||||
```bash
|
||||
# Run dry-run inside the Docker container
|
||||
docker exec -it papra pnpm maintenance:encrypt-all-documents --dry-run
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Source Installation">
|
||||
```bash
|
||||
# From your Papra server directory
|
||||
pnpm maintenance:encrypt-all-documents --dry-run
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
|
||||
</Tabs>
|
||||
|
||||
This will show you:
|
||||
- How many documents will be encrypted
|
||||
- Which documents will be affected
|
||||
- No actual encryption will be performed
|
||||
|
||||
4. **Run the migration**
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Docker Compose">
|
||||
```bash
|
||||
# Run migration inside the Docker container
|
||||
docker compose exec papra pnpm maintenance:encrypt-all-documents
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Docker">
|
||||
```bash
|
||||
# Run migration inside the Docker container
|
||||
docker exec -it papra pnpm maintenance:encrypt-all-documents
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Source Installation">
|
||||
```bash
|
||||
# From your Papra server directory
|
||||
pnpm maintenance:encrypt-all-documents
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The command will:
|
||||
- Find all unencrypted documents
|
||||
- Encrypt each document using your configured KEK
|
||||
- Update database records with encryption metadata
|
||||
- Remove original unencrypted files from storage
|
||||
- Provide progress logging throughout the process
|
||||
|
||||
5. **Verify migration success**
|
||||
|
||||
After migration:
|
||||
- Test document access through the Papra interface
|
||||
- Check that storage files are now encrypted (should start with `PP01`)
|
||||
- Verify all documents are accessible and downloadable
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="tip">
|
||||
**Migration Performance**
|
||||
|
||||
- The migration processes documents sequentially to ensure reliability
|
||||
- Large document collections may take considerable time
|
||||
- Monitor disk space during migration (temporary storage overhead)
|
||||
- Consider running during maintenance windows for production systems
|
||||
</Aside>
|
||||
|
||||
#### Troubleshooting Migration Issues
|
||||
|
||||
**Migration fails with "Document encryption is not enabled"**
|
||||
- Verify `DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true` is set
|
||||
- Restart Papra after configuration changes
|
||||
|
||||
**Migration fails with "Document encryption keys are not set"**
|
||||
- Ensure `DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS` contains valid keys
|
||||
- Verify key format is correct (64-character hex string)
|
||||
|
||||
**Migration stops or fails partway**
|
||||
- Check available disk space
|
||||
- Review Papra logs for specific error messages
|
||||
- Restore from backup and retry after fixing the issue
|
||||
|
||||
**Documents inaccessible after migration**
|
||||
- Verify encryption keys are still properly configured
|
||||
- Check that Papra can access your storage backend
|
||||
- Restore from backup if necessary
|
||||
|
||||
### Disabling Encryption
|
||||
|
||||
If you disable encryption:
|
||||
|
||||
- **Encrypted documents**: Remain encrypted but are automatically decrypted when accessed (if KEK is still available)
|
||||
- **New documents**: Are stored unencrypted
|
||||
- **Data loss risk**: If you remove the KEK while encrypted documents exist, those documents become inaccessible
|
||||
|
||||
<Aside type="caution">
|
||||
Disabling encryption doesn't automatically decrypt existing documents in storage. They remain encrypted and require the KEK for access.
|
||||
</Aside>
|
||||
|
||||
### Storage Driver Compatibility
|
||||
|
||||
The encryption layer sits between Papra and your chosen storage driver, providing consistent encryption regardless of where files are stored (S3, Azure Blob Storage, File System, etc.).
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Encryption Algorithm
|
||||
|
||||
- **Algorithm**: AES-256-GCM (Authenticated Encryption)
|
||||
- **Key size**: 256 bits (32 bytes)
|
||||
- **IV size**: 96 bits (12 bytes)
|
||||
- **Authentication tag**: 128 bits (16 bytes)
|
||||
|
||||
### File Format
|
||||
|
||||
Encrypted files use a custom format with a magic number for identification:
|
||||
|
||||
```
|
||||
| Magic (4 bytes) | IV (12 bytes) | Encrypted Data | Auth Tag (16 bytes) |
|
||||
```
|
||||
|
||||
- **Magic number**: `PP01` - identifies Papra encrypted files
|
||||
- **IV**: Initialization vector for GCM mode
|
||||
- **Encrypted Data**: The actual encrypted document content
|
||||
- **Auth Tag**: Authentication tag for integrity verification
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- **Streaming encryption**: Files are encrypted/decrypted in streams, minimizing memory usage
|
||||
- **No size overhead**: Minimal storage overhead (32 bytes per file for headers)
|
||||
- **CPU impact**: Modern processors handle AES encryption efficiently
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**"Document KEK required" error**
|
||||
- Ensure `DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS` is set
|
||||
- Verify the key format is correct (64 character hex string)
|
||||
|
||||
**"Document KEK not found" error**
|
||||
- The document was encrypted with a key version that's no longer available
|
||||
- Add the missing key version back to your configuration
|
||||
|
||||
**"Unsupported encryption algorithm" error**
|
||||
- The document uses an encryption algorithm not supported by this Papra version
|
||||
- This shouldn't occur in normal operation
|
||||
|
||||
**Performance issues**
|
||||
- Consider your storage driver's performance characteristics
|
||||
- Encryption adds minimal overhead, but network/disk I/O remains the bottleneck
|
||||
|
||||
### Verification
|
||||
|
||||
To verify encryption is working:
|
||||
|
||||
1. Upload a document after enabling encryption
|
||||
2. Check your storage backend - the file should not be readable as plain text
|
||||
3. The file should start with the magic number `PP01` if you examine it directly
|
||||
|
||||
<Aside>
|
||||
You can find complete configuration options in the [configuration reference](/self-hosting/configuration). Look for variables prefixed with `DOCUMENT_STORAGE_ENCRYPTION_`.
|
||||
</Aside>
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
|
||||
Document encryption in Papra protects against:
|
||||
|
||||
- **Storage compromise**: If your file storage is breached, documents remain encrypted
|
||||
- **Database-only breach**: Without the KEK, wrapped DEKs cannot be unwrapped
|
||||
- **Configuration exposure**: If the KEK is exposed, the files remain encrypted as long as the DEK are not exposed
|
||||
|
||||
### Limitations
|
||||
|
||||
Encryption does not protect against:
|
||||
|
||||
- **Application-level access**: Users with document access can view decrypted content
|
||||
- **Memory dumps**: Decrypted content exists temporarily in application memory
|
||||
- **Key and database compromise**: If KEKs are stolen, all DEKs can be decrypted if the database is compromised
|
||||
- **Full system compromise**: If the entire Papra instance is compromised, documents can be accessed
|
||||
102
apps/docs/src/content/docs/03-guides/06-tagging-rules.mdx
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
title: Using Tagging Rules
|
||||
description: Learn how to automate document organization with tagging rules.
|
||||
slug: guides/tagging-rules
|
||||
---
|
||||
|
||||
## What are Tagging Rules?
|
||||
|
||||
Tagging rules allow you to automatically apply tags to documents based on specific conditions. This helps maintain consistent organization without manual effort, especially when dealing with large numbers of documents.
|
||||
|
||||
## How Tagging Rules Work
|
||||
|
||||
When a tagging rule is enabled, it automatically checks new documents as they're uploaded. If a document matches the rule's conditions, the specified tags are automatically applied.
|
||||
|
||||
### Rule Components
|
||||
|
||||
Each tagging rule consists of:
|
||||
|
||||
1. **Conditions**: Rules that determine which documents should be tagged
|
||||
- Field: The document property to check (e.g., name, content)
|
||||
- Operator: How to compare the field (e.g., contains, equals)
|
||||
- Value: The text to match against
|
||||
|
||||
2. **Actions**: The tags to apply when conditions are met
|
||||
|
||||
## Applying Rules to Existing Documents
|
||||
|
||||
### The "Run Now" Feature
|
||||
|
||||
When you create a new tagging rule, it only applies to documents uploaded *after* the rule is created. To apply the rule to documents that already exist in your organization, use the **"Apply to existing documents"** button.
|
||||
|
||||
This feature is particularly useful when:
|
||||
- You create a new rule and want to organize your existing documents
|
||||
- You modify a rule and want to reprocess documents
|
||||
- You're setting up your organization and want to retroactively organize imported documents
|
||||
|
||||
### How to Apply a Rule to Existing Documents
|
||||
|
||||
1. Navigate to your organization's Tagging Rules page
|
||||
2. Find the rule you want to apply
|
||||
3. Click the **"Apply to existing documents"** button
|
||||
4. Confirm the action in the dialog
|
||||
5. The task is queued and will be processed in the background
|
||||
|
||||
The system will:
|
||||
- Queue a background task to process all documents
|
||||
- Process documents in batches to avoid overloading the system
|
||||
- Check all existing documents in your organization
|
||||
- Apply tags where the rule's conditions match
|
||||
- Show you a success message once the task is queued
|
||||
|
||||
:::tip
|
||||
Applying a rule to existing documents runs as a background task, so you don't need to wait for it to complete. The processing happens asynchronously and efficiently handles large document collections by processing them in batches.
|
||||
:::
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Creating Effective Rules
|
||||
|
||||
1. **Be specific**: Use precise conditions to avoid over-tagging
|
||||
2. **Test first**: Create a rule and test it on a few documents before applying to all existing documents
|
||||
3. **Use multiple conditions**: Combine conditions for more accurate matching
|
||||
4. **Review regularly**: Periodically review your rules to ensure they're still relevant
|
||||
|
||||
### Example Rules
|
||||
|
||||
**Invoice Classification**
|
||||
- Condition: Document name contains "invoice"
|
||||
- Action: Apply "Invoice" tag
|
||||
|
||||
**Quarterly Reports**
|
||||
- Condition: Document name contains "Q1" or "Q2" or "Q3" or "Q4"
|
||||
- Action: Apply "Report" tag
|
||||
|
||||
## Using the API
|
||||
|
||||
You can also apply tagging rules programmatically using the API. The endpoint enqueues a background task and returns immediately:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
https://api.papra.app/api/organizations/YOUR_ORG_ID/tagging-rules/RULE_ID/apply
|
||||
```
|
||||
|
||||
Response (HTTP 202 Accepted):
|
||||
```json
|
||||
{
|
||||
"taskId": "task_abc123"
|
||||
}
|
||||
```
|
||||
|
||||
Where:
|
||||
- `taskId`: The ID of the background task processing your request
|
||||
|
||||
:::note
|
||||
The API returns a task ID immediately. The actual processing happens in the background and may take some time depending on the number of documents. Task status retrieval will be available in a future release.
|
||||
:::
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [API Endpoints Documentation](/resources/api-endpoints)
|
||||
- [CLI Documentation](/resources/cli)
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
description: Troubleshooting guide for Papra
|
||||
slug: resources/troubleshooting
|
||||
---
|
||||
|
||||
You can find here some common issues and how to fix them. If you encounter an issue that is not listed here, please [open an issue](https://github.com/papra-hq/papra/issues/new/choose) or [join our Discord](https://papra.app/discord).
|
||||
|
||||
## Failed to ensure that the database directory exists
|
||||
|
||||
Upon starting the server or a script, you may encounter this error
|
||||
|
||||
```
|
||||
Failed to ensure that the database directory exists, error while creating the directory
|
||||
Error: EACCES: permission denied, mkdir './app-data/db'
|
||||
|
||||
```
|
||||
|
||||
Before accessing the DB sqlite file, the server will try to ensure that the database directory exists, and if it doesn't, it try will create it. But in case of insufficient permissions, it will fail.
|
||||
|
||||
To fix this, you can either:
|
||||
|
||||
- Create the directory manually `mkdir -p <your-app-data-dir>/db`
|
||||
- Ensure that the directory is owned by the user running the container
|
||||
- Run the server as root (not recommended)
|
||||
|
||||
## Invalid application origin
|
||||
|
||||
Papra ensures [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by validating the Origin header in requests. This check ensures that requests originate from the application or a trusted source. Any request that does not originate from a trusted origin will be rejected.
|
||||
|
||||
If you are self-hosting Papra, you may encounter an error stating that the application origin is invalid while trying to login or register.
|
||||
|
||||
To fix this, you can either:
|
||||
|
||||
- Update the `APP_BASE_URL` environment variable to match the url of your application (e.g. `https://papra.my-homelab.tld`)
|
||||
- Add the current url to the `TRUSTED_ORIGINS` environment variable if you need to allow multiple origins, comma separated. By default the `TRUSTED_ORIGINS` is set to the `APP_BASE_URL`
|
||||
- If you are using a reverse proxy, you may need to add the url to the `TRUSTED_ORIGINS` environment variable
|
||||
|
||||
|
||||
|
||||
89
apps/docs/src/content/docs/04-resources/02-cli.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: CLI Documentation
|
||||
description: Learn how to use the Papra CLI to interact with your Papra instance from the command line.
|
||||
slug: resources/cli
|
||||
---
|
||||
|
||||
The Papra CLI is a command-line interface tool that helps you interact with the Papra platform from your terminal.
|
||||
|
||||
## Installation
|
||||
|
||||
For the moment, the CLI is only available as an NPM package.
|
||||
|
||||
```bash
|
||||
# using pnpm
|
||||
pnpm i -g @papra/cli
|
||||
|
||||
# or using npm
|
||||
npm i -g @papra/cli
|
||||
|
||||
# or using yarn
|
||||
yarn add -g @papra/cli
|
||||
```
|
||||
|
||||
The CLI will be installed globally, so you can use it from anywhere in your system with the `papra` command.
|
||||
|
||||
## Configuration
|
||||
|
||||
Before using the CLI, you need to configure it with your API credentials.
|
||||
|
||||
### Initial Setup
|
||||
|
||||
To initialize the configuration, run:
|
||||
|
||||
```bash
|
||||
papra config init
|
||||
```
|
||||
|
||||
This command will prompt you for:
|
||||
- **Instance URL**: Your Papra instance URL (e.g., `https://api.papra.app`)
|
||||
- **API Key**: Your personal API key (can be created in your User Settings)
|
||||
|
||||
### Managing Configuration
|
||||
|
||||
You can manage your configuration using the following commands:
|
||||
|
||||
- `papra config list`: View your current configuration
|
||||
- `papra config set api-key`: Set or update your API key
|
||||
- `papra config set api-url`: Set or update your instance URL
|
||||
- `papra config set default-org-id`: Set a default organization ID
|
||||
|
||||
### Organization IDs
|
||||
|
||||
Since Papra supports multiple organizations, you may need to specify the organization ID when importing documents for example. If want, you can set a default organization ID in your configuration.
|
||||
|
||||
```bash
|
||||
papra config set default-org-id <organization-id>
|
||||
papra documents import <file-path>
|
||||
|
||||
# or
|
||||
papra documents import -o <organization-id> <file-path>
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Importing documents
|
||||
|
||||
The `import` command allows you to import a document into your Papra organization.
|
||||
|
||||
```bash
|
||||
papra documents import -o <organization-id> <file-path>
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
For more information about any command, you can use the `--help` flag:
|
||||
|
||||
```bash
|
||||
papra --help
|
||||
papra config --help
|
||||
papra documents --help
|
||||
```
|
||||
|
||||
|
||||
## About the CLI
|
||||
|
||||
The CLI is built using the [citty](https://github.com/unjs/citty) framework and the [Papra TS SDK](https://github.com/papra-hq/papra/tree/main/packages/api-sdk).
|
||||
|
||||
|
||||
|
||||
322
apps/docs/src/content/docs/04-resources/03-api-endpoints.mdx
Normal file
@@ -0,0 +1,322 @@
|
||||
---
|
||||
title: API Endpoints
|
||||
description: The list and details of all the API endpoints available in Papra.
|
||||
slug: resources/api-endpoints
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
The public API uses a bearer token for authentication. You can get a token by logging to your Papra account and creating an API token.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How to create an API token</summary>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
|
||||
To authenticate your requests, include the token in the `Authorization` header with the `Bearer` prefix:
|
||||
|
||||
```
|
||||
Authorization: Bearer YOUR_API_TOKEN
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
**Using cURL:**
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
https://api.papra.app/api/organizations
|
||||
```
|
||||
|
||||
**Using JavaScript (fetch):**
|
||||
```javascript
|
||||
const response = await fetch('https://api.papra.app/api/organizations', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer YOUR_API_TOKEN',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### API Key Permissions
|
||||
|
||||
When creating an API key, you can select from the following permissions:
|
||||
|
||||
**Organizations:**
|
||||
- `organizations:create` - Create new organizations
|
||||
- `organizations:read` - Read organization information and list organizations of the user
|
||||
- `organizations:update` - Update organization details
|
||||
- `organizations:delete` - Delete organizations
|
||||
|
||||
**Documents:**
|
||||
- `documents:create` - Upload and create new documents
|
||||
- `documents:read` - Read and download documents
|
||||
- `documents:update` - Update document metadata and content
|
||||
- `documents:delete` - Delete documents
|
||||
|
||||
**Tags:**
|
||||
- `tags:create` - Create new tags
|
||||
- `tags:read` - Read tag information and list tags
|
||||
- `tags:update` - Update tag details
|
||||
- `tags:delete` - Delete tags
|
||||
|
||||
## Endpoints
|
||||
|
||||
### List organizations
|
||||
|
||||
**GET** `/api/organizations`
|
||||
|
||||
List all organizations accessible to the authenticated user.
|
||||
|
||||
- Required API key permissions: `organizations:read`
|
||||
- Response (JSON)
|
||||
- `organizations`: The list of organizations.
|
||||
|
||||
### Create an organization
|
||||
|
||||
**POST** `/api/organizations`
|
||||
|
||||
Create a new organization.
|
||||
|
||||
- Required API key permissions: `organizations:create`
|
||||
- Body (JSON)
|
||||
- `name`: The organization name (3-50 characters).
|
||||
- Response (JSON)
|
||||
- `organization`: The created organization.
|
||||
|
||||
### Get an organization
|
||||
|
||||
**GET** `/api/organizations/:organizationId`
|
||||
|
||||
Get an organization by its ID.
|
||||
|
||||
- Required API key permissions: `organizations:read`
|
||||
- Response (JSON)
|
||||
- `organization`: The organization.
|
||||
|
||||
### Update an organization
|
||||
|
||||
**PUT** `/api/organizations/:organizationId`
|
||||
|
||||
Update an organization's name.
|
||||
|
||||
- Required API key permissions: `organizations:update`
|
||||
- Body (JSON)
|
||||
- `name`: The new organization name (3-50 characters).
|
||||
- Response (JSON)
|
||||
- `organization`: The updated organization.
|
||||
|
||||
### Delete an organization
|
||||
|
||||
**DELETE** `/api/organizations/:organizationId`
|
||||
|
||||
Delete an organization by its ID.
|
||||
|
||||
- Required API key permissions: `organizations:delete`
|
||||
- Response: empty (204 status code)
|
||||
|
||||
### Create a document
|
||||
|
||||
**POST** `/api/organizations/:organizationId/documents`
|
||||
|
||||
Create a new document in the organization.
|
||||
|
||||
- Required API key permissions: `documents:create`
|
||||
- Body (form-data)
|
||||
- `file`: The file to upload.
|
||||
- `ocrLanguages`: (optional) The languages to use for OCR.
|
||||
- Response (JSON)
|
||||
- `document`: The created document.
|
||||
|
||||
### List documents
|
||||
|
||||
**GET** `/api/organizations/:organizationId/documents`
|
||||
|
||||
List all documents in the organization.
|
||||
|
||||
- Required API key permissions: `documents:read`
|
||||
- Query parameters
|
||||
- `pageIndex`: (optional, default: 0) The page index to start from.
|
||||
- `pageSize`: (optional, default: 100) The number of documents to return.
|
||||
- `tags`: (optional) The tags IDs to filter by.
|
||||
- Response (JSON)
|
||||
- `documents`: The list of documents.
|
||||
- `documentsCount`: The total number of documents.
|
||||
|
||||
### List deleted documents (trash)
|
||||
|
||||
**GET** `/api/organizations/:organizationId/documents/deleted`
|
||||
|
||||
List all deleted documents (in trash) in the organization.
|
||||
|
||||
- Required API key permissions: `documents:read`
|
||||
- Query parameters
|
||||
- `pageIndex`: (optional, default: 0) The page index to start from.
|
||||
- `pageSize`: (optional, default: 100) The number of documents to return.
|
||||
- Response (JSON)
|
||||
- `documents`: The list of deleted documents.
|
||||
- `documentsCount`: The total number of deleted documents.
|
||||
|
||||
### Get a document
|
||||
|
||||
**GET** `/api/organizations/:organizationId/documents/:documentId`
|
||||
|
||||
Get a document by its ID.
|
||||
|
||||
- Required API key permissions: `documents:read`
|
||||
- Response (JSON)
|
||||
- `document`: The document.
|
||||
|
||||
### Delete a document
|
||||
|
||||
**DELETE** `/api/organizations/:organizationId/documents/:documentId`
|
||||
|
||||
Delete a document by its ID.
|
||||
|
||||
- Required API key permissions: `documents:delete`
|
||||
- Response: empty (204 status code)
|
||||
|
||||
### Get a document file
|
||||
|
||||
**GET** `/api/organizations/:organizationId/documents/:documentId/file`
|
||||
|
||||
Get a document file content by its ID.
|
||||
|
||||
- Required API key permissions: `documents:read`
|
||||
- Response: The document file stream.
|
||||
|
||||
### Search documents
|
||||
|
||||
**GET** `/api/organizations/:organizationId/documents/search`
|
||||
|
||||
Search documents in the organization by name or content.
|
||||
|
||||
- Required API key permissions: `documents:read`
|
||||
- Query parameters
|
||||
- `searchQuery`: The search query.
|
||||
- `pageIndex`: (optional, default: 0) The page index to start from.
|
||||
- `pageSize`: (optional, default: 100) The number of documents to return.
|
||||
- Response (JSON)
|
||||
- `searchResults`: The search results.
|
||||
- `documents`: The list of matching documents.
|
||||
- `id`: The document ID.
|
||||
- `name`: The document name.
|
||||
|
||||
### Get organization documents statistics
|
||||
|
||||
**GET** `/api/organizations/:organizationId/documents/statistics`
|
||||
|
||||
Get the statistics (number of documents and total size) of the documents in the organization.
|
||||
|
||||
- Required API key permissions: `documents:read`
|
||||
- Response (JSON)
|
||||
- `organizationStats`: The organization documents statistics.
|
||||
- `documentsCount`: The total number of documents.
|
||||
- `documentsSize`: The total size of the documents.
|
||||
|
||||
### Update a document
|
||||
|
||||
**PATCH** `/api/organizations/:organizationId/documents/:documentId`
|
||||
|
||||
Change the name or content (for search purposes) of a document.
|
||||
|
||||
- Required API key permissions: `documents:update`
|
||||
- Body (form-data)
|
||||
- `name`: (optional) The document name.
|
||||
- `content`: (optional) The document content.
|
||||
- Response (JSON)
|
||||
- `document`: The updated document.
|
||||
|
||||
### Get document activity
|
||||
|
||||
**GET** `/api/organizations/:organizationId/documents/:documentId/activity`
|
||||
|
||||
Get the activity log of a document.
|
||||
|
||||
- Required API key permissions: `documents:read`
|
||||
- Query parameters
|
||||
- `pageIndex`: (optional, default: 0) The page index to start from.
|
||||
- `pageSize`: (optional, default: 100) The number of documents to return.
|
||||
- Response (JSON)
|
||||
- `activities`: The list of activities.
|
||||
|
||||
### Create a tag
|
||||
|
||||
**POST** `/api/organizations/:organizationId/tags`
|
||||
|
||||
Create a new tag in the organization.
|
||||
|
||||
- Required API key permissions: `tags:create`
|
||||
- Body (form-data)
|
||||
- `name`: The tag name.
|
||||
- `color`: The tag color in hex format (e.g. `#000000`).
|
||||
- `description`: (optional) The tag description.
|
||||
- Response (JSON)
|
||||
- `tag`: The created tag.
|
||||
|
||||
### List tags
|
||||
|
||||
**GET** `/api/organizations/:organizationId/tags`
|
||||
|
||||
List all tags in the organization.
|
||||
|
||||
- Required API key permissions: `tags:read`
|
||||
- Response (JSON)
|
||||
- `tags`: The list of tags.
|
||||
|
||||
### Update a tag
|
||||
|
||||
**PUT** `/api/organizations/:organizationId/tags/:tagId`
|
||||
|
||||
Change the name, color or description of a tag.
|
||||
|
||||
- Required API key permissions: `tags:update`
|
||||
- Body
|
||||
- `name`: (optional) The tag name.
|
||||
- `color`: (optional) The tag color in hex format (e.g. `#000000`).
|
||||
- `description`: (optional) The tag description.
|
||||
- Response (JSON)
|
||||
- `tag`: The updated tag.
|
||||
|
||||
### Delete a tag
|
||||
|
||||
**DELETE** `/api/organizations/:organizationId/tags/:tagId`
|
||||
|
||||
Delete a tag by its ID.
|
||||
|
||||
- Required API key permissions: `tags:delete`
|
||||
- Response: empty (204 status code)
|
||||
|
||||
### Add a tag to a document
|
||||
|
||||
**POST** `/api/organizations/:organizationId/documents/:documentId/tags`
|
||||
|
||||
Associate a tag to a document.
|
||||
|
||||
- Required API key permissions: `tags:read` and `documents:update`
|
||||
- Body
|
||||
- `tagId`: The tag ID.
|
||||
- Response: empty (204 status code)
|
||||
|
||||
### Remove a tag from a document
|
||||
|
||||
**DELETE** `/api/organizations/:organizationId/documents/:documentId/tags/:tagId`
|
||||
|
||||
Remove a tag from a document.
|
||||
|
||||
- Required API key permissions: `tags:read` and `documents:update`
|
||||
- Response: empty (204 status code)
|
||||
|
||||
### Apply tagging rule to existing documents
|
||||
|
||||
**POST** `/api/organizations/:organizationId/tagging-rules/:taggingRuleId/apply`
|
||||
|
||||
Enqueue a background task to apply a tagging rule to all existing documents in the organization. This endpoint returns immediately with a task ID, and the processing happens asynchronously in the background. The task will check all documents and apply tags where the rule's conditions match.
|
||||
|
||||
- Required API key permissions: `tags:read` and `documents:update`
|
||||
- Response (JSON, HTTP 202)
|
||||
- `taskId`: The ID of the background task. You can use this to track the task's progress (task status retrieval coming in a future release).
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Document Deduplication
|
||||
description: How Papra prevents duplicate documents and saves storage space.
|
||||
slug: architecture/document-deduplication
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Papra automatically detects and prevents duplicate documents per organization using content hashing. This ensures that if the same file is uploaded multiple times, only one copy is stored, saving storage space and reducing clutter.
|
||||
|
||||
## How It Works
|
||||
|
||||
When a document is added to an organization (upload, email ingestion, folder sync, ...), the server computes a **SHA-256 hash** of the file content and checks if a document with the same hash already exists in that organization.
|
||||
|
||||
- If there is **no document with the same hash** in the organization, the new document is added as usual
|
||||
- If a document **with same content exists**, the upload is rejected
|
||||
- If a document **with same content was previously deleted** (in trash), it is restored instead of creating a new copy, the metadata is updated to match the newly added document
|
||||
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Hash Algorithm
|
||||
|
||||
- Papra uses **SHA-256** for content hashing.
|
||||
- Computed during streaming upload (no extra I/O)
|
||||
- 64-character hexadecimal string stored in the database
|
||||
|
||||
### Database Constraint
|
||||
|
||||
The database enforces uniqueness with a composite index:
|
||||
|
||||
```sql
|
||||
UNIQUE (organization_id, original_sha256_hash)
|
||||
```
|
||||
|
||||
This guarantees no two active documents in the same organization can have identical content.
|
||||
|
||||
### File Content Only
|
||||
|
||||
Only the **file content** is hashed and used for deduplication, filenames, upload dates, and metadata don't affect deduplication. Two files are considered duplicates if and only if their content is strictly identical.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: No-Mutation Principle
|
||||
description: Why Papra never modifies your original documents and the architectural decisions behind this choice.
|
||||
slug: architecture/no-mutation-principle
|
||||
---
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
Papra follows a fundamental principle: **documents are never mutated after upload**. When you input a document, you can always retrieve it exactly as it was uploaded.
|
||||
|
||||
## The Design Choice
|
||||
|
||||
An archiving platform should guarantee users they can retrieve their documents in their original form. This means:
|
||||
|
||||
- No conversion to different formats
|
||||
- No metadata injection into the file itself
|
||||
- No overlay of OCR-ed content on scanned PDFs
|
||||
- No processing that modifies the original file
|
||||
|
||||
The simple mental model is: **"If I input X, I'll retrieve X"**
|
||||
|
||||
## Why This Matters
|
||||
|
||||
### Trust and Reliability
|
||||
|
||||
When archiving important documents, users need absolute confidence that their files remain untouched. Whether it's a legal document, a medical record, or a personal photo, the original should be sacrosanct.
|
||||
|
||||
### Simplicity
|
||||
|
||||
This approach eliminates the mental overhead of wondering "what happened to my file?" Users don't need to understand concepts like:
|
||||
- Original vs. processed versions
|
||||
- Format conversions
|
||||
- OCR overlays
|
||||
- Metadata injection
|
||||
|
||||
### Flexibility for the Future
|
||||
|
||||
While Papra currently doesn't mutate documents, the architecture leaves room for future enhancements. If needed, a "processed" version concept could be added alongside originals, giving users the choice without forcing a particular model.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Organization Deletion & Purge
|
||||
description: How Papra handles organization deletion with a grace period and eventual purge.
|
||||
slug: architecture/organization-deletion-purge
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Papra implements a two-phase deletion process for organizations: soft deletion followed by hard deletion (purge). This provides a grace period for recovery while ensuring eventual cleanup of resources.
|
||||
|
||||
## Deletion Process
|
||||
|
||||
### Who Can Delete
|
||||
|
||||
Only the **organization owner** can delete an organization. Admins and members do not have this permission.
|
||||
|
||||
### What Happens During Deletion
|
||||
|
||||
When an organization is deleted:
|
||||
|
||||
1. **Members are removed** - All organization members are stripped from the organization, leaving them dangling
|
||||
2. **Invitations are removed** - All pending invitations are deleted
|
||||
3. **Metadata is recorded**:
|
||||
- `deletedAt`: Timestamp when the deletion occurred
|
||||
- `deletedBy`: ID of the user (owner) who deleted the organization
|
||||
- `scheduledPurgeAt`: Future date when hard deletion will occur (default: 30 days)
|
||||
|
||||
The organization itself remains in the database in a soft-deleted state, allowing for potential restoration.
|
||||
|
||||
## Purge Process
|
||||
|
||||
### When Purge Occurs
|
||||
|
||||
Hard deletion (purge) happens when `scheduledPurgeAt` is reached. By default, this is **30 days** after the deletion date.
|
||||
|
||||
### What Gets Purged
|
||||
|
||||
When an organization is purged:
|
||||
|
||||
- **All documents** are deleted from storage
|
||||
- **All database records** related to the organization are removed (cascade handles related records, like Tags, Intake Emails, etc.)
|
||||
- The organization itself is permanently deleted
|
||||
|
||||
The process handles documents in batches using an iterator to avoid memory issues with large organizations.
|
||||
|
||||
### Background Task
|
||||
|
||||
Purging is handled by a periodic background task that:
|
||||
|
||||
1. Queries for organizations with `scheduledPurgeAt` in the past
|
||||
2. For each expired organization:
|
||||
- Deletes all document files from storage
|
||||
- Hard deletes the organization (cascade handles related records)
|
||||
3. Logs the process for monitoring and debugging
|
||||
|
||||
The task continues even if individual file deletions fail, logging errors without blocking the entire purge operation.
|
||||
|
||||
## Recovery
|
||||
|
||||
Organizations can be restored before the `scheduledPurgeAt` date is reached, but only by the user who deleted them (the previous owner). After this date, recovery is no longer possible, even if the purge has not yet occurred.
|
||||
|
||||
> Note: After recovery, the organization owner must re-invite members as they were removed during deletion.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
title: Papra docs
|
||||
description: Papra documentation.
|
||||
---
|
||||
|
||||
**WIP**: This is still a work in progress. The documentation is not yet complete.
|
||||
|
||||
Papra is a minimalistic document management and archiving platform. It is designed to be simple to use and accessible to everyone. Papra is a plateform for long-term document storage and management, like a digital archive for your documents.
|
||||
74
apps/docs/src/content/docs/index.mdx
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Papra documentation
|
||||
description: Documentation for Papra, the minimalistic document archiving platform.
|
||||
hero:
|
||||
title: Papra Docs
|
||||
tagline: Documentation for Papra, the minimalistic document archiving platform.
|
||||
image:
|
||||
alt: A glittering, brightly colored logo
|
||||
dark: ../../assets/logo-dark.svg
|
||||
light: ../../assets/logo-light.svg
|
||||
actions:
|
||||
- text: Self-hosting guide
|
||||
link: /self-hosting/using-docker
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
|
||||
---
|
||||
|
||||
import { LinkCard } from '@astrojs/starlight/components';
|
||||
|
||||
|
||||
Welcome to the official documentation of Papra, an intuitive open-source document management and archiving platform. It is designed to be simple to use and accessible to everyone. Papra is a plateform for long-term document storage and management, like a digital archive for your documents.
|
||||
|
||||
Papra can be used in two different ways:
|
||||
|
||||
- As a **self-hosted solution**, using the fully packaged lightweight [Docker image](/self-hosting/using-docker).
|
||||
- As a **fully managed solution**, using our cloud service available on [papra.app](https://papra.app).
|
||||
|
||||
<div style="margin-top: 40px">
|
||||
<LinkCard
|
||||
title="Get started"
|
||||
description="Learn how to self-host Papra using Docker."
|
||||
href="/self-hosting/using-docker"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Why Papra?
|
||||
|
||||
In today's digital world, managing countless important documents efficiently and securely has become crucial. Papra helps you effortlessly keep track of everything from personal files to critical business records, providing peace of mind and enhancing productivity through a robust yet user-friendly system.
|
||||
|
||||
## Features
|
||||
|
||||
- **Document management**: Upload, store, and manage your documents in one place.
|
||||
- **Organizations**: Create organizations to manage documents with family, friends, or colleagues.
|
||||
- **Search**: Quickly search for documents with full-text search.
|
||||
- **Authentication**: User accounts and authentication.
|
||||
- **Dark Mode**: A dark theme for those late-night document management sessions.
|
||||
- **Responsive Design**: Works on all devices, from desktops to mobile phones.
|
||||
- **Open Source**: The project is open-source and free to use.
|
||||
- **Self-hosting**: Host your own instance of Papra using Docker or other methods.
|
||||
- **Tags**: Organize your documents with tags.
|
||||
- **Email ingestion**: Send/forward emails to a generated address to automatically import documents.
|
||||
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
||||
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||
- **Folder ingestion**: Automatically import documents from a folder.
|
||||
- **API, SDK and webhooks**: Build your own applications on top of Papra.
|
||||
- **CLI**: Manage your documents from the command line.
|
||||
- **i18n**: Support for multiple languages.
|
||||
- *Coming soon:* **Document sharing**: Share documents with others.
|
||||
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.
|
||||
|
||||
## Community & Open Source
|
||||
|
||||
Papra is proudly open-source under the [AGPL3](https://github.com/papra-hq/papra/blob/main/LICENSE) license. You can contribute to the project by reporting issues, suggesting features, or even contributing code.
|
||||
|
||||
|
||||
|
||||
<div style="margin-top: 40px">
|
||||
<LinkCard
|
||||
title="Get started"
|
||||
description="Learn how to self-host Papra using Docker."
|
||||
href="/self-hosting/using-docker"
|
||||
/>
|
||||
</div>
|
||||
90
apps/docs/src/content/navigation.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { StarlightUserConfig } from '@astrojs/starlight/types';
|
||||
|
||||
export const sidebar = [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{ label: 'Introduction', slug: '' },
|
||||
{ label: 'Changelog', link: '/changelog' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Self Hosting',
|
||||
items: [
|
||||
{ label: 'Using Docker', slug: 'self-hosting/using-docker' },
|
||||
{ label: 'Using Docker Compose', slug: 'self-hosting/using-docker-compose' },
|
||||
{ label: 'Docker Compose Generator', link: '/docker-compose-generator', badge: { text: 'new', variant: 'note' } },
|
||||
{ label: 'Configuration', slug: 'self-hosting/configuration' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
{
|
||||
label: 'Setup intake emails with OwlRelay',
|
||||
slug: 'guides/intake-emails-with-owlrelay',
|
||||
},
|
||||
{
|
||||
label: 'Setup intake emails with CF Email Workers',
|
||||
slug: 'guides/intake-emails-with-cloudflare-email-workers',
|
||||
},
|
||||
{
|
||||
label: 'Setup Ingestion Folder',
|
||||
slug: 'guides/setup-ingestion-folder',
|
||||
},
|
||||
{
|
||||
label: 'Setup Custom OAuth2 Providers',
|
||||
slug: 'guides/setup-custom-oauth2-providers',
|
||||
},
|
||||
{
|
||||
label: 'Document Encryption',
|
||||
slug: 'guides/document-encryption',
|
||||
},
|
||||
{
|
||||
label: 'Tagging Rules',
|
||||
slug: 'guides/tagging-rules',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Architecture',
|
||||
items: [
|
||||
{
|
||||
label: 'No-Mutation Principle',
|
||||
slug: 'architecture/no-mutation-principle',
|
||||
},
|
||||
{
|
||||
label: 'Document Deduplication',
|
||||
slug: 'architecture/document-deduplication',
|
||||
},
|
||||
{
|
||||
label: 'Organization Deletion',
|
||||
slug: 'architecture/organization-deletion-purge',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Resources',
|
||||
items: [
|
||||
{
|
||||
label: 'Troubleshooting',
|
||||
slug: 'resources/troubleshooting',
|
||||
},
|
||||
{
|
||||
label: 'CLI Documentation',
|
||||
slug: 'resources/cli',
|
||||
},
|
||||
{
|
||||
label: 'Security Policy',
|
||||
link: 'https://github.com/papra-hq/papra/blob/main/SECURITY.md',
|
||||
attrs: {
|
||||
target: '_blank',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'API Endpoints',
|
||||
slug: 'resources/api-endpoints',
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies StarlightUserConfig['sidebar'];
|
||||
487
apps/docs/src/docker-compose-generator/dc-generator.astro
Normal file
@@ -0,0 +1,487 @@
|
||||
---
|
||||
import { codeToHtml } from 'shiki';
|
||||
|
||||
const images = {
|
||||
GitHub: 'ghcr.io/papra-hq/papra',
|
||||
DockerHub: 'corentinth/papra',
|
||||
};
|
||||
|
||||
const defaultDockerCompose = `
|
||||
services:
|
||||
papra:
|
||||
image: ghcr.io/papra-hq/papra:latest
|
||||
container_name: papra
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 1221:1221
|
||||
environment:
|
||||
- AUTH_SECRET=change-me
|
||||
- APP_BASE_URL=http://localhost:1221
|
||||
volumes:
|
||||
- ./app-data:/app/app-data
|
||||
user: 1000:1000
|
||||
`.trim();
|
||||
|
||||
const dcHtml = await codeToHtml(defaultDockerCompose, { theme: 'vitesse-black', lang: 'yaml' });
|
||||
const defaultCommand = `mkdir -p ./app-data/{db,documents} && docker compose up -d`;
|
||||
---
|
||||
|
||||
|
||||
<h2 class="mt-8 mb-2">General settings</h2>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="port" class="min-w-32">External port</label>
|
||||
<input id="port" class="input-field" value="1221" type="number" min="1024" max="65535" placeholder="eg: 1221" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="app-base-url" class="min-w-32">App base URL</label>
|
||||
<input id="app-base-url" class="input-field" type="text" placeholder="eg: https://papra.example.com" value="http://localhost:1221" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="source" class="min-w-32">Image source</label>
|
||||
<select class="input-field mt-0" id="source">
|
||||
{Object.entries(images).map(([registry, imageName]) => <option class="bg-background" value={imageName}>{`${registry} - ${imageName}`}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="service-name" class="min-w-32">Service Name</label>
|
||||
<input id="service-name" class="input-field" value="papra" type="text" placeholder="eg: papra" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label
|
||||
for="auth-secret"
|
||||
class="min-w-32"
|
||||
>
|
||||
Auth secret
|
||||
</label>
|
||||
<div class="flex items-center gap-2 mt-0 w-full">
|
||||
<input class="input-field font-mono" id="auth-secret" type="text" placeholder="eg: 1234567890" />
|
||||
<button class="btn bg-muted" id="refresh-secret"> Refresh </button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="volume-path" class="min-w-32">Volume path</label>
|
||||
<input id="volume-path" class="input-field" value="./app-data" type="text" placeholder="eg: ./app-data" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="privileged-mode" class="min-w-32">Privileged mode</label>
|
||||
<div class="flex items-center gap-2 mt-0 w-full">
|
||||
<select class="input-field mt-0" id="privileged-mode">
|
||||
<option value="false" class="bg-background">Rootless</option>
|
||||
<option value="true" class="bg-background">Root</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8 mb-2">Ingestion folder</h2>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="ingestion-enabled" class="min-w-32">Enable ingestion</label>
|
||||
<div class="flex items-center gap-2 mt-0 w-full">
|
||||
<select class="input-field mt-0" id="ingestion-enabled">
|
||||
<option value="false" class="bg-background">Disabled</option>
|
||||
<option value="true" class="bg-background">Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1" id="ingestion-path-container" style="display: none;">
|
||||
<label for="ingestion-path" class="min-w-32">Ingestion path</label>
|
||||
<input id="ingestion-path" class="input-field" value="./ingestion" type="text" placeholder="eg: ./ingestion" />
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8 mb-2">Intake emails</h2>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="intake-email-enabled" class="min-w-32">Enabled</label>
|
||||
<div class="flex items-center gap-2 mt-0 w-full">
|
||||
<select class="input-field mt-0" id="intake-email-enabled">
|
||||
<option value="false" class="bg-background">Disabled</option>
|
||||
<option value="true" class="bg-background">Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1" id="intake-email-driver-container" style="display: none;">
|
||||
<label for="intake-email-driver" class="min-w-32">Driver</label>
|
||||
<div class="flex items-center gap-2 mt-0 w-full">
|
||||
<select class="input-field mt-0" id="intake-email-driver">
|
||||
<option value="owlrelay" class="bg-background">OwlRelay</option>
|
||||
<option value="random-username" class="bg-background">Cloudflare Email Worker</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="intake-email-owlrelay-config" style="display: none;" class="mt-1">
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="intake-email-owlrelay-api-key" class="min-w-32">API Key</label>
|
||||
<input id="intake-email-owlrelay-api-key" class="input-field" type="text" placeholder="owrl_*****" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="intake-email-owlrelay-webhook-url" class="min-w-32">Webhook URL</label>
|
||||
<input id="intake-email-owlrelay-webhook-url" class="input-field" type="text" placeholder="https://your-instance.com/api/intake-emails/ingest" value="https://localhost:1221/api/intake-emails/ingest" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="intake-email-cf-worker-config" style="display: none;" class="mt-1">
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<label for="intake-email-cf-email-domain" class="min-w-32">Email domain</label>
|
||||
<input id="intake-email-cf-email-domain" class="input-field" type="text" placeholder="papra.email" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1" id="intake-email-webhook-secret-container" style="display: none;">
|
||||
<label for="intake-email-webhook-secret" class="min-w-32">Webhook secret</label>
|
||||
<div class="flex items-center gap-2 mt-0 w-full">
|
||||
<input class="input-field font-mono" id="intake-email-webhook-secret" type="text" placeholder="a-random-key" />
|
||||
<button class="btn bg-muted" id="refresh-webhook-secret">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="docker-compose-output" class="mt-12" set:html={dcHtml} />
|
||||
|
||||
<pre id="command-output" class="bg-card p-4 rounded-md text-muted-foreground text-sm font-mono overflow-x-auto">{defaultCommand}</pre>
|
||||
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
<button class="btn bg-muted mt-0" id="download-button">Download docker-compose.yml</button>
|
||||
<button class="btn bg-muted mt-0" id="copy-button">Copy docker compose to clipboard</button>
|
||||
<button class="btn bg-muted mt-0" id="copy-command-button">Copy command</button>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
import { codeToHtml } from 'shiki';
|
||||
import { stringify } from 'yaml';
|
||||
|
||||
const portInput = document.getElementById('port') as HTMLInputElement;
|
||||
const sourceSelect = document.getElementById('source') as HTMLSelectElement;
|
||||
const serviceNameInput = document.getElementById('service-name') as HTMLInputElement;
|
||||
const authSecretInput = document.getElementById('auth-secret') as HTMLInputElement;
|
||||
const appBaseUrlInput = document.getElementById('app-base-url') as HTMLInputElement;
|
||||
const refreshSecretButton = document.getElementById('refresh-secret');
|
||||
const copyButton = document.getElementById('copy-button');
|
||||
const dockerComposeOutput = document.getElementById('docker-compose-output');
|
||||
const downloadButton = document.getElementById('download-button');
|
||||
const volumePathInput = document.getElementById('volume-path') as HTMLInputElement;
|
||||
const privilegedModeSelect = document.getElementById('privileged-mode') as HTMLSelectElement;
|
||||
const ingestionEnabledSelect = document.getElementById('ingestion-enabled') as HTMLSelectElement;
|
||||
const ingestionPathInput = document.getElementById('ingestion-path') as HTMLInputElement;
|
||||
const ingestionPathContainer = document.getElementById('ingestion-path-container') as HTMLDivElement;
|
||||
const intakeEmailEnabledSelect = document.getElementById('intake-email-enabled') as HTMLSelectElement;
|
||||
const intakeDriverSelect = document.getElementById('intake-email-driver') as HTMLSelectElement;
|
||||
const owlrelayConfig = document.getElementById('intake-email-owlrelay-config') as HTMLDivElement;
|
||||
const cfWorkerConfig = document.getElementById('intake-email-cf-worker-config') as HTMLDivElement;
|
||||
const owlrelayApiKeyInput = document.getElementById('intake-email-owlrelay-api-key') as HTMLInputElement;
|
||||
const owlrelayWebhookUrlInput = document.getElementById('intake-email-owlrelay-webhook-url') as HTMLInputElement;
|
||||
const cfEmailDomainInput = document.getElementById('intake-email-cf-email-domain') as HTMLInputElement;
|
||||
const webhookSecretInput = document.getElementById('intake-email-webhook-secret') as HTMLInputElement;
|
||||
const refreshWebhookSecretButton = document.getElementById('refresh-webhook-secret');
|
||||
const commandOutput = document.getElementById('command-output');
|
||||
const copyCommandButton = document.getElementById('copy-command-button');
|
||||
|
||||
// Track whether the app base URL has been customized by the user
|
||||
let isAppBaseUrlCustomized = false;
|
||||
// Track whether the webhook URL has been customized by the user
|
||||
let isWebhookUrlCustomized = false;
|
||||
|
||||
function getRandomString() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
return Array.from({ length: 48 }, () => alphabet[Math.floor(Math.random() * alphabet.length)]).join('');
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
function isDefaultAppBaseUrl(url: string, port: string): boolean {
|
||||
return url === `http://localhost:${port}`;
|
||||
}
|
||||
|
||||
function generateDefaultWebhookUrl(baseUrl: string): string {
|
||||
// Remove trailing slash if present
|
||||
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
||||
return `${cleanBaseUrl}/api/intake-emails/ingest`;
|
||||
}
|
||||
|
||||
function isDefaultWebhookUrl(webhookUrl: string, baseUrl: string): boolean {
|
||||
return webhookUrl === generateDefaultWebhookUrl(baseUrl);
|
||||
}
|
||||
|
||||
function refreshIsWebhookUrlCustomized() {
|
||||
const currentBaseUrl = appBaseUrlInput.value.trim();
|
||||
const currentWebhookUrl = owlrelayWebhookUrlInput.value.trim();
|
||||
|
||||
if (isDefaultWebhookUrl(currentWebhookUrl, currentBaseUrl)) {
|
||||
isWebhookUrlCustomized = false;
|
||||
} else {
|
||||
isWebhookUrlCustomized = true;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshIsAppBaseUrlCustomized() {
|
||||
const currentPort = portInput.value;
|
||||
const currentUrl = appBaseUrlInput.value.trim();
|
||||
|
||||
if (isDefaultAppBaseUrl(currentUrl, currentPort)) {
|
||||
isAppBaseUrlCustomized = false;
|
||||
} else {
|
||||
isAppBaseUrlCustomized = true;
|
||||
}
|
||||
}
|
||||
|
||||
function updateWebhookUrlFromBaseUrl() {
|
||||
if (!isWebhookUrlCustomized) {
|
||||
const baseUrl = appBaseUrlInput.value.trim();
|
||||
if (baseUrl) {
|
||||
owlrelayWebhookUrlInput.value = generateDefaultWebhookUrl(baseUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAppBaseUrlFromPort() {
|
||||
if (!isAppBaseUrlCustomized) {
|
||||
const port = portInput.value;
|
||||
appBaseUrlInput.value = `http://localhost:${port}`;
|
||||
// Also update webhook URL when app base URL changes
|
||||
updateWebhookUrlFromBaseUrl();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePortChange() {
|
||||
updateAppBaseUrlFromPort();
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function handleAppBaseUrlChange() {
|
||||
refreshIsAppBaseUrlCustomized();
|
||||
updateWebhookUrlFromBaseUrl();
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function handleWebhookUrlChange() {
|
||||
refreshIsWebhookUrlCustomized();
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function getDockerComposeYml() {
|
||||
const serviceName = serviceNameInput.value;
|
||||
const isRootless = privilegedModeSelect.value === 'false';
|
||||
const image = sourceSelect.value;
|
||||
const port = portInput.value;
|
||||
const authSecret = authSecretInput.value;
|
||||
const volumePath = volumePathInput.value;
|
||||
const isIngestionEnabled = ingestionEnabledSelect.value === 'true';
|
||||
const ingestionPath = ingestionPathInput.value;
|
||||
const intakeEmailEnabled = intakeEmailEnabledSelect.value === 'true';
|
||||
const intakeDriver = intakeDriverSelect.value;
|
||||
const webhookSecret = webhookSecretInput.value;
|
||||
const appBaseUrl = appBaseUrlInput.value.trim() || `http://localhost:${port}`;
|
||||
|
||||
const version = isRootless ? 'latest' : 'latest-root';
|
||||
const fullImage = `${image}:${version}`;
|
||||
|
||||
const environment = [
|
||||
`AUTH_SECRET=${authSecret}`,
|
||||
`APP_BASE_URL=${appBaseUrl}`,
|
||||
isIngestionEnabled && 'INGESTION_FOLDER_IS_ENABLED=true',
|
||||
intakeEmailEnabled && 'INTAKE_EMAILS_IS_ENABLED=true',
|
||||
intakeEmailEnabled && `INTAKE_EMAILS_DRIVER=${intakeDriver}`,
|
||||
intakeEmailEnabled && `INTAKE_EMAILS_WEBHOOK_SECRET=${webhookSecret}`,
|
||||
intakeEmailEnabled && intakeDriver === 'owlrelay' && owlrelayApiKeyInput.value && `OWLRELAY_API_KEY=${owlrelayApiKeyInput.value}`,
|
||||
intakeEmailEnabled && intakeDriver === 'owlrelay' && owlrelayWebhookUrlInput.value && `OWLRELAY_WEBHOOK_URL=${owlrelayWebhookUrlInput.value}`,
|
||||
intakeEmailEnabled && intakeDriver === 'random-username' && cfEmailDomainInput.value && `INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN=${cfEmailDomainInput.value}`,
|
||||
].flat().filter(Boolean);
|
||||
|
||||
const volumes = [
|
||||
`${volumePath}:/app/app-data`,
|
||||
isIngestionEnabled && `${ingestionPath}:/app/ingestion`,
|
||||
].filter(Boolean);
|
||||
|
||||
const dc = {
|
||||
services: {
|
||||
[serviceName]: {
|
||||
image: fullImage,
|
||||
container_name: serviceName,
|
||||
restart: 'unless-stopped',
|
||||
ports: [`${port}:1221`],
|
||||
environment,
|
||||
volumes,
|
||||
...(isRootless && {
|
||||
user: '1000:1000',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return stringify(dc);
|
||||
}
|
||||
|
||||
function getStartCommand() {
|
||||
const volumePath = volumePathInput.value;
|
||||
const volumePathNormalized = volumePath.replace(/\/$/, '');
|
||||
const volumeWithSubdirs = `${volumePathNormalized}/{db,documents}`;
|
||||
|
||||
const mkdirCommand = `mkdir -p ${volumeWithSubdirs}`;
|
||||
|
||||
const dockerCommand = 'docker compose up -d';
|
||||
|
||||
return `${mkdirCommand} && ${dockerCommand}`;
|
||||
}
|
||||
|
||||
async function updateDockerCompose() {
|
||||
const dockerCompose = getDockerComposeYml();
|
||||
const command = getStartCommand();
|
||||
|
||||
const html = await codeToHtml(dockerCompose, { theme: 'vitesse-black', lang: 'yaml' });
|
||||
|
||||
if (dockerComposeOutput) {
|
||||
dockerComposeOutput.innerHTML = html;
|
||||
}
|
||||
|
||||
if (commandOutput) {
|
||||
commandOutput.textContent = command;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
const dockerCompose = getDockerComposeYml();
|
||||
|
||||
copyToClipboard(dockerCompose);
|
||||
|
||||
if (copyButton) {
|
||||
copyButton.textContent = 'Copied!';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (copyButton) {
|
||||
copyButton.textContent = 'Copy to clipboard';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function handleRefreshSecret() {
|
||||
authSecretInput.value = getRandomString();
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
const dockerCompose = getDockerComposeYml();
|
||||
|
||||
const blob = new Blob([dockerCompose], { type: 'text/yaml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'docker-compose.yml';
|
||||
a.click();
|
||||
}
|
||||
|
||||
function handleIngestionEnabledChange() {
|
||||
const isEnabled = ingestionEnabledSelect.value === 'true';
|
||||
ingestionPathContainer.style.display = isEnabled ? 'flex' : 'none';
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function handleIntakeEmailEnabledChange() {
|
||||
const isEnabled = intakeEmailEnabledSelect.value === 'true';
|
||||
const driverContainer = document.getElementById('intake-email-driver-container');
|
||||
const webhookSecretContainer = document.getElementById('intake-email-webhook-secret-container');
|
||||
|
||||
if (driverContainer) {
|
||||
driverContainer.style.display = isEnabled ? 'flex' : 'none';
|
||||
}
|
||||
if (webhookSecretContainer) {
|
||||
webhookSecretContainer.style.display = isEnabled ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (!isEnabled) {
|
||||
// Reset driver-specific configs when disabled
|
||||
if (owlrelayConfig) {
|
||||
owlrelayConfig.style.display = 'none';
|
||||
}
|
||||
if (cfWorkerConfig) {
|
||||
cfWorkerConfig.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Show the appropriate driver config
|
||||
handleIntakeDriverChange();
|
||||
}
|
||||
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function handleIntakeDriverChange() {
|
||||
const driver = intakeDriverSelect.value;
|
||||
const isEnabled = intakeEmailEnabledSelect.value === 'true';
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (owlrelayConfig) {
|
||||
owlrelayConfig.style.display = driver === 'owlrelay' ? 'block' : 'none';
|
||||
}
|
||||
if (cfWorkerConfig) {
|
||||
cfWorkerConfig.style.display = driver === 'random-username' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function handleRefreshWebhookSecret() {
|
||||
webhookSecretInput.value = getRandomString();
|
||||
updateDockerCompose();
|
||||
}
|
||||
|
||||
function handleCopyCommand() {
|
||||
const command = getStartCommand();
|
||||
|
||||
copyToClipboard(command);
|
||||
|
||||
if (copyCommandButton) {
|
||||
copyCommandButton.textContent = 'Copied!';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (copyCommandButton) {
|
||||
copyCommandButton.textContent = 'Copy command';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
portInput.addEventListener('input', handlePortChange);
|
||||
sourceSelect.addEventListener('change', updateDockerCompose);
|
||||
serviceNameInput.addEventListener('input', updateDockerCompose);
|
||||
authSecretInput.addEventListener('input', updateDockerCompose);
|
||||
appBaseUrlInput.addEventListener('input', handleAppBaseUrlChange);
|
||||
refreshSecretButton?.addEventListener('click', handleRefreshSecret);
|
||||
copyButton?.addEventListener('click', handleCopy);
|
||||
downloadButton?.addEventListener('click', handleDownload);
|
||||
volumePathInput.addEventListener('input', updateDockerCompose);
|
||||
privilegedModeSelect.addEventListener('change', updateDockerCompose);
|
||||
ingestionEnabledSelect.addEventListener('change', handleIngestionEnabledChange);
|
||||
ingestionPathInput.addEventListener('input', updateDockerCompose);
|
||||
intakeEmailEnabledSelect.addEventListener('change', handleIntakeEmailEnabledChange);
|
||||
intakeDriverSelect.addEventListener('change', handleIntakeDriverChange);
|
||||
owlrelayApiKeyInput.addEventListener('input', updateDockerCompose);
|
||||
owlrelayWebhookUrlInput.addEventListener('input', handleWebhookUrlChange);
|
||||
cfEmailDomainInput.addEventListener('input', updateDockerCompose);
|
||||
webhookSecretInput.addEventListener('input', updateDockerCompose);
|
||||
refreshWebhookSecretButton?.addEventListener('click', handleRefreshWebhookSecret);
|
||||
copyCommandButton?.addEventListener('click', handleCopyCommand);
|
||||
|
||||
authSecretInput.value = getRandomString();
|
||||
|
||||
// Initial render
|
||||
updateDockerCompose();
|
||||
|
||||
// Initial setup
|
||||
handleIngestionEnabledChange();
|
||||
handleIntakeEmailEnabledChange();
|
||||
webhookSecretInput.value = getRandomString();
|
||||
</script>
|
||||
16
apps/docs/src/markdown.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { marked } from 'marked';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.heading = function ({ text, depth }) {
|
||||
const slug = text.toLowerCase().replace(/\W+/g, '-');
|
||||
return `
|
||||
<div class="sl-heading-wrapper level-h${depth}">
|
||||
<h${depth} id="${slug}">${text}</h${depth}>
|
||||
<a class="sl-anchor-link" href="#${slug}"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only">Section titled “Configuration files”</span></a>
|
||||
</div>
|
||||
`.trim().replace(/\n/g, '');
|
||||
};
|
||||
|
||||
export function renderMarkdown(markdown: string) {
|
||||
return marked.parse(markdown, { renderer });
|
||||
}
|
||||
55
apps/docs/src/pages/changelog.astro
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
import rawChangelog from '../../../../packages/docker/CHANGELOG.md?raw';
|
||||
import { parseChangelog } from '../changelog-parser';
|
||||
import { renderMarkdown } from '../markdown';
|
||||
|
||||
const changelog = parseChangelog(rawChangelog);
|
||||
|
||||
---
|
||||
|
||||
<StarlightPage
|
||||
frontmatter={{
|
||||
title: 'Papra changelog',
|
||||
description: 'View the changelogs of the docker images released by Papra.',
|
||||
tableOfContents: false,
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
Here are the changelogs of the docker images released by Papra.<br />
|
||||
For version after v0.9.6, Papra uses Calver as a versioning system with the format YY.MM.N where N is the number of releases in the month starting at 0 (e.g. 25.06.0 is the first release of June 2025).
|
||||
</p>
|
||||
|
||||
|
||||
{
|
||||
changelog.map(({ entries, version }) => (
|
||||
<section>
|
||||
|
||||
<h2 id={version} class="pb-1 mt-14">v{version}</h2>
|
||||
<ul>
|
||||
{entries.map(entry => (
|
||||
<li>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-foreground lh-normal changelog-entry" set:html={renderMarkdown(entry.content)} />
|
||||
<div class="text-xs mt-1 flex gap-1 flex-wrap">
|
||||
<a href={entry.pr.url} class="text-muted-foreground hover:bg-muted transition border border-muted border-solid rounded-md no-underline px-1 py-0.5">PR #{entry.pr.number}</a>
|
||||
<a href={entry.commit.url} class="text-muted-foreground hover:bg-muted transition border border-muted border-solid rounded-md no-underline px-1 py-0.5">{entry.commit.hash.slice(0, 7)}</a>
|
||||
<a href={entry.contributor.url} class="text-muted-foreground hover:bg-muted transition border border-muted border-solid rounded-md no-underline px-1 py-0.5">
|
||||
By @{entry.contributor.username}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
</StarlightPage>
|
||||
|
||||
<style is:global>
|
||||
.changelog-entry pre {
|
||||
border-radius: 6px;
|
||||
color: hsl(var(--muted-foreground) / var(--un-text-opacity));
|
||||
}
|
||||
</style>
|
||||
16
apps/docs/src/pages/docker-compose-generator.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
import DockerComposeGeneratorComp from '../docker-compose-generator/dc-generator.astro';
|
||||
---
|
||||
|
||||
<StarlightPage
|
||||
frontmatter={{
|
||||
title: 'Papra docker-compose.yml generator',
|
||||
description: 'Generate a custom docker-compose.yml file for Papra, tailored to your needs.',
|
||||
tableOfContents: false,
|
||||
}}
|
||||
>
|
||||
<p>This tool will help you generate a custom docker-compose.yml file for Papra, tailored to your needs. You can personalize the service name, the port, the auth secret, and the source image.</p>
|
||||
<p>For more configuration options, you can use the <a href="/self-hosting/configuration">configuration reference</a>.</p>
|
||||
<DockerComposeGeneratorComp />
|
||||
</StarlightPage>
|
||||
28
apps/docs/src/pages/docs-navigation.json.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { sidebar } from '../content/navigation';
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const docs = await getCollection('docs');
|
||||
|
||||
const sections = sidebar.map((section) => {
|
||||
return {
|
||||
label: section.label,
|
||||
items: section
|
||||
.items
|
||||
.filter(item => item.slug !== undefined || (item.link && !item.link.startsWith('http')))
|
||||
.map((item) => {
|
||||
const slug = item.slug ?? item.link?.replace(/^\//, '');
|
||||
|
||||
return {
|
||||
label: item.label,
|
||||
slug,
|
||||
url: new URL(slug, site).toString(),
|
||||
description: docs.find(doc => (doc.id === slug || (slug === '' && doc.id === 'index')))?.data.description,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(sections));
|
||||
};
|
||||
49
apps/docs/src/pages/papra-config-schema.json.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import type { ConfigDefinition } from 'figue';
|
||||
import { mapValues } from 'lodash-es';
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { configDefinition } from '../../../papra-server/src/modules/config/config';
|
||||
|
||||
function buildConfigSchema({ configDefinition }: { configDefinition: ConfigDefinition }) {
|
||||
const schema: any = mapValues(configDefinition, (config) => {
|
||||
if (typeof config === 'object' && config !== null && 'schema' in config && 'doc' in config) {
|
||||
return config.schema;
|
||||
} else {
|
||||
return buildConfigSchema({
|
||||
configDefinition: config as ConfigDefinition,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return z.object(schema);
|
||||
}
|
||||
|
||||
function stripRequired(schema: any) {
|
||||
if (schema.type === 'object') {
|
||||
schema.required = [];
|
||||
for (const key in schema.properties) {
|
||||
stripRequired(schema.properties[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSchema(schema: any) {
|
||||
schema.properties.$schema = {
|
||||
type: 'string',
|
||||
description: 'The schema of the configuration file, to be used by IDEs to provide autocompletion and validation',
|
||||
};
|
||||
}
|
||||
|
||||
function getConfigSchema() {
|
||||
const schema = buildConfigSchema({ configDefinition });
|
||||
const jsonSchema = zodToJsonSchema(schema, { pipeStrategy: 'output' });
|
||||
|
||||
stripRequired(jsonSchema);
|
||||
addSchema(jsonSchema);
|
||||
return jsonSchema;
|
||||
}
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
return new Response(JSON.stringify(getConfigSchema()));
|
||||
};
|
||||
15
apps/docs/src/pages/robots.txt.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
function getRobotsTxt(sitemapURL: URL) {
|
||||
return `
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${sitemapURL.href}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
export const GET: APIRoute = ({ site }) => {
|
||||
const sitemapURL = new URL('sitemap-index.xml', site);
|
||||
return new Response(getRobotsTxt(sitemapURL));
|
||||
};
|
||||
5
apps/docs/src/scripts/posthog.script.js
Normal file
@@ -0,0 +1,5 @@
|
||||
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||
posthog.init('[POSTHOG-API-KEY]', {
|
||||
api_host: '[POSTHOG-API-HOST]',
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
})
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
||||
99
apps/docs/uno.config.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
defineConfig,
|
||||
presetTypography,
|
||||
presetUno,
|
||||
transformerDirectives,
|
||||
transformerVariantGroup,
|
||||
} from 'unocss';
|
||||
import presetAnimations from 'unocss-preset-animations';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno({
|
||||
dark: {
|
||||
dark: '[data-theme="dark"]',
|
||||
light: '[data-theme="light"]',
|
||||
},
|
||||
prefix: '',
|
||||
}),
|
||||
presetAnimations(),
|
||||
presetTypography(),
|
||||
],
|
||||
transformers: [transformerVariantGroup(), transformerDirectives()],
|
||||
theme: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
animation: {
|
||||
keyframes: {
|
||||
'accordion-down':
|
||||
'{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }',
|
||||
'accordion-up':
|
||||
'{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }',
|
||||
'collapsible-down':
|
||||
'{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }',
|
||||
'collapsible-up':
|
||||
'{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }',
|
||||
'caret-blink': '{ 0%,70%,100% { opacity: 1 } 20%,50% { opacity: 0 } }',
|
||||
},
|
||||
timingFns: {
|
||||
'accordion-down': 'ease-out',
|
||||
'accordion-up': 'ease-out',
|
||||
'collapsible-down': 'ease-out',
|
||||
'collapsible-up': 'ease-out',
|
||||
'caret-blink': 'ease-out',
|
||||
},
|
||||
durations: {
|
||||
'accordion-down': '0.2s',
|
||||
'accordion-up': '0.2s',
|
||||
'collapsible-down': '0.2s',
|
||||
'collapsible-up': '0.2s',
|
||||
'caret-blink': '1.25s',
|
||||
},
|
||||
counts: {
|
||||
'caret-blink': 'infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
'input-field': 'flex h-9 w-full bg-none outline-none rounded-lg border border-border border-solid bg-inherit px-3 py-1 text-sm shadow-none placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow',
|
||||
'btn': 'text-sm font-medium hover:opacity-80 rounded-lg transition-all px-4 py-2 bg-none outline-none border-none cursor-pointer',
|
||||
},
|
||||
});
|
||||
43
apps/mobile/.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
.kotlin/
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
app-example
|
||||
|
||||
# generated native folders
|
||||
/ios
|
||||
/android
|
||||
5
apps/mobile/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Papra Mobile App
|
||||
|
||||
React Native mobile application for Papra document management platform, built with Expo.
|
||||
|
||||
// Todo: Add more details about setup, development, and usage instructions.
|
||||
55
apps/mobile/app.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "mobile",
|
||||
"slug": "mobile",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./src/assets/images/icon.png",
|
||||
"scheme": "papra",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#E6F4FE",
|
||||
"foregroundImage": "./src/assets/images/android-icon-foreground.png",
|
||||
"backgroundImage": "./src/assets/images/android-icon-background.png",
|
||||
"monochromeImage": "./src/assets/images/android-icon-monochrome.png"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"predictiveBackGestureEnabled": false
|
||||
},
|
||||
"web": {
|
||||
"output": "static",
|
||||
"favicon": "./src/assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./src/assets/images/splash-icon.png",
|
||||
"imageWidth": 200,
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff",
|
||||
"dark": {
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-secure-store"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
"reactCompiler": true
|
||||
},
|
||||
"extra": {
|
||||
"router": {},
|
||||
"eas": {
|
||||
"projectId": "f40c21f5-38e6-40d8-8627-528c1d3a533a"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { HapticTab } from '@/modules/ui/components/haptic-tab';
|
||||
import { Icon } from '@/modules/ui/components/icon';
|
||||
import { ImportTabButton } from '@/modules/ui/components/import-tab-button';
|
||||
import { useThemeColor } from '@/modules/ui/providers/use-theme-color';
|
||||
|
||||
export default function TabLayout() {
|
||||
const colors = useThemeColor();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: colors.primary,
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
tabBarStyle: {
|
||||
backgroundColor: colors.secondaryBackground,
|
||||
borderTopColor: colors.border,
|
||||
paddingTop: 15,
|
||||
paddingBottom: insets.bottom,
|
||||
height: 65 + insets.bottom,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="list"
|
||||
options={{
|
||||
title: 'Documents',
|
||||
tabBarIcon: ({ color }) => <Icon name="home" size={30} color={color} style={{ height: 30 }} />,
|
||||
tabBarLabel: () => null,
|
||||
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="import"
|
||||
options={{
|
||||
title: 'Import',
|
||||
tabBarButton: () => <ImportTabButton />,
|
||||
tabBarLabel: () => null,
|
||||
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
title: 'Settings',
|
||||
tabBarIcon: ({ color }) => <Icon name="settings" size={30} color={color} style={{ height: 30 }} />,
|
||||
tabBarLabel: () => null,
|
||||
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// This is a dummy screen that will never be rendered
|
||||
// The import tab button intercepts the press and opens a drawer instead
|
||||
export default function ImportScreen() {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DocumentsListScreen } from '@/modules/documents/screens/documents-list.screen';
|
||||
|
||||
export default DocumentsListScreen;
|
||||
@@ -0,0 +1,3 @@
|
||||
import SettingsScreen from '@/modules/users/screens/settings.screen';
|
||||
|
||||
export default SettingsScreen;
|
||||
13
apps/mobile/app/(app)/(with-organizations)/_layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { OrganizationsProvider } from '@/modules/organizations/organizations.provider';
|
||||
|
||||
export default function WithOrganizationsLayout() {
|
||||
return (
|
||||
<OrganizationsProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="organizations/create" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</OrganizationsProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { OrganizationCreateScreen } from '@/modules/organizations/screens/organization-create.screen';
|
||||
|
||||
export default OrganizationCreateScreen;
|
||||
18
apps/mobile/app/(app)/_layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
|
||||
import { ApiProvider } from '@/modules/api/providers/api.provider';
|
||||
import 'react-native-reanimated';
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<ApiProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="auth/signup" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(with-organizations)" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</ApiProvider>
|
||||
);
|
||||
}
|
||||
3
apps/mobile/app/(app)/auth/login.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { LoginScreen } from '@/modules/auth/screens/login.screen';
|
||||
|
||||
export default LoginScreen;
|
||||
3
apps/mobile/app/(app)/auth/signup.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { SignupScreen } from '@/modules/auth/screens/signup.screen';
|
||||
|
||||
export default SignupScreen;
|
||||
45
apps/mobile/app/+not-found.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Link, Stack } from 'expo-router';
|
||||
import { StyleSheet, Text, useColorScheme, View } from 'react-native';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === 'dark';
|
||||
|
||||
const styles = createStylesNotFound(isDark);
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>This screen doesn't exist.</Text>
|
||||
<Link href="/" style={styles.link}>
|
||||
<Text style={styles.linkText}>Go to home screen</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function createStylesNotFound(isDark: boolean) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 20,
|
||||
color: isDark ? '#fff' : '#000',
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
color: '#007AFF',
|
||||
},
|
||||
});
|
||||
}
|
||||
25
apps/mobile/app/_layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { AppProviders } from '@/modules/app/providers/app-providers';
|
||||
|
||||
import { useColorScheme } from '@/modules/ui/providers/use-color-scheme';
|
||||
import 'react-native-reanimated';
|
||||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<AppProviders>
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="config/server-selection" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
</AppProviders>
|
||||
);
|
||||
}
|
||||
3
apps/mobile/app/config/server-selection.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { ServerSelectionScreen } from '@/modules/config/screens/server-selection.screen';
|
||||
|
||||
export default ServerSelectionScreen;
|
||||
28
apps/mobile/app/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Redirect } from 'expo-router';
|
||||
import { configLocalStorage } from '@/modules/config/config.local-storage';
|
||||
|
||||
export default function Index() {
|
||||
const query = useQuery({
|
||||
queryKey: ['api-server-url'],
|
||||
queryFn: configLocalStorage.getApiServerBaseUrl,
|
||||
});
|
||||
|
||||
const getRedirection = () => {
|
||||
if (query.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (query.isError || query.data == null) {
|
||||
return <Redirect href="/config/server-selection" />;
|
||||
}
|
||||
|
||||
return <Redirect href="/auth/login" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{getRedirection()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
29
apps/mobile/app/modal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Link } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ThemedText } from '@/modules/ui/components/themed-text';
|
||||
import { ThemedView } from '@/modules/ui/components/themed-view';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
});
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">This is a modal</ThemedText>
|
||||
<Link href="/" dismissTo style={styles.link}>
|
||||
<ThemedText type="link">Go to home screen</ThemedText>
|
||||
</Link>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
21
apps/mobile/eas.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 16.27.0",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
29
apps/mobile/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu({
|
||||
typescript: {
|
||||
tsconfigPath: './tsconfig.json',
|
||||
overridesTypeAware: {
|
||||
'ts/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||||
'ts/strict-boolean-expressions': ['error', { allowNullableObject: true }],
|
||||
},
|
||||
|
||||
},
|
||||
stylistic: {
|
||||
semi: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
// To allow export on top of files
|
||||
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
'curly': ['error', 'all'],
|
||||
'vitest/consistent-test-it': ['error', { fn: 'test' }],
|
||||
'ts/consistent-type-definitions': ['error', 'type'],
|
||||
'style/brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
||||
'unused-imports/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}],
|
||||
},
|
||||
});
|
||||
8
apps/mobile/metro.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const { getDefaultConfig } = require('expo/metro-config');
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
// Enable package exports for Better Auth
|
||||
config.resolver.unstable_enablePackageExports = true;
|
||||
|
||||
module.exports = config;
|
||||
65
apps/mobile/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "mobile",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"dev": "pnpm start",
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/expo": "catalog:",
|
||||
"@corentinth/chisels": "catalog:",
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"@tanstack/react-form": "^1.23.8",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"better-auth": "catalog:",
|
||||
"expo": "~54.0.22",
|
||||
"expo-constants": "~18.0.10",
|
||||
"expo-document-picker": "^14.0.7",
|
||||
"expo-file-system": "^19.0.19",
|
||||
"expo-font": "~14.0.9",
|
||||
"expo-haptics": "~15.0.7",
|
||||
"expo-image": "~3.0.10",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-router": "~6.0.14",
|
||||
"expo-secure-store": "^15.0.7",
|
||||
"expo-splash-screen": "~31.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-symbols": "~1.0.7",
|
||||
"expo-system-ui": "~6.0.8",
|
||||
"expo-web-browser": "~15.0.9",
|
||||
"ofetch": "^1.4.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"valibot": "1.0.0-beta.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "catalog:",
|
||||
"@types/react": "~19.1.0",
|
||||
"eas-cli": "^16.27.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
BIN
apps/mobile/src/assets/images/android-icon-background.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/mobile/src/assets/images/android-icon-foreground.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
apps/mobile/src/assets/images/android-icon-monochrome.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
apps/mobile/src/assets/images/favicon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
apps/mobile/src/assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 384 KiB |