mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 08:17:26 +01:00
* feat: let owners leave chosen files unredacted (#941) Redacting self-citations in a bibliography reveals which papers are the authors' own. Owners can now pick files whose content is served as written; file names are still redacted. Patterns follow .gitignore rules. The form gets a file picker fed by GitHub's tree API, and the terms box grows with its content and only warns about regex symbols that can stop a term from matching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: honour !patterns, keep picked braces literal, preview unredacted README - Patterns now apply in order and the last match wins, as in .gitignore, so "*.bib" then "!private.bib" excludes private.bib instead of preserving every other file. - Picked paths escape glob characters, braces included, with a backslash; "[{]" did not survive brace expansion. - The README preview drops terms when the README is an unredacted file, and refreshes when the list changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep rule order when deduplicating unredacted-file patterns Rules apply in order and the last match wins, so dedupe keeps the last copy of a repeated pattern instead of the first. The picker also keeps a plain path in its place when it follows another rule, rather than moving it to the front as a pick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: apply unredacted-file rules to folders, as in .gitignore A rule that matches a folder now covers every file inside it, so "*.bib" then "!private" keeps private/refs.bib redacted. A trailing "/" limits a rule to folders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: folder unchecks win over broad rules; accept short commit hashes - Unchecking a folder that a broader rule such as "*.bib" still covers appends a "!/folder/" exclusion, so its files really go back to being redacted. - The tree endpoint accepts the 6+ character hashes the commit field allows and resolves them to a full SHA before reading the tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: picker counts folder rules exactly as the server does Rules ending in "/" stay patterns instead of being read as picked files, and the checkboxes now run the saved rule list (picks, then patterns) through the server's matcher, so the dialog cannot show files as redacted that the server leaves unredacted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: insert escaped terms literally in "Match as written" String.replace expanded "$&", "$'" and "$1" in the term, so the fix could corrupt the line. Splice by index instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
17 KiB
HTML
116 lines
17 KiB
HTML
<div class="container paper-page user-settings">
|
|
<header class="settings-title"><h1>Settings</h1><div class="settings-header-actions"><span>@{{ user?.username }}</span></div></header>
|
|
<nav class="settings-nav" aria-label="Settings">
|
|
<button type="button" v-for="tab in [{id:'defaults',label:'Anonymization defaults'},{id:'billing',label:'Hosting & billing'},{id:'account',label:'Account'}]" :aria-current="settingsTab === tab.id ? 'page' : null" @click="settingsTab = tab.id">{{ tab.label }}</button>
|
|
</nav>
|
|
<div v-show="settingsTab === 'defaults'">
|
|
<p class="settings-scope">For new anonymizations only. Existing repositories keep their settings.</p>
|
|
<form class="form needs-validation" name="defaultsForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () => { saveDefault($event) })">
|
|
<section class="settings-row"><div><label class="settings-label" for="terms">Terms to redact</label><p class="settings-help">One term per line.</p></div><div><textarea class="form-control" id="terms" name="terms" :rows="Math.min(14, Math.max(3, String(terms || "").split("\n").length + 1))" placeholder="Jane Smith
|
|
MIT CSAIL
|
|
jane.smith@example.org" v-field="{ state: viewState, set: value => { terms = value }, value: terms, form: "defaultsForm", options: { debounce: 250 } }"></textarea>
|
|
<details class="settings-syntax"><summary>Regex & replacement syntax</summary><p>Matches become <code>{{ site_options?.ANONYMIZATION_MASK || 'XXX' }}-[N]</code>. Use <code>term=>replacement</code> for a custom replacement. Regex is supported.</p></details>
|
|
<div class="invalid-feedback" v-show="defaultsForm?.terms?.errors?.format">Terms are in an invalid format.</div>
|
|
</div></section>
|
|
<section class="settings-row"><h2>Reviewer display</h2><div><div class="settings-checks"><label class="settings-check"><input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.link = value }, value: options?.link, form: "defaultsForm", options: {} }">Keep links</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "defaultsForm", options: {} }">Render notebooks</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "defaultsForm", options: {} }">Show images</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.pdf = value }, value: options?.pdf, form: "defaultsForm", options: {} }">Show PDFs</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="loc" name="loc" v-field="{ state: viewState, set: value => { options.loc = value }, value: options?.loc, form: "defaultsForm", options: {} }">Show line counts</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "defaultsForm", options: {} }">GitHub Pages</label></div><p class="settings-help">Images and PDFs are not anonymized.</p><p class="settings-help">GitHub Pages must use the anonymized branch.</p></div></section>
|
|
<section class="settings-row"><h2>Source updates</h2><div><label class="settings-check"><input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value => { options.update = value }, value: options?.update, form: "defaultsForm", options: {} }">Auto-update from GitHub</label><p class="settings-help">Latest commit, at most once an hour.</p></div></section>
|
|
<section class="settings-row"><label class="settings-label" for="expiration">On expiration</label><div><select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value => { options.expirationMode = value }, value: options?.expirationMode, form: "defaultsForm", options: {} }">
|
|
<option value="redirect">Redirect visitors to the GitHub repository</option>
|
|
<option value="remove" selected>Remove the anonymized repository</option>
|
|
</select><p class="settings-help">{{ options.expirationMode === 'redirect' ? 'Redirecting reveals the original repository. Choose the date per anonymization.' : 'Set the expiration date for each repository.' }}</p></div></section>
|
|
<footer class="settings-save">
|
|
<div class="settings-save-status draft-status" role="status"><span v-if="draftRestored">Your unsaved draft was restored.</span><span v-else-if="unsavedChanges">Unsaved changes{{ draftSaved ? ' · Draft saved in this tab' : '' }}</span><span v-else>{{ message ? 'Defaults saved' : 'All changes saved' }}</span></div>
|
|
<button type="button" class="draft-discard" v-if="draftRestored || unsavedChanges" @click="discardDraft()">Discard changes</button>
|
|
<button id="save" type="submit" class="btn btn-ink" :disabled="saving">{{ saving ? 'Saving…' : 'Save defaults' }}</button>
|
|
<div class="settings-save-error" role="alert" v-if="error">{{ error }}</div>
|
|
</footer>
|
|
</form>
|
|
</div>
|
|
<section id="settings-billing" v-show="settingsTab === 'billing'" :aria-busy="billingLoading || billingBusy">
|
|
<p class="settings-scope">Your plan and usage. Reviewer access is always free.</p>
|
|
<p v-if="billingLoading" role="status">Loading billing…</p>
|
|
<div v-if="billingError" class="settings-notice" role="alert">{{ billingError }} <button type="button" class="btn btn-outline-ink" @click="loadBilling()" :disabled="billingBusy">Try again</button></div>
|
|
<p v-if="billingNotice" class="settings-notice" role="status">{{ billingNotice }}</p>
|
|
<div v-if="billingPending" class="settings-notice" role="status"><strong>Confirming your payment</strong><p>Your allowance will update after confirmation. Please do not pay again.</p><button type="button" class="btn btn-outline-ink" @click="refreshBilling()" :disabled="billingBusy">Check payment status</button></div>
|
|
<section class="settings-row"><h2>Current plan</h2><div>
|
|
<div class="settings-inline"><div><strong>{{ profileAuthorGrant() ? 'Author subscription' : (billing?.enabled ? 'Free' : 'Account allowance') }}</strong><p class="settings-help" v-if="profileAuthorGrant()">{{ profileAuthorGrant().cancelAtPeriodEnd ? 'Renewal cancelled · Access until ' : 'Paid access through ' }}{{ billingDate(profileAuthorGrant().until) }}</p></div>
|
|
<button type="button" class="btn btn-outline-ink" v-if="billing?.enabled && !profileAuthorGrant() && !billingPending && !showAuthorPlans && profileAuthorPlans().length" @click="openProfileUpgrade()">View Author plan</button>
|
|
</div>
|
|
<p class="settings-help" v-if="billing?.allowance">{{ billing.allowance.repositories }} repositories · {{ billingBytes(billing.allowance.storage) }}</p>
|
|
<div id="settings-quota"><quota-usage :quota="quota" :fmt="fmt" v-if="quota"></quota-usage><p v-else role="status">Loading usage…</p></div>
|
|
<p class="settings-help">Coauthored repositories use their owner's allowance. Conference hosting is separate.</p>
|
|
<p class="settings-notice" v-if="quota?.repository && !quota.repository.unlimited && quota.repository.used >= quota.repository.total">Your repository allowance is full. <a href="/dashboard">Review your repositories</a> to remove work you no longer need.</p>
|
|
<p class="settings-notice" v-if="profilePastSubscription()">Your previous subscription is no longer active. Never-expiring repositories receive a 90-day grace period after paid access ends. <a href="/dashboard">Review repository expiration dates</a>.</p>
|
|
<p class="settings-notice" v-if="profileAuthorGrant()?.status === 'past_due'">A subscription payment is overdue. Use Manage billing to update your payment details.</p>
|
|
<p class="settings-help" v-if="profileAuthorGrant()?.cancelAtPeriodEnd">When paid access ends, never-expiring repositories enter a 90-day grace period. Dated repositories keep their expiration dates.</p>
|
|
<div class="settings-plan billing-plan" v-if="showAuthorPlans && !profileAuthorGrant() && !billingPending && profileAuthorPlans().length">
|
|
<h3 id="settings-plan-comparison" tabindex="-1">Compare plans</h3>
|
|
<table class="settings-comparison"><caption class="sr-only">Free and Author hosting</caption><thead><tr><th scope="col">Included</th><th scope="col">Free</th><th scope="col">Author</th></tr></thead><tbody>
|
|
<tr><th scope="row">Active repositories</th><td>{{ billing.allowance?.repositories }}</td><td>{{ profileSelectedPlan()?.repositories }}</td></tr>
|
|
<tr><th scope="row">Source storage</th><td>{{ billingBytes(billing.allowance?.storage) }}</td><td>{{ billingBytes(profileSelectedPlan()?.storage) }}</td></tr>
|
|
<tr><th scope="row">Expiration</th><td>Scheduled</td><td>Scheduled or never</td></tr>
|
|
<tr><th scope="row">Coauthors</th><td>Included</td><td>Included</td></tr>
|
|
<tr><th scope="row">Reviewer access</th><td>Free</td><td>Free</td></tr>
|
|
</tbody></table>
|
|
<fieldset class="settings-cycles"><legend class="sr-only">Billing frequency</legend><label v-for="plan in profileAuthorPlans()" :class="{selected: profileSelectedPlan()?.id === plan.id}"><input type="radio" name="profile-plan" :checked="profileSelectedPlan()?.id === plan.id" @change="profilePlan = plan.id">{{ billingPrice(plan) }}</label></fieldset>
|
|
<p class="settings-help">Renews {{ profileSelectedPlan()?.interval === 'year' ? 'annually' : 'monthly' }} until cancelled. Tax shown at checkout.</p><p class="settings-help">If paid access ends, never-expiring repositories expire after a 90-day grace period.</p>
|
|
<div class="settings-actions"><button type="button" class="btn btn-ink" :disabled="billingBusy || billing.checkoutEnabled === false" @click="checkout(profileSelectedPlan().id)">{{ billingBusy ? 'Please wait…' : 'Continue to checkout' }}</button><button type="button" class="btn btn-outline-ink" @click="showAuthorPlans = false">Keep current plan</button></div>
|
|
</div>
|
|
<p class="settings-help" v-if="billing && (!billing.enabled || billing.checkoutEnabled === false)">New purchases are not available. Existing billing can still be managed below.</p>
|
|
</div></section>
|
|
<section class="settings-row"><h2>Billing</h2><div><div class="settings-inline" v-if="billing?.portal"><p class="settings-help">Invoices, payment details, and renewal.</p><button type="button" class="btn btn-outline-ink" @click="billingPortal()" :disabled="billingBusy">Manage billing</button></div><p class="settings-help" v-else>No billing account available.</p><button type="button" class="settings-text-button" v-if="billing?.enabled && !billingPending" @click="refreshBilling()" :disabled="billingBusy">Refresh payment status</button></div></section>
|
|
<section class="settings-row" v-if="billing?.enabled"><h2>Repository passes</h2><div><div class="settings-inline"><p class="settings-help">One repository for 180 days.<br>No subscription or automatic renewal.</p><button type="button" class="btn btn-outline-ink" v-if="profilePassPlan()" @click="showRepositoryPass = !showRepositoryPass" :disabled="billingPending">Review a pass</button></div>
|
|
<div class="settings-pass" v-if="showRepositoryPass && profilePassPlan() && !billingPending"><label for="billing-repository">Repository ID you own</label><input id="billing-repository" class="form-control" v-model="billingRepository" placeholder="Existing anonymized repository ID" autocomplete="off"><p class="settings-help">{{ billingPrice(profilePassPlan()) }} · {{ billingBytes(profilePassPlan().storage) }} · Never expire is not included.</p><button type="button" class="btn btn-ink" :disabled="billingBusy || !billingRepository.trim() || billing.checkoutEnabled === false" @click="checkout('author_pass', billingRepository.trim())">Continue to checkout</button></div>
|
|
</div></section>
|
|
<section class="settings-row" v-if="billing?.grants?.length"><h2>Your purchases</h2><div><div class="settings-purchase" v-for="grant in billing.grants" :key="grant.scope + grant.planId + grant.until"><strong>{{ profileGrantName(grant) }}</strong><span>{{ grant.scope }}</span><p class="settings-help">{{ grant.status }}<template v-if="grant.status !== 'revoked'"> · Paid access through {{ billingDate(grant.until) }}{{ grant.cancelAtPeriodEnd ? ' · Renewal cancelled' : '' }}</template></p></div></div></section>
|
|
<section class="settings-row"><h2>Conference hosting</h2><div class="settings-inline"><p class="settings-help">Shared hosting is managed per conference.</p><a href="/conferences" class="btn btn-outline-ink">My conferences</a></div></section>
|
|
</section>
|
|
<section id="settings-account" class="settings-account-panel" v-show="settingsTab === 'account'">
|
|
<p class="settings-scope">Your identity, repository access, and account.</p>
|
|
<div class="paper-account-card">
|
|
<img width="40" height="40" class="rounded-circle" alt="" v-if="user?.photo" :src="safeUrl((user?.photo))">
|
|
<div class="paper-account-identity">
|
|
<div class="paper-account-name">@{{ user?.username }}</div>
|
|
<div class="paper-account-meta">Signed in with GitHub. Anonymous GitHub only reads your repositories.</div>
|
|
</div>
|
|
<a class="btn btn-outline-ink" href="/api/user/logout" target="_self">Sign out</a>
|
|
</div>
|
|
|
|
<form class="paper-account-email" @submit.prevent="saveAccountEmail()">
|
|
<label class="paper-field-label" for="account-email">Email address</label>
|
|
<p id="account-email-help" class="paper-section-copy">Used for account alerts. Your address is never shown on anonymous pages.</p>
|
|
<input id="account-email" class="form-control" type="email" autocomplete="email" maxlength="254" required v-model="accountEmail" aria-describedby="account-email-help" :disabled="emailLoading || emailSaving || !emailCSRFReady" @input="emailSaved = false" />
|
|
<p role="status" v-if="emailLoading">Loading your email address…</p>
|
|
<p class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="emailError">{{ emailError }}</p>
|
|
<button class="btn btn-outline-ink mt-2" type="button" v-if="!emailCSRFReady && !emailLoading" @click="loadAccountEmail()">Try again</button>
|
|
<button class="btn btn-ink mt-2" type="submit" :disabled="emailLoading || emailSaving || !emailCSRFReady">{{ emailSaving ? 'Saving…' : 'Save email address' }}</button>
|
|
<p role="status" v-if="emailSaved">Email address saved.</p>
|
|
</form>
|
|
|
|
<a class="paper-settings-connection-link" href="/connections">
|
|
<i class="fab fa-github" aria-hidden="true"></i>
|
|
<span>GitHub connections<small>Manage repository access and legacy OAuth.</small></span>
|
|
<span aria-hidden="true">→</span>
|
|
</a>
|
|
|
|
<div class="paper-danger-zone">
|
|
<div class="paper-danger-head">Delete account</div>
|
|
<p class="paper-danger-copy">
|
|
Removes all your anonymized repositories, gists, and pull requests,
|
|
revokes the application's access to your GitHub account, and erases
|
|
your personal data (username, email, access tokens). This cannot be undone.
|
|
</p>
|
|
<div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="deleteError"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> <span v-text="deleteError"></span></div>
|
|
<button type="button" class="btn btn-outline-danger" @click="confirmDeleteAccount = true" :disabled="deletingAccount">
|
|
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-trash"></use></svg>{{ deletingAccount ? 'Deleting…' : 'Delete my account' }}
|
|
</button>
|
|
<div class="settings-delete-confirm" v-if="confirmDeleteAccount">
|
|
<label for="delete-username">Type {{ user?.username }} to confirm</label>
|
|
<input id="delete-username" class="form-control" v-model="deleteUsername" autocomplete="off">
|
|
<div class="settings-actions"><button type="button" class="btn btn-outline-danger" @click="deleteAccount()" :disabled="deletingAccount || deleteUsername !== user?.username">{{ deletingAccount ? 'Deleting…' : 'Delete permanently' }}</button><button type="button" class="btn btn-outline-ink" @click="confirmDeleteAccount = false; deleteUsername = ''" :disabled="deletingAccount">Cancel</button></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|