You've already forked AstralRinth
forked from didirus/AstralRinth
Update master with new auth (#1236)
* Begin UI for threads and moderation overhaul * Hide close button on non-report threads * Fix review age coloring * Add project count * Remove action buttons from queue page and add queued date to project page * Hook up to actual data * Remove unused icon * Get up to 1000 projects in queue * prettier * more prettier * Changed all the things * lint * rebuild * Add omorphia * Workaround formatjs bug in ThreadSummary.vue * Fix notifications page on prod * Fix a few notifications and threads bugs * lockfile * Fix duplicate button styles * more fixes and polishing * More fixes * Remove legacy pages * More bugfixes * Add some error catching for reports and notifications * More error handling * fix lint * Add inbox links * Remove loading component and rename member header * Rely on threads always existing * Handle if project update notifs are not grouped * oops * Fix chips on notifications page * Import ModalModeration * finish threads * New authentication (#1234) * Initial new auth work * more auth pages * Finish most * more * fix on landing page * Finish everything but PATs + Sessions * fix threads merge bugs * fix cf pages ssr * fix most issues * Finish authentication * Fix merge --------- Co-authored-by: triphora <emma@modrinth.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
296
pages/settings/pats.vue
Normal file
296
pages/settings/pats.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<div class="universal-card">
|
||||
<Modal
|
||||
ref="patModal"
|
||||
:header="`${editPatIndex !== null ? 'Edit' : 'Create'} personal access token`"
|
||||
>
|
||||
<div class="universal-modal">
|
||||
<label for="pat-name"><span class="label__title">Name</span> </label>
|
||||
<input
|
||||
id="pat-name"
|
||||
v-model="name"
|
||||
maxlength="2048"
|
||||
type="email"
|
||||
placeholder="Enter the PAT's name..."
|
||||
/>
|
||||
<label for="pat-scopes"><span class="label__title">Scopes</span> </label>
|
||||
<div id="pat-scopes" class="checkboxes">
|
||||
<Checkbox
|
||||
v-for="(scope, index) in scopes"
|
||||
:key="scope"
|
||||
v-tooltip="
|
||||
scope.startsWith('_')
|
||||
? 'This scope is not allowed to be used with personal access tokens.'
|
||||
: null
|
||||
"
|
||||
:disabled="scope.startsWith('_')"
|
||||
:label="scope.replace('_', '')"
|
||||
:model-value="(scopesVal & (1 << index)) === 1 << index"
|
||||
@update:model-value="scopesVal ^= 1 << index"
|
||||
/>
|
||||
</div>
|
||||
<label for="pat-name"><span class="label__title">Expires</span> </label>
|
||||
<input id="pat-name" v-model="expires" type="date" />
|
||||
<p></p>
|
||||
<div class="input-group push-right">
|
||||
<button class="iconified-button" @click="$refs.patModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="editPatIndex !== null"
|
||||
:disabled="loading || !name || !expires"
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
@click="editPat"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="loading || !name || !expires"
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
@click="createPat"
|
||||
>
|
||||
<PlusIcon />
|
||||
Create PAT
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div class="header__row">
|
||||
<div class="header__title">
|
||||
<h2>Personal Access Tokens</h2>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="
|
||||
() => {
|
||||
name = null
|
||||
scopesVal = 0
|
||||
expires = null
|
||||
editPatIndex = null
|
||||
$refs.patModal.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<PlusIcon /> Create a PAT
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
PATs can be used to access Modrinth's API. For more information, see
|
||||
<a class="text-link" href="https://docs.modrinth.com">Modrinth's API documentation</a>. They
|
||||
can be created and revoked at any time.
|
||||
</p>
|
||||
<div v-for="(pat, index) in pats" :key="pat.id" class="universal-card recessed token">
|
||||
<div>
|
||||
<div>
|
||||
<strong>{{ pat.name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="pat.access_token">
|
||||
<CopyCode :text="pat.access_token" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-tooltip="
|
||||
pat.last_used ? $dayjs(pat.last_login).format('MMMM D, YYYY [at] h:mm A') : null
|
||||
"
|
||||
>
|
||||
<template v-if="pat.last_used">Last used {{ fromNow(pat.last_used) }}</template>
|
||||
<template v-else>Never used</template>
|
||||
</span>
|
||||
⋅
|
||||
<span v-tooltip="$dayjs(pat.expires).format('MMMM D, YYYY [at] h:mm A')">
|
||||
Expires {{ fromNow(pat.expires) }}
|
||||
</span>
|
||||
⋅
|
||||
<span v-tooltip="$dayjs(pat.created).format('MMMM D, YYYY [at] h:mm A')">
|
||||
Created {{ fromNow(pat.created) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button raised-button"
|
||||
@click="
|
||||
() => {
|
||||
editPatIndex = index
|
||||
name = pat.name
|
||||
scopesVal = pat.scopes
|
||||
expires = $dayjs(pat.expires).format('YYYY-MM-DD')
|
||||
$refs.patModal.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<EditIcon /> Edit token
|
||||
</button>
|
||||
<button class="iconified-button raised-button" @click="removePat(pat.id)">
|
||||
<TrashIcon /> Revoke token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusIcon, Modal, XIcon, Checkbox, TrashIcon, EditIcon, SaveIcon } from 'omorphia'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'PATs - Modrinth',
|
||||
})
|
||||
|
||||
const scopes = [
|
||||
'Read user email',
|
||||
'Read user data',
|
||||
'Write user data',
|
||||
'_Delete your account',
|
||||
'_Write auth data',
|
||||
'Read notifications',
|
||||
'Write notifications',
|
||||
'Read payouts',
|
||||
'Write payouts',
|
||||
'Read analytics',
|
||||
'Create projects',
|
||||
'Read projects',
|
||||
'Write projects',
|
||||
'Delete projects',
|
||||
'Create versions',
|
||||
'Read versions',
|
||||
'Write versions',
|
||||
'Delete versions',
|
||||
'Create reports',
|
||||
'Read reports',
|
||||
'Write reports',
|
||||
'Delete reports',
|
||||
'Read threads',
|
||||
'Write threads',
|
||||
'_Create PATs',
|
||||
'_Read PATs',
|
||||
'_Write PATs',
|
||||
'_Delete PATs',
|
||||
'_Read sessions',
|
||||
'_Delete sessions',
|
||||
]
|
||||
|
||||
const data = useNuxtApp()
|
||||
const patModal = ref()
|
||||
|
||||
const editPatIndex = ref(null)
|
||||
|
||||
const name = ref(null)
|
||||
const scopesVal = ref(0)
|
||||
const expires = ref(null)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const { data: pats, refresh } = await useAsyncData('pat', () => useBaseFetch('pat'))
|
||||
|
||||
async function createPat() {
|
||||
startLoading()
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await useBaseFetch('pat', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: name.value,
|
||||
scopes: scopesVal.value,
|
||||
expires: data.$dayjs(expires.value).toISOString(),
|
||||
},
|
||||
})
|
||||
pats.value.push(res)
|
||||
patModal.value.hide()
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function editPat() {
|
||||
startLoading()
|
||||
loading.value = true
|
||||
try {
|
||||
await useBaseFetch(`pat/${pats.value[editPatIndex.value].id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
name: name.value,
|
||||
scopes: scopesVal.value,
|
||||
expires: data.$dayjs(expires.value).toISOString(),
|
||||
},
|
||||
})
|
||||
await refresh()
|
||||
patModal.value.hide()
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function removePat(id) {
|
||||
startLoading()
|
||||
try {
|
||||
pats.value = pats.value.filter((x) => x.id !== id)
|
||||
await useBaseFetch(`pat/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
await refresh()
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.checkboxes {
|
||||
display: grid;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
@media screen and (min-width: 432px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.token {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.input-group {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user