2023-07-20 11:19:42 -07:00
< template >
< div class = "universal-card" >
2023-10-25 22:44:29 +02:00
< ConfirmModal
2023-09-29 04:44:36 +02:00
ref = "modal_confirm"
2024-01-28 19:58:23 +01:00
: title = "formatMessage(deleteModalMessages.title)"
: description = "formatMessage(deleteModalMessages.description)"
: proceed - label = "formatMessage(deleteModalMessages.action)"
2023-09-29 04:44:36 +02:00
@ proceed = "removePat(deletePatIndex)"
/ >
2023-07-20 11:19:42 -07:00
< Modal
ref = "patModal"
2024-01-28 19:58:23 +01:00
: header = "
editPatIndex !== null
? formatMessage ( createModalMessages . editTitle )
: formatMessage ( createModalMessages . createTitle )
"
2023-07-20 11:19:42 -07:00
>
< div class = "universal-modal" >
2024-01-28 19:58:23 +01:00
< label for = "pat-name" >
< span class = "label__title" > { { formatMessage ( createModalMessages . nameLabel ) } } < / span >
< / label >
2023-07-20 11:19:42 -07:00
< input
id = "pat-name"
v - model = "name"
maxlength = "2048"
type = "email"
2024-01-28 19:58:23 +01:00
: placeholder = "formatMessage(createModalMessages.namePlaceholder)"
2023-07-20 11:19:42 -07:00
/ >
2024-01-28 19:58:23 +01:00
< label for = "pat-scopes" >
< span class = "label__title" > { { formatMessage ( commonMessages . scopesLabel ) } } < / span >
< / label >
2023-07-20 11:19:42 -07:00
< div id = "pat-scopes" class = "checkboxes" >
< Checkbox
2024-01-11 16:13:03 -08:00
v - for = "scope in scopeList"
2023-07-20 11:19:42 -07:00
: key = "scope"
2024-01-12 12:55:51 -08:00
: label = "scopesToLabels(getScopeValue(scope)).join(', ')"
2024-01-11 16:13:03 -08:00
: model - value = "hasScope(scopesVal, scope)"
@ update : model - value = "scopesVal = toggleScope(scopesVal, scope)"
2023-07-20 11:19:42 -07:00
/ >
< / div >
2024-01-28 19:58:23 +01:00
< label for = "pat-name" >
< span class = "label__title" > { { formatMessage ( createModalMessages . expiresLabel ) } } < / span >
< / label >
2023-07-20 11:19:42 -07:00
< 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 / >
2024-01-28 19:58:23 +01:00
{ { formatMessage ( commonMessages . cancelButton ) } }
2023-07-20 11:19:42 -07:00
< / button >
< button
v - if = "editPatIndex !== null"
: disabled = "loading || !name || !expires"
type = "button"
class = "iconified-button brand-button"
@ click = "editPat"
>
< SaveIcon / >
2024-01-28 19:58:23 +01:00
{ { formatMessage ( commonMessages . saveChangesButton ) } }
2023-07-20 11:19:42 -07:00
< / button >
< button
v - else
: disabled = "loading || !name || !expires"
type = "button"
class = "iconified-button brand-button"
@ click = "createPat"
>
< PlusIcon / >
2024-01-28 19:58:23 +01:00
{ { formatMessage ( createModalMessages . action ) } }
2023-07-20 11:19:42 -07:00
< / button >
< / div >
< / div >
< / Modal >
< div class = "header__row" >
< div class = "header__title" >
2024-04-10 18:30:13 -07:00
< h2 > { { formatMessage ( commonSettingsMessages . pats ) } } < / h2 >
2023-07-20 11:19:42 -07:00
< / div >
< button
class = "btn btn-primary"
@ click = "
( ) => {
2024-07-06 20:57:32 -07:00
name = null ;
scopesVal = 0 ;
expires = null ;
editPatIndex = null ;
$refs . patModal . show ( ) ;
2023-07-20 11:19:42 -07:00
}
"
>
2024-01-28 19:58:23 +01:00
< PlusIcon / > { { formatMessage ( messages . create ) } }
2023-07-20 11:19:42 -07:00
< / button >
< / div >
< p >
2024-01-28 19:58:23 +01:00
< IntlFormatted :message-id = "messages.description" >
< template # doc -link = " { children } " >
< a class = "text-link" href = "https://docs.modrinth.com" >
< component : is = "() => children" / >
< / a >
< / template >
< / IntlFormatted >
2023-07-20 11:19:42 -07:00
< / 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 = "
2024-01-28 19:58:23 +01:00
pat . last _used
? formatMessage ( commonMessages . dateAtTimeTooltip , {
date : new Date ( pat . last _used ) ,
time : new Date ( pat . last _used ) ,
} )
: null
2023-07-20 11:19:42 -07:00
"
>
2024-01-28 19:58:23 +01:00
< template v-if = "pat.last_used" >
{ {
formatMessage ( tokenMessages . lastUsed , {
ago : formatRelativeTime ( pat . last _used ) ,
} )
} }
< / template >
< template v-else > {{ formatMessage ( tokenMessages.neverUsed ) }} < / template >
2023-07-20 11:19:42 -07:00
< / span >
⋅
2024-01-28 19:58:23 +01:00
< span
v - tooltip = "
formatMessage ( commonMessages . dateAtTimeTooltip , {
date : new Date ( pat . expires ) ,
time : new Date ( pat . expires ) ,
} )
"
>
2023-09-24 21:35:34 +02:00
< template v-if = "new Date(pat.expires) > new Date()" >
2024-01-28 19:58:23 +01:00
{ {
formatMessage ( tokenMessages . expiresIn , {
inTime : formatRelativeTime ( pat . expires ) ,
} )
} }
< / template >
< template v-else >
{ {
formatMessage ( tokenMessages . expiredAgo , {
ago : formatRelativeTime ( pat . expires ) ,
} )
} }
2023-09-24 21:35:34 +02:00
< / template >
2023-07-20 11:19:42 -07:00
< / span >
⋅
2024-01-28 19:58:23 +01:00
< span
v - tooltip = "
formatMessage ( commonMessages . dateAtTimeTooltip , {
date : new Date ( pat . created ) ,
time : new Date ( pat . created ) ,
} )
"
>
{ {
formatMessage ( commonMessages . createdAgoLabel , {
ago : formatRelativeTime ( pat . created ) ,
} )
} }
2023-07-20 11:19:42 -07:00
< / span >
< / template >
< / div >
< / div >
< div class = "input-group" >
< button
class = "iconified-button raised-button"
@ click = "
( ) => {
2024-07-06 20:57:32 -07:00
editPatIndex = index ;
name = pat . name ;
scopesVal = pat . scopes ;
expires = $dayjs ( pat . expires ) . format ( 'YYYY-MM-DD' ) ;
$refs . patModal . show ( ) ;
2023-07-20 11:19:42 -07:00
}
"
>
2024-01-28 19:58:23 +01:00
< EditIcon / > { { formatMessage ( tokenMessages . edit ) } }
2023-07-20 11:19:42 -07:00
< / button >
2023-09-29 04:44:36 +02:00
< button
class = "iconified-button raised-button"
@ click = "
( ) => {
2024-07-06 20:57:32 -07:00
deletePatIndex = pat . id ;
$refs . modal _confirm . show ( ) ;
2023-09-29 04:44:36 +02:00
}
"
>
2024-01-28 19:58:23 +01:00
< TrashIcon / > { { formatMessage ( tokenMessages . revoke ) } }
2023-07-20 11:19:42 -07:00
< / button >
< / div >
< / div >
< / div >
< / template >
< script setup >
2024-07-06 20:57:32 -07:00
import { PlusIcon , XIcon , TrashIcon , EditIcon , SaveIcon } from "@modrinth/assets" ;
import { Checkbox , ConfirmModal } from "@modrinth/ui" ;
2024-01-11 16:13:03 -08:00
2024-07-06 20:57:32 -07:00
import { commonSettingsMessages } from "~/utils/common-messages.ts" ;
2024-01-12 12:55:51 -08:00
import {
hasScope ,
scopeList ,
toggleScope ,
useScopes ,
getScopeValue ,
2024-07-06 20:57:32 -07:00
} from "~/composables/auth/scopes.ts" ;
2024-01-11 16:13:03 -08:00
2024-07-06 20:57:32 -07:00
import CopyCode from "~/components/ui/CopyCode.vue" ;
import Modal from "~/components/ui/Modal.vue" ;
2023-07-20 11:19:42 -07:00
2024-07-06 20:57:32 -07:00
const { formatMessage } = useVIntl ( ) ;
2024-01-28 19:58:23 +01:00
2024-07-06 20:57:32 -07:00
const formatRelativeTime = useRelativeTime ( ) ;
2024-01-28 19:58:23 +01:00
const createModalMessages = defineMessages ( {
createTitle : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.create.title" ,
defaultMessage : "Create personal access token" ,
2024-01-28 19:58:23 +01:00
} ,
editTitle : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.edit.title" ,
defaultMessage : "Edit personal access token" ,
2024-01-28 19:58:23 +01:00
} ,
nameLabel : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.create.name.label" ,
defaultMessage : "Name" ,
2024-01-28 19:58:23 +01:00
} ,
namePlaceholder : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.create.name.placeholder" ,
2024-01-28 19:58:23 +01:00
defaultMessage : "Enter the PAT's name..." ,
} ,
expiresLabel : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.create.expires.label" ,
defaultMessage : "Expires" ,
2024-01-28 19:58:23 +01:00
} ,
action : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.create.action" ,
defaultMessage : "Create PAT" ,
2024-01-28 19:58:23 +01:00
} ,
2024-07-06 20:57:32 -07:00
} ) ;
2024-01-28 19:58:23 +01:00
const deleteModalMessages = defineMessages ( {
title : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.delete.title" ,
defaultMessage : "Are you sure you want to delete this token?" ,
2024-01-28 19:58:23 +01:00
} ,
description : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.delete.description" ,
defaultMessage : "This will remove this token forever (like really forever)." ,
2024-01-28 19:58:23 +01:00
} ,
action : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.modal.delete.action" ,
defaultMessage : "Delete this token" ,
2024-01-28 19:58:23 +01:00
} ,
2024-07-06 20:57:32 -07:00
} ) ;
2024-01-28 19:58:23 +01:00
const messages = defineMessages ( {
description : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.description" ,
2024-01-28 19:58:23 +01:00
defaultMessage :
"PATs can be used to access Modrinth's API. For more information, see <doc-link>Modrinth's API documentation</doc-link>. They can be created and revoked at any time." ,
} ,
create : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.action.create" ,
defaultMessage : "Create a PAT" ,
2024-01-28 19:58:23 +01:00
} ,
2024-07-06 20:57:32 -07:00
} ) ;
2024-01-28 19:58:23 +01:00
const tokenMessages = defineMessages ( {
edit : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.token.action.edit" ,
defaultMessage : "Edit token" ,
2024-01-28 19:58:23 +01:00
} ,
revoke : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.token.action.revoke" ,
defaultMessage : "Revoke token" ,
2024-01-28 19:58:23 +01:00
} ,
lastUsed : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.token.last-used" ,
defaultMessage : "Last used {ago}" ,
2024-01-28 19:58:23 +01:00
} ,
neverUsed : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.token.never-used" ,
defaultMessage : "Never used" ,
2024-01-28 19:58:23 +01:00
} ,
expiresIn : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.token.expires-in" ,
defaultMessage : "Expires {inTime}" ,
2024-01-28 19:58:23 +01:00
} ,
expiredAgo : {
2024-07-06 20:57:32 -07:00
id : "settings.pats.token.expired-ago" ,
defaultMessage : "Expired {ago}" ,
2024-01-28 19:58:23 +01:00
} ,
2024-07-06 20:57:32 -07:00
} ) ;
2024-01-28 19:58:23 +01:00
2023-07-20 11:19:42 -07:00
definePageMeta ( {
2024-07-06 20:57:32 -07:00
middleware : "auth" ,
} ) ;
2023-07-20 11:19:42 -07:00
useHead ( {
2024-04-12 15:14:55 -07:00
title : ` ${ formatMessage ( commonSettingsMessages . pats ) } - Modrinth ` ,
2024-07-06 20:57:32 -07:00
} ) ;
2023-07-20 11:19:42 -07:00
2024-07-06 20:57:32 -07:00
const data = useNuxtApp ( ) ;
const { scopesToLabels } = useScopes ( ) ;
const patModal = ref ( ) ;
2023-07-20 11:19:42 -07:00
2024-07-06 20:57:32 -07:00
const editPatIndex = ref ( null ) ;
2023-07-20 11:19:42 -07:00
2024-07-06 20:57:32 -07:00
const name = ref ( null ) ;
const scopesVal = ref ( BigInt ( 0 ) ) ;
const expires = ref ( null ) ;
2023-07-20 11:19:42 -07:00
2024-07-06 20:57:32 -07:00
const deletePatIndex = ref ( null ) ;
2023-09-29 04:44:36 +02:00
2024-07-06 20:57:32 -07:00
const loading = ref ( false ) ;
2023-07-20 11:19:42 -07:00
2024-07-06 20:57:32 -07:00
const { data : pats , refresh } = await useAsyncData ( "pat" , ( ) => useBaseFetch ( "pat" ) ) ;
2023-07-20 11:19:42 -07:00
async function createPat ( ) {
2024-07-06 20:57:32 -07:00
startLoading ( ) ;
loading . value = true ;
2023-07-20 11:19:42 -07:00
try {
2024-07-06 20:57:32 -07:00
const res = await useBaseFetch ( "pat" , {
method : "POST" ,
2023-07-20 11:19:42 -07:00
body : {
name : name . value ,
2024-01-11 16:13:03 -08:00
scopes : Number ( scopesVal . value ) ,
2023-07-20 11:19:42 -07:00
expires : data . $dayjs ( expires . value ) . toISOString ( ) ,
} ,
2024-07-06 20:57:32 -07:00
} ) ;
pats . value . push ( res ) ;
patModal . value . hide ( ) ;
2023-07-20 11:19:42 -07:00
} catch ( err ) {
data . $notify ( {
2024-07-06 20:57:32 -07:00
group : "main" ,
2024-01-28 19:58:23 +01:00
title : formatMessage ( commonMessages . errorNotificationTitle ) ,
2023-07-20 11:19:42 -07:00
text : err . data ? err . data . description : err ,
2024-07-06 20:57:32 -07:00
type : "error" ,
} ) ;
2023-07-20 11:19:42 -07:00
}
2024-07-06 20:57:32 -07:00
loading . value = false ;
stopLoading ( ) ;
2023-07-20 11:19:42 -07:00
}
async function editPat ( ) {
2024-07-06 20:57:32 -07:00
startLoading ( ) ;
loading . value = true ;
2023-07-20 11:19:42 -07:00
try {
await useBaseFetch ( ` pat/ ${ pats . value [ editPatIndex . value ] . id } ` , {
2024-07-06 20:57:32 -07:00
method : "PATCH" ,
2023-07-20 11:19:42 -07:00
body : {
name : name . value ,
2024-01-11 16:13:03 -08:00
scopes : Number ( scopesVal . value ) ,
2023-07-20 11:19:42 -07:00
expires : data . $dayjs ( expires . value ) . toISOString ( ) ,
} ,
2024-07-06 20:57:32 -07:00
} ) ;
await refresh ( ) ;
patModal . value . hide ( ) ;
2023-07-20 11:19:42 -07:00
} catch ( err ) {
data . $notify ( {
2024-07-06 20:57:32 -07:00
group : "main" ,
2024-01-28 19:58:23 +01:00
title : formatMessage ( commonMessages . errorNotificationTitle ) ,
2023-07-20 11:19:42 -07:00
text : err . data ? err . data . description : err ,
2024-07-06 20:57:32 -07:00
type : "error" ,
} ) ;
2023-07-20 11:19:42 -07:00
}
2024-07-06 20:57:32 -07:00
loading . value = false ;
stopLoading ( ) ;
2023-07-20 11:19:42 -07:00
}
async function removePat ( id ) {
2024-07-06 20:57:32 -07:00
startLoading ( ) ;
2023-07-20 11:19:42 -07:00
try {
2024-07-06 20:57:32 -07:00
pats . value = pats . value . filter ( ( x ) => x . id !== id ) ;
2023-07-20 11:19:42 -07:00
await useBaseFetch ( ` pat/ ${ id } ` , {
2024-07-06 20:57:32 -07:00
method : "DELETE" ,
} ) ;
await refresh ( ) ;
2023-07-20 11:19:42 -07:00
} catch ( err ) {
data . $notify ( {
2024-07-06 20:57:32 -07:00
group : "main" ,
2024-01-28 19:58:23 +01:00
title : formatMessage ( commonMessages . errorNotificationTitle ) ,
2023-07-20 11:19:42 -07:00
text : err . data ? err . data . description : err ,
2024-07-06 20:57:32 -07:00
type : "error" ,
} ) ;
2023-07-20 11:19:42 -07:00
}
2024-07-06 20:57:32 -07:00
stopLoading ( ) ;
2023-07-20 11:19:42 -07:00
}
< / script >
< style lang = "scss" scoped >
. checkboxes {
display : grid ;
column - gap : 0.5 rem ;
@ media screen and ( min - width : 432 px ) {
grid - template - columns : repeat ( 2 , 1 fr ) ;
}
@ media screen and ( min - width : 800 px ) {
grid - template - columns : repeat ( 3 , 1 fr ) ;
}
}
. token {
display : flex ;
flex - direction : column ;
gap : 0.5 rem ;
@ media screen and ( min - width : 800 px ) {
flex - direction : row ;
align - items : center ;
. input - group {
margin - left : auto ;
}
}
}
< / style >