new eye icon to show/hide proton mail password during initial entry, fix proton re-linking, new release

This commit is contained in:
Egor 2026-03-28 14:25:55 -07:00
parent d5509bb231
commit 4ad7255fc2
9 changed files with 100 additions and 6 deletions

View file

@ -47,7 +47,25 @@ install-tools:
echo "signal-cli installed successfully to /usr/local/bin/signal-cli" echo "signal-cli installed successfully to /usr/local/bin/signal-cli"
check-updates: check-updates:
@go list -u -m -f '{{if not .Indirect}}{{.Path}} {{.Version}}{{if .Update}} [{{.Update.Version}}]{{end}}{{end}}' all | grep "\[" || echo "All dependencies are up to date" @echo "=== Go module updates ==="
@go list -u -m -f '{{if not .Indirect}}{{.Path}} {{.Version}}{{if .Update}} -> {{.Update.Version}}{{end}}{{end}}' all | grep " -> " || echo "All Go dependencies are up to date"
@echo ""
@echo "=== Dockerfile base image updates ==="
@for image in $$(grep -E '^FROM ' Dockerfile | awk '{print $$2}' | grep -v 'AS'); do \
echo "Checking $$image..."; \
current_digest=$$(docker inspect --format='{{index .RepoDigests 0}}' $$image 2>/dev/null | cut -d@ -f2); \
docker pull -q $$image > /dev/null 2>&1; \
latest_digest=$$(docker inspect --format='{{index .RepoDigests 0}}' $$image 2>/dev/null | cut -d@ -f2); \
if [ -z "$$current_digest" ]; then \
echo " $$image: pulled (no prior local image to compare)"; \
elif [ "$$current_digest" = "$$latest_digest" ]; then \
echo " $$image: up to date"; \
else \
echo " $$image: UPDATE AVAILABLE"; \
echo " local: $$current_digest"; \
echo " latest: $$latest_digest"; \
fi; \
done
release: release:
@if [ ! -f VERSION ]; then \ @if [ ! -f VERSION ]; then \

View file

@ -1 +1 @@
1.1.0 1.1.1

View file

@ -503,6 +503,44 @@ details[open] > .integration-header::before {
font-size: 1rem; font-size: 1rem;
} }
/* Password Toggle */
.password-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-wrapper input {
width: 100%;
padding-right: 2.5rem;
}
.password-toggle {
position: absolute;
right: 0.5rem;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--text-secondary);
display: flex;
align-items: center;
transition: color 0.2s;
}
.password-toggle:hover {
color: var(--text-primary);
}
.eye-icon {
width: 1.25rem;
height: 1.25rem;
}
.eye-hide {
display: none; /* overridden by inline style after first toggle */
}
/* Buttons */ /* Buttons */
.btn-primary, .btn-primary,
.btn-secondary, .btn-secondary,

View file

