@@ -581,6 +581,8 @@ export function createContributionsView(options = {}) {
581581
582582 let projectLocked = false ;
583583 let projectPassword = '' ;
584+ /** True when the server-side model has a password set (loaded via GET). */
585+ let serverLocked = false ;
584586
585587 /** @type {HTMLElement|null } Currently-selected history bubble. */
586588 let selectedHistoryBubble = null ;
@@ -840,7 +842,7 @@ export function createContributionsView(options = {}) {
840842 projectLocked,
841843 projectPassword,
842844 rows, authorSources, authorOrcids, authorAffIds, affiliations, loadedAssetNames,
843- sections, creditDescriptions, creditLinkedSections, selectedAuthor, doi,
845+ sections, creditDescriptions, creditLinkedSections, selectedAuthor, doi, serverLocked ,
844846 } ) ) ;
845847 } catch ( _ ) { }
846848 }
@@ -1668,6 +1670,22 @@ export function createContributionsView(options = {}) {
16681670 syncUrl ( ) ;
16691671 endpointStatus . textContent = `\u2713 Loaded \u201c${ project } \u201d \u2014 ${ loadedRows . length } contributor(s).` ;
16701672 endpointStatus . className = 'contributions-endpoint-status status-success' ;
1673+
1674+ // Apply server-side locked state
1675+ serverLocked = data . locked === true ;
1676+ if ( serverLocked ) {
1677+ projectLocked = true ;
1678+ projectLockedCheckbox . checked = true ;
1679+ projectLockedCheckbox . disabled = true ;
1680+ pwPasswordRow . style . display = '' ;
1681+ projectPasswordInput . placeholder = 'Password required to save' ;
1682+ projectPassword = '' ;
1683+ projectPasswordInput . value = '' ;
1684+ } else {
1685+ projectLockedCheckbox . disabled = false ;
1686+ projectPasswordInput . placeholder = 'Set a password' ;
1687+ }
1688+
16711689 // Collapse assets section \u2014 irrelevant when working with a loaded project
16721690 assetsOpen = false ;
16731691 assetsBody . style . display = 'none' ;
@@ -1683,6 +1701,21 @@ export function createContributionsView(options = {}) {
16831701 }
16841702 }
16851703
1704+ /**
1705+ * Hash a plaintext password with SHA-256 and return a hex string.
1706+ * Passwords are never sent in plaintext over the wire.
1707+ *
1708+ * @param {string } password
1709+ * @returns {Promise<string> }
1710+ */
1711+ async function hashPassword ( password ) {
1712+ const encoded = new TextEncoder ( ) . encode ( password ) ;
1713+ const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , encoded ) ;
1714+ return Array . from ( new Uint8Array ( hashBuffer ) )
1715+ . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) )
1716+ . join ( '' ) ;
1717+ }
1718+
16861719 async function saveToServer ( ) {
16871720 const project = projectNameInput . value . trim ( ) ;
16881721 if ( ! project ) {
@@ -1696,7 +1729,11 @@ export function createContributionsView(options = {}) {
16961729 endpointStatus . className = 'contributions-endpoint-status status-loading' ;
16971730 try {
16981731 const payload = toEndpointPayload ( rows , project , { authorOrcids, authorAffIds, affiliations, sections, creditDescriptions, creditLinkedSections, assets : loadedAssetNames , doi } ) ;
1699- const url = `${ CONTRIBUTIONS_API_BASE } /contributions/post?project=${ encodeURIComponent ( project ) } ` ;
1732+ let url = `${ CONTRIBUTIONS_API_BASE } /contributions/post?project=${ encodeURIComponent ( project ) } ` ;
1733+ if ( projectPassword ) {
1734+ const hashed = await hashPassword ( projectPassword ) ;
1735+ url += `&password=${ encodeURIComponent ( hashed ) } ` ;
1736+ }
17001737 const res = await fetch ( url , {
17011738 method : 'POST' ,
17021739 headers : { 'Content-Type' : 'application/json' } ,
@@ -1728,7 +1765,11 @@ export function createContributionsView(options = {}) {
17281765 function updateProjectButtons ( ) {
17291766 const hasProject = projectNameInput . value . trim ( ) . length > 0 ;
17301767 getBtn . disabled = ! hasProject ;
1731- postBtn . disabled = ! hasProject || rows . length === 0 ;
1768+ const hasRows = rows . length > 0 ;
1769+ // When server has a password, require the user to enter one before saving
1770+ const passwordRequired = serverLocked ;
1771+ const hasPassword = projectPasswordInput . value . trim ( ) . length > 0 ;
1772+ postBtn . disabled = ! hasProject || ! hasRows || ( passwordRequired && ! hasPassword ) ;
17321773 }
17331774
17341775 // -------------------------------------------------------------------------
@@ -1776,6 +1817,7 @@ export function createContributionsView(options = {}) {
17761817 } ) ;
17771818 projectPasswordInput . addEventListener ( 'input' , ( ) => {
17781819 projectPassword = projectPasswordInput . value ;
1820+ updateProjectButtons ( ) ;
17791821 saveDraft ( ) ;
17801822 } ) ;
17811823
@@ -1846,14 +1888,24 @@ export function createContributionsView(options = {}) {
18461888 const raw = sessionStorage . getItem ( DRAFT_KEY ) ;
18471889 if ( raw ) {
18481890 const draft = JSON . parse ( raw ) ;
1849- if ( draft . rows ?. length > 0 ) {
1891+ // If the URL specifies a project that differs from the draft, discard the
1892+ // draft and treat the URL as ground truth.
1893+ const draftProject = ( draft . projectName || '' ) . trim ( ) ;
1894+ if ( projectName && draftProject && projectName !== draftProject ) {
1895+ sessionStorage . removeItem ( DRAFT_KEY ) ;
1896+ } else if ( draft . rows ?. length > 0 ) {
18501897 assetInput . value = draft . assetNames || '' ;
18511898 projectNameInput . value = draft . projectName || '' ;
18521899 if ( draft . projectLocked ) {
18531900 projectLocked = true ;
18541901 projectLockedCheckbox . checked = true ;
18551902 pwPasswordRow . style . display = '' ;
18561903 }
1904+ if ( draft . serverLocked ) {
1905+ serverLocked = true ;
1906+ projectLockedCheckbox . disabled = true ;
1907+ projectPasswordInput . placeholder = 'Password required to save' ;
1908+ }
18571909 if ( draft . projectPassword ) {
18581910 projectPassword = draft . projectPassword ;
18591911 projectPasswordInput . value = draft . projectPassword ;
@@ -1887,6 +1939,11 @@ export function createContributionsView(options = {}) {
18871939
18881940 if ( projectName && ! draftRestored ) {
18891941 Promise . resolve ( ) . then ( loadFromServer ) ;
1942+ } else if ( draftRestored ) {
1943+ const draftProject = projectNameInput . value . trim ( ) ;
1944+ if ( draftProject ) {
1945+ Promise . resolve ( ) . then ( ( ) => fetchHistory ( draftProject ) ) ;
1946+ }
18901947 }
18911948
18921949 updateProjectButtons ( ) ;
0 commit comments