Compare commits

...

12 Commits

Author SHA1 Message Date
Elias Schneider
bd4f87b2d2 release: 0.10.0 2024-10-23 11:54:47 +02:00
Elias Schneider
6560fd9279 chore: fix wrong file name of package.json in release script 2024-10-23 11:54:35 +02:00
Elias Schneider
29d632c151 fix: cache version information for 3 hours 2024-10-23 11:48:46 +02:00
Elias Schneider
2092007752 chore: dump frontend dependencies 2024-10-23 11:37:22 +02:00
Elias Schneider
0aff6181c9 chore: improve check of required tools in one time access token script 2024-10-23 10:50:49 +02:00
Elias Schneider
824c5cb4f3 fix: no DTO was returned from exchange one time access token endpoint 2024-10-23 10:30:25 +02:00
Elias Schneider
3a300a2b51 refactor: move development scripts into seperate folder 2024-10-23 10:26:18 +02:00
Elias Schneider
a1985ce1b2 feat: add script for creating one time access token 2024-10-23 10:03:17 +02:00
Elias Schneider
b39bc4f79a refactor: save dates as unix timestamps in database 2024-10-23 10:02:11 +02:00
Elias Schneider
0a07344139 fix: improve text for initial admin account setup 2024-10-22 20:41:35 +02:00
Elias Schneider
f3f0e1d56d fix: increase callback url count 2024-10-18 20:52:56 +02:00
Elias Schneider
70ad0b4f39 feat: add version information to footer and update link if new update is available 2024-10-18 20:48:59 +02:00
30 changed files with 785 additions and 418 deletions

18
.dockerignore Normal file
View File

@@ -0,0 +1,18 @@
node_modules
# Output
.output
.vercel
/frontend/.svelte-kit
/frontend/build
/backend/bin
# Env
.env
.env.*
# Application specific
data
/scripts/development

View File

@@ -1 +1 @@
0.9.0 0.10.0

View File