@ -18,9 +18,19 @@ document.addEventListener('DOMContentLoaded', () => {
else if (action === 'delete-telegram') deleteTelegram(btn); else if (action === 'delete-telegram') deleteTelegram(btn);
else if (action === 'delete-proton') deleteProton(btn); else if (action === 'delete-proton') deleteProton(btn);
else if (action === 'reload') reloadIntegrations(); else if (action === 'reload') reloadIntegrations();
else if (action === 'toggle-password') togglePassword(btn);
}); });
}); });
function togglePassword(btn) {
const input = btn.closest('.password-wrapper').querySelector('input');
const isHidden = input.type === 'password';
input.type = isHidden ? 'text' : 'password';
btn.querySelector('.eye-show').style.display = isHidden ? 'none' : 'block';
btn.querySelector('.eye-hide').style.display = isHidden ? 'block' : 'none';
btn.setAttribute('aria-label', isHidden ? 'Hide password' : 'Show password');
}
function reloadIntegrations() { function reloadIntegrations() {
const integrations = document.getElementById('integrations'); const integrations = document.getElementById('integrations');
if (integrations) { if (integrations) {

View file

@ -21,6 +21,7 @@ func (m *Monitor) authenticateAndSetup(credStore *credentials.Store) error {
c := &protonmail.Client{ c := &protonmail.Client{
RootURL: protonAPIURL, RootURL: protonAPIURL,
AppVersion: protonAppVersion, AppVersion: protonAppVersion,
Debug: m.cfg != nil && m.cfg.VerboseLogging,
} }
var auth *protonmail.Auth var auth *protonmail.Auth

View file

@ -43,7 +43,7 @@ func (p *Integration) RegisterRoutes(router *chi.Mux, auth func(http.Handler) ht
} }
} }
RegisterRoutes(router, p.Handlers, auth, p.db, p.apiKey, logger, p) RegisterRoutes(router, p.Handlers, auth, p.db, p.apiKey, logger, p, p.cfg)
} }
func (p *Integration) Start(ctx context.Context, logger *slog.Logger) { func (p *Integration) Start(ctx context.Context, logger *slog.Logger) {

View file

@ -31,6 +31,7 @@ type Monitor struct {
startTime time.Time startTime time.Time
consecutiveErrs int consecutiveErrs int
lastConnected time.Time lastConnected time.Time
cancelPoll context.CancelFunc
} }
func NewMonitor(cfg *config.Config, logger *slog.Logger) *Monitor { func NewMonitor(cfg *config.Config, logger *slog.Logger) *Monitor {
@ -40,7 +41,17 @@ func NewMonitor(cfg *config.Config, logger *slog.Logger) *Monitor {
} }
} }
func (m *Monitor) Stop() {
if m.cancelPoll != nil {
m.cancelPoll()
m.cancelPoll = nil
}
m.client = nil
}
func (m *Monitor) Start(ctx context.Context, credStore *credentials.Store, publisher *delivery.Publisher) error { func (m *Monitor) Start(ctx context.Context, credStore *credentials.Store, publisher *delivery.Publisher) error {
m.Stop()
m.dispatcher = publisher m.dispatcher = publisher
if err := m.authenticateAndSetup(credStore); err != nil { if err := m.authenticateAndSetup(credStore); err != nil {
return err return err
@ -54,7 +65,9 @@ func (m *Monitor) Start(ctx context.Context, credStore *credentials.Store, publi
m.lastConnected = time.Now() m.lastConnected = time.Now()
m.unseenMessageIDs = make(map[string]time.Time) m.unseenMessageIDs = make(map[string]time.Time)
go m.pollEvents(ctx) pollCtx, cancel := context.WithCancel(ctx)
m.cancelPoll = cancel
go m.pollEvents(pollCtx)
return nil return nil
} }

View file

@ -8,6 +8,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"prism/service/config"
"prism/service/credentials" "prism/service/credentials"
"prism/service/util" "prism/service/util"
@ -23,6 +24,7 @@ type authHandler struct {
apiKey string apiKey string
logger *slog.Logger logger *slog.Logger
integration *Integration integration *Integration
cfg *config.Config
} }
type protonAuthRequest struct { type protonAuthRequest struct {
@ -46,6 +48,7 @@ func (h *authHandler) handleAuth(w http.ResponseWriter, r *http.Request) {
c := &protonmail.Client{ c := &protonmail.Client{
RootURL: protonAPIURL, RootURL: protonAPIURL,
AppVersion: protonAppVersion, AppVersion: protonAppVersion,
Debug: h.cfg != nil && h.cfg.VerboseLogging,
} }
authInfo, err := c.AuthInfo(req.Email) authInfo, err := c.AuthInfo(req.Email)
if err != nil { if err != nil {
@ -141,12 +144,16 @@ func (h *authHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
return return
} }
if h.integration != nil {
h.integration.monitor.Stop()
}
util.SetToast(w, "Proton Mail unlinked", "success") util.SetToast(w, "Proton Mail unlinked", "success")
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}) json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
} }
func RegisterRoutes(router *chi.Mux, handlers *Handlers, auth func(http.Handler) http.Handler, db *sql.DB, apiKey string, logger *slog.Logger, integration *Integration) { func RegisterRoutes(router *chi.Mux, handlers *Handlers, auth func(http.Handler) http.Handler, db *sql.DB, apiKey string, logger *slog.Logger, integration *Integration, cfg *config.Config) {
if handlers == nil { if handlers == nil {
return return
} }
@ -159,6 +166,7 @@ func RegisterRoutes(router *chi.Mux, handlers *Handlers, auth func(http.Handler)
apiKey: apiKey, apiKey: apiKey,
logger: logger, logger: logger,
integration: integration, integration: integration,
cfg: cfg,
} }
router.With(auth).Get("/fragment/proton", handlers.HandleFragment) router.With(auth).Get("/fragment/proton", handlers.HandleFragment)

View file

@ -9,7 +9,13 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="proton-password">Password:</label> <label for="proton-password">Password:</label>
<input type="password" id="proton-password" name="password" placeholder="Your Proton password" required> <div class="password-wrapper">
<input type="password" id="proton-password" name="password" placeholder="Your Proton password" required>
<button type="button" class="password-toggle" data-action="toggle-password" aria-label="Show password">
<svg class="eye-icon eye-show" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
<svg class="eye-icon eye-hide" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-10-7-10-7a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 10 7 10 7a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
</button>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="proton-totp">2FA Code (optional):</label> <label for="proton-totp">2FA Code (optional):</label>