mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-17 12:15:22 -06:00
Compare commits
8 Commits
@papra/app
...
@papra/app
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0616635cd6 | ||
|
|
9e7a3ba70b | ||
|
|
04990b986e | ||
|
|
097b6bf2b7 | ||
|
|
cb3ce6b1d8 | ||
|
|
405ba645f6 | ||
|
|
ab6fd6ad10 | ||
|
|
782f70ff66 |
@@ -1,5 +1,11 @@
|
||||
# @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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@papra/docs",
|
||||
"type": "module",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra documentation website",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@unocss/reset": "^0.64.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
"figue": "^2.2.2",
|
||||
"figue": "^3.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.6",
|
||||
"typescript": "^5.7.3",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ConfigDefinition, ConfigDefinitionElement } from 'figue';
|
||||
import { isArray, isEmpty, isNil } from 'lodash-es';
|
||||
import { castArray, isArray, isEmpty, isNil } from 'lodash-es';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { configDefinition } from '../../papra-server/src/modules/config/config';
|
||||
@@ -46,16 +46,21 @@ const rows = configDetails
|
||||
};
|
||||
});
|
||||
|
||||
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => `
|
||||
### ${env}
|
||||
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => {
|
||||
const envs = castArray(env);
|
||||
const [firstEnv, ...restEnvs] = envs;
|
||||
|
||||
return `
|
||||
### ${firstEnv}
|
||||
${documentation}
|
||||
|
||||
- Path: \`${path.join('.')}\`
|
||||
- Environment variable: \`${env}\`
|
||||
- Environment variable: \`${firstEnv}\` ${restEnvs.length ? `, with fallback to: ${restEnvs.map(e => `\`${e}\``).join(', ')}` : ''}
|
||||
- Default value: \`${defaultValue}\`
|
||||
|
||||
|
||||
`.trim()).join('\n\n---\n\n');
|
||||
`.trim();
|
||||
}).join('\n\n---\n\n');
|
||||
|
||||
function wrapText(text: string, maxLength = 75) {
|
||||
const words = text.split(' ');
|
||||
@@ -80,10 +85,12 @@ function wrapText(text: string, maxLength = 75) {
|
||||
|
||||
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),
|
||||
`# ${env}=${isEmptyDefaultValue ? '' : defaultValue}`,
|
||||
`# ${firstEnv}=${isEmptyDefaultValue ? '' : defaultValue}`,
|
||||
].join('\n');
|
||||
}).join('\n\n');
|
||||
|
||||
|
||||
@@ -18,8 +18,107 @@ The public API uses a bearer token for authentication. You can get a token by lo
|
||||
</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`
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @papra/app-client
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### 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.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@papra/app-client",
|
||||
"type": "module",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra frontend client",
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Alle auswählen',
|
||||
'api-keys.permissions.deselect-all': 'Alle abwählen',
|
||||
'api-keys.permissions.organizations.title': 'Organisationen',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Organisationen erstellen',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Organisationen lesen',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Organisationen aktualisieren',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Organisationen löschen',
|
||||
'api-keys.permissions.documents.title': 'Dokumente',
|
||||
'api-keys.permissions.documents.documents:create': 'Dokumente erstellen',
|
||||
'api-keys.permissions.documents.documents:read': 'Dokumente lesen',
|
||||
|
||||
@@ -415,6 +415,13 @@ export const translations = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Select all',
|
||||
'api-keys.permissions.deselect-all': 'Deselect all',
|
||||
'api-keys.permissions.organizations.title': 'Organizations',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Create organizations',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Read organizations',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Update organizations',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Delete organizations',
|
||||
'api-keys.permissions.documents.title': 'Documents',
|
||||
'api-keys.permissions.documents.documents:create': 'Create documents',
|
||||
'api-keys.permissions.documents.documents:read': 'Read documents',
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Seleccionar todo',
|
||||
'api-keys.permissions.deselect-all': 'Deseleccionar todo',
|
||||
'api-keys.permissions.organizations.title': 'Organizaciones',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Crear organizaciones',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Leer organizaciones',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Actualizar organizaciones',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Eliminar organizaciones',
|
||||
'api-keys.permissions.documents.title': 'Documentos',
|
||||
'api-keys.permissions.documents.documents:create': 'Crear documentos',
|
||||
'api-keys.permissions.documents.documents:read': 'Leer documentos',
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Tout sélectionner',
|
||||
'api-keys.permissions.deselect-all': 'Tout désélectionner',
|
||||
'api-keys.permissions.organizations.title': 'Organisations',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Créer des organisations',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Lire des organisations',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Mettre à jour des organisations',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Supprimer des organisations',
|
||||
'api-keys.permissions.documents.title': 'Documents',
|
||||
'api-keys.permissions.documents.documents:create': 'Créer des documents',
|
||||
'api-keys.permissions.documents.documents:read': 'Lire des documents',
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Seleziona tutto',
|
||||
'api-keys.permissions.deselect-all': 'Deseleziona tutto',
|
||||
'api-keys.permissions.organizations.title': 'Organizzazioni',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Crea organizzazioni',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Leggi organizzazioni',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Aggiorna organizzazioni',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Elimina organizzazioni',
|
||||
'api-keys.permissions.documents.title': 'Documenti',
|
||||
'api-keys.permissions.documents.documents:create': 'Crea documenti',
|
||||
'api-keys.permissions.documents.documents:read': 'Leggi documenti',
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Zaznacz wszystko',
|
||||
'api-keys.permissions.deselect-all': 'Odznacz wszystko',
|
||||
'api-keys.permissions.organizations.title': 'Organizacje',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Tworzenie organizacji',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Odczyt organizacji',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Aktualizacja organizacji',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Usuwanie organizacji',
|
||||
'api-keys.permissions.documents.title': 'Dokumenty',
|
||||
'api-keys.permissions.documents.documents:create': 'Tworzenie dokumentów',
|
||||
'api-keys.permissions.documents.documents:read': 'Odczyt dokumentów',
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Selecionar tudo',
|
||||
'api-keys.permissions.deselect-all': 'Desmarcar tudo',
|
||||
'api-keys.permissions.organizations.title': 'Organizações',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Criar organizações',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Ler organizações',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Atualizar organizações',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Excluir organizações',
|
||||
'api-keys.permissions.documents.title': 'Documentos',
|
||||
'api-keys.permissions.documents.documents:create': 'Criar documentos',
|
||||
'api-keys.permissions.documents.documents:read': 'Ler documentos',
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Selecionar tudo',
|
||||
'api-keys.permissions.deselect-all': 'Desselecionar tudo',
|
||||
'api-keys.permissions.organizations.title': 'Organizações',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Criar organizações',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Ler organizações',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Atualizar organizações',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Eliminar organizações',
|
||||
'api-keys.permissions.documents.title': 'Documentos',
|
||||
'api-keys.permissions.documents.documents:create': 'Criar documentos',
|
||||
'api-keys.permissions.documents.documents:read': 'Ler documentos',
|
||||
|
||||
@@ -417,6 +417,13 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API keys
|
||||
|
||||
'api-keys.permissions.select-all': 'Selectează tot',
|
||||
'api-keys.permissions.deselect-all': 'Deselectează tot',
|
||||
'api-keys.permissions.organizations.title': 'Organizații',
|
||||
'api-keys.permissions.organizations.organizations:create': 'Creează organizații',
|
||||
'api-keys.permissions.organizations.organizations:read': 'Citește organizații',
|
||||
'api-keys.permissions.organizations.organizations:update': 'Actualizează organizații',
|
||||
'api-keys.permissions.organizations.organizations:delete': 'Șterge organizații',
|
||||
'api-keys.permissions.documents.title': 'Documente',
|
||||
'api-keys.permissions.documents.documents:create': 'Creează documente',
|
||||
'api-keys.permissions.documents.documents:read': 'Citește documente',
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
// } as const;
|
||||
|
||||
export const API_KEY_PERMISSIONS = [
|
||||
{
|
||||
section: 'organizations',
|
||||
permissions: [
|
||||
'organizations:create',
|
||||
'organizations:read',
|
||||
'organizations:update',
|
||||
'organizations:delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'documents',
|
||||
permissions: [
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Component } from 'solid-js';
|
||||
import type { TranslationKeys } from '@/modules/i18n/locales.types';
|
||||
import { createSignal, For } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
||||
import { API_KEY_PERMISSIONS } from '../api-keys.constants';
|
||||
|
||||
@@ -42,34 +43,78 @@ export const ApiKeyPermissionsPicker: Component<{ permissions: string[]; onChang
|
||||
props.onChange(permissions());
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<For each={getPermissionsSections()}>
|
||||
{section => (
|
||||
<div>
|
||||
<p class="text-muted-foreground text-xs">{section.title}</p>
|
||||
const toggleSection = (sectionName: typeof API_KEY_PERMISSIONS[number]['section']) => {
|
||||
const section = API_KEY_PERMISSIONS.find(s => s.section === sectionName);
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="pl-4 flex flex-col gap-4 mt-4">
|
||||
<For each={section.permissions}>
|
||||
{permission => (
|
||||
<Checkbox
|
||||
class="flex items-center gap-2"
|
||||
checked={isPermissionSelected(permission.name)}
|
||||
onChange={() => togglePermission(permission.name)}
|
||||
>
|
||||
<CheckboxControl />
|
||||
<div class="flex flex-col gap-1">
|
||||
<CheckboxLabel class="text-sm leading-none">
|
||||
{permission.description}
|
||||
</CheckboxLabel>
|
||||
</div>
|
||||
</Checkbox>
|
||||
)}
|
||||
</For>
|
||||
const sectionPermissions: ReadonlyArray<string> = section.permissions;
|
||||
const currentPermissions = permissions();
|
||||
|
||||
const allSelected = sectionPermissions.every(p => currentPermissions.includes(p));
|
||||
|
||||
setPermissions((prev) => {
|
||||
if (allSelected) {
|
||||
return [...prev.filter(p => !sectionPermissions.includes(p))];
|
||||
}
|
||||
|
||||
return [...new Set([...prev, ...sectionPermissions])];
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="p-6 pb-8 border rounded-md mt-2">
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<For each={getPermissionsSections()}>
|
||||
{section => (
|
||||
<div>
|
||||
<Button variant="link" class="text-muted-foreground text-xs p-0 h-auto hover:no-underline" onClick={() => toggleSection(section.section)}>{section.title}</Button>
|
||||
|
||||
<div class="pl-4 flex flex-col mt-2">
|
||||
<For each={section.permissions}>
|
||||
{permission => (
|
||||
<Checkbox
|
||||
class="flex items-center gap-2"
|
||||
checked={isPermissionSelected(permission.name)}
|
||||
onChange={() => togglePermission(permission.name)}
|
||||
>
|
||||
<CheckboxControl />
|
||||
<div class="flex flex-col gap-1">
|
||||
<CheckboxLabel class="text-sm leading-none py-1">
|
||||
{permission.description}
|
||||
</CheckboxLabel>
|
||||
</div>
|
||||
</Checkbox>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-6 border-t pt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="disabled:(op-100! border-op-50 text-muted-foreground)"
|
||||
onClick={() => setPermissions(API_KEY_PERMISSIONS.flatMap(section => section.permissions))}
|
||||
disabled={permissions().length === API_KEY_PERMISSIONS.flatMap(section => section.permissions).length}
|
||||
>
|
||||
{t('api-keys.permissions.select-all')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="disabled:(op-100! border-op-50 text-muted-foreground)"
|
||||
onClick={() => setPermissions([])}
|
||||
disabled={permissions().length === 0}
|
||||
>
|
||||
{t('api-keys.permissions.deselect-all')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -96,9 +96,7 @@ export const CreateApiKeyPage: Component = () => {
|
||||
<div>
|
||||
<p class="text-sm font-bold">{t('api-keys.create.form.permissions.label')}</p>
|
||||
|
||||
<div class="p-6 pb-8 border rounded-md mt-2">
|
||||
<ApiKeyPermissionsPicker permissions={field.value ?? []} onChange={permissions => setValue(form, 'permissions', permissions)} />
|
||||
</div>
|
||||
<ApiKeyPermissionsPicker permissions={field.value ?? []} onChange={permissions => setValue(form, 'permissions', permissions)} />
|
||||
|
||||
{field.error && <div class="text-red-500 text-sm">{field.error}</div>}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @papra/app-server
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#508](https://github.com/papra-hq/papra/pull/508) [`782f70f`](https://github.com/papra-hq/papra/commit/782f70ff663634bf9ff7218edabb9885a7c6f965) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added an option to disable PRAGMA statements from sqlite task service migrations
|
||||
|
||||
- [#510](https://github.com/papra-hq/papra/pull/510) [`ab6fd6a`](https://github.com/papra-hq/papra/commit/ab6fd6ad10387f1dcd626936efc195d9d58d40ec) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added fallbacks env variables for the task worker id
|
||||
|
||||
- [#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.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@papra/app-server",
|
||||
"type": "module",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra app server",
|
||||
@@ -42,7 +42,7 @@
|
||||
"@aws-sdk/lib-storage": "^3.835.0",
|
||||
"@azure/storage-blob": "^12.27.0",
|
||||
"@cadence-mq/core": "^0.2.1",
|
||||
"@cadence-mq/driver-libsql": "^0.2.1",
|
||||
"@cadence-mq/driver-libsql": "^0.2.4",
|
||||
"@cadence-mq/driver-memory": "^0.2.0",
|
||||
"@corentinth/chisels": "^1.3.1",
|
||||
"@corentinth/friendly-ids": "^0.0.1",
|
||||
@@ -63,7 +63,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"drizzle-orm": "^0.38.4",
|
||||
"figue": "^2.2.3",
|
||||
"figue": "^3.1.1",
|
||||
"hono": "^4.8.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime-types": "^3.0.1",
|
||||
|
||||
@@ -5,8 +5,15 @@ export const API_KEY_ID_REGEX = createPrefixedIdRegex({ prefix: API_KEY_ID_PREFI
|
||||
|
||||
export const API_KEY_PREFIX = 'ppapi';
|
||||
export const API_KEY_TOKEN_LENGTH = 64;
|
||||
export const API_KEY_TOKEN_REGEX = new RegExp(`^${API_KEY_PREFIX}_[A-Za-z0-9]{${API_KEY_TOKEN_LENGTH}}$`);
|
||||
|
||||
export const API_KEY_PERMISSIONS = {
|
||||
ORGANIZATIONS: {
|
||||
CREATE: 'organizations:create',
|
||||
READ: 'organizations:read',
|
||||
UPDATE: 'organizations:update',
|
||||
DELETE: 'organizations:delete',
|
||||
},
|
||||
DOCUMENTS: {
|
||||
CREATE: 'documents:create',
|
||||
READ: 'documents:read',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createMiddleware } from 'hono/factory';
|
||||
import { createUnauthorizedError } from '../app/auth/auth.errors';
|
||||
import { getAuthorizationHeader } from '../shared/headers/headers.models';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { looksLikeAnApiKey } from './api-keys.models';
|
||||
import { createApiKeysRepository } from './api-keys.repository';
|
||||
import { getApiKey } from './api-keys.usecases';
|
||||
|
||||
@@ -31,8 +32,7 @@ export function createApiKeyMiddleware({ db }: { db: Database }) {
|
||||
throw createUnauthorizedError();
|
||||
}
|
||||
|
||||
if (isNil(token)) {
|
||||
// For type safety
|
||||
if (!looksLikeAnApiKey(token)) {
|
||||
throw createUnauthorizedError();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getApiKeyUiPrefix } from './api-keys.models';
|
||||
import { getApiKeyUiPrefix, looksLikeAnApiKey } from './api-keys.models';
|
||||
import { generateApiToken } from './api-keys.services';
|
||||
|
||||
describe('api-keys models', () => {
|
||||
describe('getApiKeyUiPrefix', () => {
|
||||
@@ -11,4 +12,39 @@ describe('api-keys models', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('looksLikeAnApiKey', () => {
|
||||
test(`validate that a token looks like an api key
|
||||
- it starts with the api key prefix
|
||||
- it has the correct length
|
||||
- it only contains alphanumeric characters`, () => {
|
||||
expect(
|
||||
looksLikeAnApiKey('ppapi_29qxv9eCbRkQQGhwrVZCEXEFjOYpXZX07G4vDK4HT03Jp7fVHyJx1b0l6e1LIEPD'),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
looksLikeAnApiKey(''),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
looksLikeAnApiKey('ppapi_'),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
looksLikeAnApiKey('ppapi_29qxv9eCbRkQQGhwrVZCEXEFjOYpXZX07G4vDK4HT03Jp7fVHyJx1b0l6e1LIEPD_extra'),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
looksLikeAnApiKey('invalidprefix_29qxv9eCbRkQQGhwrVZCEXEFjOYpXZX07G4vDK4HT03Jp7fVHyJx1b0l6e1LIEPD'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('a freshly generated token should always look like an api key', () => {
|
||||
const { token } = generateApiToken();
|
||||
|
||||
expect(
|
||||
looksLikeAnApiKey(token),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { sha256 } from '../shared/crypto/hash';
|
||||
import { API_KEY_PREFIX } from './api-keys.constants';
|
||||
import { isNil } from '../shared/utils';
|
||||
import { API_KEY_PREFIX, API_KEY_TOKEN_REGEX } from './api-keys.constants';
|
||||
|
||||
export function getApiKeyUiPrefix({ token }: { token: string }) {
|
||||
return {
|
||||
@@ -12,3 +13,12 @@ export function getApiKeyHash({ token }: { token: string }) {
|
||||
keyHash: sha256(token, { digest: 'base64url' }),
|
||||
};
|
||||
}
|
||||
|
||||
// Positional argument as TS does not like named argument with type guards
|
||||
export function looksLikeAnApiKey(token?: string | null | undefined): token is string {
|
||||
if (isNil(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return API_KEY_TOKEN_REGEX.test(token);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function registerOrganizationsRoutes(context: RouteDefinitionContext) {
|
||||
function setupGetOrganizationsRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.get(
|
||||
'/api/organizations',
|
||||
requireAuthentication(),
|
||||
requireAuthentication({ apiKeyPermissions: ['organizations:read'] }),
|
||||
async (context) => {
|
||||
const { userId } = getUser({ context });
|
||||
|
||||
@@ -45,7 +45,7 @@ function setupGetOrganizationsRoute({ app, db }: RouteDefinitionContext) {
|
||||
function setupCreateOrganizationRoute({ app, db, config }: RouteDefinitionContext) {
|
||||
app.post(
|
||||
'/api/organizations',
|
||||
requireAuthentication(),
|
||||
requireAuthentication({ apiKeyPermissions: ['organizations:create'] }),
|
||||
validateJsonBody(z.object({
|
||||
name: z.string().min(3).max(50),
|
||||
})),
|
||||
@@ -70,7 +70,7 @@ function setupCreateOrganizationRoute({ app, db, config }: RouteDefinitionContex
|
||||
function setupGetOrganizationRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.get(
|
||||
'/api/organizations/:organizationId',
|
||||
requireAuthentication(),
|
||||
requireAuthentication({ apiKeyPermissions: ['organizations:read'] }),
|
||||
validateParams(z.object({
|
||||
organizationId: organizationIdSchema,
|
||||
})),
|
||||
@@ -92,7 +92,7 @@ function setupGetOrganizationRoute({ app, db }: RouteDefinitionContext) {
|
||||
function setupUpdateOrganizationRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.put(
|
||||
'/api/organizations/:organizationId',
|
||||
requireAuthentication(),
|
||||
requireAuthentication({ apiKeyPermissions: ['organizations:update'] }),
|
||||
validateJsonBody(z.object({
|
||||
name: z.string().min(3).max(50),
|
||||
})),
|
||||
@@ -120,7 +120,7 @@ function setupUpdateOrganizationRoute({ app, db }: RouteDefinitionContext) {
|
||||
function setupDeleteOrganizationRoute({ app, db }: RouteDefinitionContext) {
|
||||
app.delete(
|
||||
'/api/organizations/:organizationId',
|
||||
requireAuthentication(),
|
||||
requireAuthentication({ apiKeyPermissions: ['organizations:delete'] }),
|
||||
validateParams(z.object({
|
||||
organizationId: organizationIdSchema,
|
||||
})),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { TaskPersistenceConfig, TaskServiceDriverDefinition } from '../../tasks.types';
|
||||
import { createLibSqlDriver, setupSchema } from '@cadence-mq/driver-libsql';
|
||||
import { safely } from '@corentinth/chisels';
|
||||
import { createClient } from '@libsql/client';
|
||||
import { createLogger } from '../../../shared/logger/logger';
|
||||
|
||||
const logger = createLogger({ namespace: 'tasks-driver:libsql' });
|
||||
|
||||
export function createLibSqlTaskServiceDriver({ taskPersistenceConfig }: { taskPersistenceConfig: TaskPersistenceConfig }): TaskServiceDriverDefinition {
|
||||
const { url, authToken, pollIntervalMs } = taskPersistenceConfig.drivers.libSql;
|
||||
const { url, authToken, pollIntervalMs, migrateWithPragma } = taskPersistenceConfig.drivers.libSql;
|
||||
|
||||
const client = createClient({ url, authToken });
|
||||
const driver = createLibSqlDriver({ client, pollIntervalMs });
|
||||
@@ -11,7 +15,15 @@ export function createLibSqlTaskServiceDriver({ taskPersistenceConfig }: { taskP
|
||||
return {
|
||||
driver,
|
||||
initialize: async () => {
|
||||
await setupSchema({ client });
|
||||
logger.debug('Initializing LibSQL task service driver');
|
||||
const [, error] = await safely(setupSchema({ client, withPragma: migrateWithPragma }));
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, 'Failed to set up LibSQL task service schema');
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info('LibSQL task service driver initialized');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ export const tasksConfig = {
|
||||
default: undefined,
|
||||
env: 'TASKS_PERSISTENCE_DRIVERS_LIBSQL_AUTH_TOKEN',
|
||||
},
|
||||
migrateWithPragma: {
|
||||
doc: 'Whether to include the PRAGMA statements when setting up the LibSQL database schema.',
|
||||
schema: booleanishSchema,
|
||||
default: true,
|
||||
env: 'TASKS_PERSISTENCE_DRIVERS_LIBSQL_MIGRATE_WITH_PRAGMA',
|
||||
},
|
||||
pollIntervalMs: {
|
||||
doc: 'The interval at which the task persistence driver polls for new tasks',
|
||||
schema: z.coerce.number().int().positive(),
|
||||
@@ -39,7 +45,7 @@ export const tasksConfig = {
|
||||
id: {
|
||||
doc: 'The id of the task worker, used to identify the worker in the Cadence cluster in case of multiple workers',
|
||||
schema: z.string().optional(),
|
||||
env: 'TASKS_WORKER_ID',
|
||||
env: ['TASKS_WORKER_ID', 'DYNO', 'RENDER_SERVICE_ID'],
|
||||
},
|
||||
},
|
||||
hardDeleteExpiredDocuments: {
|
||||
|
||||
@@ -48,6 +48,9 @@ ENV PAPRA_CONFIG_DIR=./app-data
|
||||
ENV EMAILS_DRY_RUN=true
|
||||
ENV CLIENT_BASE_URL=http://localhost:1221
|
||||
|
||||
# Disable Better Auth telemetry
|
||||
ENV BETTER_AUTH_TELEMETRY=0
|
||||
|
||||
RUN mkdir -p ./app-data/db ./app-data/documents ./ingestion
|
||||
|
||||
CMD ["pnpm", "start:with-migrations"]
|
||||
|
||||
@@ -61,4 +61,7 @@ ENV INGESTION_FOLDER_ROOT=./ingestion
|
||||
ENV EMAILS_DRY_RUN=true
|
||||
ENV CLIENT_BASE_URL=http://localhost:1221
|
||||
|
||||
# Disable Better Auth telemetry
|
||||
ENV BETTER_AUTH_TELEMETRY=0
|
||||
|
||||
CMD ["pnpm", "start:with-migrations"]
|
||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@@ -100,8 +100,8 @@ importers:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
figue:
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.3(zod@3.25.67)
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -254,8 +254,8 @@ importers:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1
|
||||
'@cadence-mq/driver-libsql':
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)
|
||||
specifier: ^0.2.4
|
||||
version: 0.2.4(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)
|
||||
'@cadence-mq/driver-memory':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(@cadence-mq/core@0.2.1)
|
||||
@@ -317,8 +317,8 @@ importers:
|
||||
specifier: ^0.38.4
|
||||
version: 0.38.4(@libsql/client@0.14.0)(kysely@0.28.2)(react@18.3.1)
|
||||
figue:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3(zod@3.25.67)
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
hono:
|
||||
specifier: ^4.8.2
|
||||
version: 4.8.2
|
||||
@@ -1076,10 +1076,10 @@ packages:
|
||||
'@cadence-mq/core@0.2.1':
|
||||
resolution: {integrity: sha512-Cu/jqR7mNhMZ1U4Boiudy2nePyf4PtqBUFGhUcsCQPJfymKcrDm4xjp8A/2tKZr5JSgkN/7L0/+mHZ27GVSryQ==}
|
||||
|
||||
'@cadence-mq/driver-libsql@0.2.1':
|
||||
resolution: {integrity: sha512-tQPmMNLLVEhvT2HdY/rHk+Cl0Yj4JFMQnoYnBYIw30kTIpKGCQWnBTf5oSmIlmc6wdIHYan+f+waVhWmkObD1w==}
|
||||
'@cadence-mq/driver-libsql@0.2.4':
|
||||
resolution: {integrity: sha512-JXsajpPXJRQolYiPzYI5rpQyTjH1g7AZMh3KYnHHs8nieLekYhU885iRPCu80RXQsYN2CJa08Vj5hgQhGP9rjw==}
|
||||
peerDependencies:
|
||||
'@cadence-mq/core': ^0.2.0
|
||||
'@cadence-mq/core': ^0.2.3
|
||||
'@libsql/client': ^0.15.9
|
||||
|
||||
'@cadence-mq/driver-memory@0.2.0':
|
||||
@@ -4654,8 +4654,8 @@ packages:
|
||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-libc@2.0.4:
|
||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||
detect-libc@2.1.0:
|
||||
resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
deterministic-object-hash@2.0.2:
|
||||
@@ -5411,10 +5411,8 @@ packages:
|
||||
fflate@0.4.8:
|
||||
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
|
||||
|
||||
figue@2.2.3:
|
||||
resolution: {integrity: sha512-Dwj/Na3KuiGeFhiM2AieEcMhgXDbHfdWrRcSGdfAspeWTEC8OXleY/Mf+yty4YIK4gxpgQcpKhQm84XTMvXUqg==}
|
||||
peerDependencies:
|
||||
zod: ^3.22.4
|
||||
figue@3.1.1:
|
||||
resolution: {integrity: sha512-LYqDGKztWh0tU5eCTzW5nK3MAp4K5ihEsnyWiNwU6jW0yVle9DqQCiMra5lMTXE21d9x882ZoOK4SitMXvaQ3Q==}
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
@@ -9515,7 +9513,7 @@ snapshots:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
cron-parser: 5.3.0
|
||||
|
||||
'@cadence-mq/driver-libsql@0.2.1(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)':
|
||||
'@cadence-mq/driver-libsql@0.2.4(@cadence-mq/core@0.2.1)(@libsql/client@0.14.0)':
|
||||
dependencies:
|
||||
'@cadence-mq/core': 0.2.1
|
||||
'@libsql/client': 0.14.0
|
||||
@@ -10752,7 +10750,7 @@ snapshots:
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11':
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
detect-libc: 2.1.0
|
||||
https-proxy-agent: 5.0.1
|
||||
make-dir: 3.1.0
|
||||
node-fetch: 2.7.0
|
||||
@@ -13308,7 +13306,7 @@ snapshots:
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
|
||||
detect-libc@2.0.4:
|
||||
detect-libc@2.1.0:
|
||||
optional: true
|
||||
|
||||
deterministic-object-hash@2.0.2:
|
||||
@@ -14356,9 +14354,9 @@ snapshots:
|
||||
|
||||
fflate@0.4.8: {}
|
||||
|
||||
figue@2.2.3(zod@3.25.67):
|
||||
figue@3.1.1:
|
||||
dependencies:
|
||||
zod: 3.25.67
|
||||
'@standard-schema/spec': 1.0.0
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
@@ -17785,7 +17783,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@5.4.19(@types/node@22.16.0))
|
||||
'@vitest/mocker': 3.2.4(vite@5.4.19(@types/node@24.0.10))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
|
||||
Reference in New Issue
Block a user