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:
parent
a5613ebb10
commit
34d63f3557
1
assets/images/utils/discord.svg
Normal file
1
assets/images/utils/discord.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#fff;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 985 B |
9
assets/images/utils/gitlab.svg
Normal file
9
assets/images/utils/gitlab.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 380 380" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" fill="currentColor">
|
||||
<g transform="matrix(1.97904,0,0,1.97904,-186.013,-186.006)">
|
||||
<g id="LOGO">
|
||||
<path d="M282.83,170.73L282.56,170.04L256.42,101.82C255.888,100.483 254.946,99.349 253.73,98.58C251.243,97.036 248.038,97.208 245.73,99.01C244.613,99.917 243.803,101.146 243.41,102.53L225.76,156.53L154.29,156.53L136.64,102.53C136.257,101.139 135.445,99.903 134.32,99C132.012,97.198 128.807,97.026 126.32,98.57C125.106,99.342 124.165,100.475 123.63,101.81L97.44,170L97.18,170.69C89.472,190.829 96.065,213.803 113.28,226.79L113.37,226.86L113.61,227.03L153.43,256.85L173.13,271.76L185.13,280.82C188.006,283.004 192.014,283.004 194.89,280.82L206.89,271.76L226.59,256.85L266.65,226.85L266.75,226.77C283.925,213.782 290.505,190.849 282.83,170.73Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
20
assets/images/utils/google.svg
Normal file
20
assets/images/utils/google.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<circle cx="50" cy="50" r="50" style="fill:white;"/>
|
||||
<g id="Google__G__Logo.svg" transform="matrix(0.0991612,0,0,0.0991612,49.3739,50)">
|
||||
<g transform="matrix(1,0,0,1,-352.8,-360)">
|
||||
<clipPath id="_clip1">
|
||||
<rect x="0" y="0" width="705.6" height="720"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<g transform="matrix(1,0,0,1,4477.16,2891.98)">
|
||||
<path d="M-4117.16,-2597.44L-4117.16,-2458.02L-3923.42,-2458.02C-3931.93,-2413.18 -3957.46,-2375.22 -3995.75,-2349.69L-3878.91,-2259.03C-3810.84,-2321.87 -3771.56,-2414.16 -3771.56,-2523.8C-3771.56,-2549.33 -3773.85,-2573.87 -3778.11,-2597.43L-4117.16,-2597.44Z" style="fill:rgb(66,133,244);fill-rule:nonzero;"/>
|
||||
<path d="M-4318.92,-2463.46L-4345.27,-2443.29L-4438.55,-2370.64C-4379.31,-2253.15 -4257.9,-2171.98 -4117.17,-2171.98C-4019.97,-2171.98 -3938.48,-2204.05 -3878.92,-2259.03L-3995.75,-2349.69C-4027.83,-2328.09 -4068.74,-2315 -4117.17,-2315C-4210.77,-2315 -4290.3,-2378.16 -4318.77,-2463.25L-4318.92,-2463.46Z" style="fill:rgb(52,168,83);fill-rule:nonzero;"/>
|
||||
<path d="M-4438.55,-2693.33C-4463.09,-2644.89 -4477.16,-2590.24 -4477.16,-2531.99C-4477.16,-2473.73 -4463.09,-2419.08 -4438.55,-2370.64C-4438.55,-2370.32 -4318.76,-2463.59 -4318.76,-2463.59C-4325.96,-2485.19 -4330.22,-2508.09 -4330.22,-2531.99C-4330.22,-2555.88 -4325.96,-2578.79 -4318.76,-2600.39L-4438.55,-2693.33Z" style="fill:rgb(251,188,5);fill-rule:nonzero;"/>
|
||||
<path d="M-4117.16,-2748.64C-4064.14,-2748.64 -4017.02,-2730.31 -3979.38,-2694.97L-3876.29,-2798.06C-3938.8,-2856.31 -4019.96,-2891.99 -4117.16,-2891.99C-4257.89,-2891.99 -4379.31,-2811.15 -4438.55,-2693.33L-4318.76,-2600.38C-4290.29,-2685.47 -4210.76,-2748.64 -4117.16,-2748.64Z" style="fill:rgb(234,67,53);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
assets/images/utils/key.svg
Normal file
1
assets/images/utils/key.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>
|
||||
|
After Width: | Height: | Size: 261 B |
1
assets/images/utils/microsoft.svg
Normal file
1
assets/images/utils/microsoft.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><title>MS-SymbolLockup</title><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>
|
||||
|
After Width: | Height: | Size: 320 B |
4
assets/images/utils/steam.svg
Normal file
4
assets/images/utils/steam.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-steam" viewBox="0 0 16 16">
|
||||
<path d="M.329 10.333A8.01 8.01 0 0 0 7.99 16C12.414 16 16 12.418 16 8s-3.586-8-8.009-8A8.006 8.006 0 0 0 0 7.468l.003.006 4.304 1.769A2.198 2.198 0 0 1 5.62 8.88l1.96-2.844-.001-.04a3.046 3.046 0 0 1 3.042-3.043 3.046 3.046 0 0 1 3.042 3.043 3.047 3.047 0 0 1-3.111 3.044l-2.804 2a2.223 2.223 0 0 1-3.075 2.11 2.217 2.217 0 0 1-1.312-1.568L.33 10.333Z"/>
|
||||
<path d="M4.868 12.683a1.715 1.715 0 0 0 1.318-3.165 1.705 1.705 0 0 0-1.263-.02l1.023.424a1.261 1.261 0 1 1-.97 2.33l-.99-.41a1.7 1.7 0 0 0 .882.84Zm3.726-6.687a2.03 2.03 0 0 0 2.027 2.029 2.03 2.03 0 0 0 2.027-2.029 2.03 2.03 0 0 0-2.027-2.027 2.03 2.03 0 0 0-2.027 2.027Zm2.03-1.527a1.524 1.524 0 1 1-.002 3.048 1.524 1.524 0 0 1 .002-3.048Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 838 B |
@ -903,6 +903,8 @@ tr.button-transparent {
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, scale 0.05s ease-in-out,
|
||||
outline 0.2s ease-in-out;
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
|
||||
{{ text }}
|
||||
<span>{{ text }}</span>
|
||||
<CheckIcon v-if="copied" />
|
||||
<ClipboardCopyIcon v-else />
|
||||
</button>
|
||||
@ -51,6 +51,12 @@ export default {
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
|
||||
outline 0.2s ease-in-out;
|
||||
|
||||
span {
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
v-else-if="
|
||||
!['resourcepack', 'shader'].includes(type) &&
|
||||
!(type === 'plugin' && search) &&
|
||||
!categories.some((x) => $tag.loaderData.dataPackLoaders.includes(x))
|
||||
!categories.some((x) => tags.loaderData.dataPackLoaders.includes(x))
|
||||
"
|
||||
class="environment"
|
||||
>
|
||||
@ -47,57 +47,52 @@
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import InfoIcon from '~/assets/images/utils/info.svg'
|
||||
import ClientIcon from '~/assets/images/utils/client.svg'
|
||||
import GlobeIcon from '~/assets/images/utils/globe.svg'
|
||||
import ServerIcon from '~/assets/images/utils/server.svg'
|
||||
export default {
|
||||
components: {
|
||||
InfoIcon,
|
||||
ClientIcon,
|
||||
ServerIcon,
|
||||
GlobeIcon,
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'mod',
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'mod',
|
||||
},
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
clientSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
typeOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
alwaysShow: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
serverSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
clientSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
typeOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
alwaysShow: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const tags = useTags()
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.environment {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<div
|
||||
:class="{
|
||||
shown: actuallyShown,
|
||||
noblur: !$orElse($cosmetics.advancedRendering, true),
|
||||
noblur: !$orElse(cosmetics.advancedRendering, true),
|
||||
}"
|
||||
class="modal-overlay"
|
||||
@click="hide"
|
||||
@ -38,6 +38,11 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const cosmetics = useCosmetics()
|
||||
|
||||
return { cosmetics }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<Chips
|
||||
id="project-type"
|
||||
v-model="projectType"
|
||||
:items="$tag.projectTypes.map((x) => x.display)"
|
||||
:items="tags.projectTypes.map((x) => x.display)"
|
||||
/>
|
||||
<label for="name">
|
||||
<span class="label__title">Name<span class="required">*</span></span>
|
||||
@ -86,9 +86,14 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
|
||||
return { tags }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projectType: this.$tag.projectTypes[0].display,
|
||||
projectType: this.tags.projectTypes[0].display,
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
@ -100,7 +105,7 @@ export default {
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
getProjectType() {
|
||||
return this.$tag.projectTypes.find((x) => this.projectType === x.display)
|
||||
return this.tags.projectTypes.find((x) => this.projectType === x.display)
|
||||
},
|
||||
getClientSide() {
|
||||
switch (this.getProjectType().id) {
|
||||
@ -137,6 +142,8 @@ export default {
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
@ -148,8 +155,8 @@ export default {
|
||||
initial_versions: [],
|
||||
team_members: [
|
||||
{
|
||||
user_id: this.$auth.user.id,
|
||||
name: this.$auth.user.username,
|
||||
user_id: auth.value.user.id,
|
||||
name: auth.value.user.username,
|
||||
role: 'Owner',
|
||||
},
|
||||
],
|
||||
@ -167,7 +174,6 @@ export default {
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Disposition': formData,
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
})
|
||||
|
||||
@ -193,7 +199,7 @@ export default {
|
||||
stopLoading()
|
||||
},
|
||||
show() {
|
||||
this.projectType = this.$tag.projectTypes[0].display
|
||||
this.projectType = this.tags.projectTypes[0].display
|
||||
this.name = ''
|
||||
this.slug = ''
|
||||
this.description = ''
|
||||
|
||||
@ -113,7 +113,6 @@ export default {
|
||||
await useBaseFetch(`project/${this.project.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
|
||||
this.$refs.modal.hide()
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
<Multiselect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
:options="$tag.reportTypes"
|
||||
:options="tags.reportTypes"
|
||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
@ -82,6 +82,11 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
|
||||
return { tags }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reportType: '',
|
||||
@ -110,7 +115,6 @@ export default {
|
||||
await useBaseFetch('report', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
|
||||
this.$refs.modal.hide()
|
||||
|
||||
@ -128,14 +128,15 @@ export default {
|
||||
async proceed() {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch(`user/${this.$auth.user.id}/payouts`, {
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch(`user/${auth.value.user.id}/payouts`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
amount: Number(this.amount.replace('$', '')),
|
||||
},
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await useAuth(this.$auth.token)
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
this.$refs.modal.hide()
|
||||
} catch (err) {
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<template v-if="$tag.rejectedStatuses.includes(notification.body.new_status)">
|
||||
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
||||
has been <Badge :type="notification.body.new_status" />
|
||||
</template>
|
||||
<template v-else>
|
||||
@ -106,6 +106,7 @@
|
||||
:raised="raised"
|
||||
:messages="getMessages()"
|
||||
class="thread-summary"
|
||||
:auth="auth"
|
||||
/>
|
||||
<div v-else-if="type === 'project_update'" class="version-list">
|
||||
<div
|
||||
@ -221,7 +222,7 @@
|
||||
>
|
||||
<CheckIcon /> Mark as read
|
||||
</button>
|
||||
<CopyCode v-if="$cosmetics.developerMode" :text="notification.id" />
|
||||
<CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
<div v-else class="input-group">
|
||||
<nuxt-link
|
||||
@ -253,7 +254,7 @@
|
||||
>
|
||||
<CheckIcon /> Mark as read
|
||||
</button>
|
||||
<CopyCode v-if="$cosmetics.developerMode" :text="notification.id" />
|
||||
<CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -301,8 +302,15 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useTags()
|
||||
|
||||
const type = computed(() =>
|
||||
!props.notification.body || props.notification.body.type === 'legacy_markdown'
|
||||
? null
|
||||
@ -357,7 +365,6 @@ async function performAction(notification, actionIndex) {
|
||||
if (actionIndex !== null) {
|
||||
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
|
||||
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
|
||||
...app.$defaultHeaders(),
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
</p>
|
||||
<Categories
|
||||
:categories="
|
||||
categories.filter((x) => !hideLoaders || !$tag.loaders.find((y) => y.name === x))
|
||||
categories.filter((x) => !hideLoaders || !tags.loaders.find((y) => y.name === x))
|
||||
"
|
||||
:type="type"
|
||||
class="tags"
|
||||
@ -209,6 +209,11 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
|
||||
return { tags }
|
||||
},
|
||||
computed: {
|
||||
projectTypeDisplay() {
|
||||
return this.$getProjectTypeForDisplay(this.type, this.categories)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="$auth.user && showInvitation" class="universal-card information invited">
|
||||
<div v-if="showInvitation" class="universal-card information invited">
|
||||
<h2>Invitation to join project</h2>
|
||||
<p>
|
||||
You've been invited be a member of this project with the role of '{{ currentMember.role }}'.
|
||||
@ -15,10 +15,9 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
$auth.user &&
|
||||
currentMember &&
|
||||
nags.filter((x) => x.condition).length > 0 &&
|
||||
(project.status === 'draft' || $tag.rejectedStatuses.includes(project.status))
|
||||
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
|
||||
"
|
||||
class="author-actions universal-card"
|
||||
>
|
||||
@ -155,6 +154,14 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
tags: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
setProcessing: {
|
||||
type: Function,
|
||||
default() {
|
||||
@ -334,7 +341,7 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
hide: !this.$tag.rejectedStatuses.includes(this.project.status),
|
||||
hide: !this.tags.rejectedStatuses.includes(this.project.status),
|
||||
condition: true,
|
||||
title: 'Resubmit for review',
|
||||
id: 'resubmit-for-review',
|
||||
@ -363,8 +370,8 @@ export default {
|
||||
)
|
||||
},
|
||||
showInvitation() {
|
||||
if (this.allMembers && this.$auth) {
|
||||
const member = this.allMembers.find((x) => x.user.id === this.$auth.user.id)
|
||||
if (this.allMembers && this.auth) {
|
||||
const member = this.allMembers.find((x) => x.user.id === this.auth.user.id)
|
||||
return member && !member.accepted
|
||||
}
|
||||
return false
|
||||
|
||||
@ -98,9 +98,10 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['switch-page'])
|
||||
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
|
||||
const tags = useTags()
|
||||
|
||||
const tempLoaders = new Set()
|
||||
let tempVersions = new Set()
|
||||
const tempReleaseChannels = new Set()
|
||||
@ -119,7 +120,7 @@ tempVersions = Array.from(tempVersions)
|
||||
|
||||
const loaderFilters = shallowRef(Array.from(tempLoaders))
|
||||
const gameVersionFilters = shallowRef(
|
||||
data.$tag.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version))
|
||||
tags.value.gameVersions.filter((gameVer) => tempVersions.includes(gameVer.version))
|
||||
)
|
||||
const versionTypeFilters = shallowRef(Array.from(tempReleaseChannels))
|
||||
const includeSnapshots = ref(route.query.s === 'true')
|
||||
|
||||
@ -63,10 +63,11 @@
|
||||
class="thread-summary"
|
||||
:raised="raised"
|
||||
:link="`/${moderation ? 'moderation' : 'dashboard'}/report/${report.id}`"
|
||||
:auth="auth"
|
||||
/>
|
||||
<div class="reporter-info">
|
||||
<ReportIcon class="inline-svg" /> Reported by
|
||||
<span v-if="$auth.user.id === report.reporterUser.id">you</span>
|
||||
<span v-if="auth.user.id === report.reporterUser.id">you</span>
|
||||
<nuxt-link v-else :to="`/user/${report.reporterUser.username}`" class="iconified-link">
|
||||
<Avatar
|
||||
:src="report.reporterUser.avatar_url"
|
||||
@ -81,7 +82,7 @@
|
||||
<span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{
|
||||
fromNow(report.created)
|
||||
}}</span>
|
||||
<CopyCode v-if="$cosmetics.developerMode" :text="report.id" class="report-id" />
|
||||
<CopyCode v-if="cosmetics.developerMode" :text="report.id" class="report-id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -117,7 +118,13 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const cosmetics = useCosmetics()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -7,11 +7,16 @@
|
||||
:link-stack="breadcrumbsStack"
|
||||
/>
|
||||
<h2>Report details</h2>
|
||||
<ReportInfo :report="report" :show-thread="false" :show-message="false" />
|
||||
<ReportInfo :report="report" :show-thread="false" :show-message="false" :auth="auth" />
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>Messages</h2>
|
||||
<ConversationThread :thread="thread" :report="report" :update-thread="updateThread" />
|
||||
<ConversationThread
|
||||
:thread="thread"
|
||||
:report="report"
|
||||
:update-thread="updateThread"
|
||||
:auth="auth"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@ -30,10 +35,12 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
const report = ref(null)
|
||||
|
||||
await fetchReport().then((result) => {
|
||||
@ -41,7 +48,7 @@ await fetchReport().then((result) => {
|
||||
})
|
||||
|
||||
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () =>
|
||||
useBaseFetch(`thread/${report.value.thread_id}`, app.$defaultHeaders())
|
||||
useBaseFetch(`thread/${report.value.thread_id}`)
|
||||
)
|
||||
const thread = computed(() => addReportMessage(rawThread.value, report.value))
|
||||
|
||||
@ -52,7 +59,7 @@ async function updateThread(newThread) {
|
||||
|
||||
async function fetchReport() {
|
||||
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () =>
|
||||
useBaseFetch(`report/${props.reportId}`, app.$defaultHeaders())
|
||||
useBaseFetch(`report/${props.reportId}`)
|
||||
)
|
||||
rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, '')
|
||||
|
||||
@ -67,7 +74,7 @@ async function fetchReport() {
|
||||
let users = []
|
||||
if (userIds.length > 0) {
|
||||
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
useBaseFetch(`users?ids=${JSON.stringify(userIds)}`, app.$defaultHeaders())
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`)
|
||||
)
|
||||
users = usersVal.value
|
||||
}
|
||||
@ -75,7 +82,7 @@ async function fetchReport() {
|
||||
let version = null
|
||||
if (versionId) {
|
||||
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
|
||||
useBaseFetch(`version/${versionId}`, app.$defaultHeaders())
|
||||
useBaseFetch(`version/${versionId}`)
|
||||
)
|
||||
version = versionVal.value
|
||||
}
|
||||
@ -89,7 +96,7 @@ async function fetchReport() {
|
||||
let project = null
|
||||
if (projectId) {
|
||||
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
|
||||
useBaseFetch(`project/${projectId}`, app.$defaultHeaders())
|
||||
useBaseFetch(`project/${projectId}`)
|
||||
)
|
||||
project = projectVal.value
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<ReportInfo
|
||||
v-for="report in reports.filter(
|
||||
(x) =>
|
||||
(moderation || x.reporterUser.id === $auth.user.id) &&
|
||||
(moderation || x.reporterUser.id === auth.user.id) &&
|
||||
(viewMode === 'open' ? x.open : !x.open)
|
||||
)"
|
||||
:key="report.id"
|
||||
@ -11,6 +11,7 @@
|
||||
:thread="report.thread"
|
||||
:moderation="moderation"
|
||||
raised
|
||||
:auth="auth"
|
||||
class="universal-card recessed"
|
||||
/>
|
||||
</template>
|
||||
@ -24,16 +25,16 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
const viewMode = ref('open')
|
||||
const reports = ref([])
|
||||
|
||||
let { data: rawReports } = await useAsyncData('report', () =>
|
||||
useBaseFetch('report', app.$defaultHeaders())
|
||||
)
|
||||
let { data: rawReports } = await useAsyncData('report', () => useBaseFetch('report'))
|
||||
|
||||
rawReports = rawReports.value.map((report) => {
|
||||
report.item_id = report.item_id.replace(/"/g, '')
|
||||
@ -53,13 +54,13 @@ const threadIds = [
|
||||
|
||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
useBaseFetch(`users?ids=${JSON.stringify(userIds)}`, app.$defaultHeaders())
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`)
|
||||
),
|
||||
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
||||
useBaseFetch(`versions?ids=${JSON.stringify(versionIds)}`, app.$defaultHeaders())
|
||||
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`)
|
||||
),
|
||||
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||
useBaseFetch(`threads?ids=${JSON.stringify(threadIds)}`, app.$defaultHeaders())
|
||||
useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`)
|
||||
),
|
||||
])
|
||||
|
||||
@ -70,7 +71,7 @@ const versionProjects = versions.value.map((version) => version.project_id)
|
||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))]
|
||||
|
||||
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
||||
useBaseFetch(`projects?ids=${JSON.stringify(projectIds)}`, app.$defaultHeaders())
|
||||
useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`)
|
||||
)
|
||||
|
||||
reports.value = rawReports.map((report) => {
|
||||
|
||||
@ -23,10 +23,15 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
|
||||
return { tags }
|
||||
},
|
||||
computed: {
|
||||
categoriesFiltered() {
|
||||
return this.$tag.categories
|
||||
.concat(this.$tag.loaders)
|
||||
return this.tags.categories
|
||||
.concat(this.tags.loaders)
|
||||
.filter(
|
||||
(x) =>
|
||||
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type)
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div v-if="$cosmetics.developerMode" class="thread-id">
|
||||
<div v-if="cosmetics.developerMode" class="thread-id">
|
||||
Thread ID: <CopyCode :text="thread.id" />
|
||||
</div>
|
||||
<div v-if="sortedMessages.length > 0" class="messages universal-card recessed">
|
||||
@ -77,7 +77,7 @@
|
||||
>
|
||||
<SendIcon /> Send
|
||||
</button>
|
||||
<template v-if="currentMember && !isStaff($auth.user)">
|
||||
<template v-if="currentMember && !isStaff(auth.user)">
|
||||
<template v-if="isRejected(project)">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
@ -98,7 +98,7 @@
|
||||
<div class="spacer"></div>
|
||||
<div class="input-group extra-options">
|
||||
<template v-if="report">
|
||||
<template v-if="isStaff($auth.user)">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
class="iconified-button danger-button"
|
||||
@ -112,7 +112,7 @@
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="project">
|
||||
<template v-if="isStaff($auth.user)">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
class="iconified-button brand-button"
|
||||
@ -216,8 +216,13 @@ const props = defineProps({
|
||||
return null
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const app = useNuxtApp()
|
||||
const cosmetics = useCosmetics()
|
||||
|
||||
const members = computed(() => {
|
||||
const members = {}
|
||||
@ -250,7 +255,7 @@ async function updateThreadLocal() {
|
||||
}
|
||||
let thread = null
|
||||
if (threadId) {
|
||||
thread = await useBaseFetch(`thread/${threadId}`, app.$defaultHeaders())
|
||||
thread = await useBaseFetch(`thread/${threadId}`)
|
||||
}
|
||||
props.updateThread(thread)
|
||||
}
|
||||
@ -265,7 +270,6 @@ async function sendReply(status = null) {
|
||||
body: replyBody.value,
|
||||
},
|
||||
},
|
||||
...app.$defaultHeaders(),
|
||||
})
|
||||
replyBody.value = ''
|
||||
await updateThreadLocal()
|
||||
@ -293,7 +297,6 @@ async function closeReport(reply) {
|
||||
body: {
|
||||
closed: true,
|
||||
},
|
||||
...app.$defaultHeaders(),
|
||||
})
|
||||
await updateThreadLocal()
|
||||
} catch (err) {
|
||||
|
||||
@ -51,6 +51,10 @@ const props = defineProps({
|
||||
return []
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const app = useNuxtApp()
|
||||
@ -60,7 +64,7 @@ const members = computed(() => {
|
||||
for (const member of props.thread.members) {
|
||||
members[member.id] = member
|
||||
}
|
||||
members[app.$auth.user.id] = app.$auth.user
|
||||
members[props.auth.user.id] = props.auth.user
|
||||
return members
|
||||
})
|
||||
|
||||
|
||||
@ -16,8 +16,12 @@ export const initAuth = async (oldToken = null) => {
|
||||
const auth = {
|
||||
user: null,
|
||||
token: '',
|
||||
headers: {},
|
||||
}
|
||||
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const authCookie = useCookie('auth-token', {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
@ -31,33 +35,66 @@ export const initAuth = async (oldToken = null) => {
|
||||
authCookie.value = oldToken
|
||||
}
|
||||
|
||||
if (route.query.code) {
|
||||
if (route.query.code && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = route.query.code
|
||||
}
|
||||
|
||||
if (authCookie.value) {
|
||||
auth.token = authCookie.value
|
||||
try {
|
||||
auth.user = await useBaseFetch('user', {
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
})
|
||||
} catch {}
|
||||
|
||||
auth.headers = {
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
if (!auth.token || !auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true
|
||||
)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
auth.token = session.session
|
||||
authCookie.value = auth.token
|
||||
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true
|
||||
)
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getAuthUrl = () => {
|
||||
export const getAuthUrl = (provider) => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useRoute()
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?url=${config.public.siteUrl}${route.fullPath}`
|
||||
return `${config.public.apiBaseUrl}auth/init?url=${config.public.siteUrl}${route.path}&provider=${provider}`
|
||||
}
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
export const useBaseFetch = async (url, options = {}) => {
|
||||
export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
|
||||
const config = useRuntimeConfig()
|
||||
const base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl
|
||||
|
||||
if (options.headers && process.server) {
|
||||
if (!options.headers) {
|
||||
options.headers = {}
|
||||
}
|
||||
|
||||
if (process.server) {
|
||||
options.headers['x-ratelimit-key'] = config.rateLimitKey
|
||||
} else if (process.server) {
|
||||
options.headers = {
|
||||
'x-ratelimit-key': config.rateLimitKey,
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipAuth) {
|
||||
const auth = await useAuth()
|
||||
|
||||
options.headers.Authorization = auth.value.token
|
||||
}
|
||||
|
||||
return await $fetch(`${base}${url}`, options)
|
||||
|
||||
@ -20,8 +20,8 @@ export const initUser = async () => {
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
const [notifications, follows] = await Promise.all([
|
||||
useBaseFetch(`user/${auth.user.id}/notifications`, auth.headers),
|
||||
useBaseFetch(`user/${auth.user.id}/follows`, auth.headers),
|
||||
useBaseFetch(`user/${auth.user.id}/notifications`),
|
||||
useBaseFetch(`user/${auth.user.id}/follows`),
|
||||
])
|
||||
|
||||
user.notifications = notifications
|
||||
@ -41,7 +41,7 @@ export const initUserNotifs = async () => {
|
||||
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.notifications = await useBaseFetch(`user/${auth.user.id}/notifications`, auth.headers)
|
||||
user.notifications = await useBaseFetch(`user/${auth.user.id}/notifications`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -54,7 +54,7 @@ export const initUserFollows = async () => {
|
||||
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`, auth.headers)
|
||||
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -67,7 +67,7 @@ export const initUserProjects = async () => {
|
||||
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`, auth.headers)
|
||||
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -75,7 +75,6 @@ export const initUserProjects = async () => {
|
||||
}
|
||||
|
||||
export const userFollowProject = async (project) => {
|
||||
const auth = (await useAuth()).value
|
||||
const user = (await useUser()).value
|
||||
|
||||
user.follows = user.follows.concat(project)
|
||||
@ -84,13 +83,11 @@ export const userFollowProject = async (project) => {
|
||||
setTimeout(() => {
|
||||
useBaseFetch(`project/${project.id}/follow`, {
|
||||
method: 'POST',
|
||||
...auth.headers,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const userUnfollowProject = async (project) => {
|
||||
const auth = (await useAuth()).value
|
||||
const user = (await useUser()).value
|
||||
|
||||
user.follows = user.follows.filter((x) => x.id !== project.id)
|
||||
@ -99,7 +96,6 @@ export const userUnfollowProject = async (project) => {
|
||||
setTimeout(() => {
|
||||
useBaseFetch(`project/${project.id}/follow`, {
|
||||
method: 'DELETE',
|
||||
...auth.headers,
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -126,3 +122,45 @@ export const userReadNotifications = async (ids) => {
|
||||
return x
|
||||
})
|
||||
}
|
||||
|
||||
export const resendVerifyEmail = async () => {
|
||||
const app = useNuxtApp()
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch('auth/email/resend_verify', {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'Email sent',
|
||||
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const logout = async () => {
|
||||
startLoading()
|
||||
const auth = await useAuth()
|
||||
try {
|
||||
await useBaseFetch(`session/${auth.value.token}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
} catch {}
|
||||
|
||||
await useAuth('none')
|
||||
useCookie('auth-token').value = null
|
||||
await navigateTo('/')
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
@ -2,13 +2,12 @@ import { useNuxtApp } from '#app'
|
||||
import { userReadNotifications } from '~/composables/user.js'
|
||||
|
||||
async function getBulk(type, ids) {
|
||||
const auth = (await useAuth()).value
|
||||
if (ids.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const url = `${type}?ids=${JSON.stringify([...new Set(ids)])}`
|
||||
const { data: bulkFetch } = await useAsyncData(url, () => useBaseFetch(url, auth.headers))
|
||||
const url = `${type}?ids=${encodeURIComponent(JSON.stringify([...new Set(ids)]))}`
|
||||
const { data: bulkFetch } = await useAsyncData(url, () => useBaseFetch(url))
|
||||
return bulkFetch.value
|
||||
}
|
||||
|
||||
@ -16,7 +15,7 @@ export async function fetchNotifications() {
|
||||
try {
|
||||
const auth = (await useAuth()).value
|
||||
const { data: notifications } = await useAsyncData(`user/${auth.user.id}/notifications`, () =>
|
||||
useBaseFetch(`user/${auth.user.id}/notifications`, auth.headers)
|
||||
useBaseFetch(`user/${auth.user.id}/notifications`)
|
||||
)
|
||||
|
||||
const projectIds = []
|
||||
@ -161,12 +160,8 @@ export function groupNotifications(notifications, includeRead = false) {
|
||||
|
||||
export async function markAsRead(ids) {
|
||||
try {
|
||||
const auth = (await useAuth()).value
|
||||
await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
})
|
||||
await userReadNotifications(ids)
|
||||
return (notifications) => {
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
export const getProjectTypeForUrl = (type, categories) => {
|
||||
const app = useNuxtApp()
|
||||
return getProjectTypeForUrlShorthand(app, type, categories)
|
||||
return getProjectTypeForUrlShorthand(type, categories)
|
||||
}
|
||||
|
||||
export const getProjectTypeForUrlShorthand = (app, type, categories) => {
|
||||
export const getProjectTypeForUrlShorthand = (type, categories, overrideTags) => {
|
||||
const tags = overrideTags ?? useTags().value
|
||||
|
||||
if (type === 'mod') {
|
||||
const isMod = categories.some((category) => {
|
||||
return app.$tag.loaderData.modLoaders.includes(category)
|
||||
return tags.loaderData.modLoaders.includes(category)
|
||||
})
|
||||
|
||||
const isPlugin = categories.some((category) => {
|
||||
return app.$tag.loaderData.allPluginLoaders.includes(category)
|
||||
return tags.loaderData.allPluginLoaders.includes(category)
|
||||
})
|
||||
|
||||
const isDataPack = categories.some((category) => {
|
||||
return app.$tag.loaderData.dataPackLoaders.includes(category)
|
||||
return tags.loaderData.dataPackLoaders.includes(category)
|
||||
})
|
||||
|
||||
if (isDataPack) {
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
export const acceptTeamInvite = async (teamId) => {
|
||||
const app = useNuxtApp()
|
||||
await useBaseFetch(`team/${teamId}/join`, {
|
||||
method: 'POST',
|
||||
...app.$defaultHeaders(),
|
||||
})
|
||||
}
|
||||
export const removeSelfFromTeam = async (teamId) => {
|
||||
const app = useNuxtApp()
|
||||
await removeTeamMember(teamId, app.$auth.user.id)
|
||||
const auth = await useAuth()
|
||||
await removeTeamMember(teamId, auth.user.id)
|
||||
}
|
||||
export const removeTeamMember = async (teamId, userId) => {
|
||||
const app = useNuxtApp()
|
||||
await useBaseFetch(`team/${teamId}/members/${userId}`, {
|
||||
method: 'DELETE',
|
||||
...app.$defaultHeaders(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,5 +1,21 @@
|
||||
<template>
|
||||
<div ref="main_page" class="layout" :class="{ 'expanded-mobile-nav': isBrowseMenuOpen }">
|
||||
<div
|
||||
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
|
||||
class="email-nag"
|
||||
>
|
||||
<template v-if="auth.user.email">
|
||||
<span>For security purposes, please verify your email address on Modrinth.</span>
|
||||
<button class="btn" @click="resendVerifyEmail">Re-send verification email</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>For security purposes, please enter your email on Modrinth.</span>
|
||||
<nuxt-link class="btn" to="/settings/account">
|
||||
<SettingsIcon />
|
||||
Visit account settings
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
<header class="site-header" role="presentation">
|
||||
<section class="navbar columns" role="navigation">
|
||||
<section class="logo column" role="presentation">
|
||||
@ -25,7 +41,6 @@
|
||||
<button
|
||||
class="control-button button-transparent"
|
||||
title="Switch theme"
|
||||
:disabled="isThemeSwitchOnHold"
|
||||
@click="changeTheme"
|
||||
>
|
||||
<MoonIcon v-if="$colorMode.value === 'light'" aria-hidden="true" />
|
||||
@ -80,7 +95,7 @@
|
||||
<span class="title">Settings</span>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
v-if="$tag.staffRoles.includes($auth.user.role)"
|
||||
v-if="tags.staffRoles.includes(auth.user.role)"
|
||||
class="item button-transparent"
|
||||
to="/moderation"
|
||||
>
|
||||
@ -88,21 +103,16 @@
|
||||
<span class="title">Moderation</span>
|
||||
</NuxtLink>
|
||||
<hr class="divider" />
|
||||
<button class="item button-transparent" @click="logout()">
|
||||
<button class="item button-transparent" @click="logoutUser()">
|
||||
<LogOutIcon class="icon" />
|
||||
<span class="dropdown-item__text">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<section v-else class="auth-prompt">
|
||||
<a
|
||||
:href="getAuthUrl()"
|
||||
class="log-in-button header-button brand-button"
|
||||
rel="noopener nofollow"
|
||||
>
|
||||
<GitHubIcon aria-hidden="true" />
|
||||
Sign in with GitHub</a
|
||||
>
|
||||
<nuxt-link class="iconified-button brand-button" to="/auth/sign-in">
|
||||
<LogInIcon /> Sign in
|
||||
</nuxt-link>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
@ -150,19 +160,13 @@
|
||||
<div>Visit your profile</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<a
|
||||
v-else
|
||||
class="iconified-button brand-button"
|
||||
:href="getAuthUrl()"
|
||||
rel="nofollow noopener"
|
||||
>
|
||||
<GitHubIcon aria-hidden="true" />
|
||||
Sign in with GitHub
|
||||
</a>
|
||||
<nuxt-link v-else class="iconified-button brand-button" to="/auth/sign-in">
|
||||
<LogInIcon /> Sign in
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="links">
|
||||
<template v-if="auth.user">
|
||||
<button class="iconified-button danger-button" @click="logout()">
|
||||
<button class="iconified-button danger-button" @click="logoutUser()">
|
||||
<LogOutIcon aria-hidden="true" />
|
||||
Log out
|
||||
</button>
|
||||
@ -187,7 +191,7 @@
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
Settings
|
||||
</NuxtLink>
|
||||
<button class="iconified-button" :disabled="isThemeSwitchOnHold" @click="changeTheme">
|
||||
<button class="iconified-button" @click="changeTheme">
|
||||
<MoonIcon v-if="$colorMode.value === 'light'" class="icon" />
|
||||
<SunIcon v-else class="icon" />
|
||||
<span class="dropdown-item__text">Change theme</span>
|
||||
@ -319,11 +323,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="iconified-button raised-button"
|
||||
:disabled="isThemeSwitchOnHold"
|
||||
@click="changeTheme"
|
||||
>
|
||||
<button class="iconified-button raised-button" @click="changeTheme">
|
||||
<MoonIcon v-if="$colorMode.value === 'light'" aria-hidden="true" />
|
||||
<SunIcon v-else aria-hidden="true" />
|
||||
Change theme
|
||||
@ -340,6 +340,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { LogInIcon } from 'omorphia'
|
||||
import HamburgerIcon from '~/assets/images/utils/hamburger.svg'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import SearchIcon from '~/assets/images/utils/search.svg'
|
||||
@ -357,7 +358,6 @@ import LogOutIcon from '~/assets/images/utils/log-out.svg'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg'
|
||||
import ChartIcon from '~/assets/images/utils/chart.svg'
|
||||
|
||||
import GitHubIcon from '~/assets/images/utils/github.svg'
|
||||
import NavRow from '~/components/ui/NavRow.vue'
|
||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
@ -365,6 +365,8 @@ import Avatar from '~/components/ui/Avatar.vue'
|
||||
const app = useNuxtApp()
|
||||
const auth = await useAuth()
|
||||
const user = await useUser()
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useTags()
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const route = useRoute()
|
||||
@ -383,9 +385,9 @@ let developerModeCounter = 0
|
||||
|
||||
function developerModeIncrement() {
|
||||
if (developerModeCounter >= 5) {
|
||||
app.$cosmetics.developerMode = !app.$cosmetics.developerMode
|
||||
cosmetics.value.developerMode = !cosmetics.value.developerMode
|
||||
developerModeCounter = 0
|
||||
if (app.$cosmetics.developerMode) {
|
||||
if (cosmetics.value.developerMode) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'Developer mode activated',
|
||||
@ -404,6 +406,10 @@ function developerModeIncrement() {
|
||||
developerModeCounter++
|
||||
}
|
||||
}
|
||||
|
||||
async function logoutUser() {
|
||||
await logout()
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
export default defineNuxtComponent({
|
||||
@ -412,7 +418,6 @@ export default defineNuxtComponent({
|
||||
isDropdownOpen: false,
|
||||
isMobileMenuOpen: false,
|
||||
isBrowseMenuOpen: false,
|
||||
isThemeSwitchOnHold: false,
|
||||
registeredSkipLink: null,
|
||||
hideDropdown: false,
|
||||
navRoutes: [
|
||||
@ -463,12 +468,8 @@ export default defineNuxtComponent({
|
||||
this.runAnalytics()
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
mounted() {
|
||||
this.runAnalytics()
|
||||
if (this.$route.query.code) {
|
||||
await useAuth(this.$route.query.code)
|
||||
window.history.replaceState(history.state, null, this.$route.path)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
runAnalytics() {
|
||||
@ -498,28 +499,8 @@ export default defineNuxtComponent({
|
||||
this.isMobileMenuOpen = false
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
useCookie('auth-token').value = null
|
||||
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Logged Out',
|
||||
text: 'You have logged out successfully!',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
useRouter()
|
||||
.push('/')
|
||||
.then(() => {
|
||||
useRouter().go()
|
||||
})
|
||||
},
|
||||
changeTheme() {
|
||||
this.isThemeSwitchOnHold = true
|
||||
updateTheme(this.$colorMode.value === 'dark' ? 'light' : 'dark', true)
|
||||
setTimeout(() => {
|
||||
this.isThemeSwitchOnHold = false
|
||||
}, 1000)
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -1147,5 +1128,15 @@ export default defineNuxtComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-nag {
|
||||
background-color: var(--color-raised-bg);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
</style>
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
export default defineNuxtRouteMiddleware(async (_to, from) => {
|
||||
const auth = await useAuth()
|
||||
|
||||
if (!auth.value.user) {
|
||||
return navigateTo(getAuthUrl(), { external: true })
|
||||
return navigateTo(`/auth/sign-in?redirect=${encodeURIComponent(from.fullPath)}`)
|
||||
}
|
||||
})
|
||||
|
||||
@ -294,12 +294,15 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
modules: ['@vintl/nuxt'],
|
||||
modules: ['@vintl/nuxt', '@nuxtjs/turnstile'],
|
||||
vintl: {
|
||||
defaultLocale: 'en-US',
|
||||
storage: 'cookie',
|
||||
parserless: 'only-prod',
|
||||
},
|
||||
turnstile: {
|
||||
siteKey: '0x4AAAAAAAHWfmKCm7cUG869',
|
||||
},
|
||||
nitro: {
|
||||
moduleSideEffects: ['@vintl/compact-number/locale-data'],
|
||||
},
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.1.2",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||
"@nuxtjs/turnstile": "^0.5.0",
|
||||
"@types/node": "^20.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
@ -44,6 +45,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"omorphia": "^0.4.31",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"vue-multiselect": "^3.0.0-alpha.2",
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
|
||||
@ -106,6 +106,8 @@
|
||||
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
||||
:all-members="allMembers"
|
||||
:update-members="updateMembers"
|
||||
:auth="auth"
|
||||
:tags="tags"
|
||||
/>
|
||||
<NuxtPage
|
||||
v-model:project="project"
|
||||
@ -149,7 +151,7 @@
|
||||
/>
|
||||
</Head>
|
||||
<ModalModeration
|
||||
v-if="$auth.user"
|
||||
v-if="auth.user"
|
||||
ref="modalModeration"
|
||||
:project="project"
|
||||
:status="moderationStatus"
|
||||
@ -161,7 +163,7 @@
|
||||
</div>
|
||||
</Modal>
|
||||
<ModalReport
|
||||
v-if="$auth.user"
|
||||
v-if="auth.user"
|
||||
ref="modal_project_report"
|
||||
:item-id="project.id"
|
||||
item-type="project"
|
||||
@ -169,7 +171,7 @@
|
||||
<div
|
||||
:class="{
|
||||
'normal-page': true,
|
||||
'alt-layout': $cosmetics.projectLayout,
|
||||
'alt-layout': cosmetics.projectLayout,
|
||||
}"
|
||||
>
|
||||
<div class="normal-page__sidebar">
|
||||
@ -229,7 +231,7 @@
|
||||
class="categories"
|
||||
>
|
||||
<Badge
|
||||
v-if="$auth.user && currentMember"
|
||||
v-if="auth.user && currentMember"
|
||||
:type="project.status"
|
||||
class="status-badge"
|
||||
/>
|
||||
@ -287,7 +289,7 @@
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="input-group">
|
||||
<template v-if="$auth.user">
|
||||
<template v-if="auth.user">
|
||||
<button class="iconified-button" @click="$refs.modal_project_report.show()">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
@ -344,7 +346,7 @@
|
||||
/>
|
||||
<div class="buttons status-buttons">
|
||||
<button
|
||||
v-if="$tag.approvedStatuses.includes(project.status)"
|
||||
v-if="tags.approvedStatuses.includes(project.status)"
|
||||
class="iconified-button"
|
||||
@click="clearMessage"
|
||||
>
|
||||
@ -354,14 +356,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$auth.user && $tag.staffRoles.includes($auth.user.role)"
|
||||
v-if="auth.user && tags.staffRoles.includes(auth.user.role)"
|
||||
class="universal-card moderation-card"
|
||||
>
|
||||
<h2>Moderation actions</h2>
|
||||
<div class="input-stack">
|
||||
<button
|
||||
v-if="
|
||||
!$tag.approvedStatuses.includes(project.status) || project.status === 'processing'
|
||||
!tags.approvedStatuses.includes(project.status) || project.status === 'processing'
|
||||
"
|
||||
class="iconified-button brand-button"
|
||||
@click="openModerationModal(requestedStatus)"
|
||||
@ -372,9 +374,9 @@
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
$tag.approvedStatuses.includes(project.status) ||
|
||||
tags.approvedStatuses.includes(project.status) ||
|
||||
project.status === 'processing' ||
|
||||
($tag.rejectedStatuses.includes(project.status) && project.status !== 'withheld')
|
||||
(tags.rejectedStatuses.includes(project.status) && project.status !== 'withheld')
|
||||
"
|
||||
class="iconified-button danger-button"
|
||||
@click="openModerationModal('withheld')"
|
||||
@ -384,9 +386,9 @@
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
$tag.approvedStatuses.includes(project.status) ||
|
||||
tags.approvedStatuses.includes(project.status) ||
|
||||
project.status === 'processing' ||
|
||||
($tag.rejectedStatuses.includes(project.status) && project.status !== 'rejected')
|
||||
(tags.rejectedStatuses.includes(project.status) && project.status !== 'rejected')
|
||||
"
|
||||
class="iconified-button danger-button"
|
||||
@click="openModerationModal('rejected')"
|
||||
@ -422,6 +424,8 @@
|
||||
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
||||
:all-members="allMembers"
|
||||
:update-members="updateMembers"
|
||||
:auth="auth"
|
||||
:tags="tags"
|
||||
/>
|
||||
<div v-else-if="project.status === 'withheld'" class="card warning" aria-label="Warning">
|
||||
{{ project.title }} has been removed from search by Modrinth's moderators. Please use
|
||||
@ -447,7 +451,7 @@
|
||||
Prism Launcher</a
|
||||
>.
|
||||
</div>
|
||||
<Promotion v-if="$tag.approvedStatuses.includes(project.status)" />
|
||||
<Promotion v-if="tags.approvedStatuses.includes(project.status)" />
|
||||
<div class="navigation-card">
|
||||
<NavRow
|
||||
:links="[
|
||||
@ -485,7 +489,7 @@
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div v-if="$auth.user && currentMember" class="input-group">
|
||||
<div v-if="auth.user && currentMember" class="input-group">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||
class="iconified-button"
|
||||
@ -808,12 +812,15 @@ const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const auth = await useAuth()
|
||||
const user = await useUser()
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useTags()
|
||||
|
||||
if (
|
||||
!route.params.id ||
|
||||
!(
|
||||
data.$tag.projectTypes.find((x) => x.id === route.params.type) ||
|
||||
tags.value.projectTypes.find((x) => x.id === route.params.type) ||
|
||||
route.params.type === 'project'
|
||||
)
|
||||
) {
|
||||
@ -833,28 +840,27 @@ try {
|
||||
{ data: featuredVersions },
|
||||
{ data: versions },
|
||||
] = await Promise.all([
|
||||
useAsyncData(
|
||||
`project/${route.params.id}`,
|
||||
() => useBaseFetch(`project/${route.params.id}`, data.$defaultHeaders()),
|
||||
{
|
||||
transform: (project) => {
|
||||
if (project) {
|
||||
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
|
||||
useAsyncData(`project/${route.params.id}`, () => useBaseFetch(`project/${route.params.id}`), {
|
||||
transform: (project) => {
|
||||
if (project) {
|
||||
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
|
||||
project.project_type = data.$getProjectTypeForUrl(
|
||||
project.project_type,
|
||||
project.loaders,
|
||||
tags.value
|
||||
)
|
||||
|
||||
project.project_type = data.$getProjectTypeForUrl(project.project_type, project.loaders)
|
||||
|
||||
if (process.client && history.state && history.state.overrideProjectType) {
|
||||
project.project_type = history.state.overrideProjectType
|
||||
}
|
||||
if (process.client && history.state && history.state.overrideProjectType) {
|
||||
project.project_type = history.state.overrideProjectType
|
||||
}
|
||||
}
|
||||
|
||||
return project
|
||||
},
|
||||
}
|
||||
),
|
||||
return project
|
||||
},
|
||||
}),
|
||||
useAsyncData(
|
||||
`project/${route.params.id}/members`,
|
||||
() => useBaseFetch(`project/${route.params.id}/members`, data.$defaultHeaders()),
|
||||
() => useBaseFetch(`project/${route.params.id}/members`),
|
||||
{
|
||||
transform: (members) => {
|
||||
members.forEach((it, index) => {
|
||||
@ -867,13 +873,13 @@ try {
|
||||
}
|
||||
),
|
||||
useAsyncData(`project/${route.params.id}/dependencies`, () =>
|
||||
useBaseFetch(`project/${route.params.id}/dependencies`, data.$defaultHeaders())
|
||||
useBaseFetch(`project/${route.params.id}/dependencies`)
|
||||
),
|
||||
useAsyncData(`project/${route.params.id}/version?featured=true`, () =>
|
||||
useBaseFetch(`project/${route.params.id}/version?featured=true`, data.$defaultHeaders())
|
||||
useBaseFetch(`project/${route.params.id}/version?featured=true`)
|
||||
),
|
||||
useAsyncData(`project/${route.params.id}/version`, () =>
|
||||
useBaseFetch(`project/${route.params.id}/version`, data.$defaultHeaders())
|
||||
useBaseFetch(`project/${route.params.id}/version`)
|
||||
),
|
||||
])
|
||||
|
||||
@ -910,23 +916,23 @@ if (project.value.project_type !== route.params.type || route.params.id !== proj
|
||||
|
||||
const members = ref(allMembers.value.filter((x) => x.accepted))
|
||||
const currentMember = ref(
|
||||
data.$auth.user ? allMembers.value.find((x) => x.user.id === data.$auth.user.id) : null
|
||||
auth.value.user ? allMembers.value.find((x) => x.user.id === auth.value.user.id) : null
|
||||
)
|
||||
|
||||
if (
|
||||
!currentMember.value &&
|
||||
data.$auth.user &&
|
||||
data.$tag.staffRoles.includes(data.$auth.user.role)
|
||||
auth.value.user &&
|
||||
tags.value.staffRoles.includes(auth.value.user.role)
|
||||
) {
|
||||
currentMember.value = {
|
||||
team_id: project.team_id,
|
||||
user: data.$auth.user,
|
||||
role: data.$auth.role,
|
||||
permissions: data.$auth.user.role === 'admin' ? 1023 : 12,
|
||||
user: auth.value.user,
|
||||
role: auth.value.role,
|
||||
permissions: auth.value.user.role === 'admin' ? 1023 : 12,
|
||||
accepted: true,
|
||||
payouts_split: 0,
|
||||
avatar_url: data.$auth.user.avatar_url,
|
||||
name: data.$auth.user.username,
|
||||
avatar_url: auth.value.user.avatar_url,
|
||||
name: auth.value.user.username,
|
||||
}
|
||||
}
|
||||
|
||||
@ -943,7 +949,7 @@ featuredVersions.value = versions.value.filter((version) => featuredIds.includes
|
||||
featuredVersions.value.sort((a, b) => {
|
||||
const aLatest = a.game_versions[a.game_versions.length - 1]
|
||||
const bLatest = b.game_versions[b.game_versions.length - 1]
|
||||
const gameVersions = data.$tag.gameVersions.map((e) => e.version)
|
||||
const gameVersions = tags.value.gameVersions.map((e) => e.version)
|
||||
return gameVersions.indexOf(aLatest) - gameVersions.indexOf(bLatest)
|
||||
})
|
||||
|
||||
@ -967,7 +973,7 @@ const featuredGalleryImage = computed(() => project.value.gallery.find((img) =>
|
||||
const requestedStatus = computed(() => project.value.requested_status ?? 'approved')
|
||||
|
||||
async function resetProject() {
|
||||
const newProject = await useBaseFetch(`project/${project.value.id}`, data.$defaultHeaders())
|
||||
const newProject = await useBaseFetch(`project/${project.value.id}`)
|
||||
|
||||
newProject.actualProjectType = JSON.parse(JSON.stringify(newProject.project_type))
|
||||
|
||||
@ -986,7 +992,6 @@ async function clearMessage() {
|
||||
moderation_message: null,
|
||||
moderation_message_body: null,
|
||||
},
|
||||
...data.$defaultHeaders(),
|
||||
})
|
||||
|
||||
project.value.moderator_message = null
|
||||
@ -1011,7 +1016,6 @@ async function setProcessing() {
|
||||
body: {
|
||||
status: 'processing',
|
||||
},
|
||||
...data.$defaultHeaders(),
|
||||
})
|
||||
|
||||
project.value.status = 'processing'
|
||||
@ -1048,7 +1052,6 @@ async function patchProject(resData, quiet = false) {
|
||||
await useBaseFetch(`project/${project.value.id}`, {
|
||||
method: 'PATCH',
|
||||
body: resData,
|
||||
...data.$defaultHeaders(),
|
||||
})
|
||||
|
||||
for (const key in resData) {
|
||||
@ -1099,7 +1102,6 @@ async function patchIcon(icon) {
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: icon,
|
||||
...data.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
await resetProject()
|
||||
@ -1136,7 +1138,7 @@ function openModerationModal(status) {
|
||||
async function updateMembers() {
|
||||
allMembers.value = await useAsyncData(
|
||||
`project/${route.params.id}/members`,
|
||||
() => useBaseFetch(`project/${route.params.id}/members`, data.$defaultHeaders()),
|
||||
() => useBaseFetch(`project/${route.params.id}/members`),
|
||||
{
|
||||
transform: (members) => {
|
||||
members.forEach((it, index) => {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<Meta name="og:description" :contcent="metaDescription" />
|
||||
</Head>
|
||||
<Modal
|
||||
v-if="$auth.user && currentMember"
|
||||
v-if="currentMember"
|
||||
ref="modal_edit_item"
|
||||
:header="editIndex === -1 ? 'Upload gallery image' : 'Edit gallery item'"
|
||||
>
|
||||
@ -127,7 +127,7 @@
|
||||
</div>
|
||||
</Modal>
|
||||
<ModalConfirm
|
||||
v-if="$auth.user && currentMember"
|
||||
v-if="currentMember"
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this gallery image?"
|
||||
description="This will remove this gallery image forever (like really forever)."
|
||||
@ -446,7 +446,6 @@ export default defineNuxtComponent({
|
||||
await useBaseFetch(url, {
|
||||
method: 'POST',
|
||||
body: this.editFile,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await this.updateProject()
|
||||
|
||||
@ -484,7 +483,6 @@ export default defineNuxtComponent({
|
||||
|
||||
await useBaseFetch(url, {
|
||||
method: 'PATCH',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
|
||||
await this.updateProject()
|
||||
@ -511,7 +509,6 @@ export default defineNuxtComponent({
|
||||
)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
|
||||
@ -528,7 +525,7 @@ export default defineNuxtComponent({
|
||||
stopLoading()
|
||||
},
|
||||
async updateProject() {
|
||||
const project = await useBaseFetch(`project/${this.project.id}`, this.$defaultHeaders())
|
||||
const project = await useBaseFetch(`project/${this.project.id}`)
|
||||
|
||||
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
|
||||
|
||||
|
||||
@ -68,6 +68,7 @@
|
||||
:project="project"
|
||||
:set-status="setStatus"
|
||||
:current-member="currentMember"
|
||||
:auth="auth"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@ -104,9 +105,10 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:project'])
|
||||
|
||||
const app = useNuxtApp()
|
||||
const auth = await useAuth()
|
||||
|
||||
const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () =>
|
||||
useBaseFetch(`thread/${props.project.thread_id}`, app.$defaultHeaders())
|
||||
useBaseFetch(`thread/${props.project.thread_id}`)
|
||||
)
|
||||
async function setStatus(status) {
|
||||
startLoading()
|
||||
@ -117,12 +119,11 @@ async function setStatus(status) {
|
||||
await useBaseFetch(`project/${props.project.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...app.$defaultHeaders(),
|
||||
})
|
||||
const project = props.project
|
||||
project.status = status
|
||||
emit('update:project', project)
|
||||
thread.value = await useBaseFetch(`thread/${thread.value.id}`, app.$defaultHeaders())
|
||||
thread.value = await useBaseFetch(`thread/${thread.value.id}`)
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
|
||||
@ -191,7 +191,7 @@
|
||||
id="project-visibility"
|
||||
v-model="visibility"
|
||||
placeholder="Select one"
|
||||
:options="$tag.approvedStatuses"
|
||||
:options="tags.approvedStatuses"
|
||||
:custom-label="(value) => $formatProjectStatus(value)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
@ -315,6 +315,11 @@ export default defineNuxtComponent({
|
||||
},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
|
||||
return { tags }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: this.project.title,
|
||||
@ -325,7 +330,7 @@ export default defineNuxtComponent({
|
||||
clientSide: this.project.client_side,
|
||||
serverSide: this.project.server_side,
|
||||
deletedIcon: false,
|
||||
visibility: this.$tag.approvedStatuses.includes(this.project.status)
|
||||
visibility: this.tags.approvedStatuses.includes(this.project.status)
|
||||
? this.project.status
|
||||
: this.project.requested_status,
|
||||
}
|
||||
@ -360,7 +365,7 @@ export default defineNuxtComponent({
|
||||
if (this.serverSide !== this.project.server_side) {
|
||||
data.server_side = this.serverSide
|
||||
}
|
||||
if (this.$tag.approvedStatuses.includes(this.project.status)) {
|
||||
if (this.tags.approvedStatuses.includes(this.project.status)) {
|
||||
if (this.visibility !== this.project.status) {
|
||||
data.status = this.visibility
|
||||
}
|
||||
@ -376,7 +381,7 @@ export default defineNuxtComponent({
|
||||
},
|
||||
methods: {
|
||||
hasModifiedVisibility() {
|
||||
const originalVisibility = this.$tag.approvedStatuses.includes(this.project.status)
|
||||
const originalVisibility = this.tags.approvedStatuses.includes(this.project.status)
|
||||
? this.project.status
|
||||
: this.project.requested_status
|
||||
|
||||
@ -407,7 +412,6 @@ export default defineNuxtComponent({
|
||||
async deleteProject() {
|
||||
await useBaseFetch(`project/${this.project.id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await initUserProjects()
|
||||
await this.$router.push('/dashboard/review')
|
||||
@ -426,7 +430,6 @@ export default defineNuxtComponent({
|
||||
async deleteIcon() {
|
||||
await useBaseFetch(`project/${this.project.id}/icon`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await this.updateIcon()
|
||||
this.$notify({
|
||||
|
||||
@ -96,7 +96,7 @@
|
||||
<Multiselect
|
||||
v-model="donationLink.platform"
|
||||
placeholder="Select platform"
|
||||
:options="$tag.donationPlatforms.map((x) => x.name)"
|
||||
:options="tags.donationPlatforms.map((x) => x.name)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
@ -155,6 +155,11 @@ export default defineNuxtComponent({
|
||||
},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
|
||||
return { tags }
|
||||
},
|
||||
data() {
|
||||
const donationLinks = JSON.parse(JSON.stringify(this.project.donation_urls))
|
||||
donationLinks.push({
|
||||
@ -195,7 +200,7 @@ export default defineNuxtComponent({
|
||||
|
||||
const donationLinks = this.donationLinks.filter((link) => link.url && link.platform)
|
||||
donationLinks.forEach((link) => {
|
||||
link.id = this.$tag.donationPlatforms.find(
|
||||
link.id = this.tags.donationPlatforms.find(
|
||||
(platform) => platform.name === link.platform
|
||||
).short
|
||||
})
|
||||
|
||||
@ -323,7 +323,6 @@ export default defineNuxtComponent({
|
||||
await useBaseFetch(`team/${this.project.team}/members`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
this.currentUsername = ''
|
||||
await this.updateMembers()
|
||||
@ -346,7 +345,6 @@ export default defineNuxtComponent({
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
await this.updateMembers()
|
||||
@ -381,7 +379,6 @@ export default defineNuxtComponent({
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
await this.updateMembers()
|
||||
@ -411,7 +408,6 @@ export default defineNuxtComponent({
|
||||
body: {
|
||||
user_id: this.allTeamMembers[index].user.id,
|
||||
},
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
@ -426,9 +422,7 @@ export default defineNuxtComponent({
|
||||
stopLoading()
|
||||
},
|
||||
async updateMembers() {
|
||||
this.allTeamMembers = (
|
||||
await useBaseFetch(`team/${this.project.team}/members`, this.$defaultHeaders())
|
||||
).map((it) => ({
|
||||
this.allTeamMembers = (await useBaseFetch(`team/${this.project.team}/members`)).map((it) => ({
|
||||
avatar_url: it.user.avatar_url,
|
||||
name: it.user.username,
|
||||
oldRole: it.role,
|
||||
|
||||
@ -152,13 +152,13 @@ export default defineNuxtComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTags: this.$sortedCategories.filter(
|
||||
selectedTags: this.$sortedCategories().filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
(this.project.categories.includes(x.name) ||
|
||||
this.project.additional_categories.includes(x.name))
|
||||
),
|
||||
featuredTags: this.$sortedCategories.filter(
|
||||
featuredTags: this.$sortedCategories().filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
this.project.categories.includes(x.name)
|
||||
@ -168,7 +168,7 @@ export default defineNuxtComponent({
|
||||
computed: {
|
||||
categoryLists() {
|
||||
const lists = {}
|
||||
this.$sortedCategories.forEach((x) => {
|
||||
this.$sortedCategories().forEach((x) => {
|
||||
if (x.project_type === this.project.actualProjectType) {
|
||||
const header = x.header
|
||||
if (!lists[header]) {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<Meta name="og:description" :content="metaDescription" />
|
||||
</Head>
|
||||
<ModalConfirm
|
||||
v-if="$auth.user && currentMember"
|
||||
v-if="currentMember"
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this version?"
|
||||
description="This will remove this version forever (like really forever)."
|
||||
@ -17,12 +17,12 @@
|
||||
@proceed="deleteVersion()"
|
||||
/>
|
||||
<ModalReport
|
||||
v-if="$auth.user"
|
||||
v-if="auth.user"
|
||||
ref="modal_version_report"
|
||||
:item-id="version.id"
|
||||
item-type="version"
|
||||
/>
|
||||
<Modal v-if="$auth.user && currentMember" ref="modal_package_mod" header="Package data pack">
|
||||
<Modal v-if="auth.user && currentMember" ref="modal_package_mod" header="Package data pack">
|
||||
<div class="modal-package-mod universal-labels">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
@ -116,7 +116,7 @@
|
||||
Create
|
||||
</button>
|
||||
<nuxt-link
|
||||
v-if="$auth.user"
|
||||
v-if="auth.user"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`"
|
||||
class="iconified-button"
|
||||
>
|
||||
@ -164,7 +164,7 @@
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
<a v-if="!$auth.user" class="iconified-button" :href="getAuthUrl()" rel="noopener nofollow">
|
||||
<a v-if="!auth.user" class="iconified-button" :href="getAuthUrl()" rel="noopener nofollow">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</a>
|
||||
@ -181,7 +181,7 @@
|
||||
<button
|
||||
v-if="
|
||||
currentMember &&
|
||||
version.loaders.some((x) => $tag.loaderData.dataPackLoaders.includes(x))
|
||||
version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))
|
||||
"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_package_mod.show()"
|
||||
@ -390,7 +390,7 @@
|
||||
</span>
|
||||
<multiselect
|
||||
v-if="
|
||||
version.loaders.some((x) => $tag.loaderData.dataPackLoaders.includes(x)) &&
|
||||
version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x)) &&
|
||||
isEditing &&
|
||||
primaryFile.hashes.sha1 !== file.hashes.sha1
|
||||
"
|
||||
@ -457,7 +457,7 @@
|
||||
<span class="file-size">({{ $formatBytes(file.size) }})</span>
|
||||
</span>
|
||||
<multiselect
|
||||
v-if="version.loaders.some((x) => $tag.loaderData.dataPackLoaders.includes(x))"
|
||||
v-if="version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))"
|
||||
v-model="newFileTypes[index]"
|
||||
class="raised-multiselect"
|
||||
placeholder="Select file type"
|
||||
@ -484,7 +484,7 @@
|
||||
</div>
|
||||
<div class="additional-files">
|
||||
<h4>Upload additional files</h4>
|
||||
<span v-if="version.loaders.some((x) => $tag.loaderData.dataPackLoaders.includes(x))">
|
||||
<span v-if="version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))">
|
||||
Used for additional files such as required/optional resource packs
|
||||
</span>
|
||||
<span v-else>Used for files such as sources or Javadocs.</span>
|
||||
@ -566,14 +566,14 @@
|
||||
v-if="isEditing"
|
||||
v-model="version.loaders"
|
||||
:options="
|
||||
$tag.loaders
|
||||
tags.loaders
|
||||
.filter((x) =>
|
||||
x.supported_project_types.includes(project.actualProjectType.toLowerCase())
|
||||
)
|
||||
.map((it) => it.name)
|
||||
"
|
||||
:custom-label="(value) => $formatCategory(value)"
|
||||
:loading="$tag.loaders.length === 0"
|
||||
:loading="tags.loaders.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
@ -593,12 +593,12 @@
|
||||
v-model="version.game_versions"
|
||||
:options="
|
||||
showSnapshots
|
||||
? $tag.gameVersions.map((x) => x.version)
|
||||
: $tag.gameVersions
|
||||
? tags.gameVersions.map((x) => x.version)
|
||||
: tags.gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((x) => x.version)
|
||||
"
|
||||
:loading="$tag.gameVersions.length === 0"
|
||||
:loading="tags.gameVersions.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
@ -772,6 +772,9 @@ export default defineNuxtComponent({
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
|
||||
const auth = await useAuth()
|
||||
const tags = useTags()
|
||||
|
||||
const path = route.name.split('-')
|
||||
const mode = path[path.length - 1]
|
||||
|
||||
@ -828,7 +831,7 @@ export default defineNuxtComponent({
|
||||
const inferredData = await inferVersionInfo(
|
||||
replaceFile,
|
||||
props.project,
|
||||
data.$tag.gameVersions
|
||||
tags.value.gameVersions
|
||||
)
|
||||
|
||||
version = {
|
||||
@ -895,6 +898,8 @@ export default defineNuxtComponent({
|
||||
const order = ['required', 'optional', 'incompatible', 'embedded']
|
||||
|
||||
return {
|
||||
auth,
|
||||
tags,
|
||||
fileTypes: ref(fileTypes),
|
||||
oldFileTypes: ref(oldFileTypes),
|
||||
isCreating: ref(isCreating),
|
||||
@ -1110,7 +1115,6 @@ export default defineNuxtComponent({
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Disposition': formData,
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -1135,13 +1139,11 @@ export default defineNuxtComponent({
|
||||
}
|
||||
}),
|
||||
},
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
|
||||
for (const hash of this.deleteFiles) {
|
||||
await useBaseFetch(`version_file/${hash}?version_id=${this.version.id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -1246,7 +1248,6 @@ export default defineNuxtComponent({
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Disposition': formData,
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
})
|
||||
|
||||
@ -1263,7 +1264,6 @@ export default defineNuxtComponent({
|
||||
|
||||
await useBaseFetch(`version/${this.version.id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
|
||||
await this.resetProjectVersions()
|
||||
@ -1279,7 +1279,7 @@ export default defineNuxtComponent({
|
||||
this.version,
|
||||
this.primaryFile,
|
||||
this.members,
|
||||
this.$tag.gameVersions,
|
||||
this.tags.gameVersions,
|
||||
this.packageLoaders
|
||||
)
|
||||
|
||||
@ -1324,12 +1324,9 @@ export default defineNuxtComponent({
|
||||
},
|
||||
async resetProjectVersions() {
|
||||
const [versions, featuredVersions, dependencies] = await Promise.all([
|
||||
useBaseFetch(`project/${this.version.project_id}/version`, this.$defaultHeaders()),
|
||||
useBaseFetch(
|
||||
`project/${this.version.project_id}/version?featured=true`,
|
||||
this.$defaultHeaders()
|
||||
),
|
||||
useBaseFetch(`project/${this.version.project_id}/dependencies`, this.$defaultHeaders()),
|
||||
useBaseFetch(`project/${this.version.project_id}/version`),
|
||||
useBaseFetch(`project/${this.version.project_id}/version?featured=true`),
|
||||
useBaseFetch(`project/${this.version.project_id}/dependencies`),
|
||||
])
|
||||
|
||||
const newCreatedVersions = this.$computeVersions(versions, this.members)
|
||||
|
||||
156
pages/auth.vue
Normal file
156
pages/auth.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtPage class="auth-container universal-card" :route="route" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.auth-container {
|
||||
width: 25rem;
|
||||
padding: var(--gap-xl);
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
margin: 2rem auto;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 700;
|
||||
min-height: 2.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn.right-icon svg {
|
||||
margin-left: var(--gap-sm);
|
||||
}
|
||||
|
||||
.btn.left-icon svg {
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: var(--gap-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button.checkbox {
|
||||
appearance: none !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.continue-btn {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.continue-btn svg {
|
||||
margin: 0 0 0 0.5rem;
|
||||
}
|
||||
|
||||
// login styles
|
||||
.third-party {
|
||||
display: grid;
|
||||
gap: var(--gap-md);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
width: 100%;
|
||||
}
|
||||
.third-party .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.third-party .btn svg {
|
||||
margin-right: var(--gap-sm);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.discord-btn {
|
||||
color: #ffffff;
|
||||
background-color: #5865f2;
|
||||
}
|
||||
|
||||
.apple-btn {
|
||||
color: var(--color-accent-contrast);
|
||||
background-color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.google-btn {
|
||||
color: #ffffff;
|
||||
background-color: #4285f4;
|
||||
}
|
||||
|
||||
.gitlab-btn {
|
||||
color: #ffffff;
|
||||
background-color: #fc6d26;
|
||||
}
|
||||
|
||||
.github-btn {
|
||||
color: #ffffff;
|
||||
background-color: #8740f1;
|
||||
}
|
||||
|
||||
.microsoft-btn {
|
||||
color: var(--color-accent-contrast);
|
||||
background-color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.text-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-divider div {
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
max-width: 5rem;
|
||||
opacity: 40%;
|
||||
border-radius: var(--radius-max);
|
||||
background-color: var(--color-base);
|
||||
}
|
||||
|
||||
.text-divider span {
|
||||
margin-inline: var(--gap-sm);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 25.5rem) {
|
||||
.third-party .btn {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
</style>
|
||||
113
pages/auth/reset-password.vue
Normal file
113
pages/auth/reset-password.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="auth-page-container">
|
||||
<h1>Reset your password</h1>
|
||||
<template v-if="step === 'choose_method'">
|
||||
<p>
|
||||
Enter your email below and we'll send a recovery link to allow you to recover your account.
|
||||
<NuxtTurnstile ref="turnstile" v-model="token" class="turnstile" />
|
||||
</p>
|
||||
<label for="email" hidden>Email or username</label>
|
||||
<input id="email" v-model="email" type="text" placeholder="Email or username" />
|
||||
<button class="btn btn-primary continue-btn" @click="recovery">Send recovery email</button>
|
||||
</template>
|
||||
<template v-else-if="step === 'passed_challenge'">
|
||||
<p>Enter your new password below to gain access to your account.</p>
|
||||
<label for="password" hidden>Password</label>
|
||||
<input id="password" v-model="newPassword" type="password" placeholder="Password" />
|
||||
<label for="confirm-password" hi2dden>Password</label>
|
||||
<input
|
||||
id="confirm-password"
|
||||
v-model="confirmNewPassword"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<button class="btn btn-primary continue-btn" @click="changePassword">Reset password</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
useHead({
|
||||
title: 'Reset Password - Modrinth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
if (auth.value.user) {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
|
||||
const step = ref('choose_method')
|
||||
|
||||
if (route.query.flow) {
|
||||
step.value = 'passed_challenge'
|
||||
}
|
||||
|
||||
const turnstile = ref()
|
||||
|
||||
const email = ref('')
|
||||
const token = ref('')
|
||||
|
||||
async function recovery() {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch('auth/password/reset', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: email.value,
|
||||
challenge: token.value,
|
||||
},
|
||||
})
|
||||
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'Email sent',
|
||||
text: 'An email with instructions has been sent to you if the email was previously saved on your account.',
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
turnstile.value?.reset()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const newPassword = ref('')
|
||||
const confirmNewPassword = ref('')
|
||||
|
||||
async function changePassword() {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch('auth/password', {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
new_password: newPassword.value,
|
||||
flow: route.query.flow,
|
||||
},
|
||||
})
|
||||
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'Password successfully reset',
|
||||
text: 'You can now log-in into your account with your new password.',
|
||||
type: 'success',
|
||||
})
|
||||
await navigateTo('/auth/sign-in')
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
turnstile.value?.reset()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
195
pages/auth/sign-in.vue
Normal file
195
pages/auth/sign-in.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="auth-page-container">
|
||||
<template v-if="flow">
|
||||
<label for="two-factor-code">
|
||||
<span class="label__title">Enter two-factor code</span>
|
||||
<span class="label__description">Please enter a two-factor code to proceed.</span>
|
||||
</label>
|
||||
<input
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCode"
|
||||
maxlength="11"
|
||||
type="text"
|
||||
placeholder="Enter code..."
|
||||
/>
|
||||
|
||||
<button class="btn btn-primary continue-btn" @click="loginTwoFactor">
|
||||
Sign in <RightArrowIcon />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Continue with</h1>
|
||||
<div class="third-party">
|
||||
<a class="btn discord-btn" :href="getAuthUrl('discord')">
|
||||
<DiscordIcon /> <span>Discord</span>
|
||||
</a>
|
||||
<a class="btn github-btn" :href="getAuthUrl('github')"
|
||||
><GitHubIcon /> <span>GitHub</span></a
|
||||
>
|
||||
<a class="btn microsoft-btn" :href="getAuthUrl('microsoft')">
|
||||
<MicrosoftIcon /> <span>Microsoft</span>
|
||||
</a>
|
||||
<a class="btn google-btn" :href="getAuthUrl('google')">
|
||||
<GoogleIcon /> <span>Google</span>
|
||||
</a>
|
||||
<a class="btn apple-btn" :href="getAuthUrl('steam')"><SteamIcon /> <span>Steam</span></a>
|
||||
<a class="btn gitlab-btn" :href="getAuthUrl('gitlab')">
|
||||
<GitLabIcon /> <span>GitLab</span></a
|
||||
>
|
||||
</div>
|
||||
<div class="text-divider">
|
||||
<div></div>
|
||||
<span>or</span>
|
||||
<div></div>
|
||||
</div>
|
||||
<label for="email" hidden>Email or username</label>
|
||||
<input id="email" v-model="email" type="text" placeholder="Email or username" />
|
||||
<label for="password" hidden>Password</label>
|
||||
<input id="password" v-model="password" type="password" placeholder="Password" />
|
||||
<div class="account-options">
|
||||
<NuxtTurnstile ref="turnstile" v-model="token" class="turnstile" />
|
||||
<nuxt-link class="text-link" to="/auth/reset-password">Forgot password?</nuxt-link>
|
||||
</div>
|
||||
<button class="btn btn-primary continue-btn" @click="loginPassword()">
|
||||
Continue <RightArrowIcon />
|
||||
</button>
|
||||
<p>
|
||||
Don't have an account yet?
|
||||
<nuxt-link
|
||||
class="text-link"
|
||||
:to="`/auth/sign-up${route.query.redirect ? `?redirect=${route.query.redirect}` : ''}`"
|
||||
>
|
||||
Create one.
|
||||
</nuxt-link>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { GitHubIcon, RightArrowIcon } from 'omorphia'
|
||||
import DiscordIcon from 'assets/images/utils/discord.svg'
|
||||
import GoogleIcon from 'assets/images/utils/google.svg'
|
||||
import SteamIcon from 'assets/images/utils/steam.svg'
|
||||
import MicrosoftIcon from 'assets/images/utils/microsoft.svg'
|
||||
import GitLabIcon from 'assets/images/utils/gitlab.svg'
|
||||
|
||||
useHead({
|
||||
title: 'Sign In - Modrinth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const route = useRoute()
|
||||
if (route.fullPath.includes('new_account=true')) {
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${route.query.code}${
|
||||
route.query.redirect ? `&redirect=${encodeURIComponent(route.query.redirect)}` : ''
|
||||
}`
|
||||
)
|
||||
} else if (route.query.code) {
|
||||
await loginHandler()
|
||||
}
|
||||
|
||||
if (auth.value.user) {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
|
||||
const data = useNuxtApp()
|
||||
|
||||
const turnstile = ref()
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const token = ref('')
|
||||
|
||||
const flow = ref(route.query.flow)
|
||||
|
||||
async function loginPassword() {
|
||||
startLoading()
|
||||
try {
|
||||
const res = await useBaseFetch('auth/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: email.value,
|
||||
password: password.value,
|
||||
challenge: token.value,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.flow) {
|
||||
flow.value = res.flow
|
||||
} else {
|
||||
await loginHandler(res.session)
|
||||
}
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
turnstile.value?.reset()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const twoFactorCode = ref(null)
|
||||
async function loginTwoFactor() {
|
||||
startLoading()
|
||||
try {
|
||||
const res = await useBaseFetch('auth/login/2fa', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
flow: flow.value,
|
||||
code: twoFactorCode.value ? twoFactorCode.value.toString() : twoFactorCode.value,
|
||||
},
|
||||
})
|
||||
|
||||
await loginHandler(res.session)
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
turnstile.value?.reset()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
async function loginHandler(token) {
|
||||
if (token) {
|
||||
await useAuth(token)
|
||||
await useUser()
|
||||
}
|
||||
|
||||
if (route.query.redirect) {
|
||||
await navigateTo(route.query.redirect)
|
||||
} else {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.totp {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.totp-codes {
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
gap: var(--gap-md);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-options {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-block-start: 0 !important;
|
||||
}
|
||||
.account-options a {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
144
pages/auth/sign-up.vue
Normal file
144
pages/auth/sign-up.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="auth-page-container">
|
||||
<h1>Create your account</h1>
|
||||
<div class="third-party">
|
||||
<a class="btn discord-btn" :href="getAuthUrl('discord')">
|
||||
<DiscordIcon /> <span>Discord</span>
|
||||
</a>
|
||||
<a class="btn github-btn" :href="getAuthUrl('github')"><GitHubIcon /> <span>GitHub</span></a>
|
||||
<a class="btn microsoft-btn" :href="getAuthUrl('microsoft')">
|
||||
<MicrosoftIcon /> <span>Microsoft</span>
|
||||
</a>
|
||||
<a class="btn google-btn" :href="getAuthUrl('google')">
|
||||
<GoogleIcon /> <span>Google</span>
|
||||
</a>
|
||||
<a class="btn apple-btn" :href="getAuthUrl('steam')"><SteamIcon /> <span>Steam</span></a>
|
||||
<a class="btn gitlab-btn" :href="getAuthUrl('gitlab')"> <GitLabIcon /> <span>GitLab</span></a>
|
||||
</div>
|
||||
<div class="text-divider">
|
||||
<div></div>
|
||||
<span>or</span>
|
||||
<div></div>
|
||||
</div>
|
||||
<label for="email" hidden>Email</label>
|
||||
<input id="email" v-model="email" type="text" placeholder="Email" />
|
||||
<label for="username" hidden>Username</label>
|
||||
<input id="username" v-model="username" type="text" placeholder="Username" />
|
||||
<label for="password" hidden>Password</label>
|
||||
<input id="password" v-model="password" type="password" placeholder="Password" />
|
||||
<label for="confirm-password" hidden>Password</label>
|
||||
<input
|
||||
id="confirm-password"
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<Checkbox v-model="subscribe" class="subscribe-btn" label="Subscribe updates about Modrinth" />
|
||||
<p>
|
||||
By creating an account, you agree to Modrinth's
|
||||
<nuxt-link to="/legal/terms" class="text-link">terms</nuxt-link> and
|
||||
<nuxt-link to="/legal/privacy" class="text-link">privacy policy</nuxt-link>.
|
||||
</p>
|
||||
<button class="btn btn-primary continue-btn" @click="createAccount">
|
||||
Create account <RightArrowIcon />
|
||||
</button>
|
||||
<p>
|
||||
Already have an account yet?
|
||||
<nuxt-link
|
||||
class="text-link"
|
||||
:to="`/auth/sign-in${route.query.redirect ? `?redirect=${route.query.redirect}` : ''}`"
|
||||
>
|
||||
Sign in.
|
||||
</nuxt-link>
|
||||
<NuxtTurnstile ref="turnstile" v-model="token" class="turnstile" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { GitHubIcon, RightArrowIcon, Checkbox } from 'omorphia'
|
||||
import DiscordIcon from 'assets/images/utils/discord.svg'
|
||||
import GoogleIcon from 'assets/images/utils/google.svg'
|
||||
import SteamIcon from 'assets/images/utils/steam.svg'
|
||||
import MicrosoftIcon from 'assets/images/utils/microsoft.svg'
|
||||
import GitLabIcon from 'assets/images/utils/gitlab.svg'
|
||||
|
||||
useHead({
|
||||
title: 'Sign Up - Modrinth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
const route = useRoute()
|
||||
|
||||
if (route.fullPath.includes('new_account=true')) {
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${route.query.code}${
|
||||
route.query.redirect ? `&redirect=${encodeURIComponent(route.query.redirect)}` : ''
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
if (auth.value.user) {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
|
||||
const data = useNuxtApp()
|
||||
|
||||
const turnstile = ref()
|
||||
|
||||
const email = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const token = ref('')
|
||||
const subscribe = ref(true)
|
||||
|
||||
async function createAccount() {
|
||||
startLoading()
|
||||
try {
|
||||
if (confirmPassword.value !== password.value) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'Passwords do not match!',
|
||||
type: 'error',
|
||||
})
|
||||
turnstile.value?.reset()
|
||||
}
|
||||
|
||||
const res = await useBaseFetch('auth/create', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
email: email.value,
|
||||
challenge: token.value,
|
||||
sign_up_newsletter: subscribe.value,
|
||||
},
|
||||
})
|
||||
|
||||
await useAuth(res.session)
|
||||
await useUser()
|
||||
|
||||
if (route.query.redirect) {
|
||||
await navigateTo(route.query.redirect)
|
||||
} else {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
turnstile.value?.reset()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.subscribe-btn {
|
||||
margin-block-start: 0 !important;
|
||||
}
|
||||
</style>
|
||||
78
pages/auth/verify-email.vue
Normal file
78
pages/auth/verify-email.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="auth-page-container">
|
||||
<template v-if="auth.user && auth.user.email_verified && !success">
|
||||
<h1>Email already verified</h1>
|
||||
<p>Your email is already verified!</p>
|
||||
<nuxt-link class="btn" link="/settings/account">
|
||||
<SettingsIcon /> Account settings
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template v-else-if="success">
|
||||
<h1>Email verification</h1>
|
||||
<p>Your email address has been successfully verified!</p>
|
||||
<nuxt-link v-if="auth.user" class="btn" to="/settings/account">
|
||||
<SettingsIcon /> Account settings
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else to="/auth/sign-in" class="btn btn-primary continue-btn">
|
||||
Sign in <RightArrowIcon />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Email verification failed</h1>
|
||||
<p>
|
||||
We were unable to verify your email.
|
||||
<template v-if="auth.user">
|
||||
Try re-sending the verification email through the button below.
|
||||
</template>
|
||||
<template v-else>
|
||||
Try re-sending the verification email through your dashboard by signing in.
|
||||
</template>
|
||||
</p>
|
||||
<button v-if="auth.user" class="btn btn-primary continue-btn" @click="resendVerifyEmail">
|
||||
Resend verification email <RightArrowIcon />
|
||||
</button>
|
||||
<nuxt-link v-else to="/auth/sign-in" class="btn btn-primary continue-btn">
|
||||
Sign in <RightArrowIcon />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { SettingsIcon, RightArrowIcon } from 'omorphia'
|
||||
|
||||
useHead({
|
||||
title: 'Verify Email - Modrinth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const success = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
if (route.query.flow) {
|
||||
try {
|
||||
const emailVerified = useState('emailVerified', () => null)
|
||||
|
||||
if (emailVerified.value === null) {
|
||||
await useBaseFetch('auth/email/verify', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
flow: route.query.flow,
|
||||
},
|
||||
})
|
||||
emailVerified.value = true
|
||||
success.value = true
|
||||
}
|
||||
|
||||
if (emailVerified.value) {
|
||||
success.value = true
|
||||
|
||||
if (auth.value.token) {
|
||||
await useAuth(auth.value.token)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
success.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
46
pages/auth/welcome.vue
Normal file
46
pages/auth/welcome.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="auth-page-container">
|
||||
<h1>Welcome to Modrinth!</h1>
|
||||
<p>
|
||||
Thank you for creating an account. You can now follow and create projects, receive updates
|
||||
about your favorite projects, and more!
|
||||
</p>
|
||||
<Checkbox v-model="subscribe" class="subscribe-btn" label="Subscribe updates about Modrinth" />
|
||||
<button class="btn btn-primary continue-btn" @click="continueSignUp">Continue</button>
|
||||
<p>
|
||||
By creating an account, you agree to Modrinth's
|
||||
<nuxt-link to="/legal/terms" class="text-link">terms</nuxt-link> and
|
||||
<nuxt-link to="/legal/privacy" class="text-link">privacy policy</nuxt-link>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Checkbox } from 'omorphia'
|
||||
|
||||
useHead({
|
||||
title: 'Welcome - Modrinth',
|
||||
})
|
||||
|
||||
const subscribe = ref(true)
|
||||
|
||||
async function continueSignUp() {
|
||||
const route = useRoute()
|
||||
|
||||
await useAuth(route.query.authToken)
|
||||
await useUser()
|
||||
|
||||
if (subscribe.value) {
|
||||
try {
|
||||
await useBaseFetch('auth/email/subscribe', {
|
||||
method: 'POST',
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (route.query.redirect) {
|
||||
await navigateTo(route.query.redirect)
|
||||
} else {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="dashboard-overview">
|
||||
<section class="universal-card dashboard-header">
|
||||
<Avatar :src="$auth.user.avatar_url" size="md" circle :alt="$auth.user.username" />
|
||||
<Avatar :src="auth.user.avatar_url" size="md" circle :alt="auth.user.username" />
|
||||
<div class="username">
|
||||
<h1>
|
||||
{{ $auth.user.username }}
|
||||
{{ auth.user.username }}
|
||||
</h1>
|
||||
<NuxtLink class="goto-link" :to="`/user/${$auth.user.username}`">
|
||||
<NuxtLink class="goto-link" :to="`/user/${auth.user.username}`">
|
||||
Visit your profile
|
||||
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
@ -26,6 +26,7 @@
|
||||
v-model:notifications="allNotifs"
|
||||
class="universal-card recessed"
|
||||
:notification="notification"
|
||||
:auth="auth"
|
||||
raised
|
||||
compact
|
||||
/>
|
||||
@ -116,14 +117,13 @@ useHead({
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
const app = useNuxtApp()
|
||||
|
||||
const [{ data: projects }, { data: payouts }] = await Promise.all([
|
||||
useAsyncData(`user/${auth.value.user.id}/projects`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/projects`, app.$defaultHeaders())
|
||||
useBaseFetch(`user/${auth.value.user.id}/projects`)
|
||||
),
|
||||
useAsyncData(`user/${auth.value.user.id}/payouts`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders())
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`)
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
v-model:notifications="allNotifs"
|
||||
class="universal-card recessed"
|
||||
:notification="notification"
|
||||
:auth="auth"
|
||||
raised
|
||||
/>
|
||||
</template>
|
||||
@ -55,6 +56,8 @@ useHead({
|
||||
title: 'Notifications - Modrinth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
@ -459,7 +459,6 @@ export default defineNuxtComponent({
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: baseData,
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<ReportView
|
||||
:auth="auth"
|
||||
:report-id="route.params.id"
|
||||
:breadcrumbs-stack="[{ href: '/dashboard/reports', label: 'Active reports' }]"
|
||||
/>
|
||||
@ -8,6 +9,7 @@
|
||||
import ReportView from '~/components/ui/report/ReportView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const auth = await useAuth()
|
||||
|
||||
useHead({
|
||||
title: `Report ${route.params.id} - Modrinth`,
|
||||
|
||||
@ -2,13 +2,14 @@
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Reports you've filed</h2>
|
||||
<ReportsList />
|
||||
<ReportsList :auth="auth" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ReportsList from '~/components/ui/report/ReportsList.vue'
|
||||
|
||||
const auth = await useAuth()
|
||||
useHead({
|
||||
title: 'Active reports - Modrinth',
|
||||
})
|
||||
|
||||
@ -39,10 +39,9 @@ useHead({
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
const app = useNuxtApp()
|
||||
|
||||
const { data: payouts } = await useAsyncData(`user/${auth.value.user.id}/payouts`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders())
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`)
|
||||
)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="animate-strong">
|
||||
<span>
|
||||
<strong
|
||||
v-for="projectType in $tag.projectTypes"
|
||||
v-for="projectType in tags.projectTypes"
|
||||
:key="projectType.id"
|
||||
class="main-header-strong"
|
||||
>
|
||||
@ -23,14 +23,14 @@
|
||||
</h2>
|
||||
<div class="button-group">
|
||||
<nuxt-link to="/mods" class="iconified-button brand-button"> Discover mods </nuxt-link>
|
||||
<a
|
||||
v-if="!$auth.user"
|
||||
:href="getAuthUrl()"
|
||||
<nuxt-link
|
||||
v-if="!auth.user"
|
||||
to="sign-up"
|
||||
class="iconified-button outline-button"
|
||||
rel="noopener nofollow"
|
||||
>
|
||||
Sign up
|
||||
</a>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else to="/dashboard/projects" class="iconified-button outline-button">
|
||||
Go to dashboard
|
||||
</nuxt-link>
|
||||
@ -530,6 +530,9 @@ import homepageProjects from '~/generated/homepage.json'
|
||||
const searchQuery = ref('better')
|
||||
const sortType = ref('relevance')
|
||||
|
||||
const auth = await useAuth()
|
||||
const tags = useTags()
|
||||
|
||||
const [{ data: searchProjects, refresh: updateSearchProjects }, { data: notifications }] =
|
||||
await Promise.all([
|
||||
useAsyncData(
|
||||
@ -1277,9 +1280,8 @@ const rows = shallowRef([
|
||||
font-size: 1.625rem;
|
||||
}
|
||||
|
||||
padding: 12rem 1rem;
|
||||
// Magic number to cover header (space in rem header occupies)
|
||||
margin-top: -5.75rem;
|
||||
margin-top: -4rem;
|
||||
padding: 11.25rem 1rem 12rem;
|
||||
}
|
||||
|
||||
.users-section-outer {
|
||||
|
||||
@ -38,9 +38,5 @@ useHead({
|
||||
title: 'Staff overview - Modrinth',
|
||||
})
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
const { data: stats } = await useAsyncData('statistics', () =>
|
||||
useBaseFetch('statistics', app.$defaultHeaders())
|
||||
)
|
||||
const { data: stats } = await useAsyncData('statistics', () => useBaseFetch('statistics'))
|
||||
</script>
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
:key="thread.id"
|
||||
:thread="thread"
|
||||
:link="getLink(thread)"
|
||||
:auth="auth"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@ -19,11 +20,8 @@ useHead({
|
||||
title: 'Moderation inbox - Modrinth',
|
||||
})
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
const { data: inbox } = await useAsyncData('thread/inbox', () =>
|
||||
useBaseFetch('thread/inbox', app.$defaultHeaders())
|
||||
)
|
||||
const auth = await useAuth()
|
||||
const { data: inbox } = await useAsyncData('thread/inbox', () => useBaseFetch('thread/inbox'))
|
||||
|
||||
function getLink(thread) {
|
||||
if (thread.report_id) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<ReportView
|
||||
:auth="auth"
|
||||
:report-id="route.params.id"
|
||||
:breadcrumbs-stack="[{ href: '/moderation/reports', label: 'Reports' }]"
|
||||
/>
|
||||
@ -7,6 +8,7 @@
|
||||
<script setup>
|
||||
import ReportView from '~/components/ui/report/ReportView.vue'
|
||||
|
||||
const auth = await useAuth()
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
|
||||
@ -2,13 +2,14 @@
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Reports</h2>
|
||||
<ReportsList moderation />
|
||||
<ReportsList :auth="auth" moderation />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ReportsList from '~/components/ui/report/ReportsList.vue'
|
||||
|
||||
const auth = await useAuth()
|
||||
useHead({
|
||||
title: 'Reports - Modrinth',
|
||||
})
|
||||
|
||||
@ -103,7 +103,7 @@ const TIME_24H = 86400000
|
||||
const TIME_48H = TIME_24H * 2
|
||||
|
||||
const { data: projects } = await useAsyncData('moderation/projects?count=1000', () =>
|
||||
useBaseFetch('moderation/projects?count=1000', app.$defaultHeaders())
|
||||
useBaseFetch('moderation/projects?count=1000')
|
||||
)
|
||||
const members = ref([])
|
||||
const projectType = ref('all')
|
||||
@ -145,37 +145,30 @@ const projectTypes = computed(() => {
|
||||
if (projects.value) {
|
||||
const teamIds = projects.value.map((x) => x.team)
|
||||
|
||||
await useAsyncData(
|
||||
'teams?ids=' + JSON.stringify(teamIds),
|
||||
() => useBaseFetch('teams?ids=' + JSON.stringify(teamIds), app.$defaultHeaders()),
|
||||
{
|
||||
transform: (result) => {
|
||||
if (result) {
|
||||
members.value = result
|
||||
const url = `teams?ids=${encodeURIComponent(JSON.stringify(teamIds))}`
|
||||
const { data: result } = await useAsyncData(url, () => useBaseFetch(url))
|
||||
|
||||
projects.value = projects.value.map((project) => {
|
||||
project.owner = members.value
|
||||
.flat()
|
||||
.find((x) => x.team_id === project.team && x.role === 'Owner').user
|
||||
project.age = project.queued ? now - app.$dayjs(project.queued) : Number.MAX_VALUE
|
||||
project.age_warning = ''
|
||||
if (project.age > TIME_24H * 2) {
|
||||
project.age_warning = 'danger'
|
||||
} else if (project.age > TIME_24H) {
|
||||
project.age_warning = 'warning'
|
||||
}
|
||||
project.inferred_project_type = app.$getProjectTypeForUrl(
|
||||
project.project_type,
|
||||
project.loaders
|
||||
)
|
||||
return project
|
||||
})
|
||||
}
|
||||
if (result.value) {
|
||||
members.value = result.value
|
||||
|
||||
return result
|
||||
},
|
||||
}
|
||||
)
|
||||
projects.value = projects.value.map((project) => {
|
||||
project.owner = members.value
|
||||
.flat()
|
||||
.find((x) => x.team_id === project.team && x.role === 'Owner').user
|
||||
project.age = project.queued ? now - app.$dayjs(project.queued) : Number.MAX_VALUE
|
||||
project.age_warning = ''
|
||||
if (project.age > TIME_24H * 2) {
|
||||
project.age_warning = 'danger'
|
||||
} else if (project.age > TIME_24H) {
|
||||
project.age_warning = 'warning'
|
||||
}
|
||||
project.inferred_project_type = app.$getProjectTypeForUrl(
|
||||
project.project_type,
|
||||
project.loaders
|
||||
)
|
||||
return project
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
:class="{
|
||||
'search-page': true,
|
||||
'normal-page': true,
|
||||
'alt-layout': $cosmetics.searchLayout,
|
||||
'alt-layout': cosmetics.searchLayout,
|
||||
}"
|
||||
>
|
||||
<Head>
|
||||
@ -65,7 +65,7 @@
|
||||
>
|
||||
<h3
|
||||
v-if="
|
||||
$tag.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
|
||||
tags.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
|
||||
.length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
@ -73,7 +73,7 @@
|
||||
Loaders
|
||||
</h3>
|
||||
<SearchFilter
|
||||
v-for="loader in $tag.loaders.filter((x) => {
|
||||
v-for="loader in tags.loaders.filter((x) => {
|
||||
if (
|
||||
projectType.id === 'mod' &&
|
||||
!showAllLoaders &&
|
||||
@ -83,11 +83,11 @@
|
||||
) {
|
||||
return false
|
||||
} else if (projectType.id === 'mod' && showAllLoaders) {
|
||||
return $tag.loaderData.modLoaders.includes(x.name)
|
||||
return tags.loaderData.modLoaders.includes(x.name)
|
||||
} else if (projectType.id === 'plugin') {
|
||||
return $tag.loaderData.pluginLoaders.includes(x.name)
|
||||
return tags.loaderData.pluginLoaders.includes(x.name)
|
||||
} else if (projectType.id === 'datapack') {
|
||||
return $tag.loaderData.dataPackLoaders.includes(x.name)
|
||||
return tags.loaderData.dataPackLoaders.includes(x.name)
|
||||
} else {
|
||||
return x.supported_project_types.includes(projectType.actual)
|
||||
}
|
||||
@ -113,7 +113,7 @@
|
||||
<section v-if="projectType.id === 'plugin'" aria-label="Platform loader filters">
|
||||
<h3
|
||||
v-if="
|
||||
$tag.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
|
||||
tags.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
|
||||
.length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
@ -121,8 +121,8 @@
|
||||
Proxies
|
||||
</h3>
|
||||
<SearchFilter
|
||||
v-for="loader in $tag.loaders.filter((x) =>
|
||||
$tag.loaderData.pluginPlatformLoaders.includes(x.name)
|
||||
v-for="loader in tags.loaders.filter((x) =>
|
||||
tags.loaderData.pluginPlatformLoaders.includes(x.name)
|
||||
)"
|
||||
:key="loader.name"
|
||||
ref="platformFilters"
|
||||
@ -167,8 +167,8 @@
|
||||
v-model="selectedVersions"
|
||||
:options="
|
||||
showSnapshots
|
||||
? $tag.gameVersions.map((x) => x.version)
|
||||
: $tag.gameVersions
|
||||
? tags.gameVersions.map((x) => x.version)
|
||||
: tags.gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((x) => x.version)
|
||||
"
|
||||
@ -195,7 +195,7 @@
|
||||
</aside>
|
||||
<section class="normal-page__content">
|
||||
<div
|
||||
v-if="projectType.id === 'modpack' && $orElse($cosmetics.modpacksAlphaNotice, true)"
|
||||
v-if="projectType.id === 'modpack' && $orElse(cosmetics.modpacksAlphaNotice, true)"
|
||||
class="card information"
|
||||
aria-label="Information"
|
||||
>
|
||||
@ -267,7 +267,7 @@
|
||||
v-model="maxResults"
|
||||
placeholder="Select one"
|
||||
class="labeled-control__control"
|
||||
:options="maxResultsForView[$cosmetics.searchDisplayMode[projectType.id]]"
|
||||
:options="maxResultsForView[cosmetics.searchDisplayMode[projectType.id]]"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
@ -276,13 +276,13 @@
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-tooltip="$capitalizeString($cosmetics.searchDisplayMode[projectType.id]) + ' view'"
|
||||
:aria-label="$capitalizeString($cosmetics.searchDisplayMode[projectType.id]) + ' view'"
|
||||
v-tooltip="$capitalizeString(cosmetics.searchDisplayMode[projectType.id]) + ' view'"
|
||||
:aria-label="$capitalizeString(cosmetics.searchDisplayMode[projectType.id]) + ' view'"
|
||||
class="square-button"
|
||||
@click="cycleSearchDisplayMode()"
|
||||
>
|
||||
<GridIcon v-if="$cosmetics.searchDisplayMode[projectType.id] === 'grid'" />
|
||||
<ImageIcon v-else-if="$cosmetics.searchDisplayMode[projectType.id] === 'gallery'" />
|
||||
<GridIcon v-if="cosmetics.searchDisplayMode[projectType.id] === 'grid'" />
|
||||
<ImageIcon v-else-if="cosmetics.searchDisplayMode[projectType.id] === 'gallery'" />
|
||||
<ListIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
@ -302,7 +302,7 @@
|
||||
<div
|
||||
id="search-results"
|
||||
class="project-list"
|
||||
:class="'display-mode--' + $cosmetics.searchDisplayMode[projectType.id]"
|
||||
:class="'display-mode--' + cosmetics.searchDisplayMode[projectType.id]"
|
||||
role="list"
|
||||
aria-label="Search results"
|
||||
>
|
||||
@ -310,7 +310,7 @@
|
||||
v-for="result in results?.hits"
|
||||
:id="result.slug ? result.slug : result.project_id"
|
||||
:key="result.project_id"
|
||||
:display="$cosmetics.searchDisplayMode[projectType.id]"
|
||||
:display="cosmetics.searchDisplayMode[projectType.id]"
|
||||
:featured-image="result.featured_gallery ? result.featured_gallery : result.gallery[0]"
|
||||
:type="result.project_type"
|
||||
:author="result.author"
|
||||
@ -367,6 +367,9 @@ const showAllLoaders = ref(false)
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useTags()
|
||||
|
||||
const query = ref('')
|
||||
const facets = ref([])
|
||||
const orFacets = ref([])
|
||||
@ -444,7 +447,7 @@ if (route.query.o) {
|
||||
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1
|
||||
}
|
||||
|
||||
projectType.value = data.$tag.projectTypes.find(
|
||||
projectType.value = tags.value.projectTypes.find(
|
||||
(x) => x.id === route.path.substring(1, route.path.length - 1)
|
||||
)
|
||||
|
||||
@ -481,15 +484,15 @@ const {
|
||||
formattedFacets.push(orFacets.value)
|
||||
} else if (projectType.value.id === 'plugin') {
|
||||
formattedFacets.push(
|
||||
data.$tag.loaderData.allPluginLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
|
||||
tags.value.loaderData.allPluginLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
|
||||
)
|
||||
} else if (projectType.value.id === 'mod') {
|
||||
formattedFacets.push(
|
||||
data.$tag.loaderData.modLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
|
||||
tags.value.loaderData.modLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
|
||||
)
|
||||
} else if (projectType.value.id === 'datapack') {
|
||||
formattedFacets.push(
|
||||
data.$tag.loaderData.dataPackLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
|
||||
tags.value.loaderData.dataPackLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
|
||||
)
|
||||
}
|
||||
|
||||
@ -644,7 +647,7 @@ function getSearchUrl(offset, useObj) {
|
||||
const categoriesMap = computed(() => {
|
||||
const categories = {}
|
||||
|
||||
for (const category of data.$sortedCategories) {
|
||||
for (const category of data.$sortedCategories()) {
|
||||
if (categories[category.header]) {
|
||||
categories[category.header].push(category)
|
||||
} else {
|
||||
@ -747,9 +750,9 @@ function onSearchChangeToTop(newPageNumber) {
|
||||
}
|
||||
|
||||
function cycleSearchDisplayMode() {
|
||||
data.$cosmetics.searchDisplayMode[projectType.value.id] = data.$cycleValue(
|
||||
data.$cosmetics.searchDisplayMode[projectType.value.id],
|
||||
data.$tag.projectViewModes
|
||||
cosmetics.value.searchDisplayMode[projectType.value.id] = data.$cycleValue(
|
||||
cosmetics.value.searchDisplayMode[projectType.value.id],
|
||||
tags.value.projectViewModes
|
||||
)
|
||||
saveCosmetics()
|
||||
setClosestMaxResults()
|
||||
@ -775,7 +778,7 @@ function onMaxResultsChange(newPageNumber) {
|
||||
}
|
||||
|
||||
function setClosestMaxResults() {
|
||||
const view = data.$cosmetics.searchDisplayMode[projectType.value.id]
|
||||
const view = cosmetics.value.searchDisplayMode[projectType.value.id]
|
||||
const maxResultsOptions = maxResultsForView.value[view] ?? [20]
|
||||
const currentMax = maxResults.value
|
||||
if (!maxResultsOptions.includes(currentMax)) {
|
||||
|
||||
@ -7,11 +7,17 @@
|
||||
<NavStackItem link="/settings" label="Appearance">
|
||||
<PaintbrushIcon />
|
||||
</NavStackItem>
|
||||
<template v-if="$auth.user">
|
||||
<template v-if="auth.user">
|
||||
<h3>User settings</h3>
|
||||
<NavStackItem link="/settings/account" label="Account">
|
||||
<UserIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/pats" label="PATs">
|
||||
<KeyIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/sessions" label="Sessions">
|
||||
<ShieldIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/monetization" label="Monetization">
|
||||
<CurrencyIcon />
|
||||
</NavStackItem>
|
||||
@ -31,8 +37,11 @@ import NavStackItem from '~/components/ui/NavStackItem.vue'
|
||||
import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg'
|
||||
import ShieldIcon from '~/assets/images/utils/shield.svg'
|
||||
import KeyIcon from '~/assets/images/utils/key.svg'
|
||||
|
||||
const route = useRoute()
|
||||
const auth = await useAuth()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -9,54 +9,259 @@
|
||||
:has-to-type="true"
|
||||
@proceed="deleteAccount"
|
||||
/>
|
||||
|
||||
<Modal ref="modal_revoke_token" header="Revoke your Modrinth token">
|
||||
<div class="modal-revoke-token markdown-body">
|
||||
<p>
|
||||
Revoking your Modrinth token can have unintended consequences. Please be aware that the
|
||||
following could break:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Any application that uses your token to access the API.</li>
|
||||
<li>Gradle - if Minotaur is given a incorrect token, your Gradle builds could fail.</li>
|
||||
<li>
|
||||
GitHub - if you use a GitHub action that uses the Modrinth API, it will cause errors.
|
||||
</li>
|
||||
</ul>
|
||||
<p>If you are willing to continue, complete the following steps:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/settings/connections/applications/3acffb2e808d16d4b226"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Head to the Modrinth Application page on GitHub.
|
||||
</a>
|
||||
Make sure to be logged into the GitHub account you used for Modrinth!
|
||||
</li>
|
||||
<li>Press the big red "Revoke Access" button next to the "Permissions" header.</li>
|
||||
</ol>
|
||||
<p>Once you have completed those steps, press the continue button below.</p>
|
||||
<p>
|
||||
<strong>
|
||||
This will log you out of Modrinth, however, when you log back in, your token will be
|
||||
regenerated.
|
||||
</strong>
|
||||
</p>
|
||||
<div class="button-group">
|
||||
<button class="iconified-button" @click="$refs.modal_revoke_token.hide()">
|
||||
<CrossIcon />
|
||||
<Modal ref="changeEmailModal" :header="`${auth.user.email ? 'Change' : 'Add'} email`">
|
||||
<div class="universal-modal">
|
||||
<p>Your account information is not displayed publicly.</p>
|
||||
<label for="email-input"><span class="label__title">Email address</span> </label>
|
||||
<input
|
||||
id="email-input"
|
||||
v-model="email"
|
||||
maxlength="2048"
|
||||
type="email"
|
||||
:placeholder="`Enter your email address...`"
|
||||
/>
|
||||
<div class="input-group push-right">
|
||||
<button class="iconified-button" @click="$refs.changeEmailModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="logout">
|
||||
<RightArrowIcon />
|
||||
Log out
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!email"
|
||||
@click="saveEmail()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
ref="managePasswordModal"
|
||||
:header="`${
|
||||
removePasswordMode ? 'Remove' : auth.user.has_password ? 'Change' : 'Add'
|
||||
} password`"
|
||||
>
|
||||
<div class="universal-modal">
|
||||
<ul v-if="newPassword !== confirmNewPassword" class="known-errors">
|
||||
<li>Input passwords do not match!</li>
|
||||
</ul>
|
||||
<label v-if="removePasswordMode" for="old-password">
|
||||
<span class="label__title">Confirm password</span>
|
||||
<span class="label__description">Please enter your password to proceed.</span>
|
||||
</label>
|
||||
<label v-else-if="auth.user.has_password" for="old-password">
|
||||
<span class="label__title">Old password</span>
|
||||
</label>
|
||||
<input
|
||||
v-if="auth.user.has_password"
|
||||
id="old-password"
|
||||
v-model="oldPassword"
|
||||
maxlength="2048"
|
||||
type="password"
|
||||
:placeholder="`${removePasswordMode ? 'Confirm' : 'Old'} password`"
|
||||
/>
|
||||
<template v-if="!removePasswordMode">
|
||||
<label for="new-password"><span class="label__title">New password</span></label>
|
||||
<input
|
||||
id="new-password"
|
||||
v-model="newPassword"
|
||||
maxlength="2048"
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
/>
|
||||
<label for="confirm-new-password"
|
||||
><span class="label__title">Confirm new password</span></label
|
||||
>
|
||||
<input
|
||||
id="confirm-new-password"
|
||||
v-model="confirmNewPassword"
|
||||
maxlength="2048"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</template>
|
||||
<p></p>
|
||||
<div class="input-group push-right">
|
||||
<button class="iconified-button" @click="$refs.managePasswordModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<template v-if="removePasswordMode">
|
||||
<button type="button" class="iconified-button danger-button" @click="savePassword">
|
||||
<TrashIcon />
|
||||
Remove password
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="auth.user.has_password && auth.user.auth_providers.length > 0"
|
||||
type="button"
|
||||
class="iconified-button danger-button"
|
||||
@click="removePasswordMode = true"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove password
|
||||
</button>
|
||||
<button type="button" class="iconified-button brand-button" @click="savePassword">
|
||||
<SaveIcon />
|
||||
Save password
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
ref="manageTwoFactorModal"
|
||||
:header="`${
|
||||
auth.user.has_totp && twoFactorStep === 0 ? 'Remove' : 'Setup'
|
||||
} two-factor authentication`"
|
||||
>
|
||||
<div class="universal-modal">
|
||||
<template v-if="auth.user.has_totp && twoFactorStep === 0">
|
||||
<label for="two-factor-code">
|
||||
<span class="label__title">Enter two-factor code</span>
|
||||
<span class="label__description">Please enter a two-factor code to proceed.</span>
|
||||
</label>
|
||||
<input
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCode"
|
||||
maxlength="11"
|
||||
type="text"
|
||||
placeholder="Enter code..."
|
||||
/>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors">The code entered is incorrect!</p>
|
||||
<div class="input-group push-right">
|
||||
<button class="iconified-button" @click="$refs.manageTwoFactorModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button danger-button" @click="removeTwoFactor">
|
||||
<TrashIcon />
|
||||
Remove 2FA
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="twoFactorStep === 0">
|
||||
<p>
|
||||
Two-factor authentication keeps your account secure by requiring access to a second
|
||||
device in order to sign in.
|
||||
<br /><br />
|
||||
Scan the QR code with <a href="https://authy.com/">Authy</a>,
|
||||
<a href="https://www.microsoft.com/en-us/security/mobile-authenticator-app">
|
||||
Microsoft Authenticator</a
|
||||
>, or any other 2FA app to begin.
|
||||
</p>
|
||||
<qrcode-vue
|
||||
v-if="twoFactorSecret"
|
||||
:value="`otpauth://totp/${encodeURIComponent(
|
||||
auth.user.email
|
||||
)}?secret=${twoFactorSecret}&issuer=Modrinth`"
|
||||
:size="250"
|
||||
:margin="2"
|
||||
level="H"
|
||||
/>
|
||||
<p>
|
||||
If the QR code does not scan, you can manually enter the secret:
|
||||
<strong>{{ twoFactorSecret }}</strong>
|
||||
</p>
|
||||
</template>
|
||||
<template v-if="twoFactorStep === 1">
|
||||
<label for="verify-code">
|
||||
<span class="label__title">Verify code</span>
|
||||
<span class="label__description"
|
||||
>Enter the one-time code from authenticator to verify access.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="verify-code"
|
||||
v-model="twoFactorCode"
|
||||
maxlength="6"
|
||||
type="number"
|
||||
placeholder="Enter code..."
|
||||
/>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors">The code entered is incorrect!</p>
|
||||
</template>
|
||||
<template v-if="twoFactorStep === 2">
|
||||
<p>
|
||||
Download and save these back-up codes in a safe place. You can use these in-place of a
|
||||
2FA code if you ever lose access to your device! You should protect these codes like
|
||||
your password.
|
||||
</p>
|
||||
<p>Backup codes can only be used once.</p>
|
||||
<ul>
|
||||
<li v-for="code in backupCodes" :key="code">{{ code }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div class="input-group push-right">
|
||||
<button v-if="twoFactorStep === 1" class="iconified-button" @click="twoFactorStep = 0">
|
||||
<LeftArrowIcon />
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
v-if="twoFactorStep !== 2"
|
||||
class="iconified-button"
|
||||
@click="$refs.manageTwoFactorModal.hide()"
|
||||
>
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="twoFactorStep <= 1"
|
||||
class="iconified-button brand-button"
|
||||
@click="twoFactorStep === 1 ? verifyTwoFactorCode() : (twoFactorStep = 1)"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
v-if="twoFactorStep === 2"
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.manageTwoFactorModal.hide()"
|
||||
>
|
||||
<CheckIcon />
|
||||
Complete setup
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal ref="manageProvidersModal" header="Authentication providers">
|
||||
<div class="universal-modal">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
<div class="table-cell table-text">Provider</div>
|
||||
<div class="table-cell table-text">Actions</div>
|
||||
</div>
|
||||
<div v-for="provider in authProviders" :key="provider.id" class="table-row">
|
||||
<div class="table-cell table-text">
|
||||
<span><component :is="provider.icon" /> {{ provider.display }}</span>
|
||||
</div>
|
||||
<div class="table-cell table-text manage">
|
||||
<button
|
||||
v-if="auth.user.auth_providers.includes(provider.id)"
|
||||
class="btn"
|
||||
@click="removeAuthProvider(provider.id)"
|
||||
>
|
||||
<TrashIcon /> Remove
|
||||
</button>
|
||||
<a v-else class="btn" :href="`${getAuthUrl(provider.id)}&token=${auth.token}`">
|
||||
<ExternalIcon /> Add
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="input-group push-right">
|
||||
<button class="iconified-button brand-button" @click="$refs.manageProvidersModal.hide()">
|
||||
<CheckIcon />
|
||||
Finish editing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<section class="universal-card">
|
||||
<h2>User profile</h2>
|
||||
<p>Visit your user profile to edit your profile information.</p>
|
||||
@ -66,53 +271,83 @@
|
||||
</section>
|
||||
|
||||
<section class="universal-card">
|
||||
<h2>Account information</h2>
|
||||
<p>Your account information is not displayed publicly.</p>
|
||||
<ul class="known-errors">
|
||||
<li v-if="hasMonetizationEnabled() && !email">
|
||||
You must have an email address set since you are enrolled in the Creator Monetization
|
||||
Program.
|
||||
</li>
|
||||
</ul>
|
||||
<label for="email-input"><span class="label__title">Email address</span> </label>
|
||||
<input
|
||||
id="email-input"
|
||||
v-model="email"
|
||||
maxlength="2048"
|
||||
type="email"
|
||||
:placeholder="`Enter your email address...`"
|
||||
/>
|
||||
<div class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="hasMonetizationEnabled() && !email"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<h2>Account security</h2>
|
||||
|
||||
<section class="universal-card">
|
||||
<h2>Authorization token</h2>
|
||||
<p>
|
||||
Your authorization token can be used with the Modrinth API, the Minotaur Gradle plugin, and
|
||||
other applications that interact with Modrinth's API. Be sure to keep this secret!
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<button type="button" class="iconified-button" value="Copy to clipboard" @click="copyToken">
|
||||
<template v-if="copied">
|
||||
<CheckIcon />
|
||||
Copied token to clipboard
|
||||
</template>
|
||||
<template v-else> <CopyIcon />Copy token to clipboard </template>
|
||||
</button>
|
||||
<button type="button" class="iconified-button" @click="$refs.modal_revoke_token.show()">
|
||||
<SlashIcon />
|
||||
Revoke token
|
||||
</button>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme-selector">
|
||||
<span class="label__title">Email</span>
|
||||
<span class="label__description">Changes the email associated with your account.</span>
|
||||
</label>
|
||||
<div>
|
||||
<button class="iconified-button" @click="$refs.changeEmailModal.show()">
|
||||
<template v-if="auth.user.email">
|
||||
<EditIcon />
|
||||
Change email
|
||||
</template>
|
||||
<template v-else>
|
||||
<PlusIcon />
|
||||
Add email
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme-selector">
|
||||
<span class="label__title">Password</span>
|
||||
<span v-if="auth.user.has_password" class="label__description">
|
||||
Change <template v-if="auth.user.auth_providers.length > 0">or remove</template> the
|
||||
password used to login to your account.
|
||||
</span>
|
||||
<span v-else class="label__description">
|
||||
Set a permanent password to login to your account.
|
||||
</span>
|
||||
</label>
|
||||
<div>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
() => {
|
||||
oldPassword = ''
|
||||
newPassword = ''
|
||||
confirmNewPassword = ''
|
||||
removePasswordMode = false
|
||||
$refs.managePasswordModal.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<KeyIcon />
|
||||
<template v-if="auth.user.has_password"> Change password </template>
|
||||
<template v-else> Add password </template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme-selector">
|
||||
<span class="label__title">Two-factor authentication</span>
|
||||
<span class="label__description">
|
||||
Add an additional layer of security to your account during login.
|
||||
</span>
|
||||
</label>
|
||||
<div>
|
||||
<button class="iconified-button" @click="showTwoFactorModal">
|
||||
<template v-if="auth.user.has_totp"> <TrashIcon /> Remove 2FA </template>
|
||||
<template v-else> <PlusIcon /> Setup 2FA </template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme-selector">
|
||||
<span class="label__title">Manage authentication providers</span>
|
||||
<span class="label__description">
|
||||
Add or remove sign-on methods from your account, including GitHub, GitLab, Microsoft,
|
||||
Discord, Steam, and Google.
|
||||
</span>
|
||||
</label>
|
||||
<div>
|
||||
<button class="iconified-button" @click="$refs.manageProvidersModal.show()">
|
||||
<SettingsIcon /> Manage providers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -134,128 +369,273 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import {
|
||||
EditIcon,
|
||||
UserIcon,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
XIcon,
|
||||
LeftArrowIcon,
|
||||
RightArrowIcon,
|
||||
CheckIcon,
|
||||
GitHubIcon,
|
||||
ExternalIcon,
|
||||
} from 'omorphia'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import DiscordIcon from 'assets/images/utils/discord.svg'
|
||||
import GoogleIcon from 'assets/images/utils/google.svg'
|
||||
import SteamIcon from 'assets/images/utils/steam.svg'
|
||||
import MicrosoftIcon from 'assets/images/utils/microsoft.svg'
|
||||
import GitLabIcon from 'assets/images/utils/gitlab.svg'
|
||||
import KeyIcon from '~/assets/images/utils/key.svg'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import CopyIcon from '~/assets/images/utils/clipboard-copy.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
import SlashIcon from '~/assets/images/utils/slash.svg'
|
||||
useHead({
|
||||
title: 'Account settings - Modrinth',
|
||||
})
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Modal,
|
||||
ModalConfirm,
|
||||
CrossIcon,
|
||||
RightArrowIcon,
|
||||
CheckIcon,
|
||||
SaveIcon,
|
||||
UserIcon,
|
||||
CopyIcon,
|
||||
TrashIcon,
|
||||
SlashIcon,
|
||||
},
|
||||
async setup() {
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const data = useNuxtApp()
|
||||
const auth = await useAuth()
|
||||
|
||||
const changeEmailModal = ref()
|
||||
const email = ref(auth.value.user.email)
|
||||
async function saveEmail() {
|
||||
if (!email.value) {
|
||||
return
|
||||
}
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch(`auth/email`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
email: email.value,
|
||||
},
|
||||
})
|
||||
changeEmailModal.value.hide()
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const managePasswordModal = ref()
|
||||
const removePasswordMode = ref(false)
|
||||
const oldPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmNewPassword = ref('')
|
||||
async function savePassword() {
|
||||
if (newPassword.value !== confirmNewPassword.value) {
|
||||
return
|
||||
}
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch(`auth/password`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
old_password: auth.value.user.has_password ? oldPassword.value : null,
|
||||
new_password: removePasswordMode.value ? null : newPassword.value,
|
||||
},
|
||||
})
|
||||
managePasswordModal.value.hide()
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const manageTwoFactorModal = ref()
|
||||
const twoFactorSecret = ref(null)
|
||||
const twoFactorFlow = ref(null)
|
||||
const twoFactorStep = ref(0)
|
||||
async function showTwoFactorModal() {
|
||||
twoFactorStep.value = 0
|
||||
twoFactorCode.value = null
|
||||
twoFactorIncorrect.value = false
|
||||
if (auth.value.user.has_totp) {
|
||||
manageTwoFactorModal.value.show()
|
||||
return
|
||||
}
|
||||
|
||||
twoFactorSecret.value = null
|
||||
twoFactorFlow.value = null
|
||||
backupCodes.value = []
|
||||
manageTwoFactorModal.value.show()
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
const res = await useBaseFetch('auth/2fa/get_secret', {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
twoFactorSecret.value = res.secret
|
||||
twoFactorFlow.value = res.flow
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
return { auth }
|
||||
const twoFactorIncorrect = ref(false)
|
||||
const twoFactorCode = ref(null)
|
||||
const backupCodes = ref([])
|
||||
async function verifyTwoFactorCode() {
|
||||
startLoading()
|
||||
try {
|
||||
const res = await useBaseFetch('auth/2fa', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
code: twoFactorCode.value ? twoFactorCode.value.toString() : '',
|
||||
flow: twoFactorFlow.value,
|
||||
},
|
||||
})
|
||||
|
||||
backupCodes.value = res.backup_codes
|
||||
twoFactorStep.value = 2
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
twoFactorIncorrect.value = true
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function removeTwoFactor() {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch('auth/2fa', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
code: twoFactorCode.value ? twoFactorCode.value.toString() : '',
|
||||
},
|
||||
})
|
||||
manageTwoFactorModal.value.hide()
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
twoFactorIncorrect.value = true
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const authProviders = [
|
||||
{
|
||||
id: 'github',
|
||||
display: 'GitHub',
|
||||
icon: GitHubIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copied: false,
|
||||
email: this.auth.user.email,
|
||||
showKnownErrors: false,
|
||||
}
|
||||
{
|
||||
id: 'gitlab',
|
||||
display: 'GitLab',
|
||||
icon: GitLabIcon,
|
||||
},
|
||||
head: {
|
||||
title: 'Account settings - Modrinth',
|
||||
{
|
||||
id: 'steam',
|
||||
display: 'Steam',
|
||||
icon: SteamIcon,
|
||||
},
|
||||
methods: {
|
||||
async copyToken() {
|
||||
this.copied = true
|
||||
await navigator.clipboard.writeText(this.auth.token)
|
||||
},
|
||||
async deleteAccount() {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch(`user/${this.auth.user.id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
useCookie('auth-token').value = null
|
||||
alert('Please note that logging back in with GitHub will create a new account.')
|
||||
window.location.href = '/'
|
||||
|
||||
stopLoading()
|
||||
},
|
||||
logout() {
|
||||
this.$refs.modal_revoke_token.hide()
|
||||
useCookie('auth-token').value = null
|
||||
|
||||
window.location.href = getAuthUrl()
|
||||
},
|
||||
hasMonetizationEnabled() {
|
||||
return (
|
||||
this.auth.user.payout_data.payout_wallet &&
|
||||
this.auth.user.payout_data.payout_wallet_type &&
|
||||
this.auth.user.payout_data.payout_address
|
||||
)
|
||||
},
|
||||
async saveChanges() {
|
||||
if (this.hasMonetizationEnabled() && !this.email) {
|
||||
this.showKnownErrors = true
|
||||
return
|
||||
}
|
||||
startLoading()
|
||||
try {
|
||||
const data = {
|
||||
email: this.email ? this.email : null,
|
||||
}
|
||||
|
||||
await useBaseFetch(`user/${this.auth.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await useAuth(this.auth.token)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
},
|
||||
{
|
||||
id: 'discord',
|
||||
display: 'Discord',
|
||||
icon: DiscordIcon,
|
||||
},
|
||||
})
|
||||
{
|
||||
id: 'microsoft',
|
||||
display: 'Microsoft',
|
||||
icon: MicrosoftIcon,
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
display: 'Google',
|
||||
icon: GoogleIcon,
|
||||
},
|
||||
]
|
||||
|
||||
async function removeAuthProvider(provider) {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch(`user/${auth.value.user.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
useCookie('auth-token').value = null
|
||||
window.location.href = '/'
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.modal-revoke-token {
|
||||
padding: var(--spacing-card-bg);
|
||||
canvas {
|
||||
margin: 0 auto;
|
||||
border-radius: var(--size-rounded-card);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
.table-row {
|
||||
grid-template-columns: 1fr 10rem;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: auto 0;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="search-layout-toggle"
|
||||
v-model="$cosmetics.searchLayout"
|
||||
v-model="cosmetics.searchLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
@ -49,7 +49,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="project-layout-toggle"
|
||||
v-model="$cosmetics.projectLayout"
|
||||
v-model="cosmetics.projectLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
@ -71,8 +71,8 @@
|
||||
</label>
|
||||
<Multiselect
|
||||
:id="projectType + '-search-display-mode'"
|
||||
v-model="$cosmetics.searchDisplayMode[projectType.id]"
|
||||
:options="$tag.projectViewModes"
|
||||
v-model="cosmetics.searchDisplayMode[projectType.id]"
|
||||
:options="tags.projectViewModes"
|
||||
:custom-label="$capitalizeString"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
@ -94,7 +94,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="advanced-rendering"
|
||||
v-model="$cosmetics.advancedRendering"
|
||||
v-model="cosmetics.advancedRendering"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
@ -107,7 +107,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="modpacks-alpha-notice"
|
||||
v-model="$cosmetics.modpacksAlphaNotice"
|
||||
v-model="cosmetics.modpacksAlphaNotice"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
@ -124,7 +124,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="external-links-new-tab"
|
||||
v-model="$cosmetics.externalLinksNewTab"
|
||||
v-model="cosmetics.externalLinksNewTab"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
@ -141,9 +141,15 @@ export default defineNuxtComponent({
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
setup() {
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useTags()
|
||||
|
||||
return { cosmetics, tags }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchDisplayMode: this.$cosmetics.searchDisplayMode,
|
||||
searchDisplayMode: this.cosmetics.searchDisplayMode,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
@ -151,7 +157,7 @@ export default defineNuxtComponent({
|
||||
},
|
||||
computed: {
|
||||
listTypes() {
|
||||
const types = this.$tag.projectTypes.map((type) => {
|
||||
const types = this.tags.projectTypes.map((type) => {
|
||||
return {
|
||||
id: type.id,
|
||||
name: this.$formatProjectType(type.id) + ' search',
|
||||
|
||||
@ -184,7 +184,6 @@ export default defineNuxtComponent({
|
||||
await useBaseFetch(`user/${this.auth.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await useAuth(this.auth.token)
|
||||
|
||||
|
||||
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>
|
||||
89
pages/settings/sessions.vue
Normal file
89
pages/settings/sessions.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="universal-card">
|
||||
<h2>Sessions</h2>
|
||||
<p>
|
||||
Here are all the devices that are currently logged in with your Modrinth account. You can log
|
||||
out of each one individually.
|
||||
<br /><br />
|
||||
If you see an entry you don't recognize, log out of that device and change your Modrinth
|
||||
account password immediately.
|
||||
</p>
|
||||
<div v-for="session in sessions" :key="session.id" class="universal-card recessed session">
|
||||
<div>
|
||||
<div>
|
||||
<strong>
|
||||
{{ session.os ?? 'Unknown OS' }} ⋅ {{ session.platform ?? 'Unknown platform' }} ⋅
|
||||
{{ session.ip }}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="session.city">{{ session.city }}, {{ session.country }} ⋅</template>
|
||||
<span v-tooltip="$dayjs(session.last_login).format('MMMM D, YYYY [at] h:mm A')">
|
||||
Last accessed {{ fromNow(session.last_login) }}
|
||||
</span>
|
||||
⋅
|
||||
<span v-tooltip="$dayjs(session.created).format('MMMM D, YYYY [at] h:mm A')">
|
||||
Created {{ fromNow(session.created) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<i v-if="session.current">Current session</i>
|
||||
<button v-else class="iconified-button raised-button" @click="revokeSession(session.id)">
|
||||
<XIcon /> Revoke session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { XIcon } from 'omorphia'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Sessions - Modrinth',
|
||||
})
|
||||
|
||||
const data = useNuxtApp()
|
||||
const { data: sessions, refresh } = await useAsyncData('session/list', () =>
|
||||
useBaseFetch('session/list')
|
||||
)
|
||||
|
||||
async function revokeSession(id) {
|
||||
startLoading()
|
||||
try {
|
||||
sessions.value = sessions.value.filter((x) => x.id !== id)
|
||||
await useBaseFetch(`session/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
await refresh()
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.session {
|
||||
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>
|
||||
@ -46,7 +46,7 @@
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<button
|
||||
v-else-if="$auth.user && $auth.user.id === user.id"
|
||||
v-else-if="auth.user && auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
@click="isEditing = true"
|
||||
>
|
||||
@ -54,7 +54,7 @@
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
v-else-if="$auth.user"
|
||||
v-else-if="auth.user"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_report.show()"
|
||||
>
|
||||
@ -81,7 +81,7 @@
|
||||
@click="
|
||||
() => {
|
||||
isEditing = false
|
||||
user = JSON.parse(JSON.stringify($auth.user))
|
||||
user = JSON.parse(JSON.stringify(auth.user))
|
||||
previewImage = null
|
||||
icon = null
|
||||
}
|
||||
@ -96,7 +96,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="$tag.staffRoles.includes(user.role)" :type="user.role" />
|
||||
<Badge v-if="tags.staffRoles.includes(user.role)" :type="user.role" />
|
||||
<Badge v-else-if="projects.length > 0" type="creator" />
|
||||
</div>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
|
||||
@ -129,16 +129,6 @@
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text"> User ID: <CopyCode :text="user.id" /> </span>
|
||||
</div>
|
||||
<a
|
||||
v-if="githubUrl"
|
||||
:href="githubUrl"
|
||||
:target="$external()"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="sidebar__item github-button iconified-button"
|
||||
>
|
||||
<GitHubIcon aria-hidden="true" />
|
||||
View GitHub profile
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@ -161,7 +151,7 @@
|
||||
/>
|
||||
<div class="input-group">
|
||||
<NuxtLink
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
v-if="auth.user && auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
to="/dashboard/projects"
|
||||
>
|
||||
@ -169,13 +159,13 @@
|
||||
Manage projects
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-tooltip="$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'"
|
||||
:aria-label="$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'"
|
||||
v-tooltip="$capitalizeString(cosmetics.searchDisplayMode.user) + ' view'"
|
||||
:aria-label="$capitalizeString(cosmetics.searchDisplayMode.user) + ' view'"
|
||||
class="square-button"
|
||||
@click="cycleSearchDisplayMode()"
|
||||
>
|
||||
<GridIcon v-if="$cosmetics.searchDisplayMode.user === 'grid'" />
|
||||
<ImageIcon v-else-if="$cosmetics.searchDisplayMode.user === 'gallery'" />
|
||||
<GridIcon v-if="cosmetics.searchDisplayMode.user === 'grid'" />
|
||||
<ImageIcon v-else-if="cosmetics.searchDisplayMode.user === 'gallery'" />
|
||||
<ListIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
@ -183,7 +173,7 @@
|
||||
<div
|
||||
v-if="projects.length > 0"
|
||||
class="project-list"
|
||||
:class="'display-mode--' + $cosmetics.searchDisplayMode.user"
|
||||
:class="'display-mode--' + cosmetics.searchDisplayMode.user"
|
||||
>
|
||||
<ProjectCard
|
||||
v-for="project in (route.params.projectType !== undefined
|
||||
@ -199,7 +189,7 @@
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:display="$cosmetics.searchDisplayMode.user"
|
||||
:display="cosmetics.searchDisplayMode.user"
|
||||
:featured-image="
|
||||
project.gallery
|
||||
.slice()
|
||||
@ -216,7 +206,7 @@
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="
|
||||
$auth.user && ($auth.user.id === user.id || $tag.staffRoles.includes($auth.user.role))
|
||||
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
|
||||
? project.status
|
||||
: null
|
||||
"
|
||||
@ -226,7 +216,7 @@
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="$auth.user && $auth.user.id === user.id" class="text">
|
||||
<span v-if="auth.user && auth.user.id === user.id" class="text">
|
||||
You don't have any projects.<br />
|
||||
Would you like to
|
||||
<a class="link" @click.prevent="$refs.modal_creation.show()"> create one</a>?
|
||||
@ -242,7 +232,6 @@ import ProjectCard from '~/components/ui/ProjectCard.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import Promotion from '~/components/ads/Promotion.vue'
|
||||
|
||||
import GitHubIcon from '~/assets/images/utils/github.svg'
|
||||
import ReportIcon from '~/assets/images/utils/report.svg'
|
||||
import SunriseIcon from '~/assets/images/utils/sunrise.svg'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg'
|
||||
@ -266,23 +255,25 @@ import Avatar from '~/components/ui/Avatar.vue'
|
||||
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
const auth = await useAuth()
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useTags()
|
||||
|
||||
let user, projects
|
||||
try {
|
||||
;[{ data: user }, { data: projects }] = await Promise.all([
|
||||
useAsyncData(`user/${route.params.id}`, () =>
|
||||
useBaseFetch(`user/${route.params.id}`, data.$defaultHeaders())
|
||||
),
|
||||
useAsyncData(`user/${route.params.id}`, () => useBaseFetch(`user/${route.params.id}`)),
|
||||
useAsyncData(
|
||||
`user/${route.params.id}/projects`,
|
||||
() => useBaseFetch(`user/${route.params.id}/projects`, data.$defaultHeaders()),
|
||||
() => useBaseFetch(`user/${route.params.id}/projects`),
|
||||
{
|
||||
transform: (projects) => {
|
||||
for (const project of projects) {
|
||||
project.categories = project.categories.concat(project.loaders)
|
||||
project.project_type = data.$getProjectTypeForUrl(
|
||||
project.project_type,
|
||||
project.categories
|
||||
project.categories,
|
||||
tags.value
|
||||
)
|
||||
}
|
||||
|
||||
@ -307,12 +298,6 @@ if (!user.value) {
|
||||
})
|
||||
}
|
||||
|
||||
let githubUrl
|
||||
try {
|
||||
const githubUser = await $fetch(`https://api.github.com/user/` + user.value.github_id)
|
||||
githubUrl = ref(githubUser.html_url)
|
||||
} catch {}
|
||||
|
||||
if (user.value.username !== route.params.id) {
|
||||
await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 })
|
||||
}
|
||||
@ -369,13 +354,12 @@ async function saveChanges() {
|
||||
try {
|
||||
if (icon.value) {
|
||||
await useBaseFetch(
|
||||
`user/${data.$auth.user.id}/icon?ext=${
|
||||
`user/${auth.value.user.id}/icon?ext=${
|
||||
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
|
||||
}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: icon.value,
|
||||
...data.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -384,16 +368,15 @@ async function saveChanges() {
|
||||
email: user.value.email,
|
||||
bio: user.value.bio,
|
||||
}
|
||||
if (user.value.username !== data.$auth.user.username) {
|
||||
if (user.value.username !== auth.value.user.username) {
|
||||
reqData.username = user.value.username
|
||||
}
|
||||
|
||||
await useBaseFetch(`user/${data.$auth.user.id}`, {
|
||||
await useBaseFetch(`user/${auth.value.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: reqData,
|
||||
...data.$defaultHeaders(),
|
||||
})
|
||||
await useAuth(data.$auth.token)
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
isEditing.value = false
|
||||
} catch (err) {
|
||||
@ -409,9 +392,9 @@ async function saveChanges() {
|
||||
}
|
||||
|
||||
function cycleSearchDisplayMode() {
|
||||
data.$cosmetics.searchDisplayMode.user = data.$cycleValue(
|
||||
data.$cosmetics.searchDisplayMode.user,
|
||||
data.$tag.projectViewModes
|
||||
cosmetics.value.searchDisplayMode.user = data.$cycleValue(
|
||||
cosmetics.value.searchDisplayMode.user,
|
||||
tags.value.projectViewModes
|
||||
)
|
||||
saveCosmetics()
|
||||
}
|
||||
@ -504,10 +487,6 @@ export default defineNuxtComponent({
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
await useAuth()
|
||||
await useUser()
|
||||
const themeStore = useTheme()
|
||||
|
||||
nuxtApp.hook('app:mounted', () => {
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const authStore = await useAuth()
|
||||
await useUser()
|
||||
const cosmeticsStore = useCosmetics()
|
||||
const tagsStore = useTags()
|
||||
|
||||
nuxtApp.provide('auth', authStore.value)
|
||||
nuxtApp.provide('cosmetics', cosmeticsStore.value)
|
||||
nuxtApp.provide('tag', tagsStore.value)
|
||||
nuxtApp.provide('notify', (notif) => addNotification(notif))
|
||||
})
|
||||
@ -1,29 +1,10 @@
|
||||
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const tagStore = nuxtApp.$tag
|
||||
const authStore = nuxtApp.$auth
|
||||
|
||||
nuxtApp.provide('defaultHeaders', () => {
|
||||
const obj = { headers: {} }
|
||||
|
||||
if (process.server) {
|
||||
const config = useRuntimeConfig()
|
||||
if (config.rateLimitKey) {
|
||||
obj.headers['x-ratelimit-key'] = config.rateLimitKey || ''
|
||||
}
|
||||
}
|
||||
|
||||
if (authStore.user) {
|
||||
obj.headers.Authorization = authStore.token
|
||||
}
|
||||
|
||||
return obj
|
||||
})
|
||||
nuxtApp.provide('formatNumber', formatNumber)
|
||||
nuxtApp.provide('capitalizeString', capitalizeString)
|
||||
nuxtApp.provide('formatMoney', formatMoney)
|
||||
nuxtApp.provide('formatVersion', (versionsArray) => formatVersions(versionsArray, tagStore))
|
||||
nuxtApp.provide('formatVersion', (versionsArray) => formatVersions(versionsArray))
|
||||
nuxtApp.provide('orElse', (first, otherwise) => first ?? otherwise)
|
||||
nuxtApp.provide('external', () => {
|
||||
const cosmeticsStore = useCosmetics().value
|
||||
@ -95,15 +76,17 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
.sort((a, b) => nuxtApp.$dayjs(b.date_published) - nuxtApp.$dayjs(a.date_published))
|
||||
})
|
||||
nuxtApp.provide('getProjectTypeForDisplay', (type, categories) => {
|
||||
const tagStore = useTags()
|
||||
|
||||
if (type === 'mod') {
|
||||
const isPlugin = categories.some((category) => {
|
||||
return tagStore.loaderData.allPluginLoaders.includes(category)
|
||||
return tagStore.value.loaderData.allPluginLoaders.includes(category)
|
||||
})
|
||||
const isMod = categories.some((category) => {
|
||||
return tagStore.loaderData.modLoaders.includes(category)
|
||||
return tagStore.value.loaderData.modLoaders.includes(category)
|
||||
})
|
||||
const isDataPack = categories.some((category) => {
|
||||
return tagStore.loaderData.dataPackLoaders.includes(category)
|
||||
return tagStore.value.loaderData.dataPackLoaders.includes(category)
|
||||
})
|
||||
|
||||
if (isMod && isPlugin && isDataPack) {
|
||||
@ -123,25 +106,29 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
|
||||
return type
|
||||
})
|
||||
nuxtApp.provide('getProjectTypeForUrl', (type, loaders) =>
|
||||
getProjectTypeForUrlShorthand(nuxtApp, type, loaders)
|
||||
nuxtApp.provide('getProjectTypeForUrl', (type, loaders, tags) =>
|
||||
getProjectTypeForUrlShorthand(type, loaders, tags)
|
||||
)
|
||||
nuxtApp.provide('cycleValue', cycleValue)
|
||||
const sortedCategories = tagStore.categories.slice().sort((a, b) => {
|
||||
const headerCompare = a.header.localeCompare(b.header)
|
||||
if (headerCompare !== 0) {
|
||||
return headerCompare
|
||||
}
|
||||
if (a.header === 'resolutions' && b.header === 'resolutions') {
|
||||
return a.name.replace(/\D/g, '') - b.name.replace(/\D/g, '')
|
||||
} else if (a.header === 'performance impact' && b.header === 'performance impact') {
|
||||
const x = ['potato', 'low', 'medium', 'high', 'screenshot']
|
||||
nuxtApp.provide('sortedCategories', () => {
|
||||
const tagStore = useTags()
|
||||
|
||||
return x.indexOf(a.name) - x.indexOf(b.name)
|
||||
}
|
||||
return 0
|
||||
return tagStore.value.categories.slice().sort((a, b) => {
|
||||
const headerCompare = a.header.localeCompare(b.header)
|
||||
if (headerCompare !== 0) {
|
||||
return headerCompare
|
||||
}
|
||||
if (a.header === 'resolutions' && b.header === 'resolutions') {
|
||||
return a.name.replace(/\D/g, '') - b.name.replace(/\D/g, '')
|
||||
} else if (a.header === 'performance impact' && b.header === 'performance impact') {
|
||||
const x = ['potato', 'low', 'medium', 'high', 'screenshot']
|
||||
|
||||
return x.indexOf(a.name) - x.indexOf(b.name)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
})
|
||||
nuxtApp.provide('sortedCategories', sortedCategories)
|
||||
nuxtApp.provide('notify', (notif) => addNotification(notif))
|
||||
})
|
||||
export const formatNumber = (number, abbreviate = true) => {
|
||||
const x = +number
|
||||
@ -257,8 +244,9 @@ export const formatProjectStatus = (name) => {
|
||||
return capitalizeString(name)
|
||||
}
|
||||
|
||||
export const formatVersions = (versionArray, tag) => {
|
||||
const allVersions = tag.gameVersions.slice().reverse()
|
||||
export const formatVersions = (versionArray) => {
|
||||
const tag = useTags()
|
||||
const allVersions = tag.value.gameVersions.slice().reverse()
|
||||
const allReleases = allVersions.filter((x) => x.version_type === 'release')
|
||||
|
||||
const intervals = []
|
||||
|
||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@ -34,6 +34,9 @@ dependencies:
|
||||
omorphia:
|
||||
specifier: ^0.4.31
|
||||
version: 0.4.31
|
||||
qrcode.vue:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0(vue@3.3.4)
|
||||
vue-multiselect:
|
||||
specifier: ^3.0.0-alpha.2
|
||||
version: 3.0.0-alpha.2
|
||||
@ -48,6 +51,9 @@ devDependencies:
|
||||
'@nuxtjs/eslint-config-typescript':
|
||||
specifier: ^12.0.0
|
||||
version: 12.0.0(eslint@8.41.0)(typescript@5.0.4)
|
||||
'@nuxtjs/turnstile':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
'@types/node':
|
||||
specifier: ^20.1.0
|
||||
version: 20.1.0
|
||||
@ -1357,6 +1363,17 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@nuxtjs/turnstile@0.5.0:
|
||||
resolution: {integrity: sha512-EmEnYNDRavdmv9HXnInHVR5nSvjDG92s6pOrxd+ugqImkrnIQJttSd4DPq9UNhwUI/wLUL7z3VclVyQwNT2O7Q==}
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.6.1
|
||||
defu: 6.1.2
|
||||
pathe: 1.1.1
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@pkgjs/parseargs@0.11.0:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@ -6024,6 +6041,14 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/qrcode.vue@3.4.0(vue@3.3.4):
|
||||
resolution: {integrity: sha512-4XeImbv10Fin16Fl2DArCMhGyAdvIg2jb7vDT+hZiIAMg/6H6mz9nUZr/dR8jBcun5VzNzkiwKhiqOGbloinwA==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
dependencies:
|
||||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user