diff --git a/README.md b/README.md index 5ab7e4f..abc7989 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,16 @@ application. The application implements the [Tella Nearby Sharing protocol](https://github.com/Horizontal-org/Tella-P2P-Protocol) with the following endpoints: - Default Port: 53320 -- `POST /api/v1/ping` - Initial handshake for manual connections -- `POST /api/v1/register` - Device registration with PIN authentication -- `POST /api/v1/prepare-upload` - Prepare file transfer session -- `PUT /api/v1/upload` - File upload with binary data -* `POST /api/v1/close-connection` +- `POST /api/v2/ping` - Initial handshake for manual connections +- `POST /api/v2/register` - Device registration with PIN authentication +- `POST /api/v2/prepare-upload` - Prepare file transfer session +- `PUT /api/v2/upload` - File upload with binary data +- `POST /api/v2/close-connection` + +Legacy routes (transition to v2 is incompatible with v1): + +- `POST /api/v1/ping` - returns 400 Client error +- `POST /api/v1/register` - returns 403 Rejected ## Platform-Specific Notes diff --git a/backend/app/app.go b/backend/app/app.go index 45791b4..746f62b 100644 --- a/backend/app/app.go +++ b/backend/app/app.go @@ -112,6 +112,15 @@ func (a *App) ConfirmRegistration() error { return a.registrationHandler.ConfirmRegistration() } +// called as part of manual connection, when the receiver has confirmed the "receiver cert hash verification" by +// pressing button "confirm and continue" +func (a *App) ManualConfirmationReceiverForReceiver() error { + if a.registrationHandler == nil { + return errRegistrationNotInit + } + return a.registrationHandler.SendPingResponse() +} + func (a *App) RejectRegistration() error { if a.registrationHandler == nil { return errRegistrationNotInit diff --git a/backend/core/modules/registration/handler.go b/backend/core/modules/registration/handler.go index 7edb5ca..aa9f295 100644 --- a/backend/core/modules/registration/handler.go +++ b/backend/core/modules/registration/handler.go @@ -32,6 +32,7 @@ type Handler struct { service Service ctx context.Context pendingRegistration *PendingRegistration + pendingPingResponse chan struct{} mu sync.RWMutex } @@ -42,27 +43,69 @@ func NewHandler(service Service, ctx context.Context) *Handler { } } +func (h *Handler) ResetPingResponse() { + if h.pendingPingResponse != nil { + close(h.pendingPingResponse) + } + h.pendingPingResponse = nil +} + +func (h *Handler) Reset() { + h.mu.Lock() + h.ResetPingResponse() + h.pendingRegistration = nil + h.mu.Unlock() +} + func (h *Handler) HandlePing(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } - runtime.EventsEmit(h.ctx, "ping-received", map[string]interface{}{ - "timestamp": time.Now().Unix(), - "message": "Device attempting to connect", - "state": "waiting", - }) + // we only react & respond to the first ping request we receive. + // + // TODO (2026-06-22): would like to tie the confirmation action to the ip doing the ip request, so that we + // can selectively only respond to that ping request. the current frontend structure makes this a bit difficult / fraught. + // + // barring that we decide to only react & respond to the first received ping request. this is also inline with the + // "single connection"-centric model of tella's p2p protocol. i.e. we only respond to ping if sender not + // registered/registration not in progress. + if h.pendingPingResponse == nil { + h.pendingPingResponse = make(chan struct{}) + runtime.EventsEmit(h.ctx, "ping-received", map[string]interface{}{ + "timestamp": time.Now().Unix(), + "message": "Device attempting to connect", + "state": "waiting", + }) + } + // NOTE (2026-06-22): this style of solution only handles one ping response at a time: the first ping received + <-h.pendingPingResponse w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(struct { - Status string `json:"status"` - }{ - Status: "ok", - }) + json.NewEncoder(w).Encode(map[string]bool{"senderShowHash": true}) +} + +func (h *Handler) SendPingResponse() error { + // channel should never be nil here, but then again sometimes 'never' does happen :) + if h.pendingPingResponse != nil { + h.pendingPingResponse <- struct{}{} + } + return nil } -func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { +// rememberSenderFingerprint pins the sender certificate hash. calling changes tls config of package server, making it stricter in terms +// of requiring tls client certs. the tls config change requires restarting the https server instance. +// +// lockAndGetSenderFingerprintCandidate locks the current candidate for sender fingerprint, +// preventing it from being changed without discarding the session and starting over. +// this locking behaviour is intended to limit the attack surface and prevent mismatches in what is presented to a user +// and what may be pinned. +// +// we lock after authorisation to limit attacks that are intended to sabotage the connection +// establishment by prematurely forcing the fingerprint to be locked and thus would prevent an authentic sender from +// having their fingerprint from being chosen. +func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request, rememberSenderFingerprint func (string) error, lockAndGetSenderFingerprintCandidate func() string) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -92,7 +135,34 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { return } - // Store the pending registration + if authorised, err := h.service.IsAuthorised(request.PIN, request.Nonce); !authorised { + if errors.Is(err, ErrPinInvalid) { + http.Error(w, "Invalid PIN", http.StatusUnauthorized) + // TODO (2025-06-22): reset ping channel to allow for another attempt + // TODO (2026-06-22): maybe reset isn't actually needed - does mobile send another ping request if PIN is incorrect + // or does it simply send another register request? + // + // NOTE FOR REVIEWERS: this has been commented out until the team decides whether another connection attempt with + // a change PIN is possible with the current sender interfaces - seems like "discard & start over" renders making another attempt moot + // h.ResetPingResponse() + } + if errors.Is(err, ErrTooManyAttempts) { + http.Error(w, "Too many requests", http.StatusTooManyRequests) + } + return + } + + // do this after authorisation (PIN / nonce check) because getSenderFingerprintCandidate locks the current candidate, + // preventing it from being changed without discarding the session and starting over. this locking behaviour is to limit the attack + // surface of what is presented to a user and what may be pinned + // + // it's only a claimed sender until verification has been mutually confirmed + certificateHashClaimedSender := lockAndGetSenderFingerprintCandidate() + if len(certificateHashClaimedSender) != 64 { + http.Error(w, "Missing required parameters", http.StatusBadRequest) + return + } + h.mu.Lock() h.pendingRegistration = &PendingRegistration{ PIN: request.PIN, @@ -107,6 +177,7 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { "timestamp": time.Now().Unix(), "message": "Sender is requesting to register", "state": "confirm", + "senderCertificateHash": certificateHashClaimedSender, }) // Wait for user confirmation or timeout @@ -117,6 +188,19 @@ func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) { h.mu.Lock() h.pendingRegistration = nil + // if we're sending a successful response it was because the pin was valid! + // since PIN was valid, we can now save the sender's certificate hash and restart the server + // note: since `rememberSenderFingerprint` requires a restart of the https server, we likely have to send the + // response before restarting? + err = rememberSenderFingerprint(certificateHashClaimedSender) + // TODO cblgh(2026-03-15): figure out something less catastrophic for the app than just panic here. but https + // handler is a hard place to recover from the death of the https server ^^' + // + // maybe emit some kind of event to frontend to signal catastrophic failure && need to restart? + // runtime.EventsEmit(h.ctx, "register-request-received", map[string]interface{}{ + if err != nil { + panic(err) + } h.mu.Unlock() case err := <-h.pendingRegistration.Error: diff --git a/backend/core/modules/registration/ports.go b/backend/core/modules/registration/ports.go index fd13156..652a21a 100644 --- a/backend/core/modules/registration/ports.go +++ b/backend/core/modules/registration/ports.go @@ -1,6 +1,7 @@ package registration type Service interface { + IsAuthorised(pin, nonce string) (bool, error) CreateSession(pin string, nonce string) (string, error) SetPINCode(pinCode string) ForgetSession(sessionID string) diff --git a/backend/core/modules/registration/service.go b/backend/core/modules/registration/service.go index 6e63092..a6923ee 100644 --- a/backend/core/modules/registration/service.go +++ b/backend/core/modules/registration/service.go @@ -32,16 +32,26 @@ func NewService(ctx context.Context) Service { } } -func (s *service) CreateSession(pin, nonce string) (string, error) { +var ErrTooManyAttempts = errors.New("Too many invalid attempts") +var ErrPinInvalid = errors.New("Invalid PIN") + +func (s *service) IsAuthorised(pin, nonce string) (bool, error) { // TODO cblgh(2026-02-17): guard ratelimiter with mutex alt. use sync.Map to prevent crash from malicious behaviour? // TODO cblgh(2026-03-11) change this rate limiter to not be on the nonce any more? if s.rateLimiter[nonce] >= 3 { // check this with the team - return "", errors.New("too many invalid attempts") + return false, ErrTooManyAttempts } if pin != s.pinCode { s.rateLimiter[nonce]++ - return "", errors.New("Invalid pin") + return false, ErrPinInvalid + } + return true, nil +} + +func (s *service) CreateSession(pin, nonce string) (string, error) { + if authorised, err := s.IsAuthorised(pin, nonce); !authorised { + return "", err } sessionID := uuid.New().String() diff --git a/backend/core/modules/server/handler.go b/backend/core/modules/server/handler.go index a787618..48a2897 100644 --- a/backend/core/modules/server/handler.go +++ b/backend/core/modules/server/handler.go @@ -24,10 +24,26 @@ func NewHandler( } } -func (h *Handler) SetupRoutes() { - h.mux.HandleFunc("/api/v1/ping", h.registrationHandler.HandlePing) - h.mux.HandleFunc("/api/v1/register", h.registrationHandler.HandleRegister) - h.mux.HandleFunc("/api/v1/prepare-upload", h.transferHandler.HandlePrepare) - h.mux.HandleFunc("/api/v1/upload", h.transferHandler.HandleUpload) - h.mux.HandleFunc("/api/v1/close-connection", h.transferHandler.HandleCloseConnection) +// TODO (2026-06-15): keep /api/v1/ping /api/v1/register around and serve legacy responses to sent queries there +func (h *Handler) SetupRoutes(pinFingerprint func (string) error, getSenderFingerprintCandidate func () string) { + h.mux.HandleFunc("/api/v2/ping", h.registrationHandler.HandlePing) + h.mux.HandleFunc("/api/v2/register", func(res http.ResponseWriter, req *http.Request) { + h.registrationHandler.HandleRegister(res, req, pinFingerprint, getSenderFingerprintCandidate) + }) + h.mux.HandleFunc("/api/v2/prepare-upload", h.transferHandler.HandlePrepare) + h.mux.HandleFunc("/api/v2/upload", h.transferHandler.HandleUpload) + h.mux.HandleFunc("/api/v2/close-connection", h.transferHandler.HandleCloseConnection) + + // handle v1 legacy routes + h.mux.HandleFunc("/api/v1/ping", func (res http.ResponseWriter, req *http.Request) { + // TODO (2026-06-18): collectively decide what to respond to old v1-pings. Will 400 crash clients or is it OK? + http.Error(res, "Bad request", http.StatusBadRequest) + }) + h.mux.HandleFunc("/api/v1/register", func(res http.ResponseWriter, req *http.Request) { + http.Error(res, "Rejected", http.StatusForbidden) + }) + + h.mux.HandleFunc("/", func (res http.ResponseWriter, req *http.Request) { + http.Error(res, "Not found", http.StatusNotFound) + }) } diff --git a/backend/core/modules/server/service.go b/backend/core/modules/server/service.go index 6057edb..0ef3772 100644 --- a/backend/core/modules/server/service.go +++ b/backend/core/modules/server/service.go @@ -5,6 +5,8 @@ import ( crand "crypto/rand" ctls "crypto/tls" "fmt" + "crypto/x509" + "crypto/sha256" "math/big" "net" "net/http" @@ -28,6 +30,11 @@ import ( var log = devlog.Logger("server") type service struct { + pinnedSenderCertificateHash string // pinned fingerprint for sender + // fingerprintCandidate is derived from incoming requests when + // tlsConfig.ClientAuth is set to RequestClientCert pre-mTLS establishment (post mTLS config is set to RequireAnyClientCert) + fingerprintCandidate string + fingerprintCandidateLockedIn bool limitingMiddleware http.Handler nonceManager *nonces.NonceManager limiter *RateLimitingWare @@ -116,6 +123,9 @@ func (s *service) Start(port int) error { return errStart } + // reset registrationHandler state + s.registrationHandler.Reset() + // Generate new PIN for each start s.pin = generateRandomPIN() s.registrationService.SetPINCode(s.pin) @@ -139,11 +149,65 @@ func (s *service) Start(port int) error { Organization: []string{"Tella"}, IPAddresses: ips, }) + if err != nil { log("failed to generate TLS config: %v", err) return errStart } + // do not require any client certs when server is freshly started + tlsConfig.ClientAuth = ctls.RequestClientCert + // NOTE cblgh(2026-03-15): set up a custom cert pool using the pinned cert? + // to allow use of tls.Config.ClientAuth: tls.RequireAndVerifyClientCert + // c.f https://stackoverflow.com/a/63317898 + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + log("number of certs being passed in: %d", len(rawCerts)) + // currently pre-mtls and not strictly requiring cert + if (tlsConfig.ClientAuth == ctls.RequestClientCert && len(rawCerts) == 0) { + return nil + } + // we're post-mtls, this should not occur for legitimate requests + if (tlsConfig.ClientAuth == ctls.RequireAnyClientCert && len(rawCerts) == 0) { + return errors.New("Sender certificate missing") + } + + log("VerifyPeerCertificate called") + + sha256CertHash := sha256.Sum256(rawCerts[0]) + hexSHA256CertHash := fmt.Sprintf("%x", sha256CertHash) + + // in this section we use a mutex because we want to be sure to never set s.fingerprintCandidate to something else while it is being + // fetched by s.GetSenderFingerprintCandidate for being presented to the user + s.mu.Lock() + if (tlsConfig.ClientAuth == ctls.RequestClientCert) { + // prevent multiple register POST (with different senderCertificateHash values) from being sent in succession once + // hash is displayed + // NOTE: this is not our session-long pinning - we haven't definitively set s.pinnedFingerprintCandidate! + // we only set s.pinnedSenderCertificateHash on successful sender certificate hash + // verification i.e. calling s.PinFingerprint() from registration/handler.go + if !s.fingerprintCandidateLockedIn { + // still pre-mtls but we have now have a candidate to use for senderFingerprint. + s.fingerprintCandidate = hexSHA256CertHash + log("sender fingerprint candidate %s", s.fingerprintCandidate) + } else { + if hexSHA256CertHash != s.fingerprintCandidate { + s.mu.Unlock() + return errors.New("Hash of incoming request certificate did not match candidate for sender certificate hash") + } + } + s.mu.Unlock() + return nil + } + s.mu.Unlock() + + // we should only reach this point in the routine once we have established mTLS and have a pinned sender certificate hash + log("incoming cert hash\n%x\n", sha256CertHash) + if hexSHA256CertHash != s.pinnedSenderCertificateHash { + return errors.New("Hash of incoming request certificate did not pinned sender certificate hash") + } + return nil + } + s.tlsConfig = tlsConfig mux := http.NewServeMux() @@ -153,7 +217,7 @@ func (s *service) Start(port int) error { // TODO (2026-02-19): dhekra / iOS closes the server when the transfer is explicitly stopped handler := NewHandler(mux, s.registrationHandler, transferHandler) - handler.SetupRoutes() + handler.SetupRoutes(s.PinFingerprint, s.GetSenderFingerprintCandidate) s.limitingMiddleware = s.limiter.Handler(mux) s.port = port @@ -200,6 +264,44 @@ func (s *service) startServer() error { return nil } +func (s *service) GetSenderFingerprintCandidate() string { + var candidate string + // use mutex because we want to be sure to never set s.fingerprintCandidate to something else while it is being + // fetched for being presented to the user. + // + // to limit attack surface, we only every allow setting one fingerprint + // candidate once the sender fingerprint candidate has been displayed by the user + s.mu.Lock() + candidate = s.fingerprintCandidate + log("sender fingerprint candidate locked to %s", candidate) + s.fingerprintCandidateLockedIn = true + s.mu.Unlock() + return candidate +} + +// PinFingerprint pins the hexadecimal-encoded SHA256 hash of the raw certificate bytes. The TLS config is changed to require a client cert, which necessitates restarting the https server instance. +func (s *service) PinFingerprint(senderFingerprint string) error { + log("Pin fingerprint called") + if len(senderFingerprint) != 64 { + return errors.New("expected fingerprint string length of 64ch") + } + s.pinnedSenderCertificateHash = senderFingerprint + // terminate the previous instance + shutdownCtx, cancel := context.WithTimeout(s.ctx, 1500*time.Millisecond) + defer cancel() + if err := s.server.Shutdown(shutdownCtx); err != nil { + log("Graceful shutdown failed: %v, forcing close\n", err) + } + log("Stopped server & restarting") + // change the tls config to require client certs on connection going forward. + // we use `tls.RequireAnyClientCert` as we do not have the full client cert + // => can't create and pass a cert pool that will allow tls.VerifyCertificate to succeed + s.tlsConfig.ClientAuth = ctls.RequireAnyClientCert + // NOTE cblgh(2026-03-15): we need to allocate a new instance of http.Server and set the updated TLS config on it + // before restarting the server for the config changes to take effect + return s.startServer() +} + func (s *service) Stop(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() @@ -221,6 +323,9 @@ func (s *service) Stop(ctx context.Context) error { s.server = nil s.tlsConfig = nil s.limitingMiddleware = nil + s.pinnedSenderCertificateHash = "" + s.fingerprintCandidate = "" + s.fingerprintCandidateLockedIn = false log("HTTPS Server stopped\n") diff --git a/backend/core/modules/transfer/service.go b/backend/core/modules/transfer/service.go index 1b3ecbd..f4e414a 100644 --- a/backend/core/modules/transfer/service.go +++ b/backend/core/modules/transfer/service.go @@ -225,7 +225,7 @@ func (s *service) AcceptTransfer(sessionID string) error { log("Transfer accepted for session: %s", sessionID) return nil default: - fmt.Errorf("failed to send acceptance response") + log("failed to send acceptance response") return errAccept } } diff --git a/backend/utils/devlog/devlog.go b/backend/utils/devlog/devlog.go index c1fc978..5c1a608 100644 --- a/backend/utils/devlog/devlog.go +++ b/backend/utils/devlog/devlog.go @@ -6,7 +6,7 @@ import ( func Log(usageContext, msg string, args ...interface{}) { if IsDevelop() { - line := fmt.Sprintf(msg) + line := msg if len(args) > 0 { line = fmt.Sprintf(msg, args...) } diff --git a/backend/utils/filestoreutils/filestoreutils.go b/backend/utils/filestoreutils/filestoreutils.go index 6081f84..5a96156 100644 --- a/backend/utils/filestoreutils/filestoreutils.go +++ b/backend/utils/filestoreutils/filestoreutils.go @@ -310,7 +310,6 @@ func ExportSingleFile(db *sql.DB, dbKey []byte, id int64, tvault *os.File, expor decryptedData, fileName, err := decryptAndGetFilename(db, id, dbKey, tvault) if err != nil { return "", err - return "", errExportFile } defer util.SecureZeroMemory(decryptedData) diff --git a/backend/utils/tls/certificate.go b/backend/utils/tls/certificate.go index 4b6146d..f8f465c 100644 --- a/backend/utils/tls/certificate.go +++ b/backend/utils/tls/certificate.go @@ -50,8 +50,8 @@ func GenerateTLSConfig(ctx context.Context, config Config) (*tls.Config, error) //generate hash of certificate hash := sha256.Sum256(cert.Certificate[0]) hashStr := hex.EncodeToString(hash[:]) - log("Hash value: %s", hashStr) - runtime.EventsEmit(ctx, "certificate-hash", hashStr) + log("Receiver certificate hash: %s", hashStr) + runtime.EventsEmit(ctx, "receiver-certificate-hash", hashStr) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8c7afd5..fa0c7f4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,12 +8,10 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@types/qrcode": "^1.5.5", - "dompurify": "^3.3.1", - "qrcode": "^1.5.4", + "dompurify": "^3.4.9", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^7.6.3", + "react-router-dom": "^7.15.1", "styled-components": "^6.1.15" }, "devDependencies": { @@ -71,7 +69,6 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -508,6 +505,9 @@ "version": "24.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -519,21 +519,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/qrcode": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", - "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -619,28 +610,6 @@ "vite": "^3.0.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/browserslist": { "version": "4.24.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", @@ -661,7 +630,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -675,14 +643,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -713,32 +673,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -747,12 +681,15 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/css-color-keywords": { @@ -799,24 +736,10 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", - "license": "(MPL-2.0 OR Apache-2.0)", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", + "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -828,11 +751,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "node_modules/esbuild": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", @@ -1221,18 +1139,6 @@ "node": ">=6" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1268,14 +1174,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -1325,14 +1223,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1365,17 +1255,6 @@ "node": ">=6" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1443,47 +1322,6 @@ "dev": true, "license": "MIT" }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1497,14 +1335,6 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -1539,28 +1369,11 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1573,7 +1386,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1600,10 +1412,9 @@ } }, "node_modules/react-router": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz", - "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", - "license": "MIT", + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", + "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -1622,12 +1433,11 @@ } }, "node_modules/react-router-dom": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz", - "integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==", - "license": "MIT", + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz", + "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==", "dependencies": { - "react-router": "7.6.3" + "react-router": "7.15.1" }, "engines": { "node": ">=20.0.0" @@ -1637,19 +1447,6 @@ "react-dom": ">=18" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -1706,16 +1503,10 @@ "semver": "bin/semver.js" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" }, "node_modules/shallowequal": { "version": "1.1.0", @@ -1740,30 +1531,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/styled-components": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.15.tgz", @@ -1834,7 +1601,10 @@ "node_modules/undici-types": { "version": "7.12.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==" + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/update-browserslist-db": { "version": "1.1.1", @@ -1873,7 +1643,6 @@ "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.15.9", "postcss": "^8.4.18", @@ -1918,68 +1687,12 @@ } } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 641ed25..081edf3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,12 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "@types/qrcode": "^1.5.5", - "dompurify": "^3.3.1", - "qrcode": "^1.5.4", + "dompurify": "^3.4.9", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^7.6.3", + "react-router-dom": "^7.15.1", "styled-components": "^6.1.15" }, "devDependencies": { diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 54c1a95..45e8587 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -7ffc15a19169c7b5683105ae1996cbdb \ No newline at end of file +c9585cde90818cf8406778fed55158bd \ No newline at end of file diff --git a/frontend/src/Components/Auth/Login.tsx b/frontend/src/Components/Auth/Login.tsx index 16061a9..3b285b2 100644 --- a/frontend/src/Components/Auth/Login.tsx +++ b/frontend/src/Components/Auth/Login.tsx @@ -74,7 +74,7 @@ export function Login({ onLoginSuccess, initialError = '' }: LoginProps) { type="submit" disabled={loading} > - {loading ? 'Loading...' : 'LOG IN'} + {loading ? 'Loading...' : 'UNLOCK'} diff --git a/frontend/src/Components/CertificateHash/CertificateHash.tsx b/frontend/src/Components/CertificateHash/CertificateHash.tsx deleted file mode 100644 index 55e80c6..0000000 --- a/frontend/src/Components/CertificateHash/CertificateHash.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// frontend/src/Components/CertificateHash/CertificateHash.tsx -import React, { useState, useEffect } from 'react'; -import { EventsOn } from '../../../wailsjs/runtime/runtime'; -import { log } from "../../util/util" - -interface Props { - serverRunning: boolean; -} - -function formatHash(hashString: string): string { - const input = []; - for (let i = 0; i <= hashString.length; i += 4) { - // grab groups of 4 characters each - let entry = hashString.slice(i, i+4) + " "; - // output a newline after four groups of 4 - if (((i+4) % 16) === 0) { - entry += "\n"; - } - input.push(entry); - } - // trim the last newline and return as a string - return input.join("").trim(); -} - -export function CertificateHash({ serverRunning }: Props) { - const [certHash, setCertHash] = useState(''); - - useEffect(() => { - log("Subscribing to certificate-hash events"); - - const cleanup = EventsOn("certificate-hash", (data) => { - log("Received certificate hash event:", data); - setCertHash(data.toString()); - }); - - return () => { - log("Cleaning up certificate-hash subscription"); - cleanup(); - }; - }, []); - - if (!serverRunning || !certHash) { - return null; - } - - return ( -
-

Certificate Hash:

-
{formatHash(certHash)}
-
- ); -} diff --git a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx index 898e2e4..be7b89f 100644 --- a/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx +++ b/frontend/src/Components/CertificateHash/CertificateVerificationModal.tsx @@ -1,37 +1,74 @@ import styled from 'styled-components'; +import { formatHash } from "../../util/util" +import { SpinnerModal } from "../NearbySharing/SpinnerModal"; +import { ErrorDialog } from "../NearbySharing/ErrorDialog"; + +interface NearbySharingError { + text: string; + button: string; + hasError: boolean; +} interface CertificateVerificationModalProps { isOpen: boolean; - certificateHash: string; - modalState: 'waiting' | 'confirm'; - onConfirm: () => void; + receiverCertificateHash: string; + senderCertificateHash: string; + senderConfirmedReceiver: boolean; + nearbySharingError: NearbySharingError; + modalState: 'CONFIRM_RECEIVER' | 'WAITING_FOR_SENDER_CONFIRM_RECEIVER' | 'CONFIRM_SENDER' | 'WAITING_FOR_SENDER_CONFIRM_SENDER'; + onConfirmReceiverHash: () => void; + onConfirmSenderHash: () => void; onDiscard: () => void; } -function formatHash(hashString: string): string { - const input = []; - for (let i = 0; i <= hashString.length; i += 4) { - // grab groups of 4 characters each - let entry = hashString.slice(i, i+4) + " "; - // output a newline after four groups of 4 - if (((i+4) % 16) === 0) { - entry += "\n"; - } - input.push(entry); - } - // trim the last newline and return as a string - return input.join("").trim(); -} - export function CertificateVerificationModal({ isOpen, - certificateHash, + receiverCertificateHash, + senderCertificateHash, + senderConfirmedReceiver, + nearbySharingError, modalState, - onConfirm, - onDiscard + onDiscard, + onConfirmReceiverHash, + onConfirmSenderHash }: CertificateVerificationModalProps) { if (!isOpen) return null; + const getHashForVerification = () => { + if (modalState === "CONFIRM_RECEIVER") { return receiverCertificateHash } + if (modalState === "CONFIRM_SENDER") { return senderCertificateHash } + return "" + } + + const isWaitingForSender = ( + modalState === "CONFIRM_SENDER" && !senderConfirmedReceiver + ) + + const getStepTitle = () => { + if (modalState === "CONFIRM_RECEIVER") { return "Step 1: Confirm recipient hash" } + if (modalState === "CONFIRM_SENDER") { return "Step 2: Confirm sender hash" } + return "Step X: Confirm Y" + } + + // TODO (2026-06-17): no timeout or error graphic is triggered currently + // TODO (2026-06-18): implement logic somewhere in frontend to not display step 1 after waiting has been started (or step 2 is already being displayed) + // currently this is taken care of by the ping behaviour (only 1 ping request allowed to be responded to) + if (nearbySharingError.hasError) { + return ( + + ) + } + if (isWaitingForSender) { + return ( + + ) + } return ( @@ -39,36 +76,39 @@ export function CertificateVerificationModal({ Verification - - - To ensure that the connection is safe, make sure that the sequence of numbers below matches what is shown on the other device. - - - -
-                {formatHash(certificateHash)}
-            
-
- - - If the sequences do not match, the connection may not be secure and should be discarded. - -
- + + {getStepTitle()} + + + +
+        {formatHash(getHashForVerification())}
+        
+
+ + + Make sure that this sequence matches what is shown on the sender's device. + + + If the sequence on your device does not match the sequence on the sender's device, the connection may not be secure and should be discarded. + + DISCARD AND START OVER - {modalState === 'waiting' ? ( - - WAITING FOR THE SENDER... - - ) : ( - - CONFIRM AND CONNECT - - )} + { modalState === "CONFIRM_RECEIVER" ? ( + + CONFIRM AND CONTINUE + + ) + : ( + + CONFIRM AND CONNECT + + ) + }
@@ -92,17 +132,16 @@ const ModalOverlay = styled.div` const ModalContainer = styled.div` background-color: white; border-radius: 8px; - max-width: 500px; + max-width: 550px; width: 90%; max-height: 80vh; overflow: hidden; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 2rem 4rem 2rem 4rem; `; const ModalHeader = styled.div` - padding: 1.5rem; - border-bottom: 1px solid #e9ecef; - text-align: center; + padding-top: 1.5rem; `; const Title = styled.h2` @@ -112,57 +151,56 @@ const Title = styled.h2` font-weight: 600; `; -const ModalContent = styled.div` - padding: 2rem 1.5rem; - text-align: center; -`; - const Description = styled.p` - color: #6c757d; - margin-bottom: 2rem; + color: #5F6368; + font-weight: 600; font-size: 1rem; line-height: 1.5; `; -const HashContainer = styled.div` - background-color: #f8f9fa; +const ModalFooter = styled.div` + padding: 1.5rem 0; + display: flex; + gap: 1rem; + justify-content: end; +`; + +const Button = styled.button` + padding: 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + text-transform: uppercase; + min-width: 80px; +`; + +const HashContainer = styled.div<{ $isSender?: boolean }>` + background-color: ${props => props.$isSender ? "#071013CC" : "#D9D9D9"}; border: 1px solid #e9ecef; border-radius: 8px; - padding: 1.5rem; + padding: 0rem 1.5rem; + display: flex; + justify-content: center; `; -const HashText = styled.code` +const HashText = styled.div<{ $isSender?: boolean }>` font-family: 'Courier New', monospace; font-size: 1rem; color: #212529; + color: ${props => props.$isSender ? "white" : "black"}; word-break: break-all; line-height: 1.6; `; const Warning = styled.p` - color: #6c757d; - font-size: 0.9rem; + color: #5F6368; + font-size: 1rem; line-height: 1.5; margin: 0; -`; - -const ModalFooter = styled.div` - padding: 1.5rem; - border-top: 1px solid #e9ecef; - display: flex; - gap: 1rem; - justify-content: center; -`; - -const Button = styled.button` - padding: 0.75rem 1.5rem; - border-radius: 4px; - font-size: 0.875rem; - font-weight: 600; - cursor: pointer; - transition: background-color 0.2s; - text-transform: uppercase; - min-width: 140px; + padding-top: 1rem; + text-align: left; `; const DiscardButton = styled(Button)` diff --git a/frontend/src/Components/CertificateHash/index.ts b/frontend/src/Components/CertificateHash/index.ts deleted file mode 100644 index 430fdd0..0000000 --- a/frontend/src/Components/CertificateHash/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CertificateHash' \ No newline at end of file diff --git a/frontend/src/Components/NearbySharing/Connect.tsx b/frontend/src/Components/NearbySharing/Connect.tsx index 533321f..151f00e 100644 --- a/frontend/src/Components/NearbySharing/Connect.tsx +++ b/frontend/src/Components/NearbySharing/Connect.tsx @@ -2,129 +2,72 @@ import styled from 'styled-components'; import { useState, useEffect } from 'react'; import { PinDisplay } from "../PinDisplay"; import { GetServerPIN, GetDefaultPort } from '../../../wailsjs/go/app/App'; -import QRCode from 'qrcode'; -import qrIcon from "../../assets/images/icons/qr.svg"; import phoneIcon from "../../assets/images/icons/phone.svg"; interface ConnectStepProps { serverRunning: boolean; localIPs: string[]; certificateHash: string; - isQRMode: boolean; - onModeChange?: (isQRMode: boolean) => void; } -export function ConnectStep({ serverRunning, localIPs, certificateHash, isQRMode, onModeChange }: ConnectStepProps) { - const [qrCodeDataURL, setQrCodeDataURL] = useState(''); +export function ConnectStep({ serverRunning, localIPs, certificateHash}: ConnectStepProps) { const [pin, setPin] = useState(''); const [serverPort, setServerPort] = useState(-1); useEffect(() => { - const generateQR = async () => { - const defaultPort = await GetDefaultPort(); - setServerPort(defaultPort); - - if (serverRunning && localIPs.length > 0 && certificateHash && pin) { - try { - const qrData = { - ip_address: localIPs, - port: defaultPort, - certificate_hash: certificateHash, - pin: pin - }; - - const dataURL = await QRCode.toDataURL(JSON.stringify(qrData)); - setQrCodeDataURL(dataURL); - } catch (error) { - console.error('Failed to generate QR code:', error); - } + const setPort = async () => { + const defaultPort = await GetDefaultPort(); + setServerPort(defaultPort); } - }; - const fetchPIN = async () => { - if (serverRunning) { - try { - const currentPIN = await GetServerPIN(); - setPin(currentPIN); - } catch (error) { - console.error('Failed to get PIN:', error); - } - } - }; - - fetchPIN(); - generateQR(); + const fetchPIN = async () => { + if (serverRunning) { + try { + const currentPIN = await GetServerPIN(); + setPin(currentPIN); + } catch (error) { + console.error('Failed to get PIN:', error); + } + } + }; + setPort() + fetchPIN(); }, [serverRunning, localIPs, certificateHash, pin]); - function toggleQRCode() { - const newMode = !isQRMode; - onModeChange?.(newMode); - } return ( - {isQRMode - ? "Show this QR code for the sender to scan." - : "The sender needs to input the following to connect to your device." - } + The sender should input the following information in Tella on their phone. - {isQRMode ? ( - - Your QR code - - ): ( Your device information - )} - - {isQRMode ? ( - - {qrCodeDataURL ? ( - - ) : ( -
Generating QR code...
- )} -
- ) : ( - <> + <> - IP ADDRESS - {localIPs.join(', ')} + IP addresses + {localIPs.join(', ')} - PIN - + PIN + - Port - {serverPort} + Port + {serverPort} - - )} - - - {isQRMode - ? "Having trouble with the QR code?" - : "Go back to automatic connection" - } - - - - {isQRMode ? 'CONNECT MANUALLY' : 'SEE QR CODE'} - +
- You will automatically move to the next screen as soon as the connection with the sender is established + You will automatically move to the next screen as soon as the connection with the sender is established.
); @@ -137,8 +80,8 @@ const StepContent = styled.div` `; const StepTitle = styled.h2` - font-size: 1.2rem; - font-weight: 600; + font-size: 1rem; + font-weight: 400; color: ##5F6368; margin-bottom: 2rem; `; @@ -148,6 +91,7 @@ const DeviceInfoCard = styled.div` border-radius: 8px; margin-bottom: 2rem; text-align: left; + padding-bottom: 1rem; `; const DeviceInfoHeader = styled.div` @@ -199,54 +143,12 @@ const BackToAutoButton = styled.p` padding-top: 1.5rem; `; -const QRCodeButton = styled.button` - background: none; - border: 1px solid #6c757d; - color: #8B8E8F; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - font-size: 0.875rem; - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0 auto 2rem; - font-weight: 700; - - &:hover { - background-color: #f8f9fa; - } -`; - const AutoMoveText = styled.p` font-size: 0.875rem; color: #6c757d; font-style: italic; `; -const QRCodeContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - padding: 1rem; -`; - -const QRCodeImage = styled.img` - max-width: 150px; - width: 100%; - height: auto; -`; - -const QRIcon = styled.div` - width: 1.5rem; - height: 1.5rem; - flex-shrink: 0; - background-image: url(${qrIcon}); - background-size: contain; - background-repeat: no-repeat; - background-position: center; -`; - const IconTitleContainer = styled.div` display: flex; align-items: center; diff --git a/frontend/src/Components/NearbySharing/ErrorDialog.tsx b/frontend/src/Components/NearbySharing/ErrorDialog.tsx new file mode 100644 index 0000000..50632a6 --- /dev/null +++ b/frontend/src/Components/NearbySharing/ErrorDialog.tsx @@ -0,0 +1,103 @@ +import styled, { keyframes } from 'styled-components'; + +interface ErrorDialogProps { + onClose: () => void; + text: string; + buttonLabel: string; +} + +export function ErrorDialog({ + onClose, text, buttonLabel +}: ErrorDialogProps) { + return ( + + + + Connection failed + + + + {text} + + + + + + {buttonLabel} + + + + + ); +} + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`; + +const ModalContainer = styled.div` + background-color: white; + border-radius: 8px; + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow: hidden; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 2rem 4rem 2rem 4rem; +`; + +const ModalHeader = styled.div` + padding-top: 1.5rem; +`; + +const Title = styled.h2` + margin: 0; + color: #212529; + font-size: 1.5rem; + font-weight: 600; +`; + +const Description = styled.p` + color: #6c757d; + margin-bottom: 2rem; + font-size: 1rem; + line-height: 1.5; +`; + +const ModalFooter = styled.div` + padding: 1.5rem 0; + display: flex; + gap: 1rem; + justify-content: end; +`; + +const Button = styled.button` + padding: 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + text-transform: uppercase; + min-width: 80px; +`; + +const CancelButton = styled(Button)` + text-transform: uppcase; + background-color: white; + color: #6c757d; + border: 1px solid #D9D9D9; + + &:hover { + background-color: #f8f9fa; + } +`; diff --git a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx index 95eb8ae..d1e474d 100644 --- a/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/Hooks/useNearbySharing.tsx @@ -1,12 +1,12 @@ import { useState, useEffect, useRef } from "react"; import { useNavigate } from 'react-router-dom'; -import { GetLocalIPs, RejectRegistration, ConfirmRegistration, StopTransfer } from "../../../../wailsjs/go/app/App"; +import { GetLocalIPs, RejectRegistration, ManualConfirmationReceiverForReceiver, ConfirmRegistration, StopTransfer } from "../../../../wailsjs/go/app/App"; import { EventsOn } from "../../../../wailsjs/runtime/runtime"; import { useServer } from "../../../Contexts/ServerContext"; import { log } from "../../../util/util" type FlowStep = 'intro' | 'connect' | 'accept' | 'receive' | 'results'; -type ModalState = 'waiting' | 'confirm'; +type ManualConfirmationState = 'CONFIRM_RECEIVER' | 'CONFIRM_SENDER' interface FileInfo { id: string; @@ -15,6 +15,12 @@ interface FileInfo { fileType: string; } +interface NearbySharingError { + text: string; + button: string; + hasError: boolean; +} + interface TransferData { sessionId: string; title: string; @@ -38,6 +44,9 @@ export function useNearbySharing() { // Network state const [localIPs, setLocalIPs] = useState([]); + + // Error state text + const [nearbySharingError, setNearbySharingError] = useState({ text: "", button: "", hasError: false}); // Transfer state const [currentSessionId, setCurrentSessionId] = useState(''); @@ -45,12 +54,10 @@ export function useNearbySharing() { // Certificate verification state const [showVerificationModal, setShowVerificationModal] = useState(false); - const [certificateHash, setCertificateHash] = useState(''); - const [modalState, setModalState] = useState('waiting'); - - // Connection mode state - const [isUsingQRMode, setIsUsingQRMode] = useState(true); - const isUsingQRModeRef = useRef(true); + const [receiverCertificateHash, setReceiverCertificateHash] = useState(''); + const [senderCertificateHash, setSenderCertificateHash] = useState(''); + const [modalState, setModalState] = useState('CONFIRM_RECEIVER'); + const [senderConfirmedReceiver, setSenderConfirmedReceiver] = useState(false) // Initialize network info and event listeners useEffect(() => { @@ -60,45 +67,35 @@ export function useNearbySharing() { setLocalIPs(ips); } catch (error) { console.error('Failed to get network info:', error); + // TODO (2026-06-18): make sure that this will actually be seen (currently part of CertVerificationModal!) + setNearbySharingError({ text: "Failed to get network info and could not start server.", button: "Start over", hasError: true } as NearbySharingError) } }; fetchNetworkInfo(); const cleanupPingListener = EventsOn("ping-received", (data) => { - log("Ping received from iOS device:", data); + log("Ping received:", data); setShowVerificationModal(true); - setModalState('waiting') + setModalState('CONFIRM_RECEIVER') + }); + + // TODO (2026-06-18): actually emit 'nearby-sharing-error' somewhere in the backend + const cleanupErrorListener = EventsOn("nearby-sharing-error", (data) => { + let err = data as NearbySharingError + err.hasError = true + setNearbySharingError(err) }); const cleanupRegisterListener = EventsOn("register-request-received", (data) => { log("Register request received:", data); - log("Current QR mode:", isUsingQRModeRef.current); - - if (isUsingQRModeRef.current) { - log("🔗 QR mode active - auto-confirming registration"); - // Auto-confirm for QR mode - ConfirmRegistration() - .then(() => { - log("✅ QR registration confirmed successfully"); - setCurrentStep('accept'); - }) - .catch((error) => { - console.error("❌ Failed to auto-confirm QR registration:", error); - // Fall back to manual confirmation - setModalState('confirm'); - setShowVerificationModal(true); - }); - } else { - log("📱 Manual mode - showing confirmation modal"); - // Manual mode - show certificate verification modal - setModalState('confirm'); - } + setSenderCertificateHash(data.senderCertificateHash); + setSenderConfirmedReceiver(true); }); - const cleanupCertListener = EventsOn("certificate-hash", (data) => { - log("Certificate hash received:", data); - setCertificateHash(data.toString()); + const cleanupCertListener = EventsOn("receiver-certificate-hash", (data) => { + log("Receiver Certificate hash received:", data); + setReceiverCertificateHash(data.toString()); }); const cleanupPrepareRequest = EventsOn("prepare-upload-request", (data) => { @@ -108,6 +105,9 @@ export function useNearbySharing() { setCurrentSessionId(requestData.sessionId); }); + // TODO (2026-06-22): implement event in backend and handler here in frontend that signals that register timed out or + // max PIN registration attempts has been reached + const cleanupFileReceived = EventsOn("file-received", () => { setTransferData(prev => { if (prev !== null) { @@ -133,6 +133,7 @@ export function useNearbySharing() { return () => { cleanupFileReceived(); cleanupPingListener(); + cleanupErrorListener(); cleanupRegisterListener(); cleanupCertListener(); cleanupPrepareRequest(); @@ -153,9 +154,14 @@ export function useNearbySharing() { return await stopServer(); }; - // Certificate verification handlers + // {Receiver, Sender} Certificate Hash verification handlers + const handleReceiverConfirmReceiver = async () => { + await ManualConfirmationReceiverForReceiver() + setModalState("CONFIRM_SENDER") + } + const handleVerificationConfirm = async () => { - log("✅ Certificate verification CONFIRMED"); + log("✅ Sender Certificate Hash: verification CONFIRMED"); try { await ConfirmRegistration(); setShowVerificationModal(false); @@ -168,15 +174,18 @@ export function useNearbySharing() { }; const handleVerificationDiscard = async () => { - log("❌ Certificate verification DISCARDED"); + log("❌ Verification DISCARDED"); try { await RejectRegistration(); } catch (error) { console.error("Failed to reject registration:", error); } + // reset state setShowVerificationModal(false); - setModalState('waiting'); + setModalState('CONFIRM_RECEIVER'); + setSenderConfirmedReceiver(false); + await handleStopServer(); setCurrentStep('intro'); }; @@ -252,11 +261,11 @@ export function useNearbySharing() { setCurrentSessionId(''); setTransferData(null); setShowVerificationModal(false); - setCertificateHash(''); - setModalState('waiting'); + setReceiverCertificateHash(''); + setSenderCertificateHash(''); + setModalState('CONFIRM_RECEIVER'); setCurrentStep('intro'); - setIsUsingQRMode(true); - isUsingQRModeRef.current = true; + setSenderConfirmedReceiver(false); }; return { @@ -267,23 +276,17 @@ export function useNearbySharing() { localIPs, currentSessionId, transferData, + nearbySharingError, showVerificationModal, - certificateHash, + receiverCertificateHash, + senderCertificateHash, + senderConfirmedReceiver, modalState, - isUsingQRMode, - - // State setters - - // QR mode handler - handleQRModeChange: (isQR: boolean) => { - setIsUsingQRMode(isQR); - isUsingQRModeRef.current = isQR; - log("QR mode changed to:", isQR); - }, - + // Actions handleBack, handleContinue, + handleReceiverConfirmReceiver, handleVerificationConfirm, handleVerificationDiscard, handleFileRequestAccept, diff --git a/frontend/src/Components/NearbySharing/NearbySharing.tsx b/frontend/src/Components/NearbySharing/NearbySharing.tsx index 8128fab..fb2b7ad 100644 --- a/frontend/src/Components/NearbySharing/NearbySharing.tsx +++ b/frontend/src/Components/NearbySharing/NearbySharing.tsx @@ -7,6 +7,7 @@ import { ConnectStep } from "./Connect"; import { IntroStep } from "./Intro"; import { ResultsStep } from "./Results"; import { useNearbySharing } from "./Hooks/useNearbySharing" +import { log } from "../../util/util" export function NearbySharing() { const { @@ -15,15 +16,16 @@ export function NearbySharing() { localIPs, currentSessionId, transferData, + nearbySharingError, showVerificationModal, - certificateHash, + receiverCertificateHash, + senderCertificateHash, + senderConfirmedReceiver, modalState, - isUsingQRMode, - - handleQRModeChange, handleContinue, handleVerificationConfirm, + handleReceiverConfirmReceiver, handleVerificationDiscard, handleFileRequestAccept, handleFileRequestReject, @@ -54,9 +56,7 @@ export function NearbySharing() { )} @@ -91,15 +91,21 @@ export function NearbySharing() { ); } + // TODO (2026-06-18): add CertificateVerificationModal inside one of the steps? instead of free-floating + const Container = styled.div` display: flex; @@ -112,20 +118,16 @@ const Container = styled.div` const Header = styled.div` display: flex; align-items: center; - justify-content: flex-start; + justify-content: center; position: relative; - padding: 1rem 2rem; + padding: 2rem 2rem 0rem 2rem; background-color: white; - border-bottom: 1px solid #CFCFCF; `; const Title = styled.h1` - position: absolute; - left: 50%; - transform: translateX(-50%); font-size: 1.5rem; - font-weight: 600; - color: #212529; + font-weight: 700; + color: ##404040; margin: 0; `; diff --git a/frontend/src/Components/NearbySharing/SpinnerModal.tsx b/frontend/src/Components/NearbySharing/SpinnerModal.tsx new file mode 100644 index 0000000..be1bc4a --- /dev/null +++ b/frontend/src/Components/NearbySharing/SpinnerModal.tsx @@ -0,0 +1,131 @@ +import styled, { keyframes } from 'styled-components'; + +interface SpinnerModalProps { + onCancel: () => void; +} + +export function SpinnerModal({ + onCancel +}: SpinnerModalProps) { + return ( + + + + Verification + + + + Waiting for the sender to also confirm. + + + + + + + + + + + + + CANCEL + + + + + ); +} + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`; + +const ModalContainer = styled.div` + background-color: white; + border-radius: 8px; + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow: hidden; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 2rem 4rem 2rem 4rem; +`; + +const ModalHeader = styled.div` + padding-top: 1.5rem; +`; + +const Title = styled.h2` + margin: 0; + color: #212529; + font-size: 1.5rem; + font-weight: 600; +`; + +const Description = styled.p` + color: #6c757d; + margin-bottom: 2rem; + font-size: 1rem; + line-height: 1.5; +`; + +const ModalFooter = styled.div` + padding: 1.5rem 0; + display: flex; + gap: 1rem; + justify-content: end; +`; + +const Button = styled.button` + padding: 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + text-transform: uppercase; + min-width: 80px; +`; + +const CancelButton = styled(Button)` + background-color: white; + color: #6c757d; + border: 1px solid #D9D9D9; + + &:hover { + background-color: #f8f9fa; + } +`; + +const SpinnerContainer = styled.div` + text-align: center; +`; + +const spinnerAnimOne = keyframes` + 100% {transform:rotate(360deg)} +`; + +const spinnerAnimTwo = keyframes` + 0% {stroke-dasharray:0 150;stroke-dashoffset:0} + 47.5% {stroke-dasharray:42 150;stroke-dashoffset:-16} + 95%,100%{stroke-dasharray:42 150;stroke-dashoffset:-59} +`; + +const SpinnerG = styled.g` + transform-origin:center; + animation:${spinnerAnimOne} 2s linear infinite +`; + +const SpinnerCircle = styled.circle` + stroke-linecap:round;animation:${spinnerAnimTwo} 1.5s ease-in-out infinite +`; + diff --git a/frontend/src/Components/NearbySharing/StepIndicator.tsx b/frontend/src/Components/NearbySharing/StepIndicator.tsx index c84b7a7..cd55e52 100644 --- a/frontend/src/Components/NearbySharing/StepIndicator.tsx +++ b/frontend/src/Components/NearbySharing/StepIndicator.tsx @@ -31,6 +31,7 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) { } }; + // TODO (2026-06-17): add centered visual line connecting each interior step circle return ( {FLOW_STEPS.map((step, index) => { @@ -129,4 +130,4 @@ const StepConnector = styled.div` height: 1px; background-color: #CFCFCF; z-index: -1; -`; \ No newline at end of file +`; diff --git a/frontend/src/util/util.ts b/frontend/src/util/util.ts index e4c2d38..96d4cdc 100644 --- a/frontend/src/util/util.ts +++ b/frontend/src/util/util.ts @@ -12,3 +12,18 @@ export async function log(msg: string, ...rest: any[]): Promise { console.log(msg, ...rest) } } + +export function formatHash(hashString: string): string { + const input = []; + for (let i = 0; i <= hashString.length; i += 4) { + // grab groups of 4 characters each + let entry = hashString.slice(i, i+4) + " "; + // output a newline after four groups of 4 + if (((i+4) % 16) === 0) { + entry += "\n"; + } + input.push(entry); + } + // trim the last newline and return as a string + return input.join("").trim(); +} diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index 844e386..bdefb0a 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -35,6 +35,8 @@ export function IsServerRunning():Promise; export function LockApp():Promise; +export function ManualConfirmationReceiverForReceiver():Promise; + export function RejectRegistration():Promise; export function RejectTransfer(arg1:string):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index c83fcaf..26f25f0 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -66,6 +66,10 @@ export function LockApp() { return window['go']['app']['App']['LockApp'](); } +export function ManualConfirmationReceiverForReceiver() { + return window['go']['app']['App']['ManualConfirmationReceiverForReceiver'](); +} + export function RejectRegistration() { return window['go']['app']['App']['RejectRegistration'](); }