chore: update CHANGELOG.md for version 0.11.1-alpha, highlighting secure sign-in improvements and update package version
This commit is contained in:
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.11.1-alpha] - 2026-02-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Safer sign-in from the Ciphera hub.** When you open Pulse from the Ciphera Apps page, your credentials are no longer visible in the browser address bar. Sign-in now uses a secure one-time code that expires in seconds, so your session stays private even if someone sees your screen or browser history.
|
||||||
|
|
||||||
## [0.11.0-alpha] - 2026-02-22
|
## [0.11.0-alpha] - 2026-02-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -168,7 +174,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.11.0-alpha...HEAD
|
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.11.1-alpha...HEAD
|
||||||
|
[0.11.1-alpha]: https://github.com/ciphera-net/pulse/compare/v0.11.0-alpha...v0.11.1-alpha
|
||||||
[0.11.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.10.0-alpha...v0.11.0-alpha
|
[0.11.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.10.0-alpha...v0.11.0-alpha
|
||||||
[0.10.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.9.0-alpha...v0.10.0-alpha
|
[0.10.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.9.0-alpha...v0.10.0-alpha
|
||||||
[0.9.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.8.0-alpha...v0.9.0-alpha
|
[0.9.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.8.0-alpha...v0.9.0-alpha
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface UserPayload {
|
|||||||
/** Error type returned to client for mapping to user-facing copy (no sensitive details). */
|
/** Error type returned to client for mapping to user-facing copy (no sensitive details). */
|
||||||
export type AuthExchangeErrorType = 'network' | 'expired' | 'invalid' | 'server'
|
export type AuthExchangeErrorType = 'network' | 'expired' | 'invalid' | 'server'
|
||||||
|
|
||||||
export async function exchangeAuthCode(code: string, codeVerifier: string, redirectUri: string) {
|
export async function exchangeAuthCode(code: string, codeVerifier: string | null, redirectUri: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${AUTH_API_URL}/oauth/token`, {
|
const res = await fetch(`${AUTH_API_URL}/oauth/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -45,7 +45,7 @@ export async function exchangeAuthCode(code: string, codeVerifier: string, redir
|
|||||||
code,
|
code,
|
||||||
client_id: 'pulse-app',
|
client_id: 'pulse-app',
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
code_verifier: codeVerifier,
|
code_verifier: codeVerifier || '',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { logger } from '@/lib/utils/logger'
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { useAuth } from '@/lib/auth/context'
|
import { useAuth } from '@/lib/auth/context'
|
||||||
import { AUTH_URL, default as apiRequest } from '@/lib/api/client'
|
import { AUTH_URL, default as apiRequest } from '@/lib/api/client'
|
||||||
import { exchangeAuthCode, setSessionAction } from '@/app/actions/auth'
|
import { exchangeAuthCode } from '@/app/actions/auth'
|
||||||
import { authMessageFromErrorType, type AuthErrorType } from '@ciphera-net/ui'
|
import { authMessageFromErrorType, type AuthErrorType } from '@ciphera-net/ui'
|
||||||
import { LoadingOverlay } from '@ciphera-net/ui'
|
import { LoadingOverlay } from '@ciphera-net/ui'
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ function AuthCallbackContent() {
|
|||||||
const code = searchParams.get('code')
|
const code = searchParams.get('code')
|
||||||
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
||||||
const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/auth/callback' : ''
|
const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/auth/callback' : ''
|
||||||
if (!code || !codeVerifier) return
|
if (!code) return
|
||||||
const result = await exchangeAuthCode(code, codeVerifier, redirectUri)
|
const result = await exchangeAuthCode(code, codeVerifier, redirectUri)
|
||||||
if (result.success && result.user) {
|
if (result.success && result.user) {
|
||||||
// * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
|
// * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
|
||||||
@@ -47,59 +47,25 @@ function AuthCallbackContent() {
|
|||||||
}, [searchParams, login, router])
|
}, [searchParams, login, router])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// * Prevent double execution (React Strict Mode or fast re-renders)
|
|
||||||
if (processedRef.current && !isRetrying) return
|
if (processedRef.current && !isRetrying) return
|
||||||
|
|
||||||
// * Check for direct token passing (from auth-frontend direct login)
|
|
||||||
// * This flow exposes tokens in URL, kept for legacy support.
|
|
||||||
// * Recommended: Use Authorization Code flow (below)
|
|
||||||
const token = searchParams.get('token')
|
|
||||||
const refreshToken = searchParams.get('refresh_token')
|
|
||||||
|
|
||||||
if (token && refreshToken) {
|
|
||||||
processedRef.current = true
|
|
||||||
const handleDirectTokens = async () => {
|
|
||||||
const result = await setSessionAction(token, refreshToken)
|
|
||||||
if (result.success && result.user) {
|
|
||||||
// * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
|
|
||||||
try {
|
|
||||||
const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me')
|
|
||||||
const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role }
|
|
||||||
login(merged)
|
|
||||||
} catch {
|
|
||||||
login(result.user)
|
|
||||||
}
|
|
||||||
if (typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')) {
|
|
||||||
router.push('/welcome')
|
|
||||||
} else {
|
|
||||||
const raw = searchParams.get('returnTo') || '/'
|
|
||||||
const safe = (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/'
|
|
||||||
router.push(safe)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setError(authMessageFromErrorType('invalid'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleDirectTokens()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = searchParams.get('code')
|
const code = searchParams.get('code')
|
||||||
|
if (!code) return
|
||||||
|
|
||||||
const state = searchParams.get('state')
|
const state = searchParams.get('state')
|
||||||
|
|
||||||
if (!code || !state) return
|
|
||||||
|
|
||||||
const storedState = localStorage.getItem('oauth_state')
|
const storedState = localStorage.getItem('oauth_state')
|
||||||
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
||||||
|
|
||||||
if (!codeVerifier) {
|
// * Full OAuth flow (app-initiated): validate state + use PKCE
|
||||||
setError('Missing code verifier')
|
// * Session-authorized flow (from auth hub): no stored state or verifier
|
||||||
return
|
const isFullOAuth = !!storedState && !!codeVerifier
|
||||||
}
|
|
||||||
if (state !== storedState) {
|
if (isFullOAuth) {
|
||||||
logger.error('State mismatch', { received: state, stored: storedState })
|
if (state !== storedState) {
|
||||||
setError('Invalid state')
|
logger.error('State mismatch', { received: state, stored: storedState })
|
||||||
return
|
setError('Invalid state')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processedRef.current = true
|
processedRef.current = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.11.0-alpha",
|
"version": "0.11.1-alpha",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user