chore(git): Merge branch 'staging' into issue-274
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 45s

This commit is contained in:
Stixx 2025-01-02 10:49:41 +01:00
commit 18c07556ea
7 changed files with 113 additions and 83 deletions

View File

@ -33,7 +33,7 @@ const App = () => {
window.location.href.split(`${window.location.origin}/#`)[1] window.location.href.split(`${window.location.origin}/#`)[1]
) )
return `${appPublicRoutes.login}?callbackPath=${callbackPathEncoded}` return `${appPublicRoutes.landingPage}?callbackPath=${callbackPathEncoded}`
} }
// Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true // Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true

View File

@ -17,7 +17,7 @@ interface MarkFormFieldProps {
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
handleSelectedMarkValueChange: (value: string) => void handleSelectedMarkValueChange: (value: string) => void
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void
selectedMark: CurrentUserMark selectedMark: CurrentUserMark | null
selectedMarkValue: string selectedMarkValue: string
} }
@ -34,26 +34,23 @@ const MarkFormField = ({
}: MarkFormFieldProps) => { }: MarkFormFieldProps) => {
const [displayActions, setDisplayActions] = useState(true) const [displayActions, setDisplayActions] = useState(true)
const [complete, setComplete] = useState(false) const [complete, setComplete] = useState(false)
const isReadyToSign = () => const isReadyToSign = () =>
isCurrentUserMarksComplete(currentUserMarks) || isCurrentUserMarksComplete(currentUserMarks) ||
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue) isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
const isCurrent = (currentMark: CurrentUserMark) => const isCurrent = (currentMark: CurrentUserMark) =>
currentMark.id === selectedMark.id && !complete currentMark.id === selectedMark?.id && !complete
const isDone = (currentMark: CurrentUserMark) => const isDone = (currentMark: CurrentUserMark) =>
isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted
const findNext = () => { const findNext = () => {
return ( return (
currentUserMarks[selectedMark.id] || currentUserMarks[selectedMark!.id] ||
findNextIncompleteCurrentUserMark(currentUserMarks) findNextIncompleteCurrentUserMark(currentUserMarks)
) )
} }
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
console.log('handle form submit runs...')
// Without this line, we lose mark values when switching // Without this line, we lose mark values when switching
handleCurrentUserMarkChange(selectedMark) handleCurrentUserMarkChange(selectedMark!)
if (!complete) { if (!complete) {
isReadyToSign() isReadyToSign()
@ -63,15 +60,16 @@ const MarkFormField = ({
} }
const toggleActions = () => setDisplayActions(!displayActions) const toggleActions = () => setDisplayActions(!displayActions)
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type) const markLabel = selectedMark
? getToolboxLabelByMarkType(selectedMark.mark.type)
: ''
const handleCurrentUserMarkClick = (mark: CurrentUserMark) => { const handleCurrentUserMarkClick = (mark: CurrentUserMark) => {
setComplete(false) setComplete(false)
handleCurrentUserMarkChange(mark) handleCurrentUserMarkChange(mark)
} }
const handleSelectCompleteMark = () => { const handleSelectCompleteMark = () => {
handleCurrentUserMarkChange(selectedMark) if (currentUserMarks.length) handleCurrentUserMarkChange(selectedMark!)
setComplete(true) setComplete(true)
} }
@ -106,14 +104,15 @@ const MarkFormField = ({
<div className={styles.actionsWrapper}> <div className={styles.actionsWrapper}>
<div className={styles.actionsTop}> <div className={styles.actionsTop}>
<div className={styles.actionsTopInfo}> <div className={styles.actionsTopInfo}>
{!complete && ( {!complete && selectedMark ? (
<p className={styles.actionsTopInfoText}>Add {markLabel}</p> <p className={styles.actionsTopInfoText}>Add {markLabel}</p>
) : (
<p className={styles.actionsTopInfoText}>Finish</p>
)} )}
{complete && <p className={styles.actionsTopInfoText}>Finish</p>}
</div> </div>
</div> </div>
<div className={styles.inputWrapper}> <div className={styles.inputWrapper}>
{!complete && ( {!complete && selectedMark ? (
<form onSubmit={(e) => handleFormSubmit(e)}> <form onSubmit={(e) => handleFormSubmit(e)}>
<MarkInput <MarkInput
markType={selectedMark.mark.type} markType={selectedMark.mark.type}
@ -129,9 +128,7 @@ const MarkFormField = ({
</Button> </Button>
</div> </div>
</form> </form>
)} ) : (
{complete && (
<div className={styles.actionsBottom}> <div className={styles.actionsBottom}>
<Button <Button
onClick={handleSignAndComplete} onClick={handleSignAndComplete}

View File

@ -101,21 +101,22 @@ const PdfMarking = (props: PdfMarkingProps) => {
*/ */
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => { const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault() event.preventDefault()
if (!selectedMarkValue || !selectedMark) return if (selectedMarkValue && selectedMark) {
const updatedMark: CurrentUserMark = getUpdatedMark(
selectedMark,
selectedMarkValue
)
const updatedMark: CurrentUserMark = getUpdatedMark( setSelectedMarkValue(EMPTY)
selectedMark, const updatedCurrentUserMarks = updateCurrentUserMarks(
selectedMarkValue currentUserMarks,
) updatedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(null)
setUpdatedMarks(updatedMark.mark)
}
setSelectedMarkValue(EMPTY)
const updatedCurrentUserMarks = updateCurrentUserMarks(
currentUserMarks,
updatedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(null)
setUpdatedMarks(updatedMark.mark)
handleSign() handleSign()
} }
@ -152,28 +153,24 @@ const PdfMarking = (props: PdfMarkingProps) => {
centerIcon={faPen} centerIcon={faPen}
rightIcon={faCircleInfo} rightIcon={faCircleInfo}
> >
{currentUserMarks?.length > 0 && ( <PdfView
<PdfView currentFile={currentFile}
currentFile={currentFile} files={files}
files={files} handleMarkClick={handleMarkClick}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
selectedMark={selectedMark}
currentUserMarks={currentUserMarks}
otherUserMarks={otherUserMarks}
/>
)}
</StickySideColumns>
{selectedMark !== null && (
<MarkFormField
handleSubmit={handleSubmit}
handleSelectedMarkValueChange={handleChange}
selectedMark={selectedMark}
selectedMarkValue={selectedMarkValue} selectedMarkValue={selectedMarkValue}
selectedMark={selectedMark}
currentUserMarks={currentUserMarks} currentUserMarks={currentUserMarks}
handleCurrentUserMarkChange={handleCurrentUserMarkChange} otherUserMarks={otherUserMarks}
/> />
)} </StickySideColumns>
<MarkFormField
handleSubmit={handleSubmit}
handleSelectedMarkValueChange={handleChange}
selectedMark={selectedMark}
selectedMarkValue={selectedMarkValue}
currentUserMarks={currentUserMarks}
handleCurrentUserMarkChange={handleCurrentUserMarkChange}
/>
</Container> </Container>
</> </>
) )

View File

@ -1,9 +1,12 @@
import { EventTemplate, UnsignedEvent } from 'nostr-tools' import { EventTemplate, UnsignedEvent } from 'nostr-tools'
import { WindowNostr } from 'nostr-tools/nip07'
import { EventEmitter } from 'tseep' import { EventEmitter } from 'tseep'
import store from '../store/store' import store from '../store/store'
import { SignedEvent } from '../types' import { SignedEvent } from '../types'
import { LoginMethodContext } from '../services/LoginMethodStrategy/loginMethodContext' import { LoginMethodContext } from '../services/LoginMethodStrategy/loginMethodContext'
import { clear, unixNow } from '../utils'
import { LoginMethod } from '../store/auth/types'
import { logout as nostrLogout } from 'nostr-login'
import { userLogOutAction } from '../store/actions'
export class NostrController extends EventEmitter { export class NostrController extends EventEmitter {
private static instance: NostrController private static instance: NostrController
@ -11,13 +14,6 @@ export class NostrController extends EventEmitter {
private constructor() { private constructor() {
super() super()
} }
private getNostrObject = () => {
if (window.nostr) return window.nostr as WindowNostr
throw new Error(
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
)
}
public static getInstance(): NostrController { public static getInstance(): NostrController {
if (!NostrController.instance) { if (!NostrController.instance) {
@ -72,7 +68,22 @@ export class NostrController extends EventEmitter {
const loginMethod = store.getState().auth.loginMethod const loginMethod = store.getState().auth.loginMethod
const context = new LoginMethodContext(loginMethod) const context = new LoginMethodContext(loginMethod)
return await context.signEvent(event) const authkey = store.getState().auth.usersPubkey
const signedEvent = await context.signEvent(event)
const pubkey = signedEvent.pubkey
// Forcefully log out the user if we detect missmatch between pubkeys
// Allow undefined authkey, intial log in
if (authkey && authkey !== pubkey) {
if (loginMethod === LoginMethod.nostrLogin) {
nostrLogout()
}
store.dispatch(userLogOutAction())
clear()
throw new Error('User missmatch.\n\nPlease log in again.')
}
return signedEvent
} }
nip04Encrypt = async (receiver: string, content: string): Promise<string> => { nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
@ -97,23 +108,37 @@ export class NostrController extends EventEmitter {
} }
/** /**
* Function will capture the public key from the nostr extension or if no extension present * Function will capture the public key from signedEvent
* function wil capture the public key from the local storage
*/ */
capturePublicKey = async (): Promise<string> => { capturePublicKey = async (): Promise<string> => {
const nostr = this.getNostrObject() try {
const pubKey = await nostr.getPublicKey().catch((err: unknown) => { const timestamp = unixNow()
if (err instanceof Error) { const { href } = window.location
return Promise.reject(err.message)
} else { const authEvent: EventTemplate = {
return Promise.reject(JSON.stringify(err)) kind: 27235,
tags: [
['u', href],
['method', 'GET']
],
content: '',
created_at: timestamp
} }
})
if (!pubKey) { const signedAuthEvent = await this.signEvent(authEvent)
return Promise.reject('Error getting public key, user canceled') const pubkey = signedAuthEvent.pubkey
if (!pubkey) {
return Promise.reject('Error getting public key, user canceled')
}
return Promise.resolve(pubkey)
} catch (error) {
if (error instanceof Error) {
return Promise.reject(error.message)
} else {
return Promise.reject(JSON.stringify(error))
}
} }
return Promise.resolve(pubKey)
} }
} }

View File

@ -58,12 +58,12 @@ export const MainLayout = () => {
} }
const login = useCallback(async () => { const login = useCallback(async () => {
dispatch(updateLoginMethod(LoginMethod.nostrLogin))
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const authController = new AuthController() const authController = new AuthController()
const pubkey = await nostrController.capturePublicKey() const pubkey = await nostrController.capturePublicKey()
dispatch(updateLoginMethod(LoginMethod.nostrLogin))
const redirectPath = const redirectPath =
await authController.authAndGetMetadataAndRelaysMap(pubkey) await authController.authAndGetMetadataAndRelaysMap(pubkey)

View File

@ -123,15 +123,19 @@ const isLast = <T>(index: number, arr: T[]) => index === arr.length - 1
const isCurrentValueLast = ( const isCurrentValueLast = (
currentUserMarks: CurrentUserMark[], currentUserMarks: CurrentUserMark[],
selectedMark: CurrentUserMark, selectedMark: CurrentUserMark | null,
selectedMarkValue: string selectedMarkValue: string
) => { ) => {
const filteredMarks = currentUserMarks.filter( if (selectedMark && currentUserMarks.length > 0) {
(mark) => mark.id !== selectedMark.id const filteredMarks = currentUserMarks.filter(
) (mark) => mark.id !== selectedMark.id
return ( )
isCurrentUserMarksComplete(filteredMarks) && selectedMarkValue.length > 0 return (
) isCurrentUserMarksComplete(filteredMarks) && selectedMarkValue.length > 0
)
}
return true
} }
const getUpdatedMark = ( const getUpdatedMark = (

View File

@ -365,11 +365,21 @@ export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
* @returns The user application data or null if an error occurs or no data is found. * @returns The user application data or null if an error occurs or no data is found.
*/ */
export const getUsersAppData = async (): Promise<UserAppData | null> => { export const getUsersAppData = async (): Promise<UserAppData | null> => {
// Get an instance of the NostrController
const nostrController = NostrController.getInstance()
// Initialize an array to hold relay URLs // Initialize an array to hold relay URLs
const relays: string[] = [] const relays: string[] = []
// Retrieve the user's public key and relay map from the Redux store // Retrieve the user's public key and relay map from the Redux store
const usersPubkey = store.getState().auth.usersPubkey! const usersPubkey = store.getState().auth.usersPubkey!
// Decryption can fail down in the code if extension options changed
// Forcefully log out the user if we detect missmatch between pubkeys
if (usersPubkey !== (await nostrController.capturePublicKey())) {
return null
}
const relayMap = store.getState().relays?.map const relayMap = store.getState().relays?.map
// Check if relayMap is undefined in the Redux store // Check if relayMap is undefined in the Redux store
@ -450,9 +460,6 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
} }
} }
// Get an instance of the NostrController
const nostrController = NostrController.getInstance()
// Decrypt the encrypted content // Decrypt the encrypted content
const decrypted = await nostrController const decrypted = await nostrController
.nip04Decrypt(usersPubkey, encryptedContent) .nip04Decrypt(usersPubkey, encryptedContent)