Compare commits

...

13 Commits

Author SHA1 Message Date
Elias Schneider
cddfe8fa4c release: 0.35.4 2025-03-01 20:42:53 +01:00
Jonas
ef25f6b6b8 fix: profile picture of other user can't be updated (#273) 2025-03-01 20:42:29 +01:00
Elias Schneider
1652cc65f3 fix: support POST for OIDC userinfo endpoint 2025-03-01 20:42:00 +01:00
Elias Schneider
4bafee4f58 fix: add groups scope and claim to well known endpoint 2025-03-01 20:41:30 +01:00
Elias Schneider
e46471cc2d release: 0.35.3 2025-02-25 20:34:37 +01:00
Elias Schneider
fde951b543 fix(ldap): sync error if LDAP user collides with an existing user 2025-02-25 20:34:13 +01:00
Kyle Mendell
01a9de0b04 fix: add option to manually select SMTP TLS method (#268)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-02-25 19:10:20 +01:00
Elias Schneider
a1131bca9a release: 0.35.2 2025-02-24 09:40:48 +01:00
Elias Schneider
9a167d4076 fix: delete profile picture if user gets deleted 2025-02-24 09:40:14 +01:00
Elias Schneider
887c5e462a fix: updating profile picture of other user updates own profile picture 2025-02-24 09:35:44 +01:00
Elias Schneider
20eba1378e release: 0.35.1 2025-02-22 14:59:43 +01:00
Elias Schneider
a6ae7ae287 fix: add validation that PUBLIC_APP_URL can't contain a path 2025-02-22 14:59:10 +01:00
Elias Schneider
840a672fc3 fix: binary profile picture can't be imported from LDAP 2025-02-22 14:51:21 +01:00
20 changed files with 137 additions and 29 deletions

View File

@@ -1 +1 @@
0.35.0
0.35.4

View File

@@ -1,3 +1,36 @@
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.3...v) (2025-03-01)
### Bug Fixes
* add `groups` scope and claim to well known endpoint ([4bafee4](https://github.com/pocket-id/pocket-id/commit/4bafee4f58f5a76898cf66d6192916d405eea389))
* profile picture of other user can't be updated ([#273](https://github.com/pocket-id/pocket-id/issues/273)) ([ef25f6b](https://github.com/pocket-id/pocket-id/commit/ef25f6b6b84b52f1310d366d40aa3769a6fe9bef))
* support POST for OIDC userinfo endpoint ([1652cc6](https://github.com/pocket-id/pocket-id/commit/1652cc65f3f966d018d81a1ae22abb5ff1b4c47b))
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.2...v) (2025-02-25)
### Bug Fixes
* add option to manually select SMTP TLS method ([#268](https://github.com/pocket-id/pocket-id/issues/268)) ([01a9de0](https://github.com/pocket-id/pocket-id/commit/01a9de0b04512c62d0f223de33d711f93c49b9cc))
* **ldap:** sync error if LDAP user collides with an existing user ([fde951b](https://github.com/pocket-id/pocket-id/commit/fde951b543281fedf9f602abae26b50881e3d157))
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.1...v) (2025-02-24)
### Bug Fixes
* delete profile picture if user gets deleted ([9a167d4](https://github.com/pocket-id/pocket-id/commit/9a167d4076872e5e3e5d78d2a66ef7203ca5261b))
* updating profile picture of other user updates own profile picture ([887c5e4](https://github.com/pocket-id/pocket-id/commit/887c5e462a50c8fb579ca6804f1a643d8af78fe8))
## [](https://github.com/pocket-id/pocket-id/compare/v0.35.0...v) (2025-02-22)
### Bug Fixes
* add validation that `PUBLIC_APP_URL` can't contain a path ([a6ae7ae](https://github.com/pocket-id/pocket-id/commit/a6ae7ae28713f7fc8018ae2aa7572986df3e1a5b))
* binary profile picture can't be imported from LDAP ([840a672](https://github.com/pocket-id/pocket-id/commit/840a672fc35ca8476caf86d7efaba9d54bce86aa))
## [](https://github.com/pocket-id/pocket-id/compare/v0.34.0...v) (2025-02-19)

View File

@@ -2,6 +2,7 @@ package common
import (
"log"
"net/url"
"github.com/caarlos0/env/v11"
_ "github.com/joho/godotenv/autoload"
@@ -61,4 +62,12 @@ func init() {
if EnvConfig.DbProvider == DbProviderSqlite && EnvConfig.SqliteDBPath == "" {
log.Fatal("Missing SQLITE_DB_PATH environment variable")
}
parsedAppUrl, err := url.Parse(EnvConfig.AppURL)
if err != nil {
log.Fatal("PUBLIC_APP_URL is not a valid URL")
}
if parsedAppUrl.Path != "" {
log.Fatal("PUBLIC_APP_URL must not contain a path")
}
}

View File

@@ -94,6 +94,11 @@ type NotSignedInError struct{}
func (e *NotSignedInError) Error() string { return "You are not signed in" }
func (e *NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
type MissingAccessToken struct{}
func (e *MissingAccessToken) Error() string { return "Missing access token" }
func (e *MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized }
type MissingPermissionError struct{}
func (e *MissingPermissionError) Error() string {

View File

@@ -23,6 +23,7 @@ func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
group.POST("/oidc/token", oc.createTokensHandler)
group.GET("/oidc/userinfo", oc.userInfoHandler)
group.POST("/oidc/userinfo", oc.userInfoHandler)
group.POST("/oidc/end-session", oc.EndSessionHandler)
group.GET("/oidc/end-session", oc.EndSessionHandler)
@@ -111,7 +112,14 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
}
func (oc *OidcController) userInfoHandler(c *gin.Context) {
token := strings.Split(c.GetHeader("Authorization"), " ")[1]
authHeaderSplit := strings.Split(c.GetHeader("Authorization"), " ")
if len(authHeaderSplit) != 2 {
c.Error(&common.MissingAccessToken{})
return
}
token := authHeaderSplit[1]
jwtClaims, err := oc.jwtService.VerifyOauthAccessToken(token)
if err != nil {
c.Error(err)

View File

@@ -33,7 +33,7 @@ func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
group.GET("/users/:id/profile-picture.png", uc.getUserProfilePictureHandler)
group.GET("/users/me/profile-picture.png", jwtAuthMiddleware.Add(false), uc.getCurrentUserProfilePictureHandler)
group.PUT("/users/:id/profile-picture", jwtAuthMiddleware.Add(true), uc.updateUserProfilePictureHandler)
group.PUT("/users/me/profile-picture", jwtAuthMiddleware.Add(false), uc.updateUserProfilePictureHandler)
group.PUT("/users/me/profile-picture", jwtAuthMiddleware.Add(false), uc.updateCurrentUserProfilePictureHandler)
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createOneTimeAccessTokenHandler)
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
@@ -172,7 +172,7 @@ func (uc *UserController) getCurrentUserProfilePictureHandler(c *gin.Context) {
}
func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
userID := c.GetString("userID")
userID := c.Param("id")
fileHeader, err := c.FormFile("file")
if err != nil {
c.Error(err)

View File

@@ -37,8 +37,8 @@ func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
"end_session_endpoint": appUrl + "/api/oidc/end-session",
"jwks_uri": appUrl + "/.well-known/jwks.json",
"scopes_supported": []string{"openid", "profile", "email"},
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username", "picture"},
"scopes_supported": []string{"openid", "profile", "email", "groups"},
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username", "picture", "groups"},
"response_types_supported": []string{"code", "id_token"},
"subject_types_supported": []string{"public"},
"id_token_signing_alg_values_supported": []string{"RS256"},

View File

@@ -21,7 +21,7 @@ type AppConfigUpdateDto struct {
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
SmtpUser string `json:"smtpUser"`
SmtpPassword string `json:"smtpPassword"`
SmtpTls string `json:"smtpTls"`
SmtpTls string `json:"smtpTls" binding:"required,oneof=none starttls tls"`
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
LdapEnabled string `json:"ldapEnabled" binding:"required"`
LdapUrl string `json:"ldapUrl"`

View File

@@ -27,6 +27,7 @@ func NewAppConfigService(db *gorm.DB) *AppConfigService {
if err := service.InitDbConfig(); err != nil {
log.Fatalf("Failed to initialize app config service: %v", err)
}
return service
}
@@ -96,8 +97,8 @@ var defaultDbConfig = model.AppConfig{
},
SmtpTls: model.AppConfigVariable{
Key: "smtpTls",
Type: "bool",
DefaultValue: "true",
Type: "string",
DefaultValue: "none",
},
SmtpSkipCertVerify: model.AppConfigVariable{
Key: "smtpSkipCertVerify",

View File

@@ -115,18 +115,22 @@ func (srv *EmailService) getSmtpClient() (client *smtp.Client, err error) {
}
// Connect to the SMTP server
if srv.appConfigService.DbConfig.SmtpTls.Value == "false" {
// Connect to the SMTP server based on TLS setting
switch srv.appConfigService.DbConfig.SmtpTls.Value {
case "none":
client, err = srv.connectToSmtpServer(smtpAddress)
} else if port == "465" {
case "tls":
client, err = srv.connectToSmtpServerUsingImplicitTLS(
smtpAddress,
tlsConfig,
)
} else {
case "starttls":
client, err = srv.connectToSmtpServerUsingStartTLS(
smtpAddress,
tlsConfig,
)
default:
return nil, fmt.Errorf("invalid SMTP TLS setting: %s", srv.appConfigService.DbConfig.SmtpTls.Value)
}
if err != nil {
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
@@ -94,7 +95,6 @@ func (s *LdapService) SyncGroups() error {
ldapGroupIDs := make(map[string]bool)
for _, value := range result.Entries {
var usersToAddDto dto.UserGroupUpdateUsersDto
var membersUserId []string
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
@@ -112,7 +112,16 @@ func (s *LdapService) SyncGroups() error {
singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0]
var databaseUser model.User
s.db.Where("username = ?", singleMember).Where("ldap_id IS NOT NULL").First(&databaseUser)
err := s.db.Where("username = ? AND ldap_id IS NOT NULL", singleMember).First(&databaseUser).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// The user collides with a non-LDAP user, so we skip it
continue
} else {
return err
}
}
membersUserId = append(membersUserId, databaseUser.ID)
}
@@ -123,7 +132,7 @@ func (s *LdapService) SyncGroups() error {
LdapID: value.GetAttributeValue(uniqueIdentifierAttribute),
}
usersToAddDto = dto.UserGroupUpdateUsersDto{
usersToAddDto := dto.UserGroupUpdateUsersDto{
UserIDs: membersUserId,
}
@@ -263,7 +272,7 @@ func (s *LdapService) SyncUsers() error {
// Delete users that no longer exist in LDAP
for _, user := range ldapUsersInDb {
if _, exists := ldapUserIDs[*user.LdapID]; !exists {
if err := s.db.Delete(&model.User{}, "ldap_id = ?", user.LdapID).Error; err != nil {
if err := s.userService.DeleteUser(user.ID); err != nil {
log.Printf("Failed to delete user %s with: %v", user.Username, err)
} else {
log.Printf("Deleted user %s", user.Username)
@@ -292,7 +301,7 @@ func (s *LdapService) SaveProfilePicture(userId string, pictureString string) er
} else {
// If the photo is a string, we assume that it's a binary string
reader = bytes.NewReader([]byte("pictureString"))
reader = bytes.NewReader([]byte(pictureString))
}
// Update the profile picture

View File

@@ -128,6 +128,12 @@ func (s *UserService) DeleteUser(userID string) error {
return &common.LdapUserUpdateError{}
}
// Delete the profile picture
profilePicturePath := fmt.Sprintf("%s/profile-pictures/%s.png", common.EnvConfig.UploadPath, userID)
if err := os.Remove(profilePicturePath); err != nil && !os.IsNotExist(err) {
return err
}
return s.db.Delete(&user).Error
}

View File

@@ -0,0 +1 @@
UPDATE app_config_variables SET value = 'true' WHERE key = 'smtpTls';

View File

@@ -0,0 +1,7 @@
UPDATE app_config_variables AS target
SET value = CASE
WHEN target.value = 'true' AND (SELECT value FROM app_config_variables WHERE key = 'smtpPort' LIMIT 1) = '587' THEN 'starttls'
WHEN target.value = 'true' THEN 'tls'
ELSE 'none'
END
WHERE target.key = 'smtpTls';

View File

@@ -0,0 +1 @@
UPDATE app_config_variables SET value = 'true' WHERE key = 'smtpTls';

View File

@@ -0,0 +1,7 @@
UPDATE app_config_variables
SET value = CASE
WHEN value = 'true' AND (SELECT value FROM app_config_variables WHERE key = 'smtpPort' LIMIT 1) = '587' THEN 'starttls'
WHEN value = 'true' THEN 'tls'
ELSE 'none'
END
WHERE key = 'smtpTls';

View File

@@ -1,12 +1,12 @@
{
"name": "pocket-id-frontend",
"version": "0.30.0",
"version": "0.35.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pocket-id-frontend",
"version": "0.30.0",
"version": "0.35.2",
"dependencies": {
"@simplewebauthn/browser": "^13.1.0",
"@tailwindcss/vite": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "pocket-id-frontend",
"version": "0.35.0",
"version": "0.35.4",
"private": true,
"type": "module",
"scripts": {

View File

@@ -15,7 +15,7 @@ export type AllAppConfig = AppConfig & {
smtpFrom: string;
smtpUser: string;
smtpPassword: string;
smtpTls: boolean;
smtpTls: 'none' | 'starttls' | 'tls';
smtpSkipCertVerify: boolean;
emailLoginNotificationEnabled: boolean;
// LDAP

View File

@@ -1,9 +1,11 @@
<script lang="ts">
import { env } from '$env/dynamic/public';
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
import { openConfirmDialog } from '$lib/components/confirm-dialog';
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
import FormInput from '$lib/components/form/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import Label from '$lib/components/ui/label/label.svelte';
import * as Select from '$lib/components/ui/select';
import AppConfigService from '$lib/services/app-config-service';
import type { AllAppConfig } from '$lib/types/application-configuration';
import { createForm } from '$lib/utils/form-util';
@@ -20,6 +22,11 @@
const appConfigService = new AppConfigService();
const uiConfigDisabled = env.PUBLIC_UI_CONFIG_DISABLED === 'true';
const tlsOptions = {
none: 'None',
starttls: 'StartTLS',
tls: 'TLS'
};
let isSendingTestEmail = $state(false);
@@ -29,7 +36,7 @@
smtpUser: z.string(),
smtpPassword: z.string(),
smtpFrom: z.string().email(),
smtpTls: z.boolean(),
smtpTls: z.enum(['none', 'starttls', 'tls']),
smtpSkipCertVerify: z.boolean(),
emailOneTimeAccessEnabled: z.boolean(),
emailLoginNotificationEnabled: z.boolean()
@@ -96,12 +103,22 @@
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
<FormInput label="SMTP Password" type="password" bind:input={$inputs.smtpPassword} />
<FormInput label="SMTP From" bind:input={$inputs.smtpFrom} />
<CheckboxWithLabel
id="tls"
label="TLS"
description="Enable TLS for the SMTP connection."
bind:checked={$inputs.smtpTls.value}
/>
<div class="grid gap-2">
<Label class="mb-0" for="smtp-tls">SMTP TLS Option</Label>
<Select.Root
selected={{ value: $inputs.smtpTls.value, label: tlsOptions[$inputs.smtpTls.value] }}
onSelectedChange={(v) => ($inputs.smtpTls.value = v!.value)}
>
<Select.Trigger>
<Select.Value placeholder="Email TLS Option" />
</Select.Trigger>
<Select.Content>
<Select.Item value="none" label="None" />
<Select.Item value="starttls" label="StartTLS" />
<Select.Item value="tls" label="TLS" />
</Select.Content>
</Select.Root>
</div>
<CheckboxWithLabel
id="skip-cert-verify"
label="Skip Certificate Verification"