mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-10 23:32: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)
|
## [](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 --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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 (
|
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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,9 @@ export type AppConfigRawResponse = {
|
|||||||
type: string;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
export type AppVersionInformation = {
|
||||||
|
isUpToDate: boolean;
|
||||||
|
newestVersion: string;
|
||||||
|
currentVersion: string;
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
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">
|
<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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
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 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"
|
||||||
Reference in New Issue
Block a user