mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-10 07:12:59 +03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d071694cd | ||
|
|
39e403d00f | ||
|
|
4e858420e9 | ||
|
|
2d78349b38 | ||
|
|
9ed2adb0f8 | ||
|
|
43790dc1be | ||
|
|
7fbc356d8d |
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04 # Using an older version because of https://github.com/actions/runner-images/issues/11471
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
${{ github.repository }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.30.0...v) (2025-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to override the UI configuration with environment variables ([4e85842](https://github.com/pocket-id/pocket-id/commit/4e858420e9d9713e19f3b35c45c882403717f72f))
|
||||
* add warning for only having one passkey configured ([#220](https://github.com/pocket-id/pocket-id/issues/220)) ([39e403d](https://github.com/pocket-id/pocket-id/commit/39e403d00f3870f9e960427653a1d9697da27a6f))
|
||||
* display source in user and group table ([#225](https://github.com/pocket-id/pocket-id/issues/225)) ([9ed2adb](https://github.com/pocket-id/pocket-id/commit/9ed2adb0f8da13725fd9a4ef6a7798c377d13513))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* user linking in ldap group sync ([#222](https://github.com/pocket-id/pocket-id/issues/222)) ([2d78349](https://github.com/pocket-id/pocket-id/commit/2d78349b381d7ca10f47d3c03cef685a576b1b49))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.29.0...v) (2025-02-08)
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ type EnvConfigSchema struct {
|
||||
Host string `env:"HOST"`
|
||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
||||
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||
UiConfigDisabled bool `env:"PUBLIC_UI_CONFIG_DISABLED"`
|
||||
}
|
||||
|
||||
var EnvConfig = &EnvConfigSchema{
|
||||
@@ -38,6 +39,7 @@ var EnvConfig = &EnvConfigSchema{
|
||||
Host: "0.0.0.0",
|
||||
MaxMindLicenseKey: "",
|
||||
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
|
||||
UiConfigDisabled: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -184,3 +184,10 @@ func (e *OidcAccessDeniedError) Error() string {
|
||||
}
|
||||
|
||||
func (e *OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type UiConfigDisabledError struct{}
|
||||
|
||||
func (e *UiConfigDisabledError) Error() string {
|
||||
return "The configuration can't be changed since the UI configuration is disabled"
|
||||
}
|
||||
func (e *UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
@@ -188,12 +188,15 @@ var defaultDbConfig = model.AppConfig{
|
||||
}
|
||||
|
||||
func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]model.AppConfigVariable, error) {
|
||||
var savedConfigVariables []model.AppConfigVariable
|
||||
if common.EnvConfig.UiConfigDisabled {
|
||||
return nil, &common.UiConfigDisabledError{}
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
rt := reflect.ValueOf(input).Type()
|
||||
rv := reflect.ValueOf(input)
|
||||
|
||||
var savedConfigVariables []model.AppConfigVariable
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
field := rt.Field(i)
|
||||
key := field.Tag.Get("json")
|
||||
@@ -254,9 +257,13 @@ func (s *AppConfigService) ListAppConfig(showAll bool) ([]model.AppConfigVariabl
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set the value to the default value if it is empty
|
||||
for i := range configuration {
|
||||
if configuration[i].Value == "" && configuration[i].DefaultValue != "" {
|
||||
if common.EnvConfig.UiConfigDisabled {
|
||||
// Set the value to the environment variable if the UI config is disabled
|
||||
configuration[i].Value = s.getConfigVariableFromEnvironmentVariable(configuration[i].Key, configuration[i].DefaultValue)
|
||||
|
||||
} else if configuration[i].Value == "" && configuration[i].DefaultValue != "" {
|
||||
// Set the value to the default value if it is empty
|
||||
configuration[i].Value = configuration[i].DefaultValue
|
||||
}
|
||||
}
|
||||
@@ -355,12 +362,25 @@ func (s *AppConfigService) LoadDbConfigFromDb() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if storedConfigVar.Value == "" && storedConfigVar.DefaultValue != "" {
|
||||
if common.EnvConfig.UiConfigDisabled {
|
||||
storedConfigVar.Value = s.getConfigVariableFromEnvironmentVariable(currentConfigVar.Key, storedConfigVar.DefaultValue)
|
||||
} else if storedConfigVar.Value == "" && storedConfigVar.DefaultValue != "" {
|
||||
storedConfigVar.Value = storedConfigVar.DefaultValue
|
||||
}
|
||||
|
||||
dbConfigField.Set(reflect.ValueOf(storedConfigVar))
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AppConfigService) getConfigVariableFromEnvironmentVariable(key, fallbackValue string) string {
|
||||
environmentVariableName := utils.CamelCaseToScreamingSnakeCase(key)
|
||||
|
||||
if value, exists := os.LookupEnv(environmentVariableName); exists {
|
||||
return value
|
||||
}
|
||||
|
||||
return fallbackValue
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ func (s *LdapService) SyncGroups() error {
|
||||
singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0]
|
||||
|
||||
var databaseUser model.User
|
||||
s.db.Where("username = ?", singleMember).First(&databaseUser)
|
||||
s.db.Where("username = ?", singleMember).Where("ldap_id IS NOT NULL").First(&databaseUser)
|
||||
|
||||
membersUserId = append(membersUserId, databaseUser.ID)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
@@ -62,3 +64,12 @@ func CamelCaseToSnakeCase(s string) string {
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func CamelCaseToScreamingSnakeCase(s string) string {
|
||||
// Insert underscores before uppercase letters (except the first one)
|
||||
re := regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
||||
snake := re.ReplaceAllString(s, `${1}_${2}`)
|
||||
|
||||
// Convert to uppercase
|
||||
return strings.ToUpper(snake)
|
||||
}
|
||||
|
||||
9482
frontend/package-lock.json
generated
9482
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "0.30.0",
|
||||
"version": "0.31.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,17 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
import { LucideX } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { type Variant, alertVariants } from './index.js';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||
variant?: Variant;
|
||||
dismissibleId?: string;
|
||||
};
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export let variant: $$Props['variant'] = 'default';
|
||||
export let dismissibleId: $$Props['dismissibleId'] = undefined;
|
||||
export { className as class };
|
||||
|
||||
let isVisible = !dismissibleId;
|
||||
|
||||
onMount(() => {
|
||||
if (dismissibleId) {
|
||||
const dismissedAlerts = JSON.parse(localStorage.getItem('dismissed-alerts') || '[]');
|
||||
isVisible = !dismissedAlerts.includes(dismissibleId);
|
||||
}
|
||||
});
|
||||
|
||||
function dismiss() {
|
||||
if (dismissibleId) {
|
||||
const dismissedAlerts = JSON.parse(localStorage.getItem('dismissed-alerts') || '[]');
|
||||
localStorage.setItem('dismissed-alerts', JSON.stringify([...dismissedAlerts, dismissibleId]));
|
||||
isVisible = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
|
||||
<slot />
|
||||
</div>
|
||||
{#if isVisible}
|
||||
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
|
||||
<slot />
|
||||
{#if dismissibleId}
|
||||
<button on:click={dismiss} class="absolute top-0 right-0 m-3 text-black dark:text-white"><LucideX class="w-4" /></button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -62,9 +62,20 @@
|
||||
>Please add a passkey to prevent losing access to your account.</Alert.Description
|
||||
>
|
||||
</Alert.Root>
|
||||
{:else if passkeys.length == 1}
|
||||
<Alert.Root variant="warning" dismissibleId="single-passkey">
|
||||
<LucideAlertTriangle class="size-4" />
|
||||
<Alert.Title>Single Passkey Configured</Alert.Title>
|
||||
<Alert.Description
|
||||
>It is recommended to add more than one passkey to avoid loosing access to your account.</Alert.Description
|
||||
>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
|
||||
<fieldset disabled={!$appConfigStore.allowOwnAccountEdit || (!!account.ldapId && $appConfigStore.ldapEnabled)}>
|
||||
<fieldset
|
||||
disabled={!$appConfigStore.allowOwnAccountEdit ||
|
||||
(!!account.ldapId && $appConfigStore.ldapEnabled)}
|
||||
>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Account Details</Card.Title>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { env } from '$env/dynamic/public';
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
@@ -13,6 +14,7 @@
|
||||
let { data } = $props();
|
||||
let appConfig = $state(data.appConfig);
|
||||
|
||||
const uiConfigDisabled = env.PUBLIC_UI_CONFIG_DISABLED === 'true';
|
||||
const appConfigService = new AppConfigService();
|
||||
|
||||
async function updateAppConfig(updatedAppConfig: Partial<AllAppConfig>) {
|
||||
@@ -55,26 +57,28 @@
|
||||
<title>Application Configuration</title>
|
||||
</svelte:head>
|
||||
|
||||
<CollapsibleCard id="application-configuration-general" title="General" defaultExpanded>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
<fieldset class="flex flex-col gap-5" disabled={uiConfigDisabled}>
|
||||
<CollapsibleCard id="application-configuration-general" title="General" defaultExpanded>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="application-configuration-email"
|
||||
title="Email"
|
||||
description="Enable email notifications to alert users when a login is detected from a new device or
|
||||
<CollapsibleCard
|
||||
id="application-configuration-email"
|
||||
title="Email"
|
||||
description="Enable email notifications to alert users when a login is detected from a new device or
|
||||
location."
|
||||
>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="application-configuration-ldap"
|
||||
title="LDAP"
|
||||
description="Configure LDAP settings to sync users and groups from an LDAP server."
|
||||
>
|
||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-ldap"
|
||||
title="LDAP"
|
||||
description="Configure LDAP settings to sync users and groups from an LDAP server."
|
||||
>
|
||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</fieldset>
|
||||
|
||||
<CollapsibleCard id="application-configuration-images" title="Images">
|
||||
<UpdateApplicationImages callback={updateImages} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import { Badge } from '$lib/components/ui/badge/index';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
@@ -35,7 +36,7 @@
|
||||
toast.success('User group deleted successfully');
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -50,6 +51,7 @@
|
||||
{ label: 'Friendly Name', sortColumn: 'friendlyName' },
|
||||
{ label: 'Name', sortColumn: 'name' },
|
||||
{ label: 'User Count', sortColumn: 'userCount' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: 'Source' }] : []),
|
||||
{ label: 'Actions', hidden: true }
|
||||
]}
|
||||
>
|
||||
@@ -57,6 +59,12 @@
|
||||
<Table.Cell>{item.friendlyName}</Table.Cell>
|
||||
<Table.Cell>{item.name}</Table.Cell>
|
||||
<Table.Cell>{item.userCount}</Table.Cell>
|
||||
{#if $appConfigStore.ldapEnabled}
|
||||
<Table.Cell>
|
||||
<Badge variant={item.ldapId ? 'default' : 'outline'}>{item.ldapId ? 'LDAP' : 'Local'}</Badge
|
||||
>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
<Table.Cell class="flex justify-end">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { User } from '$lib/types/user.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
@@ -14,7 +15,6 @@
|
||||
import Ellipsis from 'lucide-svelte/icons/ellipsis';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import OneTimeLinkModal from './one-time-link-modal.svelte';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
|
||||
let { users = $bindable() }: { users: Paginated<User> } = $props();
|
||||
let requestOptions: SearchPaginationSortRequest | undefined = $state();
|
||||
@@ -49,30 +49,13 @@
|
||||
{requestOptions}
|
||||
onRefresh={async (options) => (users = await userService.list(options))}
|
||||
columns={[
|
||||
{
|
||||
label: 'First name',
|
||||
sortColumn: 'firstName'
|
||||
},
|
||||
{
|
||||
label: 'Last name',
|
||||
sortColumn: 'lastName'
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
sortColumn: 'email'
|
||||
},
|
||||
{
|
||||
label: 'Username',
|
||||
sortColumn: 'username'
|
||||
},
|
||||
{
|
||||
label: 'Role',
|
||||
sortColumn: 'isAdmin'
|
||||
},
|
||||
{
|
||||
label: 'Actions',
|
||||
hidden: true
|
||||
}
|
||||
{ label: 'First name', sortColumn: 'firstName' },
|
||||
{ label: 'Last name', sortColumn: 'lastName' },
|
||||
{ label: 'Email', sortColumn: 'email' },
|
||||
{ label: 'Username', sortColumn: 'username' },
|
||||
{ label: 'Role', sortColumn: 'isAdmin' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: 'Source' }] : []),
|
||||
{ label: 'Actions', hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
@@ -80,9 +63,15 @@
|
||||
<Table.Cell>{item.lastName}</Table.Cell>
|
||||
<Table.Cell>{item.email}</Table.Cell>
|
||||
<Table.Cell>{item.username}</Table.Cell>
|
||||
<Table.Cell class="hidden lg:table-cell">
|
||||
<Table.Cell>
|
||||
<Badge variant="outline">{item.isAdmin ? 'Admin' : 'User'}</Badge>
|
||||
</Table.Cell>
|
||||
{#if $appConfigStore.ldapEnabled}
|
||||
<Table.Cell>
|
||||
<Badge variant={item.ldapId ? 'default' : 'outline'}>{item.ldapId ? 'LDAP' : 'Local'}</Badge
|
||||
>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
<Table.Cell>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
|
||||
|
||||
Reference in New Issue
Block a user