mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-29 02:36:35 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0de4b55dc4 | ||
|
|
78c88f5339 | ||
|
|
60e7dafa01 | ||
|
|
2ccabf835c | ||
|
|
590cb02f6c | ||
|
|
8c96ab9574 | ||
|
|
3484daf870 | ||
|
|
cfbc0d6d35 |
@@ -1,3 +1,10 @@
|
|||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.12.0...v) (2024-10-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add ability to define expiration of one time link ([2ccabf8](https://github.com/stonith404/pocket-id/commit/2ccabf835c2c923d6986d9cafb4e878f5110b91a))
|
||||||
|
|
||||||
## [](https://github.com/stonith404/pocket-id/compare/v0.11.0...v) (2024-10-28)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.11.0...v) (2024-10-28)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ COPY --from=backend-builder /app/backend/images ./backend/images
|
|||||||
COPY ./scripts ./scripts
|
COPY ./scripts ./scripts
|
||||||
RUN chmod +x ./scripts/*.sh
|
RUN chmod +x ./scripts/*.sh
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 80
|
||||||
ENV APP_ENV=production
|
ENV APP_ENV=production
|
||||||
|
|
||||||
# Use a shell form to run both the frontend and backend
|
# Use a shell form to run both the frontend and backend
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -85,28 +85,23 @@ Required tools:
|
|||||||
|
|
||||||
You can now sign in with the admin account on `http://localhost/login/setup`.
|
You can now sign in with the admin account on `http://localhost/login/setup`.
|
||||||
|
|
||||||
### Add Pocket ID as an OIDC provider
|
### Nginx Reverse Proxy
|
||||||
|
|
||||||
You can add a new OIDC client on `https://<your-domain>/settings/admin/oidc-clients`
|
To use Nginx in front of Pocket ID, add the following configuration to increase the header buffer size because, as SvelteKit generates larger headers.
|
||||||
|
|
||||||
After you have added the client, you can obtain the client ID and client secret.
|
```nginx
|
||||||
|
proxy_busy_buffers_size 512k;
|
||||||
|
proxy_buffers 4 512k;
|
||||||
|
proxy_buffer_size 256k;
|
||||||
|
```
|
||||||
|
|
||||||
You may need the following information:
|
## Proxy Services with Pocket ID
|
||||||
|
|
||||||
- **Authorization URL**: `https://<your-domain>/authorize`
|
|
||||||
- **Token URL**: `https://<your-domain>/api/oidc/token`
|
|
||||||
- **Userinfo URL**: `https://<your-domain>/api/oidc/userinfo`
|
|
||||||
- **Certificate URL**: `https://<your-domain>/.well-known/jwks.json`
|
|
||||||
- **OIDC Discovery URL**: `https://<your-domain>/.well-known/openid-configuration`
|
|
||||||
- **Scopes**: At least `openid email`. Optionally you can add `profile` and `groups`.
|
|
||||||
|
|
||||||
### Proxy Services with Pocket ID
|
|
||||||
|
|
||||||
As the goal of Pocket ID is to stay simple, we don't have a built-in proxy provider. However, you can use [OAuth2 Proxy](https://oauth2-proxy.github.io/) to add authentication to your services that don't support OIDC.
|
As the goal of Pocket ID is to stay simple, we don't have a built-in proxy provider. However, you can use [OAuth2 Proxy](https://oauth2-proxy.github.io/) to add authentication to your services that don't support OIDC.
|
||||||
|
|
||||||
See the [guide](docs/proxy-services.md) for more information.
|
See the [guide](docs/proxy-services.md) for more information.
|
||||||
|
|
||||||
### Update
|
## Update
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
@@ -149,7 +144,7 @@ docker compose up -d
|
|||||||
pm2 start caddy --name pocket-id-caddy -- run --config Caddyfile
|
pm2 start caddy --name pocket-id-caddy -- run --config Caddyfile
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Variable | Default Value | Recommended to change | Description |
|
| Variable | Default Value | Recommended to change | Description |
|
||||||
| ---------------------- | ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
|||||||
BIN
docs/imgs/jelly_fin_img.png
Normal file
BIN
docs/imgs/jelly_fin_img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
docs/imgs/jelly_fin_img2.png
Normal file
BIN
docs/imgs/jelly_fin_img2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
BIN
docs/imgs/jelly_fin_img3.png
Normal file
BIN
docs/imgs/jelly_fin_img3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
55
docs/jellyfin.md
Normal file
55
docs/jellyfin.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Jellyfin SSO Integration Guide
|
||||||
|
|
||||||
|
> Due to the current limitations of the Jellyfin SSO plugin, this integration will only work in a browser. When tested, the Jellyfin app did not work and displayed an error, even when custom menu buttons were created.
|
||||||
|
|
||||||
|
> To view the original references and a full list of capabilities, please visit the [Jellyfin SSO OpenID Section](https://github.com/9p4/jellyfin-plugin-sso?tab=readme-ov-file#openid).
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- [Jellyfin Server](https://jellyfin.org/downloads/server)
|
||||||
|
- [Jellyfin SSO Plugin](https://github.com/9p4/jellyfin-plugin-sso)
|
||||||
|
- HTTPS connection to your Jellyfin server
|
||||||
|
|
||||||
|
### OIDC - Pocket ID Setup
|
||||||
|
To start, we need to create a new SSO resource in our Jellyfin application.
|
||||||
|
|
||||||
|
> Replace the `JELLYFINDOMAIN` and `PROVIDER` elements in the URL.
|
||||||
|
|
||||||
|
1. Log into the admin panel, and go to OIDC Clients -> Add OIDC Client.
|
||||||
|
2. **Name**: Jellyfin (or any name you prefer)
|
||||||
|
3. **Callback URL**: `https://JELLYFINDOMAIN.com/sso/OID/redirect/PROVIDER`
|
||||||
|
4. For this example, we’ll be using the provider named "test_resource."
|
||||||
|
5. Click **Save**. Keep the page open, as we will need the OID client ID and OID secret.
|
||||||
|
|
||||||
|
### OIDC Client - Jellyfin SSO Resource
|
||||||
|
|
||||||
|
1. Visit the plugin page (<i>Administration Dashboard -> My Plugins -> SSO-Auth</i>).
|
||||||
|
2. Enter the <i>OID Provider Name (we used "test_resource" as our name in the callback URL), Open ID, OID Secret, and mark it as enabled.</i>
|
||||||
|
3. The following steps are optional based on your needs. In this guide, we’ll be managing only regular users, not admins.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> To manage user access through groups, follow steps **4, 5, and 6**. Otherwise, leave it blank and skip to step 7.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Under <i>Roles</i>, type the name of the group you want to use. **Note:** This must be the group name, not the label. Double-check in Pocket ID, as an incorrect name will lock users out.
|
||||||
|
5. Skip every field until you reach the **Role Claim** field, and type `groups`.
|
||||||
|
> This step is crucial if you want to manage users through groups.
|
||||||
|
6. Repeat the above step under **Request Additional Scopes**. This will pull the group scope during the sign-in process; otherwise, the previous steps won’t work.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. Skip the remaining fields until you reach **Scheme Override**. Enter `https` here. If omitted, it will attempt to use HTTP first, which will break as WebAuthn requires an HTTPS connection.
|
||||||
|
8. Click **Save** and restart Jellyfin.
|
||||||
|
|
||||||
|
### Optional Step - Custom Home Button
|
||||||
|
Follow the [guide to create a login button on the login page](https://github.com/9p4/jellyfin-plugin-sso?tab=readme-ov-file#creating-a-login-button-on-the-main-page) to add a custom button on your sign-in page. This step is optional, as you could also provide the sign-in URL via a bookmark or other means.
|
||||||
|
|
||||||
|
### Signing into Your Jellyfin Instance
|
||||||
|
Done! You have successfully set up SSO for your Jellyfin instance using Pocket ID.
|
||||||
|
|
||||||
|
> **Note:** Sometimes there may be a brief delay when using the custom menu option. This is related to the Jellyfin plugin and not Pocket ID.
|
||||||
|
|
||||||
|
If your users already have accounts, as long as their Pocket ID username matches their Jellyfin ID, they will be logged in automatically. Otherwise, a new user will be created with access to all of your folders. Of course, you can modify this in your configuration as desired.
|
||||||
|
|
||||||
|
This setup will only work if sign-in is performed using the `https://jellyfin.example.com/sso/OID/start/PROVIDER` URL. This URL initiates the SSO plugin and applies all the configurations we completed above.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --port 3000",
|
"dev": "vite dev --port 3000",
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ export default class UserService extends APIService {
|
|||||||
await this.api.delete(`/users/${id}`);
|
await this.api.delete(`/users/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneTimeAccessToken(userId: string) {
|
async createOneTimeAccessToken(userId: string, expiresAt: Date) {
|
||||||
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
||||||
userId,
|
userId,
|
||||||
expiresAt: new Date(Date.now() + 1000 * 60 * 5).toISOString()
|
expiresAt
|
||||||
});
|
});
|
||||||
return res.data.token;
|
return res.data.token;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import Input from '$lib/components/ui/input/input.svelte';
|
import Input from '$lib/components/ui/input/input.svelte';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
import * as Select from '$lib/components/ui/select/index.js';
|
||||||
|
import UserService from '$lib/services/user-service';
|
||||||
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
oneTimeLink = $bindable()
|
userId = $bindable()
|
||||||
}: {
|
}: {
|
||||||
oneTimeLink: string | null;
|
userId: string | null;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
const userService = new UserService();
|
||||||
|
|
||||||
|
let oneTimeLink: string | null = $state(null);
|
||||||
|
let selectedExpiration: keyof typeof availableExpirations = $state('1 hour');
|
||||||
|
|
||||||
|
let availableExpirations = {
|
||||||
|
'1 hour': 60 * 60,
|
||||||
|
'12 hours': 60 * 60 * 12,
|
||||||
|
'1 day': 60 * 60 * 24,
|
||||||
|
'1 week': 60 * 60 * 24 * 7,
|
||||||
|
'1 month': 60 * 60 * 24 * 30
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createOneTimeAccessToken() {
|
||||||
|
try {
|
||||||
|
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
||||||
|
const token = await userService.createOneTimeAccessToken(userId!, expiration);
|
||||||
|
oneTimeLink = `${$page.url.origin}/login/${token}`;
|
||||||
|
} catch (e) {
|
||||||
|
axiosErrorToast(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onOpenChange(open: boolean) {
|
function onOpenChange(open: boolean) {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
oneTimeLink = null;
|
oneTimeLink = null;
|
||||||
|
userId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root open={!!oneTimeLink} {onOpenChange}>
|
<Dialog.Root open={!!userId} {onOpenChange}>
|
||||||
<Dialog.Content class="max-w-md">
|
<Dialog.Content class="max-w-md">
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>One Time Link</Dialog.Title>
|
<Dialog.Title>One Time Link</Dialog.Title>
|
||||||
@@ -25,9 +54,36 @@
|
|||||||
have lost it.</Dialog.Description
|
have lost it.</Dialog.Description
|
||||||
>
|
>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
<div>
|
{#if oneTimeLink === null}
|
||||||
<Label for="one-time-link">One Time Link</Label>
|
<div>
|
||||||
|
<Label for="expiration">Expiration</Label>
|
||||||
|
<Select.Root
|
||||||
|
selected={{
|
||||||
|
label: Object.keys(availableExpirations)[0],
|
||||||
|
value: Object.keys(availableExpirations)[0]
|
||||||
|
}}
|
||||||
|
onSelectedChange={(v) =>
|
||||||
|
(selectedExpiration = v!.value as keyof typeof availableExpirations)}
|
||||||
|
>
|
||||||
|
<Select.Trigger class="h-9 ">
|
||||||
|
<Select.Value>{selectedExpiration}</Select.Value>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each Object.keys(availableExpirations) as key}
|
||||||
|
<Select.Item value={key}>{key}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onclick={() => createOneTimeAccessToken()}
|
||||||
|
disabled={!selectedExpiration}
|
||||||
|
>
|
||||||
|
Generate Link
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Label for="one-time-link" class="sr-only">One Time Link</Label>
|
||||||
<Input id="one-time-link" value={oneTimeLink} readonly />
|
<Input id="one-time-link" value={oneTimeLink} readonly />
|
||||||
</div>
|
{/if}
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { goto } from '$app/navigation';
|
||||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||||
import { Badge } from '$lib/components/ui/badge/index';
|
import { Badge } from '$lib/components/ui/badge/index';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { buttonVariants } from '$lib/components/ui/button';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
users = initialUsers;
|
users = initialUsers;
|
||||||
});
|
});
|
||||||
|
|
||||||
let oneTimeLink = $state<string | null>(null);
|
let userIdToCreateOneTimeLink: string | null = $state(null);;
|
||||||
|
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
@@ -48,15 +48,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createOneTimeAccessToken(userId: string) {
|
|
||||||
try {
|
|
||||||
const token = await userService.createOneTimeAccessToken(userId);
|
|
||||||
oneTimeLink = `${$page.url.origin}/login/${token}`;
|
|
||||||
} catch (e) {
|
|
||||||
axiosErrorToast(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AdvancedTable
|
<AdvancedTable
|
||||||
@@ -82,22 +73,20 @@
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger asChild let:builder>
|
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
|
||||||
<Button aria-haspopup="true" size="icon" variant="ghost" builders={[builder]}>
|
<Ellipsis class="h-4 w-4" />
|
||||||
<Ellipsis class="h-4 w-4" />
|
<span class="sr-only">Toggle menu</span>
|
||||||
<span class="sr-only">Toggle menu</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content align="end">
|
<DropdownMenu.Content align="end">
|
||||||
<DropdownMenu.Item on:click={() => createOneTimeAccessToken(item.id)}
|
<DropdownMenu.Item onclick={() => (userIdToCreateOneTimeLink = item.id)}
|
||||||
><LucideLink class="mr-2 h-4 w-4" />One-time link</DropdownMenu.Item
|
><LucideLink class="mr-2 h-4 w-4" />One-time link</DropdownMenu.Item
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item href="/settings/admin/users/{item.id}"
|
<DropdownMenu.Item onclick={() => goto(`/settings/admin/users/${item.id}`)}
|
||||||
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
><LucidePencil class="mr-2 h-4 w-4" /> Edit</DropdownMenu.Item
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="text-red-500 focus:!text-red-700"
|
class="text-red-500 focus:!text-red-700"
|
||||||
on:click={() => deleteUser(item)}
|
onclick={() => deleteUser(item)}
|
||||||
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
><LucideTrash class="mr-2 h-4 w-4" />Delete</DropdownMenu.Item
|
||||||
>
|
>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
@@ -106,4 +95,4 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</AdvancedTable>
|
</AdvancedTable>
|
||||||
|
|
||||||
<OneTimeLinkModal {oneTimeLink} />
|
<OneTimeLinkModal userId={userIdToCreateOneTimeLink} />
|
||||||
|
|||||||
@@ -57,8 +57,13 @@ test('Create one time access token', async ({ page }) => {
|
|||||||
.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` })
|
.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` })
|
||||||
.getByRole('button')
|
.getByRole('button')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'One-time link' }).click();
|
await page.getByRole('menuitem', { name: 'One-time link' }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('One Time Link').getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: '12 hours' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Generate Link' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('textbox', { name: 'One Time Link' })).toHaveValue(
|
await expect(page.getByRole('textbox', { name: 'One Time Link' })).toHaveValue(
|
||||||
/http:\/\/localhost\/login\/.*/
|
/http:\/\/localhost\/login\/.*/
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user