Compare commits

...

15 Commits

Author SHA1 Message Date
Elias Schneider
47e164b4b5 release: 0.11.0 2024-10-25 21:53:25 +02:00
Elias Schneider
18c5103c20 fix: powered by link text color in light mode 2024-10-25 21:35:27 +02:00
Elias Schneider
5565f60d6d feat: add email_verified claim 2024-10-25 21:33:54 +02:00
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
37 changed files with 860 additions and 446 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.11.0

View File

@@ -1,3 +1,31 @@
## [](https://github.com/stonith404/pocket-id/compare/v0.10.0...v) (2024-10-25)
### Features
* add `email_verified` claim ([5565f60](https://github.com/stonith404/pocket-id/commit/5565f60d6d62ca24bedea337e21effc13e5853a5))
### Bug Fixes
* powered by link text color in light mode ([18c5103](https://github.com/stonith404/pocket-id/commit/18c5103c20ce79abdc0f724cdedd642c09269e78))
## [](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)

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 ./scripts ./scripts
RUN chmod +x ./scripts/*.sh
EXPOSE 3000
ENV APP_ENV=production

View File

@@ -161,8 +161,14 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
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.JSON(http.StatusOK, user)
c.JSON(http.StatusOK, userDto)
}
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {

View File

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

View File

@@ -14,6 +14,7 @@ type AppConfigVariableDto struct {
type AppConfigUpdateDto struct {
AppName string `json:"appName" binding:"required,min=1,max=30"`
SessionDuration string `json:"sessionDuration" binding:"required"`
EmailsVerified string `json:"emailsVerified" binding:"required"`
EmailEnabled string `json:"emailEnabled" binding:"required"`
SmtHost string `json:"smtpHost"`
SmtpPort string `json:"smtpPort"`

View File

@@ -2,7 +2,9 @@ package dto
import (
"errors"
"github.com/stonith404/pocket-id/backend/internal/model/types"
"reflect"
"time"
)
// 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 {
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/google/uuid"
"github.com/stonith404/pocket-id/backend/internal/model"
"github.com/stonith404/pocket-id/backend/internal/utils"
"gorm.io/gorm"
"log"
"time"
@@ -30,22 +29,22 @@ type Jobs struct {
// ClearWebauthnSessions deletes WebAuthn sessions that have expired
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
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
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
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) {

View File

@@ -14,6 +14,7 @@ type AppConfig struct {
LogoLightImageType AppConfigVariable
LogoDarkImageType AppConfigVariable
SessionDuration AppConfigVariable
EmailsVerified AppConfigVariable
EmailEnabled AppConfigVariable
SmtpHost AppConfigVariable

View File

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

View File

@@ -4,8 +4,8 @@ import (
"database/sql/driver"
"encoding/json"
"errors"
datatype "github.com/stonith404/pocket-id/backend/internal/model/types"
"gorm.io/gorm"
"time"
)
type UserAuthorizedOidcClient struct {
@@ -23,7 +23,7 @@ type OidcAuthorizationCode struct {
Code string
Scope string
Nonce string
ExpiresAt time.Time
ExpiresAt datatype.DateTime
UserID string
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 (
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"time"
"github.com/stonith404/pocket-id/backend/internal/model/types"
)
type User struct {
@@ -61,7 +61,7 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential
type OneTimeAccessToken struct {
Base
Token string
ExpiresAt time.Time
ExpiresAt datatype.DateTime
UserID string
User User

View File

@@ -41,6 +41,11 @@ var defaultDbConfig = model.AppConfig{
Type: "number",
Value: "60",
},
EmailsVerified: model.AppConfigVariable{
Key: "emailsVerified",
Type: "bool",
Value: "false",
},
BackgroundImageType: model.AppConfigVariable{
Key: "backgroundImageType",
Type: "string",

View File

@@ -6,6 +6,7 @@ import (
"github.com/stonith404/pocket-id/backend/internal/common"
"github.com/stonith404/pocket-id/backend/internal/dto"
"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"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
@@ -115,7 +116,7 @@ func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret strin
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
}
@@ -314,6 +315,7 @@ func (s *OidcService) GetUserClaimsForClient(userID string, clientID string) (ma
if strings.Contains(scope, "email") {
claims["email"] = user.Email
claims["email_verified"] = s.appConfigService.DbConfig.EmailsVerified.Value == "true"
}
if strings.Contains(scope, "groups") {
@@ -350,7 +352,7 @@ func (s *OidcService) createAuthorizationCode(clientID string, userID string, sc
}
oidcAuthorizationCode := model.OidcAuthorizationCode{
ExpiresAt: time.Now().Add(15 * time.Minute),
ExpiresAt: datatype.DateTime(time.Now().Add(15 * time.Minute)),
Code: randomString,
ClientID: clientID,
UserID: userID,

View File

@@ -6,6 +6,7 @@ import (
"encoding/base64"
"fmt"
"github.com/fxamacker/cbor/v2"
"github.com/stonith404/pocket-id/backend/internal/model/types"
"log"
"os"
"time"
@@ -111,7 +112,7 @@ func (s *TestService) SeedDatabase() error {
Code: "auth-code",
Scope: "openid profile",
Nonce: "nonce",
ExpiresAt: time.Now().Add(1 * time.Hour),
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
UserID: users[0].ID,
ClientID: oidcClients[0].ID,
}
@@ -121,7 +122,7 @@ func (s *TestService) SeedDatabase() error {
accessToken := model.OneTimeAccessToken{
Token: "one-time-token",
ExpiresAt: time.Now().Add(1 * time.Hour),
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
UserID: users[0].ID,
}
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/dto"
"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"
"gorm.io/gorm"
"time"
@@ -95,7 +96,7 @@ func (s *UserService) CreateOneTimeAccessToken(userID string, expiresAt time.Tim
oneTimeAccessToken := model.OneTimeAccessToken{
UserID: userID,
ExpiresAt: expiresAt,
ExpiresAt: datatype.DateTime(expiresAt),
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) {
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) {
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",
"version": "0.0.1",
"version": "0.11.0",
"private": true,
"scripts": {
"dev": "vite dev --port 3000",
@@ -12,31 +12,31 @@
"format": "prettier --write ."
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/adapter-node": "^5.2.5",
"@sveltejs/kit": "^2.6.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@playwright/test": "^1.48.1",
"@sveltejs/adapter-auto": "^3.3.0",
"@sveltejs/adapter-node": "^5.2.8",
"@sveltejs/kit": "^2.7.2",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.1",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.7.4",
"@types/node": "^22.7.9",
"autoprefixer": "^10.4.20",
"cbor-js": "^0.1.0",
"eslint": "^9.11.1",
"eslint": "^9.13.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.1",
"globals": "^15.10.0",
"eslint-plugin-svelte": "^2.46.0",
"globals": "^15.11.0",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"prettier-plugin-tailwindcss": "^0.6.8",
"svelte": "^5.0.0-next.262",
"svelte-check": "^4.0.4",
"tailwindcss": "^3.4.13",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"typescript-eslint": "^8.8.0",
"vite": "^5.4.8"
"svelte": "^5.0.5",
"svelte-check": "^4.0.5",
"tailwindcss": "^3.4.14",
"tslib": "^2.8.0",
"typescript": "^5.6.3",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
},
"type": "module",
"dependencies": {
@@ -47,11 +47,11 @@
"crypto": "^1.0.1",
"formsnap": "^1.0.1",
"jsonwebtoken": "^9.0.2",
"lucide-svelte": "^0.447.0",
"lucide-svelte": "^0.453.0",
"mode-watcher": "^0.4.1",
"svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.19.0",
"tailwind-merge": "^2.5.3",
"sveltekit-superforms": "^2.20.0",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"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 axios from 'axios';
import APIService from './api-service';
export default class AppConfigService extends APIService {
@@ -12,14 +14,19 @@ export default class AppConfigService extends APIService {
const appConfig: Partial<AllAppConfig> = {};
data.forEach(({ key, value }) => {
(appConfig as any)[key] = value;
(appConfig as any)[key] = this.parseValue(value);
});
return appConfig as AllAppConfig;
}
async update(appConfig: AllAppConfig) {
const res = await this.api.put('/application-configuration', appConfig);
// Convert all values to string
const appConfigConvertedToString = {};
for (const key in appConfig) {
(appConfigConvertedToString as any)[key] = (appConfig as any)[key].toString();
}
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
return res.data as AllAppConfig;
}
@@ -45,4 +52,31 @@ export default class AppConfigService extends APIService {
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
};
}
private parseValue(value: string) {
if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
} else if (!isNaN(Number(value))) {
return Number(value);
} else {
return value;
}
}
}

View File

@@ -1,18 +1,26 @@
export type AllAppConfig = {
export type AppConfig = {
appName: string;
sessionDuration: string;
emailEnabled: string;
};
export type AllAppConfig = AppConfig & {
sessionDuration: number;
emailsVerified: boolean;
emailEnabled: boolean;
smtpHost: string;
smtpPort: string;
smtpPort: number;
smtpFrom: string;
smtpUser: string;
smtpPassword: string;
};
export type AppConfig = AllAppConfig;
export type AppConfigRawResponse = {
key: string;
type: string;
value: string;
}[];
export type AppVersionInformation = {
isUpToDate: boolean;
newestVersion: string;
currentVersion: string;
};

View File

@@ -1,5 +1,5 @@
export function debounced<T extends (...args: any[]) => void>(func: T, delay: number) {
let debounceTimeout: number | undefined;
let debounceTimeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
if (debounceTimeout !== undefined) {
@@ -10,4 +10,4 @@ export function debounced<T extends (...args: any[]) => void>(func: T, delay: nu
func(...args);
}, delay);
};
}
}

View File

@@ -33,11 +33,19 @@
<Logo class="h-10 w-10" />
</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">
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 data.token === 'setup'}
You're about to sign in to the initial admin account. Anyone with this link can access the
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>
<Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button>
</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">
import { page } from '$app/stores';
import userStore from '$lib/stores/user-store';
import { LucideExternalLink } from 'lucide-svelte';
import type { Snippet } from 'svelte';
import type { LayoutData } from './$types';
let {
children
children,
data
}: {
children: Snippet;
data: LayoutData;
} = $props();
const { versionInformation } = data;
let links = $state([
{ href: '/settings/account', label: 'My Account' },
{ href: '/settings/audit-log', label: 'Audit Log' }
@@ -26,8 +32,10 @@
</script>
<section>
<div class="bg-muted/40 min-h-screen w-full">
<main class="mx-auto flex max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row">
<div class="bg-muted/40 flex min-h-[calc(100vh-64px)] w-full flex-col justify-between">
<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 class="mx-auto grid w-full gap-2">
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
@@ -41,6 +49,15 @@
{label}
</a>
{/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>
</div>
</div>
@@ -48,5 +65,15 @@
{@render children()}
</div>
</main>
<div class="flex flex-col items-center">
<p class="text-muted-foreground py-3 text-xs">
Powered by <a
class="text-foreground"
href="https://github.com/stonith404/pocket-id"
target="_blank">Pocket ID</a
>
({versionInformation.currentVersion})
</p>
</div>
</div>
</section>

View File

@@ -15,10 +15,10 @@
} = $props();
let isLoading = $state(false);
let emailEnabled = $state(appConfig.emailEnabled == 'true');
let emailEnabled = $state(appConfig.emailEnabled);
const updatedAppConfig = {
emailEnabled: emailEnabled.toString(),
emailEnabled: appConfig.emailEnabled,
smtpHost: appConfig.smtpHost,
smtpPort: appConfig.smtpPort,
smtpUser: appConfig.smtpUser,
@@ -28,13 +28,13 @@
const formSchema = z.object({
smtpHost: z.string().min(1),
smtpPort: z.string().min(1),
smtpPort: z.number().min(1),
smtpUser: z.string().min(1),
smtpPassword: z.string().min(1),
smtpFrom: z.string().email()
});
const { inputs, ...form } = createForm< typeof formSchema>(formSchema, updatedAppConfig);
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
async function onSubmit() {
const data = form.validate();
@@ -42,15 +42,15 @@
isLoading = true;
await callback({
...data,
emailEnabled: 'true'
emailEnabled: true
}).finally(() => (isLoading = false));
toast.success('Email configuration updated successfully');
return true;
}
async function onDisable() {
await callback({ emailEnabled: 'false' });
emailEnabled = false;
await callback({ emailEnabled });
toast.success('Email disabled successfully');
}
@@ -64,7 +64,7 @@
<form onsubmit={onSubmit}>
<div class="mt-5 grid grid-cols-2 gap-5">
<FormInput label="SMTP Host" bind:input={$inputs.smtpHost} />
<FormInput label="SMTP Port" bind:input={$inputs.smtpPort} />
<FormInput label="SMTP Port" type="number" bind:input={$inputs.smtpPort} />
<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} />

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import FormInput from '$lib/components/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Label } from '$lib/components/ui/label';
import type { AllAppConfig } from '$lib/types/application-configuration';
import { createForm } from '$lib/utils/form-util';
import { toast } from 'svelte-sonner';
@@ -18,20 +20,14 @@
const updatedAppConfig = {
appName: appConfig.appName,
sessionDuration: appConfig.sessionDuration
sessionDuration: appConfig.sessionDuration,
emailsVerified: appConfig.emailsVerified
};
const formSchema = z.object({
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'
}
)
sessionDuration: z.number().min(1).max(43200),
emailsVerified: z.boolean()
});
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
@@ -49,9 +45,21 @@
<FormInput label="Application Name" bind:input={$inputs.appName} />
<FormInput
label="Session Duration"
type="number"
description="The duration of a session in minutes before the user has to sign in again."
bind:input={$inputs.sessionDuration}
/>
<div class="items-top mt-5 flex space-x-2">
<Checkbox id="admin-privileges" bind:checked={$inputs.emailsVerified.value} />
<div class="grid gap-1.5 leading-none">
<Label for="admin-privileges" class="mb-0 text-sm font-medium leading-none">
Emails Verified
</Label>
<p class="text-muted-foreground text-[0.8rem]">
Whether the user's email should be marked as verified for the OIDC clients.
</p>
</div>
</div>
</div>
<div class="mt-5 flex justify-end">
<Button {isLoading} type="submit">Save</Button>

View File

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

View File

@@ -1,5 +1,6 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import packageJson from "./package.json" assert { type: "json" };
/** @type {import('@sveltejs/kit').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.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
version: {
name: packageJson.version,
}
}
};

View File

@@ -4,7 +4,7 @@ import { cleanupBackend } from './utils/cleanup.util';
test.beforeEach(cleanupBackend);
test('Create user group', async ({ page }) => {
test('Create user group', async ({ page, baseURL }) => {
await page.goto('/settings/admin/user-groups');
const group = userGroups.humanResources;
@@ -14,7 +14,9 @@ test('Create user group', async ({ page }) => {
await page.getByRole('button', { name: 'Save' }).click();
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('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 part=$2
IFS='.' read -r -a parts <<< "$version"
IFS='.' read -r -a parts <<<"$version"
if [ "$part" == "minor" ]; then
parts[1]=$((parts[1] + 1))
parts[2]=0
@@ -30,12 +30,15 @@ else
fi
# Update the .version file with the new version
echo $NEW_VERSION > .version
echo $NEW_VERSION >.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
if ! command -v conventional-changelog &> /dev/null
then
if ! command -v conventional-changelog &>/dev/null; then
echo "conventional-changelog not found, installing..."
npm install -g conventional-changelog-cli
fi
@@ -55,4 +58,4 @@ git tag "v$NEW_VERSION"
git push
git push --tags
echo "Release process complete. New version: $NEW_VERSION"
echo "Release process complete. New version: $NEW_VERSION"