@@ -1,3 +1,19 @@
## [](https://github.com/stonith404/pocket-id/compare/v0.9.0...v) (2024-10-23)
### Features
* add script for creating one time access token ([a1985ce](https://github.com/stonith404/pocket-id/commit/a1985ce1b200550e91c5cb42a8d19899dcec831e))
* add version information to footer and update link if new update is available ([70ad0b4](https://github.com/stonith404/pocket-id/commit/70ad0b4f39699fd81ffdfd5c8d6839f49348be78))
### Bug Fixes
* cache version information for 3 hours ([29d632c](https://github.com/stonith404/pocket-id/commit/29d632c1514d6edacdfebe6deae4c95fc5a0f621))
* improve text for initial admin account setup ([0a07344](https://github.com/stonith404/pocket-id/commit/0a0734413943b1fff27d8f4ccf07587e207e2189))
* increase callback url count ([f3f0e1d](https://github.com/stonith404/pocket-id/commit/f3f0e1d56d7656bdabbd745a4eaf967f63193b6c))
* no DTO was returned from exchange one time access token endpoint ([824c5cb](https://github.com/stonith404/pocket-id/commit/824c5cb4f3d6be7f940c1758112fbe9322df5768))
## [](https://github.com/stonith404/pocket-id/compare/v0.8.1...v) (2024-10-18) ## [](https://github.com/stonith404/pocket-id/compare/v0.8.1...v) (2024-10-18)

View File

@@ -36,6 +36,7 @@ COPY --from=backend-builder /app/backend/email-templates ./backend/email-templat
COPY --from=backend-builder /app/backend/images ./backend/images COPY --from=backend-builder /app/backend/images ./backend/images
COPY ./scripts ./scripts COPY ./scripts ./scripts
RUN chmod +x ./scripts/*.sh
EXPOSE 3000 EXPOSE 3000
ENV APP_ENV=production ENV APP_ENV=production

View File

@@ -161,8 +161,14 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
return return
} }
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
utils.ControllerError(c, err)
return
}
c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true) c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true)
c.JSON(http.StatusOK, user) c.JSON(http.StatusOK, userDto)
} }
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) { func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {

View File

@@ -2,7 +2,9 @@ package dto
import ( import (
"errors" "errors"
"github.com/stonith404/pocket-id/backend/internal/model/types"
"reflect" "reflect"
"time"
) )
// MapStructList maps a list of source structs to a list of destination structs // MapStructList maps a list of source structs to a list of destination structs
@@ -95,7 +97,18 @@ func mapStructInternal(sourceVal reflect.Value, destVal reflect.Value) error {
if err := mapStructInternal(sourceField, destField); err != nil { if err := mapStructInternal(sourceField, destField); err != nil {
return err return err
} }
} else {
// Type switch for specific type conversions
switch sourceField.Interface().(type) {
case datatype.DateTime:
// Convert datatype.DateTime to time.Time
if sourceField.Type() == reflect.TypeOf(datatype.DateTime{}) && destField.Type() == reflect.TypeOf(time.Time{}) {
dateValue := sourceField.Interface().(datatype.DateTime)
destField.Set(reflect.ValueOf(dateValue.ToTime()))
}
}
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import (
"github.com/go-co-op/gocron/v2" "github.com/go-co-op/gocron/v2"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stonith404/pocket-id/backend/internal/model" "github.com/stonith404/pocket-id/backend/internal/model"
"github.com/stonith404/pocket-id/backend/internal/utils"
"gorm.io/gorm" "gorm.io/gorm"
"log" "log"
"time" "time"
@@ -30,22 +29,22 @@ type Jobs struct {
// ClearWebauthnSessions deletes WebAuthn sessions that have expired // ClearWebauthnSessions deletes WebAuthn sessions that have expired
func (j *Jobs) clearWebauthnSessions() error { func (j *Jobs) clearWebauthnSessions() error {
return j.db.Delete(&model.WebauthnSession{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error return j.db.Delete(&model.WebauthnSession{}, "expires_at < ?", time.Now().Unix()).Error
} }
// ClearOneTimeAccessTokens deletes one-time access tokens that have expired // ClearOneTimeAccessTokens deletes one-time access tokens that have expired
func (j *Jobs) clearOneTimeAccessTokens() error { func (j *Jobs) clearOneTimeAccessTokens() error {
return j.db.Debug().Delete(&model.OneTimeAccessToken{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error return j.db.Debug().Delete(&model.OneTimeAccessToken{}, "expires_at < ?", time.Now().Unix()).Error
} }
// ClearOidcAuthorizationCodes deletes OIDC authorization codes that have expired // ClearOidcAuthorizationCodes deletes OIDC authorization codes that have expired
func (j *Jobs) clearOidcAuthorizationCodes() error { func (j *Jobs) clearOidcAuthorizationCodes() error {
return j.db.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error return j.db.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", time.Now().Unix()).Error
} }
// ClearAuditLogs deletes audit logs older than 90 days // ClearAuditLogs deletes audit logs older than 90 days
func (j *Jobs) clearAuditLogs() error { func (j *Jobs) clearAuditLogs() error {
return j.db.Delete(&model.AuditLog{}, "created_at < ?", utils.FormatDateForDb(time.Now().AddDate(0, 0, -90))).Error return j.db.Delete(&model.AuditLog{}, "created_at < ?", time.Now().AddDate(0, 0, -90).Unix()).Error
} }
func registerJob(scheduler gocron.Scheduler, name string, interval string, job func() error) { func registerJob(scheduler gocron.Scheduler, name string, interval string, job func() error) {

View File

@@ -2,6 +2,7 @@ package model
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
model "github.com/stonith404/pocket-id/backend/internal/model/types"
"gorm.io/gorm" "gorm.io/gorm"
"time" "time"
) )
@@ -9,12 +10,13 @@ import (
// Base contains common columns for all tables. // Base contains common columns for all tables.
type Base struct { type Base struct {
ID string `gorm:"primaryKey;not null"` ID string `gorm:"primaryKey;not null"`
CreatedAt time.Time CreatedAt model.DateTime
} }
func (b *Base) BeforeCreate(_ *gorm.DB) (err error) { func (b *Base) BeforeCreate(_ *gorm.DB) (err error) {
if b.ID == "" { if b.ID == "" {
b.ID = uuid.New().String() b.ID = uuid.New().String()
} }
b.CreatedAt = model.DateTime(time.Now())
return return
} }

View File

@@ -4,8 +4,8 @@ import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"errors" "errors"
datatype "github.com/stonith404/pocket-id/backend/internal/model/types"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type UserAuthorizedOidcClient struct { type UserAuthorizedOidcClient struct {
@@ -23,7 +23,7 @@ type OidcAuthorizationCode struct {
Code string Code string
Scope string Scope string
Nonce string Nonce string
ExpiresAt time.Time ExpiresAt datatype.DateTime
UserID string UserID string
User User User User

View File

@@ -0,0 +1,47 @@
package datatype
import (
"database/sql/driver"
"time"
)
// DateTime custom type for time.Time to store date as unix timestamp in the database
type DateTime time.Time
func (date *DateTime) Scan(value interface{}) (err error) {
*date = DateTime(value.(time.Time))
return
}
func (date DateTime) Value() (driver.Value, error) {
return time.Time(date).Unix(), nil
}
func (date DateTime) UTC() time.Time {
return time.Time(date).UTC()
}
func (date DateTime) ToTime() time.Time {
return time.Time(date)
}
// GormDataType gorm common data type
func (date DateTime) GormDataType() string {
return "date"
}
func (date DateTime) GobEncode() ([]byte, error) {
return time.Time(date).GobEncode()
}
func (date *DateTime) GobDecode(b []byte) error {
return (*time.Time)(date).GobDecode(b)
}
func (date DateTime) MarshalJSON() ([]byte, error) {
return time.Time(date).MarshalJSON()
}
func (date *DateTime) UnmarshalJSON(b []byte) error {
return (*time.Time)(date).UnmarshalJSON(b)
}

View File

@@ -3,7 +3,7 @@ package model
import ( import (
"github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
"time" "github.com/stonith404/pocket-id/backend/internal/model/types"
) )
type User struct { type User struct {
@@ -61,7 +61,7 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential
type OneTimeAccessToken struct { type OneTimeAccessToken struct {
Base Base
Token string Token string
ExpiresAt time.Time ExpiresAt datatype.DateTime
UserID string UserID string
User User User User

View File

@@ -6,6 +6,7 @@ import (
"github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/common"
"github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/dto"
"github.com/stonith404/pocket-id/backend/internal/model" "github.com/stonith404/pocket-id/backend/internal/model"
datatype "github.com/stonith404/pocket-id/backend/internal/model/types"
"github.com/stonith404/pocket-id/backend/internal/utils" "github.com/stonith404/pocket-id/backend/internal/utils"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
@@ -115,7 +116,7 @@ func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret strin
return "", "", common.ErrOidcInvalidAuthorizationCode return "", "", common.ErrOidcInvalidAuthorizationCode
} }
if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.Before(time.Now()) { if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.ToTime().Before(time.Now()) {
return "", "", common.ErrOidcInvalidAuthorizationCode return "", "", common.ErrOidcInvalidAuthorizationCode
} }
@@ -350,7 +351,7 @@ func (s *OidcService) createAuthorizationCode(clientID string, userID string, sc
} }
oidcAuthorizationCode := model.OidcAuthorizationCode{ oidcAuthorizationCode := model.OidcAuthorizationCode{
ExpiresAt: time.Now().Add(15 * time.Minute), ExpiresAt: datatype.DateTime(time.Now().Add(15 * time.Minute)),
Code: randomString, Code: randomString,
ClientID: clientID, ClientID: clientID,
UserID: userID, UserID: userID,

View File

@@ -6,6 +6,7 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/fxamacker/cbor/v2" "github.com/fxamacker/cbor/v2"
"github.com/stonith404/pocket-id/backend/internal/model/types"
"log" "log"
"os" "os"
"time" "time"
@@ -111,7 +112,7 @@ func (s *TestService) SeedDatabase() error {
Code: "auth-code", Code: "auth-code",
Scope: "openid profile", Scope: "openid profile",
Nonce: "nonce", Nonce: "nonce",
ExpiresAt: time.Now().Add(1 * time.Hour), ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
UserID: users[0].ID, UserID: users[0].ID,
ClientID: oidcClients[0].ID, ClientID: oidcClients[0].ID,
} }
@@ -121,7 +122,7 @@ func (s *TestService) SeedDatabase() error {
accessToken := model.OneTimeAccessToken{ accessToken := model.OneTimeAccessToken{
Token: "one-time-token", Token: "one-time-token",
ExpiresAt: time.Now().Add(1 * time.Hour), ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
UserID: users[0].ID, UserID: users[0].ID,
} }
if err := tx.Create(&accessToken).Error; err != nil { if err := tx.Create(&accessToken).Error; err != nil {

View File

@@ -5,6 +5,7 @@ import (
"github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/common"
"github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/dto"
"github.com/stonith404/pocket-id/backend/internal/model" "github.com/stonith404/pocket-id/backend/internal/model"
"github.com/stonith404/pocket-id/backend/internal/model/types"
"github.com/stonith404/pocket-id/backend/internal/utils" "github.com/stonith404/pocket-id/backend/internal/utils"
"gorm.io/gorm" "gorm.io/gorm"
"time" "time"
@@ -95,7 +96,7 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim
oneTimeAccessToken := model.OneTimeAccessToken{ oneTimeAccessToken := model.OneTimeAccessToken{
UserID: userID, UserID: userID,
ExpiresAt: expiresAt, ExpiresAt: datatype.DateTime(expiresAt),
Token: randomString, Token: randomString,
} }
@@ -108,7 +109,7 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim
func (s *UserService) ExchangeOneTimeAccessToken(token string) (model.User, string, error) { func (s *UserService) ExchangeOneTimeAccessToken(token string) (model.User, string, error) {
var oneTimeAccessToken model.OneTimeAccessToken var oneTimeAccessToken model.OneTimeAccessToken
if err := s.db.Where("token = ? AND expires_at > ?", token, utils.FormatDateForDb(time.Now())).Preload("User").First(&oneTimeAccessToken).Error; err != nil { if err := s.db.Where("token = ? AND expires_at > ?", token, time.Now().Unix()).Preload("User").First(&oneTimeAccessToken).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return model.User{}, "", common.ErrTokenInvalidOrExpired return model.User{}, "", common.ErrTokenInvalidOrExpired
} }

View File

@@ -1,8 +0,0 @@
package utils
import "time"
func FormatDateForDb(time time.Time) string {
const layout = "2006-01-02 15:04:05.000-07:00"
return time.Format(layout)
}

View File

@@ -0,0 +1,28 @@
-- Convert the Unix timestamps back to DATETIME format
UPDATE user_groups
SET created_at = datetime(created_at, 'unixepoch');
UPDATE users
SET created_at = datetime(created_at, 'unixepoch');
UPDATE audit_logs
SET created_at = datetime(created_at, 'unixepoch');
UPDATE oidc_authorization_codes
SET created_at = datetime(created_at, 'unixepoch'),
expires_at = datetime(expires_at, 'unixepoch');
UPDATE oidc_clients
SET created_at = datetime(created_at, 'unixepoch');
UPDATE one_time_access_tokens
SET created_at = datetime(created_at, 'unixepoch'),
expires_at = datetime(expires_at, 'unixepoch');
UPDATE webauthn_credentials
SET created_at = datetime(created_at, 'unixepoch');
UPDATE webauthn_sessions
SET created_at = datetime(created_at, 'unixepoch'),
expires_at = datetime(expires_at, 'unixepoch');

View File

@@ -0,0 +1,27 @@
-- Convert the DATETIME fields to Unix timestamps (in seconds)
UPDATE user_groups
SET created_at = strftime('%s', created_at);
UPDATE users
SET created_at = strftime('%s', created_at);
UPDATE audit_logs
SET created_at = strftime('%s', created_at);
UPDATE oidc_authorization_codes
SET created_at = strftime('%s', created_at),
expires_at = strftime('%s', expires_at);
UPDATE oidc_clients
SET created_at = strftime('%s', created_at);
UPDATE one_time_access_tokens
SET created_at = strftime('%s', created_at),
expires_at = strftime('%s', expires_at);
UPDATE webauthn_credentials
SET created_at = strftime('%s', created_at);
UPDATE webauthn_sessions
SET created_at = strftime('%s', created_at),
expires_at = strftime('%s', expires_at);

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "0.0.1", "version": "0.10.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev --port 3000", "dev": "vite dev --port 3000",
@@ -12,31 +12,31 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.47.2", "@playwright/test": "^1.48.1",
"@sveltejs/adapter-auto": "^3.2.5", "@sveltejs/adapter-auto": "^3.3.0",
"@sveltejs/adapter-node": "^5.2.5", "@sveltejs/adapter-node": "^5.2.8",
"@sveltejs/kit": "^2.6.1", "@sveltejs/kit": "^2.7.2",
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/jsonwebtoken": "^9.0.7", "@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.7.4", "@types/node": "^22.7.9",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"cbor-js": "^0.1.0", "cbor-js": "^0.1.0",
"eslint": "^9.11.1", "eslint": "^9.13.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.1", "eslint-plugin-svelte": "^2.46.0",
"globals": "^15.10.0", "globals": "^15.11.0",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7", "prettier-plugin-svelte": "^3.2.7",
"prettier-plugin-tailwindcss": "^0.6.8", "prettier-plugin-tailwindcss": "^0.6.8",
"svelte": "^5.0.0-next.262", "svelte": "^5.0.5",
"svelte-check": "^4.0.4", "svelte-check": "^4.0.5",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.14",
"tslib": "^2.7.0", "tslib": "^2.8.0",
"typescript": "^5.6.2", "typescript": "^5.6.3",
"typescript-eslint": "^8.8.0", "typescript-eslint": "^8.11.0",
"vite": "^5.4.8" "vite": "^5.4.10"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -47,11 +47,11 @@
"crypto": "^1.0.1", "crypto": "^1.0.1",
"formsnap": "^1.0.1", "formsnap": "^1.0.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-svelte": "^0.447.0", "lucide-svelte": "^0.453.0",
"mode-watcher": "^0.4.1", "mode-watcher": "^0.4.1",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.19.0", "sveltekit-superforms": "^2.20.0",
"tailwind-merge": "^2.5.3", "tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"zod": "^3.23.8" "zod": "^3.23.8"
} }

View File

@@ -1,4 +1,6 @@
import { version as currentVersion } from '$app/environment';
import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration'; import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration';
import axios from 'axios';
import APIService from './api-service'; import APIService from './api-service';
export default class AppConfigService extends APIService { export default class AppConfigService extends APIService {
@@ -45,4 +47,19 @@ export default class AppConfigService extends APIService {
await this.api.put(`/application-configuration/background-image`, formData); await this.api.put(`/application-configuration/background-image`, formData);
} }
async getVersionInformation() {
const response = (
await axios.get('https://api.github.com/repos/stonith404/pocket-id/releases/latest')
).data;
const newestVersion = response.tag_name.replace('v', '');
const isUpToDate = newestVersion === currentVersion;
return {
isUpToDate,
newestVersion,
currentVersion
};
}
} }

View File

@@ -16,3 +16,9 @@ export type AppConfigRawResponse = {
type: string; type: string;
value: string; value: string;
}[]; }[];
export type AppVersionInformation = {
isUpToDate: boolean;
newestVersion: string;
currentVersion: string;
};

View File

@@ -33,11 +33,19 @@
<Logo class="h-10 w-10" /> <Logo class="h-10 w-10" />
</div> </div>
</div> </div>
<h1 class="font-playfair mt-5 text-4xl font-bold">One Time Access</h1> <h1 class="font-playfair mt-5 text-4xl font-bold">
{data.token === 'setup' ? `${$appConfigStore.appName} Setup` : 'One Time Access'}
</h1>
<p class="text-muted-foreground mt-2"> <p class="text-muted-foreground mt-2">
You've been granted one-time access to your {$appConfigStore.appName} account. Please note that if {#if data.token === 'setup'}
you continue, this link will become invalid. To avoid this, make sure to add a passkey. Otherwise, You're about to sign in to the initial admin account. Anyone with this link can access the
you'll need to request a new link. account until a passkey is added. Please set up a passkey as soon as possible to prevent
unauthorized access.
{:else}
You've been granted one-time access to your {$appConfigStore.appName} account. Please note that
if you continue, this link will become invalid. To avoid this, make sure to add a passkey. Otherwise,
you'll need to request a new link.
{/if}
</p> </p>
<Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button> <Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button>
</SignInWrapper> </SignInWrapper>

View File

@@ -0,0 +1,24 @@
import AppConfigService from '$lib/services/app-config-service';
import type { AppVersionInformation } from '$lib/types/application-configuration';
import type { LayoutServerLoad } from './$types';
let versionInformation: AppVersionInformation;
let versionInformationLastUpdated: number;
export const load: LayoutServerLoad = async () => {
const appConfigService = new AppConfigService();
// Cache the version information for 3 hours
const cacheExpired =
versionInformationLastUpdated &&
Date.now() - versionInformationLastUpdated > 1000 * 60 * 60 * 3;
if (!versionInformation || cacheExpired) {
versionInformation = await appConfigService.getVersionInformation();
versionInformationLastUpdated = Date.now();
}
return {
versionInformation
};
};

View File

@@ -1,14 +1,20 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import { LucideExternalLink } from 'lucide-svelte';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { LayoutData } from './$types';
let { let {
children children,
data
}: { }: {
children: Snippet; children: Snippet;
data: LayoutData;
} = $props(); } = $props();
const { versionInformation } = data;
let links = $state([ let links = $state([
{ href: '/settings/account', label: 'My Account' }, { href: '/settings/account', label: 'My Account' },
{ href: '/settings/audit-log', label: 'Audit Log' } { href: '/settings/audit-log', label: 'Audit Log' }
@@ -26,8 +32,10 @@
</script> </script>
<section> <section>
<div class="bg-muted/40 min-h-screen w-full"> <div class="bg-muted/40 flex min-h-[calc(100vh-64px)] w-full flex-col justify-between">
<main class="mx-auto flex max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row"> <main
class="mx-auto flex w-full max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row"
>
<div> <div>
<div class="mx-auto grid w-full gap-2"> <div class="mx-auto grid w-full gap-2">
<h1 class="mb-5 text-3xl font-semibold">Settings</h1> <h1 class="mb-5 text-3xl font-semibold">Settings</h1>
@@ -41,6 +49,15 @@
{label} {label}
</a> </a>
{/each} {/each}
{#if $userStore?.isAdmin && !versionInformation.isUpToDate}
<a
href="https://github.com/stonith404/pocket-id/releases/latest"
target="_blank"
class="flex items-center gap-2"
>
Update Pocket ID <LucideExternalLink class="my-auto inline-block h-3 w-3" />
</a>
{/if}
</nav> </nav>
</div> </div>
</div> </div>
@@ -48,5 +65,15 @@
{@render children()} {@render children()}
</div> </div>
</main> </main>
<div class="flex flex-col items-center">
<p class="text-muted-foreground py-3 text-xs">
Powered by <a
class="text-white"
href="https://github.com/stonith404/pocket-id"
target="_blank">Pocket ID</a
>
({versionInformation.currentVersion})
</p>
</div>
</div> </div>
</section> </section>

View File

@@ -16,7 +16,7 @@
children?: Snippet; children?: Snippet;
} = $props(); } = $props();
const limit = 5; const limit = 20;
</script> </script>
<div {...restProps}> <div {...restProps}>
@@ -25,15 +25,15 @@
{#each callbackURLs as _, i} {#each callbackURLs as _, i}
<div class="flex gap-x-2"> <div class="flex gap-x-2">
<Input data-testid={`callback-url-${i + 1}`} bind:value={callbackURLs[i]} /> <Input data-testid={`callback-url-${i + 1}`} bind:value={callbackURLs[i]} />
{#if callbackURLs.length > 1} {#if callbackURLs.length > 1}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
on:click={() => callbackURLs = callbackURLs.filter((_, index) => index !== i)} on:click={() => (callbackURLs = callbackURLs.filter((_, index) => index !== i))}
> >
<LucideMinus class="h-4 w-4" /> <LucideMinus class="h-4 w-4" />
</Button> </Button>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
@@ -46,7 +46,7 @@
class="mt-2" class="mt-2"
variant="secondary" variant="secondary"
size="sm" size="sm"
on:click={() => callbackURLs = [...callbackURLs, '']} on:click={() => (callbackURLs = [...callbackURLs, ''])}
> >
<LucidePlus class="mr-1 h-4 w-4" /> <LucidePlus class="mr-1 h-4 w-4" />
Add another Add another

View File

@@ -1,5 +1,6 @@
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import packageJson from "./package.json" assert { type: "json" };
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
@@ -12,6 +13,9 @@ const config = {
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(), adapter: adapter(),
version: {
name: packageJson.version,
}
} }
}; };

View File

@@ -4,7 +4,7 @@ import { cleanupBackend } from './utils/cleanup.util';
test.beforeEach(cleanupBackend); test.beforeEach(cleanupBackend);
test('Create user group', async ({ page }) => { test('Create user group', async ({ page, baseURL }) => {
await page.goto('/settings/admin/user-groups'); await page.goto('/settings/admin/user-groups');
const group = userGroups.humanResources; const group = userGroups.humanResources;
@@ -14,7 +14,9 @@ test('Create user group', async ({ page }) => {
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('User group created successfully'); await expect(page.getByRole('status')).toHaveText('User group created successfully');
expect(page.url()).toMatch(/\/settings\/admin\/user-groups\/[a-f0-9-]+/);
const expectedRoute = new RegExp(`${baseURL}/settings/admin/user-groups/[a-f0-9-]+`);
expect(page.url()).toMatch(expectedRoute);
await expect(page.getByLabel('Friendly Name')).toHaveValue(group.friendlyName); await expect(page.getByLabel('Friendly Name')).toHaveValue(group.friendlyName);
await expect(page.getByLabel('Name', { exact: true })).toHaveValue(group.name); await expect(page.getByLabel('Name', { exact: true })).toHaveValue(group.name);

View File

@@ -0,0 +1,75 @@
# Default database path
DB_PATH="./backend/data/pocket-id.db"
# Parse command-line arguments for the -d flag (database path)
while getopts ":d:" opt; do
case $opt in
d)
DB_PATH="$OPTARG"
;;
\?)
echo "Invalid option -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
# Ensure username or email is provided as a parameter
if [ -z "$1" ]; then
echo "Usage: $0 [-d <database_path>] <username or email>"
echo " -d Specify the database path (optional, defaults to ./backend/data/pocket-id.db)"
exit 1
fi
USER_IDENTIFIER="$1"
# Check and try to install the required commands
check_and_install() {
local cmd=$1
local pkg=$2
if ! command -v "$cmd" &>/dev/null; then
if command -v apk &>/dev/null; then
echo "$cmd not found. Installing..."
apk add "$pkg" --no-cache
else
echo "$cmd is not installed, please install it manually."
exit 1
fi
fi
}
check_and_install sqlite3 sqlite
check_and_install uuidgen uuidgen
# Generate a 16-character alphanumeric secret token
SECRET_TOKEN=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 16)
# Get the current Unix timestamp for creation and expiration (1 hour from now)
CREATED_AT=$(date +%s)
EXPIRES_AT=$((CREATED_AT + 3600))
# Retrieve user_id from the users table based on username or email
USER_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM users WHERE username='$USER_IDENTIFIER' OR email='$USER_IDENTIFIER';")
# Check if user exists
if [ -z "$USER_ID" ]; then
echo "User not found for username/email: $USER_IDENTIFIER"
exit 1
fi
# Insert the one-time token into the one_time_access_tokens table
sqlite3 "$DB_PATH" <<EOF
INSERT INTO one_time_access_tokens (id, created_at, token, expires_at, user_id)
VALUES ('$(uuidgen)', '$CREATED_AT', '$SECRET_TOKEN', '$EXPIRES_AT', '$USER_ID');
EOF
if [ $? -eq 0 ]; then
echo "A one-time access token valid for 1 hour has been created for \"$USER_IDENTIFIER\"."
echo "Use the following URL to sign in once: ${PUBLIC_APP_URL:=https://<your-pocket-id-domain>}/login/$SECRET_TOKEN"
else
echo "Error creating access token."
exit 1
fi

View File

@@ -6,7 +6,7 @@ increment_version() {
local version=$1 local version=$1
local part=$2 local part=$2
IFS='.' read -r -a parts <<< "$version" IFS='.' read -r -a parts <<<"$version"
if [ "$part" == "minor" ]; then if [ "$part" == "minor" ]; then
parts[1]=$((parts[1] + 1)) parts[1]=$((parts[1] + 1))
parts[2]=0 parts[2]=0
@@ -30,12 +30,15 @@ else
fi fi
# Update the .version file with the new version # Update the .version file with the new version
echo $NEW_VERSION > .version echo $NEW_VERSION >.version
git add .version git add .version
# Update version in frontend/package.json
jq --arg new_version "$NEW_VERSION" '.version = $new_version' frontend/package.json >frontend/package_tmp.json && mv frontend/package_tmp.json frontend/package.json
git add frontend/package.json
# Check if conventional-changelog is installed, if not install it # Check if conventional-changelog is installed, if not install it
if ! command -v conventional-changelog &> /dev/null if ! command -v conventional-changelog &>/dev/null; then
then
echo "conventional-changelog not found, installing..." echo "conventional-changelog not found, installing..."
npm install -g conventional-changelog-cli npm install -g conventional-changelog-cli
fi fi
@@ -55,4 +58,4 @@ git tag "v$NEW_VERSION"
git push git push
git push --tags git push --tags
echo "Release process complete. New version: $NEW_VERSION" echo "Release process complete. New version: $NEW_VERSION"