mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-08 17:23:23 +03:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bbb92ae19 | ||
|
|
cc407e17d4 | ||
|
|
5749d0532f | ||
|
|
089005d1af | ||
|
|
4a808c86ac | ||
|
|
83954926f5 | ||
|
|
475b932f9d | ||
|
|
df0cd38dee | ||
|
|
7b4418958e |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,3 +1,26 @@
|
||||
## [](https://github.com/stonith404/pocket-id/compare/v0.1.2...v) (2024-08-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing passkey flags to make icloud passkeys work ([cc407e1](https://github.com/stonith404/pocket-id/commit/cc407e17d409041ed88b959ce13bd581663d55c3))
|
||||
* logo not white in dark mode ([5749d05](https://github.com/stonith404/pocket-id/commit/5749d0532fc38bf2fc66571878b7c71643895c9e))
|
||||
|
||||
## [](https://github.com/stonith404/pocket-id/compare/v0.1.1...v) (2024-08-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add option to change session duration ([475b932](https://github.com/stonith404/pocket-id/commit/475b932f9d0ec029ada844072e9d89bebd4e902c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* a non admin user was able to make himself an admin ([df0cd38](https://github.com/stonith404/pocket-id/commit/df0cd38deeea516c47b26a080eed522f19f7290f))
|
||||
* background image not loading ([7b44189](https://github.com/stonith404/pocket-id/commit/7b4418958ebfffffd216ef5ba7313cfaad9bc9fa))
|
||||
* background image on mobile ([4a808c8](https://github.com/stonith404/pocket-id/commit/4a808c86ac204f9b58cfa02f5ceb064162a87076))
|
||||
* disable search engine indexing ([8395492](https://github.com/stonith404/pocket-id/commit/83954926f5ee328ebf75a75bb47b380ec0680378))
|
||||
|
||||
## [](https://github.com/stonith404/pocket-id/compare/v0.1.0...v) (2024-08-12)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg id="a"
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1015 1015">
|
||||
<path d="M838.28,380.27c-13.38-135.36-124.37-245.08-263.77-257.28h-299.08c-26.25,0-47.57,21.21-47.7,47.46-1.2,233.2-2.39,466.4-3.59,699.61-.14,26.44,21.26,47.95,47.7,47.95h102.86c24.66,0,45.25-18.79,47.5-43.35,11.45-124.75,22.89-249.51,34.34-374.26-43.84-29.69-66.46-88.34-40.37-148.47,10.44-24.06,29.57-43.41,53.58-53.98,86.02-37.84,169.22,24.14,169.22,105.56,0,40.38-20.47,75.98-51.6,96.99,6.71,56.71,13.42,113.43,20.14,170.14,1.2,10.14,11.21,16.74,21.03,13.93,134.16-38.42,223.25-167.79,209.75-304.31Z"/>
|
||||
<path id="a" d="M838.28,380.27c-13.38-135.36-124.37-245.08-263.77-257.28h-299.08c-26.25,0-47.57,21.21-47.7,47.46-1.2,233.2-2.39,466.4-3.59,699.61-.14,26.44,21.26,47.95,47.7,47.95h102.86c24.66,0,45.25-18.79,47.5-43.35,11.45-124.75,22.89-249.51,34.34-374.26-43.84-29.69-66.46-88.34-40.37-148.47,10.44-24.06,29.57-43.41,53.58-53.98,86.02-37.84,169.22,24.14,169.22,105.56,0,40.38-20.47,75.98-51.6,96.99,6.71,56.71,13.42,113.43,20.14,170.14,1.2,10.14,11.21,16.74,21.03,13.93,134.16-38.42,223.25-167.79,209.75-304.31Z"/>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#a path {
|
||||
|
||||
|
Before Width: | Height: | Size: 864 B After Width: | Height: | Size: 871 B |
@@ -36,6 +36,11 @@ func NewDefaultDbConfig() model.ApplicationConfiguration {
|
||||
IsPublic: true,
|
||||
Value: "Pocket ID",
|
||||
},
|
||||
SessionDuration: model.ApplicationConfigurationVariable{
|
||||
Key: "sessionDuration",
|
||||
Type: "number",
|
||||
Value: "60",
|
||||
},
|
||||
BackgroundImageType: model.ApplicationConfigurationVariable{
|
||||
Key: "backgroundImageType",
|
||||
Type: "string",
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -73,10 +74,11 @@ func GenerateIDToken(user model.User, clientID string, scope string, nonce strin
|
||||
|
||||
// GenerateAccessToken generates an access token for the given user.
|
||||
func GenerateAccessToken(user model.User) (tokenString string, err error) {
|
||||
sessionDurationInMinutes, _ := strconv.Atoi(DbConfig.SessionDuration.Value)
|
||||
claim := accessTokenJWTClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: user.ID,
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(sessionDurationInMinutes) * time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Audience: jwt.ClaimStrings{utils.GetHostFromURL(EnvConfig.AppURL)},
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
func RegisterConfigurationRoutes(group *gin.RouterGroup) {
|
||||
group.GET("/application-configuration", listApplicationConfigurationHandler)
|
||||
group.GET("/application-configuration/all", middleware.JWTAuth(true), listAllApplicationConfigurationHandler)
|
||||
group.PUT("/application-configuration", updateApplicationConfigurationHandler)
|
||||
|
||||
group.GET("/application-configuration/logo", getLogoHandler)
|
||||
@@ -27,24 +28,11 @@ func RegisterConfigurationRoutes(group *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func listApplicationConfigurationHandler(c *gin.Context) {
|
||||
// Return also the private configuration variables if the user is admin and showAll is true
|
||||
showAll := c.GetBool("userIsAdmin") && c.DefaultQuery("showAll", "false") == "true"
|
||||
listApplicationConfiguration(c, false)
|
||||
}
|
||||
|
||||
var configuration []model.ApplicationConfigurationVariable
|
||||
var err error
|
||||
|
||||
if showAll {
|
||||
err = common.DB.Find(&configuration).Error
|
||||
} else {
|
||||
err = common.DB.Find(&configuration, "is_public = true").Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.UnknownHandlerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, configuration)
|
||||
func listAllApplicationConfigurationHandler(c *gin.Context) {
|
||||
listApplicationConfiguration(c, true)
|
||||
}
|
||||
|
||||
func updateApplicationConfigurationHandler(c *gin.Context) {
|
||||
@@ -188,3 +176,21 @@ func updateImage(c *gin.Context, imageName string, oldImageType string) {
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func listApplicationConfiguration(c *gin.Context, showAll bool) {
|
||||
var configuration []model.ApplicationConfigurationVariable
|
||||
var err error
|
||||
|
||||
if showAll {
|
||||
err = common.DB.Find(&configuration).Error
|
||||
} else {
|
||||
err = common.DB.Find(&configuration, "is_public = true").Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.UnknownHandlerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, configuration)
|
||||
}
|
||||
|
||||
@@ -117,11 +117,11 @@ func createUserHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func updateUserHandler(c *gin.Context) {
|
||||
updateUser(c, c.Param("id"))
|
||||
updateUser(c, c.Param("id"), false)
|
||||
}
|
||||
|
||||
func updateCurrentUserHandler(c *gin.Context) {
|
||||
updateUser(c, c.GetString("userID"))
|
||||
updateUser(c, c.GetString("userID"), true)
|
||||
}
|
||||
|
||||
func createOneTimeAccessTokenHandler(c *gin.Context) {
|
||||
@@ -222,7 +222,7 @@ func getSetupAccessTokenHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
func updateUser(c *gin.Context, userID string) {
|
||||
func updateUser(c *gin.Context, userID string, updateOwnUser bool) {
|
||||
var user model.User
|
||||
if err := common.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -232,14 +232,22 @@ func updateUser(c *gin.Context, userID string) {
|
||||
utils.UnknownHandlerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
var updatedUser model.User
|
||||
if err := c.ShouldBindJSON(&updatedUser); err != nil {
|
||||
utils.HandlerError(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.DB.Model(&user).Updates(&updatedUser).Error; err != nil {
|
||||
user.FirstName = updatedUser.FirstName
|
||||
user.LastName = updatedUser.LastName
|
||||
user.Email = updatedUser.Email
|
||||
user.Username = updatedUser.Username
|
||||
user.Username = updatedUser.Username
|
||||
if !updateOwnUser {
|
||||
user.IsAdmin = updatedUser.IsAdmin
|
||||
}
|
||||
|
||||
if err := common.DB.Save(user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
if err := checkDuplicatedFields(user); err != nil {
|
||||
utils.HandlerError(c, http.StatusBadRequest, err.Error())
|
||||
@@ -250,8 +258,7 @@ func updateUser(c *gin.Context, userID string) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, updatedUser)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
func checkDuplicatedFields(user model.User) error {
|
||||
|
||||
@@ -98,6 +98,8 @@ func verifyRegistrationHandler(c *gin.Context) {
|
||||
PublicKey: credential.PublicKey,
|
||||
Transport: credential.Transport,
|
||||
UserID: user.ID,
|
||||
BackupEligible: credential.Flags.BackupEligible,
|
||||
BackupState: credential.Flags.BackupState,
|
||||
}
|
||||
if err := common.DB.Create(&credentialToStore).Error; err != nil {
|
||||
utils.UnknownHandlerError(c, err)
|
||||
|
||||
@@ -12,6 +12,7 @@ type ApplicationConfiguration struct {
|
||||
AppName ApplicationConfigurationVariable
|
||||
BackgroundImageType ApplicationConfigurationVariable
|
||||
LogoImageType ApplicationConfigurationVariable
|
||||
SessionDuration ApplicationConfigurationVariable
|
||||
}
|
||||
|
||||
type ApplicationConfigurationUpdateDto struct {
|
||||
|
||||
@@ -35,6 +35,10 @@ func (u User) WebAuthnCredentials() []webauthn.Credential {
|
||||
AttestationType: credential.AttestationType,
|
||||
PublicKey: credential.PublicKey,
|
||||
Transport: credential.Transport,
|
||||
Flags: webauthn.CredentialFlags{
|
||||
BackupState: credential.BackupState,
|
||||
BackupEligible: credential.BackupEligible,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ type WebauthnCredential struct {
|
||||
|
||||
Name string `json:"name"`
|
||||
CredentialID string `json:"credentialID"`
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
PublicKey []byte `json:"-"`
|
||||
AttestationType string `json:"attestationType"`
|
||||
Transport AuthenticatorTransportList `json:"-"`
|
||||
|
||||
BackupEligible bool `json:"backupEligible"`
|
||||
BackupState bool `json:"backupState"`
|
||||
|
||||
UserID string
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE webauthn_credentials ADD COLUMN backup_eligible BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE webauthn_credentials ADD COLUMN backup_state BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE webauthn_credentials DROP COLUMN backup_eligible;
|
||||
ALTER TABLE webauthn_credentials DROP COLUMN backup_state;
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/api/application-configuration/favicon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
let {
|
||||
input = $bindable(),
|
||||
label,
|
||||
description,
|
||||
children
|
||||
}: {
|
||||
input: FormInput<string | boolean | number>;
|
||||
label: string;
|
||||
description?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
@@ -18,13 +20,18 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Label for={id}>{label}</Label>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<Input {id} bind:value={input.value} />
|
||||
{/if}
|
||||
{#if input.error}
|
||||
<p class="text-sm text-red-500">{input.error}</p>
|
||||
<Label class="mb-0" for={id}>{label}</Label>
|
||||
{#if description}
|
||||
<p class="text-muted-foreground text-xs mt-1">{description}</p>
|
||||
{/if}
|
||||
<div class="mt-2">
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<Input {id} bind:value={input.value} />
|
||||
{/if}
|
||||
{#if input.error}
|
||||
<p class="text-sm text-red-500">{input.error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
@@ -12,8 +11,8 @@
|
||||
($userStore!.firstName.charAt(0) + $userStore!.lastName?.charAt(0)).toUpperCase()
|
||||
);
|
||||
|
||||
function logout() {
|
||||
webauthnService.logout();
|
||||
async function logout() {
|
||||
await webauthnService.logout();
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
@@ -31,7 +30,7 @@
|
||||
{$userStore?.firstName}
|
||||
{$userStore?.lastName}
|
||||
</p>
|
||||
<p class="text-xs leading-none text-muted-foreground">{$userStore?.email}</p>
|
||||
<p class="text-muted-foreground text-xs leading-none">{$userStore?.email}</p>
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
{/if}
|
||||
</div>
|
||||
<img
|
||||
src="/images/sign-in.jpg"
|
||||
src="/api/application-configuration/background-image"
|
||||
class="h-screen w-[calc(100vw-650px)] rounded-l-[60px] object-cover"
|
||||
alt="Login background"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-screen items-center justify-center bg-[url('/images/sign-in.jpg')] bg-cover bg-center text-center lg:hidden"
|
||||
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center lg:hidden"
|
||||
>
|
||||
<Card.Root class="mx-3">
|
||||
<Card.CardContent class="px-4 py-10 sm:p-10">
|
||||
|
||||
@@ -6,12 +6,12 @@ import APIService from './api-service';
|
||||
|
||||
export default class ApplicationConfigurationService extends APIService {
|
||||
async list(showAll = false) {
|
||||
const { data } = await this.api.get<ApplicationConfigurationRawResponse>(
|
||||
'/application-configuration',
|
||||
{
|
||||
params: { showAll }
|
||||
}
|
||||
);
|
||||
let url = '/application-configuration';
|
||||
if (showAll) {
|
||||
url += '/all';
|
||||
}
|
||||
|
||||
const { data } = await this.api.get<ApplicationConfigurationRawResponse>(url);
|
||||
|
||||
const applicationConfiguration: Partial<AllApplicationConfiguration> = {};
|
||||
data.forEach(({ key, value }) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
export type AllApplicationConfiguration = {
|
||||
appName: string;
|
||||
sessionDuration: string;
|
||||
};
|
||||
|
||||
export type ApplicationConfiguration = AllApplicationConfiguration;
|
||||
|
||||
@@ -5,6 +5,6 @@ export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const applicationConfigurationService = new ApplicationConfigurationService(
|
||||
cookies.get('access_token')
|
||||
);
|
||||
const applicationConfiguration = await applicationConfigurationService.list();
|
||||
const applicationConfiguration = await applicationConfigurationService.list(true);
|
||||
return { applicationConfiguration };
|
||||
};
|
||||
|
||||
@@ -16,11 +16,21 @@
|
||||
let isLoading = $state(false);
|
||||
|
||||
const updatedApplicationConfiguration: AllApplicationConfiguration = {
|
||||
appName: applicationConfiguration.appName
|
||||
appName: applicationConfiguration.appName,
|
||||
sessionDuration: applicationConfiguration.sessionDuration
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
appName: z.string().min(2).max(30)
|
||||
appName: z.string().min(2).max(30),
|
||||
sessionDuration: z.string().refine(
|
||||
(val) => {
|
||||
const num = Number(val);
|
||||
return Number.isInteger(num) && num >= 1 && num <= 43200;
|
||||
},
|
||||
{
|
||||
message: 'Session duration must be between 1 and 43200 minutes'
|
||||
}
|
||||
)
|
||||
});
|
||||
type FormSchema = typeof formSchema;
|
||||
|
||||
@@ -35,10 +45,14 @@
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="flex gap-3">
|
||||
<div class="w-full">
|
||||
<FormInput label="Application Name" bind:input={$inputs.appName} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-5">
|
||||
<FormInput label="Application Name" bind:input={$inputs.appName} />
|
||||
|
||||
<FormInput
|
||||
label="Session Duration"
|
||||
description="The duration of a session in minutes before the user has to sign in again."
|
||||
bind:input={$inputs.sessionDuration}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
|
||||
@@ -7,6 +7,7 @@ test('Update general configuration', async ({ page }) => {
|
||||
await page.goto('/settings/admin/application-configuration');
|
||||
|
||||
await page.getByLabel('Name').fill('Updated Name');
|
||||
await page.getByLabel('Session Duration').fill('30');
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
|
||||
await expect(page.getByTestId('application-name')).toHaveText('Updated Name');
|
||||
|
||||
Reference in New Issue
Block a user