Compare commits

...

6 Commits

Author SHA1 Message Date
Elias Schneider
089005d1af release: 0.1.2 2024-08-13 21:06:40 +02:00
Elias Schneider
4a808c86ac fix: background image on mobile 2024-08-13 21:06:29 +02:00
Elias Schneider
83954926f5 fix: disable search engine indexing 2024-08-13 20:55:50 +02:00
Elias Schneider
475b932f9d feat: add option to change session duration 2024-08-13 20:51:10 +02:00
Elias Schneider
df0cd38dee fix: a non admin user was able to make himself an admin 2024-08-13 20:18:41 +02:00
Elias Schneider
7b4418958e fix: background image not loading 2024-08-13 18:32:21 +02:00
17 changed files with 113 additions and 54 deletions

View File

@@ -1 +1 @@
0.1.1
0.1.2

View File

@@ -1,3 +1,18 @@
## [](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)

View File

@@ -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",

View File

@@ -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)},
},

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -12,6 +12,7 @@ type ApplicationConfiguration struct {
AppName ApplicationConfigurationVariable
BackgroundImageType ApplicationConfigurationVariable
LogoImageType ApplicationConfigurationVariable
SessionDuration ApplicationConfigurationVariable
}
type ApplicationConfigurationUpdateDto struct {

View File

@@ -21,7 +21,7 @@ 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:"-"`

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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">

View File

@@ -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 }) => {

View File

@@ -1,6 +1,7 @@
export type AllApplicationConfiguration = {
appName: string;
sessionDuration: string;
};
export type ApplicationConfiguration = AllApplicationConfiguration;

View File

@@ -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 };
};

View File

@@ -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>

View File

@@ -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');