mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-10 07:12:56 +03:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd4f87b2d2 | ||
|
|
6560fd9279 | ||
|
|
29d632c151 | ||
|
|
2092007752 | ||
|
|
0aff6181c9 | ||
|
|
824c5cb4f3 | ||
|
|
3a300a2b51 | ||
|
|
a1985ce1b2 | ||
|
|
b39bc4f79a | ||
|
|
0a07344139 | ||
|
|
f3f0e1d56d | ||
|
|
70ad0b4f39 |
18
.dockerignore
Normal file
18
.dockerignore
Normal 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
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
47
backend/internal/model/types/date_time.go
Normal file
47
backend/internal/model/types/date_time.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -350,7 +351,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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
28
backend/migrations/20241023072742_unix-timestamps.down.sql
Normal file
28
backend/migrations/20241023072742_unix-timestamps.down.sql
Normal 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');
|
||||
27
backend/migrations/20241023072742_unix-timestamps.up.sql
Normal file
27
backend/migrations/20241023072742_unix-timestamps.up.sql
Normal 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);
|
||||
743
frontend/package-lock.json
generated
743
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "0.0.1",
|
||||
"version": "0.10.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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -45,4 +47,19 @@ 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,9 @@ export type AppConfigRawResponse = {
|
||||
type: string;
|
||||
value: string;
|
||||
}[];
|
||||
|
||||
export type AppVersionInformation = {
|
||||
isUpToDate: boolean;
|
||||
newestVersion: string;
|
||||
currentVersion: string;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
24
frontend/src/routes/settings/+layout.server.ts
Normal file
24
frontend/src/routes/settings/+layout.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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-white"
|
||||
href="https://github.com/stonith404/pocket-id"
|
||||
target="_blank">Pocket ID</a
|
||||
>
|
||||
({versionInformation.currentVersion})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
75
scripts/create-one-time-access-token.sh
Normal file
75
scripts/create-one-time-access-token.sh
Normal 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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user