Compare commits
13 Commits
e2ae03f2a6
...
cc49ab1655
Author | SHA1 | Date | |
---|---|---|---|
cc49ab1655 | |||
06c60b3491 | |||
b584a7b07f | |||
410e420a46 | |||
eeb0f6eac1 | |||
fb622f918a | |||
a50bad417f | |||
c395729446 | |||
eaa92d2fe4 | |||
a9e382d713 | |||
974244025e | |||
ae41076673 | |||
cc7f7f40c4 |
@ -13,20 +13,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func NewUserDTO(row *repository.User) types.UserDTO {
|
||||
return types.UserDTO{
|
||||
ID: row.ID,
|
||||
Email: row.Email,
|
||||
FullName: row.FullName,
|
||||
IsAdmin: row.IsAdmin,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
LastLogin: row.LastLogin,
|
||||
PhoneNumber: row.PhoneNumber,
|
||||
ProfilePicture: row.ProfilePicture,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := util.GetRequestUserId(r.Context())
|
||||
if !ok {
|
||||
@ -55,7 +41,7 @@ func (h *AdminHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var items []types.UserDTO
|
||||
|
||||
for _, user := range users {
|
||||
items = append(items, NewUserDTO(&user))
|
||||
items = append(items, types.NewUserDTO(&user))
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
@ -88,7 +74,7 @@ func (h *AdminHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := encoder.Encode(NewUserDTO(&user)); err != nil {
|
||||
if err := encoder.Encode(types.NewUserDTO(&user)); err != nil {
|
||||
web.Error(w, "failed to encode user dto", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
@ -162,17 +162,7 @@ func (h *AuthHandler) getProfile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(types.UserDTO{
|
||||
ID: user.ID,
|
||||
FullName: user.FullName,
|
||||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
IsAdmin: user.IsAdmin,
|
||||
LastLogin: user.LastLogin,
|
||||
ProfilePicture: user.ProfilePicture,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}); err != nil {
|
||||
if err := json.NewEncoder(w).Encode(types.NewUserDTO(&user)); err != nil {
|
||||
web.Error(w, "failed to encode user profile", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
@ -37,4 +37,6 @@ type User struct {
|
||||
ProfilePicture *string `json:"profile_picture"`
|
||||
CreatedBy *uuid.UUID `json:"created_by"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
AvatarVerified bool `json:"avatar_verified"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
const findAdminUsers = `-- name: FindAdminUsers :many
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified FROM users WHERE created_by = $1
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified, avatar_verified, verified FROM users WHERE created_by = $1
|
||||
`
|
||||
|
||||
func (q *Queries) FindAdminUsers(ctx context.Context, createdBy *uuid.UUID) ([]User, error) {
|
||||
@ -37,6 +37,8 @@ func (q *Queries) FindAdminUsers(ctx context.Context, createdBy *uuid.UUID) ([]U
|
||||
&i.ProfilePicture,
|
||||
&i.CreatedBy,
|
||||
&i.EmailVerified,
|
||||
&i.AvatarVerified,
|
||||
&i.Verified,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -49,7 +51,7 @@ func (q *Queries) FindAdminUsers(ctx context.Context, createdBy *uuid.UUID) ([]U
|
||||
}
|
||||
|
||||
const findAllUsers = `-- name: FindAllUsers :many
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified FROM users
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified, avatar_verified, verified FROM users
|
||||
`
|
||||
|
||||
func (q *Queries) FindAllUsers(ctx context.Context) ([]User, error) {
|
||||
@ -74,6 +76,8 @@ func (q *Queries) FindAllUsers(ctx context.Context) ([]User, error) {
|
||||
&i.ProfilePicture,
|
||||
&i.CreatedBy,
|
||||
&i.EmailVerified,
|
||||
&i.AvatarVerified,
|
||||
&i.Verified,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -86,7 +90,7 @@ func (q *Queries) FindAllUsers(ctx context.Context) ([]User, error) {
|
||||
}
|
||||
|
||||
const findUserEmail = `-- name: FindUserEmail :one
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified FROM users WHERE email = $1 LIMIT 1
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified, avatar_verified, verified FROM users WHERE email = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) FindUserEmail(ctx context.Context, email string) (User, error) {
|
||||
@ -105,12 +109,14 @@ func (q *Queries) FindUserEmail(ctx context.Context, email string) (User, error)
|
||||
&i.ProfilePicture,
|
||||
&i.CreatedBy,
|
||||
&i.EmailVerified,
|
||||
&i.AvatarVerified,
|
||||
&i.Verified,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const findUserId = `-- name: FindUserId :one
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified FROM users WHERE id = $1 LIMIT 1
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified, avatar_verified, verified FROM users WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) FindUserId(ctx context.Context, id uuid.UUID) (User, error) {
|
||||
@ -129,6 +135,8 @@ func (q *Queries) FindUserId(ctx context.Context, id uuid.UUID) (User, error) {
|
||||
&i.ProfilePicture,
|
||||
&i.CreatedBy,
|
||||
&i.EmailVerified,
|
||||
&i.AvatarVerified,
|
||||
&i.Verified,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package types
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@ -16,4 +17,24 @@ type UserDTO struct {
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
PhoneNumber *string `json:"phone_number"`
|
||||
ProfilePicture *string `json:"profile_picture"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
AvatarVerified bool `json:"avatar_verified"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
func NewUserDTO(row *repository.User) UserDTO {
|
||||
return UserDTO{
|
||||
ID: row.ID,
|
||||
Email: row.Email,
|
||||
FullName: row.FullName,
|
||||
IsAdmin: row.IsAdmin,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
LastLogin: row.LastLogin,
|
||||
PhoneNumber: row.PhoneNumber,
|
||||
ProfilePicture: row.ProfilePicture,
|
||||
EmailVerified: row.EmailVerified,
|
||||
AvatarVerified: row.AvatarVerified,
|
||||
Verified: row.Verified,
|
||||
}
|
||||
}
|
||||
|
12
migrations/00008_add_verification_levels.sql
Normal file
12
migrations/00008_add_verification_levels.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE users
|
||||
ADD COLUMN avatar_verified BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- +goose StatementEnd
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE users
|
||||
DROP COLUMN avatar_verified;
|
||||
|
||||
-- +goose StatementEnd
|
12
migrations/00009_add_complete_verify.sql
Normal file
12
migrations/00009_add_complete_verify.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE users
|
||||
ADD COLUMN verified BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- +goose StatementEnd
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE users
|
||||
DROP COLUMN verified;
|
||||
|
||||
-- +goose StatementEnd
|
339
web/package-lock.json
generated
339
web/package-lock.json
generated
@ -8,7 +8,6 @@
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"axios": "^1.9.0",
|
||||
"idb": "^8.0.3",
|
||||
@ -58,6 +57,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
@ -113,6 +113,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
|
||||
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.1",
|
||||
@ -146,6 +147,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
@ -187,6 +189,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -196,6 +199,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -229,6 +233,7 @@
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
|
||||
"integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.1"
|
||||
@ -272,19 +277,11 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@ -299,6 +296,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
|
||||
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@ -317,6 +315,7 @@
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@ -326,6 +325,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
||||
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
@ -335,126 +335,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.16.7",
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/hash": "^0.9.2",
|
||||
"@emotion/memoize": "^0.9.0",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"convert-source-map": "^1.5.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"find-root": "^1.1.0",
|
||||
"source-map": "^0.5.7",
|
||||
"stylis": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/cache": {
|
||||
"version": "11.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
|
||||
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.9.0",
|
||||
"@emotion/sheet": "^1.4.0",
|
||||
"@emotion/utils": "^1.4.2",
|
||||
"@emotion/weak-memoize": "^0.4.0",
|
||||
"stylis": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
|
||||
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/memoize": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
|
||||
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
|
||||
"@emotion/utils": "^1.4.2",
|
||||
"@emotion/weak-memoize": "^0.4.0",
|
||||
"hoist-non-react-statics": "^3.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/serialize": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
|
||||
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/hash": "^0.9.2",
|
||||
"@emotion/memoize": "^0.9.0",
|
||||
"@emotion/unitless": "^0.10.0",
|
||||
"@emotion/utils": "^1.4.2",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/sheet": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
|
||||
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/unitless": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
|
||||
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
|
||||
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/utils": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
|
||||
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/weak-memoize": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
|
||||
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
|
||||
@ -2068,12 +1948,6 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz",
|
||||
@ -2423,21 +2297,6 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"resolve": "^1.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -2519,6 +2378,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -2642,31 +2502,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"parse-json": "^5.0.0",
|
||||
"path-type": "^4.0.0",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig/node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -2686,12 +2521,14 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@ -2768,15 +2605,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@ -2876,6 +2704,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -3149,12 +2978,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@ -3398,15 +3221,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
|
||||
@ -3434,6 +3248,7 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
@ -3463,27 +3278,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -3537,6 +3331,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@ -3556,6 +3351,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
@ -3571,12 +3367,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@ -3865,12 +3655,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@ -4029,6 +3813,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@ -4134,6 +3919,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.0.0"
|
||||
@ -4142,24 +3928,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"lines-and-columns": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path": {
|
||||
"version": "0.12.7",
|
||||
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||
@ -4191,21 +3959,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -4372,12 +4125,6 @@
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-jwt": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-jwt/-/react-jwt-1.3.0.tgz",
|
||||
@ -4439,30 +4186,11 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@ -4608,15 +4336,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@ -4639,12 +4358,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
||||
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@ -4658,18 +4371,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",
|
||||
|
@ -11,7 +11,6 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"axios": "^1.9.0",
|
||||
"idb": "^8.0.3",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 59 KiB |
@ -19,6 +19,12 @@ import ApiServiceEditPage from "./pages/Admin/ApiServices/Update";
|
||||
import AdminUsersPage from "./pages/Admin/Users";
|
||||
import AdminViewUserPage from "./pages/Admin/Users/View";
|
||||
import AdminCreateUserPage from "./pages/Admin/Users/Create";
|
||||
import VerificationLayout from "./layout/VerificationLayout";
|
||||
import VerifyStartPage from "./pages/Verify";
|
||||
import VerifyEmailPage from "./pages/Verify/Email";
|
||||
import VerifyEmailOtpPage from "./pages/Verify/Email/OTP";
|
||||
import VerifyAvatarPage from "./pages/Verify/Avatar";
|
||||
import VerifyReviewPage from "./pages/Verify/Review";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -77,6 +83,38 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/verify",
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{
|
||||
path: "/verify",
|
||||
element: <VerificationLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <VerifyStartPage />,
|
||||
},
|
||||
{
|
||||
path: "email",
|
||||
element: <VerifyEmailPage />,
|
||||
},
|
||||
{
|
||||
path: "email/otp",
|
||||
element: <VerifyEmailOtpPage />,
|
||||
},
|
||||
{
|
||||
path: "avatar",
|
||||
element: <VerifyAvatarPage />,
|
||||
},
|
||||
{
|
||||
path: "review",
|
||||
element: <VerifyReviewPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/auth",
|
||||
element: <AuthLayout />,
|
||||
|
93
web/src/components/ui/stepper.tsx
Normal file
93
web/src/components/ui/stepper.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
type Step = {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
type StepperProps = {
|
||||
steps: Step[];
|
||||
currentStep: string;
|
||||
};
|
||||
|
||||
export const Stepper: React.FC<StepperProps> = ({ steps, currentStep }) => {
|
||||
const stepIndex = useMemo(
|
||||
() => steps.findIndex((s) => s.id === currentStep),
|
||||
[currentStep, steps],
|
||||
);
|
||||
|
||||
const percent = useMemo(() => {
|
||||
return steps.length === 1
|
||||
? 100
|
||||
: Math.round((stepIndex / (steps.length - 1)) * 100);
|
||||
}, [stepIndex, steps.length]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center w-full max-w-2xl mx-auto mb-5 sm:mb-8 gap-5 relative">
|
||||
{steps.map((step, idx) => (
|
||||
<>
|
||||
<div
|
||||
key={idx}
|
||||
className={`sm:flex p-4 pb-0 sm:p-0 flex-1 items-center ${idx < stepIndex ? "opacity-70" : ""} ${idx === stepIndex ? "flex" : "hidden"}`}
|
||||
>
|
||||
{/* Step circle */}
|
||||
<div
|
||||
className={`relative z-10 flex items-center justify-center w-12 h-12 min-w-12 sm:w-10 sm:h-10 sm:min-w-10 rounded-full
|
||||
${
|
||||
idx < stepIndex
|
||||
? "bg-blue-400 text-white"
|
||||
: idx === stepIndex
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-100 dark:bg-gray-800/60 text-gray-500"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{idx < stepIndex ? (
|
||||
// Check icon for completed steps
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
(step.icon ?? <span className="font-bold">{idx + 1}</span>)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step label */}
|
||||
<div className="flex flex-col ml-2 mr-2 sm:ml-4 sm:mr-4">
|
||||
<span className="text-base text-gray-700 dark:text-gray-200 sm:text-sm font-medium">
|
||||
{step.label}
|
||||
</span>
|
||||
{step.description && (
|
||||
<span className="text-sm sm:text-xs text-gray-500 dark:text-gray-400">
|
||||
{step.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* {idx < steps.length - 1 && (
|
||||
<div className="flex-1 h-1 mx-2 min-w sm:mx-4 rounded bg-gray-300 dark:bg-gray-600" />
|
||||
)} */}
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
<div className="sm:hidden relative h-1 w-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
|
||||
<div
|
||||
className="absolute left-0 top-0 h-full bg-blue-500 transition-all ease-in duration-500"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -31,6 +31,8 @@ const AuthLayout = () => {
|
||||
const authenticate = useAuth((state) => state.authenticate);
|
||||
const hasAuthenticated = useAuth((state) => state.hasAuthenticated);
|
||||
|
||||
const authProfile = useAuth((s) => s.profile);
|
||||
|
||||
const signInRequired = useAuth((state) => state.signInRequired);
|
||||
|
||||
const location = useLocation();
|
||||
@ -136,6 +138,14 @@ const AuthLayout = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!signInRequired &&
|
||||
authProfile?.email_verified === false &&
|
||||
!location.pathname.startsWith("/verify")
|
||||
) {
|
||||
return <Navigate to="/verify" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BackgroundLayout>
|
||||
<Outlet />
|
||||
|
@ -6,8 +6,9 @@ export interface IBackgroundLayoutProps {
|
||||
|
||||
const BackgroundLayout: FC<IBackgroundLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
// <div className="relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]">
|
||||
<div className="relative min-h-screen bg-[url(/overlay.jpg)] bg-[#f8f9fb] dark:bg-gradient-to-br from-[#101112] to-[#041758]">
|
||||
// <div className="relative min-h-screen bg-[url(/overlay.jpg)] bg-[#f8f9fb] dark:bg-gradient-to-br from-[#101112] to-[#041758]">
|
||||
// <div className="relative min-h-screen bg-cover bg-center bg-[url(/overlay.jpg)] bg-[#f8f9fb] dark:bg-[#101112] dark:bg-[url(/background-dark.png)]">
|
||||
<div className="relative min-h-screen bg-cover bg-center bg-[#f8f9fb] dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
51
web/src/layout/VerificationLayout.tsx
Normal file
51
web/src/layout/VerificationLayout.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Stepper } from "@/components/ui/stepper";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { useVerify } from "@/store/verify";
|
||||
import { Eye, MailCheck, ScanFace } from "lucide-react";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { Outlet, useLocation } from "react-router";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: "email",
|
||||
icon: <MailCheck size={18} />,
|
||||
label: "Verify Email",
|
||||
description: "Confirm your address",
|
||||
},
|
||||
{
|
||||
id: "avatar",
|
||||
icon: <ScanFace size={20} />,
|
||||
label: "Profile Picture",
|
||||
description: "Add profile image",
|
||||
},
|
||||
{
|
||||
id: "review",
|
||||
icon: <Eye size={20} />,
|
||||
label: "Done",
|
||||
description: "Review & Quit",
|
||||
},
|
||||
];
|
||||
|
||||
const VerificationLayout: FC = () => {
|
||||
const location = useLocation();
|
||||
const profile = useAuth((s) => s.profile);
|
||||
|
||||
const step = useVerify((s) => s.step);
|
||||
const loadStep = useVerify((s) => s.loadStep);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile) loadStep(profile);
|
||||
}, [loadStep, profile]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen max-h-screen overflow-y-auto flex flex-col items-center sm:justify-center bg-white/50 dark:bg-black/50">
|
||||
<div className="w-full h-full sm:w-auto sm:h-auto">
|
||||
{location.pathname.replace(/\/$/i, "") !== "/verify" &&
|
||||
step != null && <Stepper steps={steps} currentStep={step} />}
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationLayout;
|
180
web/src/pages/Verify/Avatar/index.tsx
Normal file
180
web/src/pages/Verify/Avatar/index.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { User } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState, type FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const VerifyAvatarPage: FC = () => {
|
||||
const profile = useAuth((s) => s.profile);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [stream, setStream] = useState<MediaStream | null>(null);
|
||||
const [avatar, setAvatar] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [takingPicture, setTakingPicture] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile?.profile_picture) setAvatar(profile.profile_picture);
|
||||
}, [profile?.profile_picture]);
|
||||
|
||||
// Request camera stream
|
||||
useEffect(() => {
|
||||
if (!takingPicture) return;
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
setError("Camera not supported on this device/browser.");
|
||||
return;
|
||||
}
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: true })
|
||||
.then((mediaStream) => {
|
||||
setStream(mediaStream);
|
||||
setError(null);
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = mediaStream;
|
||||
}
|
||||
})
|
||||
.catch(() => setError("Unable to access camera."));
|
||||
return () => {
|
||||
// Clean up camera stream when component unmounts or stops taking picture
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
setStream(null);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [takingPicture]);
|
||||
|
||||
const handleTakePicture = useCallback(() => {
|
||||
setTakingPicture(true);
|
||||
}, []);
|
||||
|
||||
const handleCapture = useCallback(() => {
|
||||
if (!videoRef.current || !canvasRef.current) return;
|
||||
const context = canvasRef.current.getContext("2d");
|
||||
if (!context) return;
|
||||
// Set canvas size to video size
|
||||
canvasRef.current.width = videoRef.current.videoWidth;
|
||||
canvasRef.current.height = videoRef.current.videoHeight;
|
||||
context.drawImage(
|
||||
videoRef.current,
|
||||
0,
|
||||
0,
|
||||
videoRef.current.videoWidth,
|
||||
videoRef.current.videoHeight,
|
||||
);
|
||||
const imageData = canvasRef.current.toDataURL("image/png");
|
||||
setAvatar(imageData);
|
||||
setTakingPicture(false);
|
||||
}, []);
|
||||
|
||||
const handleSelectFromDevice = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file.type.startsWith("image/")) {
|
||||
setError("Please select an image file.");
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setAvatar(event.target?.result as string);
|
||||
setError(null);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRetake = useCallback(() => {
|
||||
setAvatar(null);
|
||||
setTakingPicture(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full sm:max-w-sm mx-auto p-4">
|
||||
<div className="flex flex-col gap-2 w-full max-w-xs mx-auto">
|
||||
<h1 className="text-xl font-medium dark:text-gray-200">
|
||||
Profile Picture
|
||||
</h1>
|
||||
<p className="dark:text-gray-400 mb-6">
|
||||
Please take a photo of yourself for your avatar in order to continue.
|
||||
</p>
|
||||
<div className="relative w-48 h-48 mx-auto rounded-full border-4 border-blue-500 overflow-hidden bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-6">
|
||||
{takingPicture ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
className="w-full h-full object-cover rounded-lg bg-black"
|
||||
/>
|
||||
) : avatar ? (
|
||||
<img
|
||||
src={avatar}
|
||||
alt="Avatar"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-4xl text-gray-400">
|
||||
<User size={48} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{error && <div className="text-red-500 text-center">{error}</div>}
|
||||
|
||||
{/* File input (hidden) */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
{!avatar && !takingPicture && (
|
||||
<Button onClick={handleTakePicture}>Take Photo</Button>
|
||||
)}
|
||||
{!avatar && !takingPicture && (
|
||||
<Button variant="outlined" onClick={handleSelectFromDevice}>
|
||||
Pick from Device
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{takingPicture && (
|
||||
<div className="flex flex-col items-center gap-2 w-full">
|
||||
<Button onClick={handleCapture}>Capture</Button>
|
||||
<Button variant="outlined" onClick={() => setTakingPicture(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hidden canvas for snapshot */}
|
||||
<canvas ref={canvasRef} style={{ display: "none" }} />
|
||||
|
||||
{avatar && (
|
||||
<>
|
||||
<Link to="/verify/review" className="w-full">
|
||||
<Button className="w-full">Next</Button>
|
||||
</Link>
|
||||
<Button
|
||||
className="border-yellow-500 text-yellow-500 hover:border-yellow-600 hover:text-yellow-600"
|
||||
variant="outlined"
|
||||
onClick={handleRetake}
|
||||
>
|
||||
Retake/Choose Another
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyAvatarPage;
|
24
web/src/pages/Verify/Email/OTP/index.tsx
Normal file
24
web/src/pages/Verify/Email/OTP/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { type FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const VerifyEmailOtpPage: FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-stretch gap-2 max-w-sm mx-auto p-4">
|
||||
<h1 className="text-xl font-medium dark:text-gray-200">
|
||||
OTP Verification
|
||||
</h1>
|
||||
<p className="text-sm dark:text-gray-400">
|
||||
We've sent you verification code on your email address, please open your
|
||||
mailbox and enter the verification code in order to continue.
|
||||
</p>
|
||||
<Input placeholder="Enter OTP" />
|
||||
<Link to="/verify/avatar" className="w-full">
|
||||
<Button className="mt-3 w-full">Verify</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmailOtpPage;
|
49
web/src/pages/Verify/Email/index.tsx
Normal file
49
web/src/pages/Verify/Email/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import maskEmail from "@/util/maskEmail";
|
||||
import { useCallback, useMemo, useState, type FC } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const VerifyEmailPage: FC = () => {
|
||||
const profile = useAuth((s) => s.profile);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const matches = useMemo(
|
||||
() => email === profile?.email,
|
||||
[email, profile?.email],
|
||||
);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (matches) {
|
||||
navigate("/verify/email/otp");
|
||||
}
|
||||
}, [matches, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-stretch gap-2 sm:max-w-sm mx-auto p-4">
|
||||
<h1 className="text-xl font-medium dark:text-gray-200">E-Mail Address</h1>
|
||||
<p className="text-sm dark:text-gray-400">
|
||||
Please fill in your email address used in this account.
|
||||
</p>
|
||||
<Input
|
||||
value={email}
|
||||
placeholder={maskEmail(profile?.email ?? "")}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className={`mt-3 ${!matches ? "opacity-60" : ""}`}
|
||||
onClick={handleNext}
|
||||
disabled={!matches}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmailPage;
|
37
web/src/pages/Verify/Review/index.tsx
Normal file
37
web/src/pages/Verify/Review/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Avatar from "@/feature/Avatar";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import type { FC } from "react";
|
||||
|
||||
const VerifyReviewPage: FC = () => {
|
||||
const profile = useAuth((s) => s.profile);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-stretch gap-2 sm:max-w-sm mx-auto p-4">
|
||||
<h1 className="text-xl font-medium dark:text-gray-200">
|
||||
You're all setup!
|
||||
</h1>
|
||||
<p className="text-sm dark:text-gray-400">
|
||||
You've just finished your account verification. Now you can finally
|
||||
access your account and use it for home services.
|
||||
</p>
|
||||
|
||||
<Avatar
|
||||
avatarId={profile?.profile_picture ?? undefined}
|
||||
iconSize={64}
|
||||
className="w-48 h-48 min-w-48 mx-auto mt-4"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-center mb-5">
|
||||
<h2 className="dark:text-gray-200 text-2xl">{profile?.full_name}</h2>
|
||||
|
||||
<p className="dark:text-gray-400 text-sm">{profile?.email}</p>
|
||||
</div>
|
||||
|
||||
<Button className="mt-4">Back Home</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyReviewPage;
|
36
web/src/pages/Verify/index.tsx
Normal file
36
web/src/pages/Verify/index.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const VerifyStartPage: FC = () => {
|
||||
const profile = useAuth((s) => s.profile);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-5 w-full h-screen px-4 sm:px-0 sm:max-w-xl sm:h-auto text-center">
|
||||
<img src="/icon.png" className="w-16 h-16" alt="icon" />
|
||||
<h1 className="text-2xl dark:text-gray-200 font-medium">
|
||||
Welcome to Home Guard!
|
||||
</h1>
|
||||
<p className="text-base dark:text-gray-500">
|
||||
Before you can access your account, we need to first verify your email
|
||||
address and setup your profile.
|
||||
</p>
|
||||
<p className="text-base dark:text-gray-500">
|
||||
You will be prompted to upload your real picture as well.{" "}
|
||||
<Link to="#" className="text-blue-500">
|
||||
Learn more.
|
||||
</Link>
|
||||
</p>
|
||||
<Link to="/verify/email">
|
||||
<Button className="flex items-center gap-2">
|
||||
<ArrowRight />
|
||||
Start Verification
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyStartPage;
|
34
web/src/store/verify.ts
Normal file
34
web/src/store/verify.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { create } from "zustand";
|
||||
import { useAuth } from "./auth";
|
||||
import type { UserProfile } from "@/types";
|
||||
|
||||
export type VerifyStep = "email" | "avatar" | "review";
|
||||
|
||||
export interface IVerifyState {
|
||||
step: VerifyStep | null;
|
||||
|
||||
loadStep: (profile: UserProfile) => void;
|
||||
}
|
||||
|
||||
export const useVerify = create<IVerifyState>((set) => ({
|
||||
step: null,
|
||||
|
||||
loadStep: (profile) => {
|
||||
if (!profile.email_verified) {
|
||||
set({ step: "email" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile.avatar_verified) {
|
||||
set({ step: "avatar" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile.verified) {
|
||||
set({ step: "review" });
|
||||
return;
|
||||
}
|
||||
|
||||
set({ step: null });
|
||||
},
|
||||
}));
|
@ -2,6 +2,9 @@ export interface UserProfile {
|
||||
id: string;
|
||||
full_name: string;
|
||||
email: string;
|
||||
email_verified: boolean;
|
||||
avatar_verified: boolean;
|
||||
verified: boolean;
|
||||
phone_number: string;
|
||||
is_admin: boolean;
|
||||
last_login: string;
|
||||
|
22
web/src/util/maskEmail.ts
Normal file
22
web/src/util/maskEmail.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export default function maskEmail(email: string): string {
|
||||
// Validate email (very basic)
|
||||
if (!email.includes("@")) return email;
|
||||
|
||||
const [user, domain] = email.split("@");
|
||||
const [domainName, ...tldParts] = domain.split(".");
|
||||
|
||||
// Mask user part
|
||||
const maskedUser =
|
||||
user.length > 2 ? `${user[0]}***${user[user.length - 1]}` : `${user[0]}***`;
|
||||
|
||||
// Mask domain part (except TLD)
|
||||
const maskedDomain =
|
||||
domainName.length > 2
|
||||
? `${domainName[0]}***${domainName[domainName.length - 1]}`
|
||||
: `${domainName[0]}***`;
|
||||
|
||||
// Join TLD
|
||||
const tld = tldParts.length ? "." + tldParts.join(".") : "";
|
||||
|
||||
return `${maskedUser}@${maskedDomain}${tld}`;
|
||||
}
|
Reference in New Issue
Block a user