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:
Prospector
2023-07-20 11:19:42 -07:00
committed by GitHub
parent a5613ebb10
commit 34d63f3557
72 changed files with 2373 additions and 711 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -903,6 +903,8 @@ tr.button-transparent {
cursor: pointer; cursor: pointer;
width: fit-content; width: fit-content;
height: 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; text-decoration: none;

View File

@@ -1,6 +1,6 @@
<template> <template>
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText"> <button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
{{ text }} <span>{{ text }}</span>
<CheckIcon v-if="copied" /> <CheckIcon v-if="copied" />
<ClipboardCopyIcon v-else /> <ClipboardCopyIcon v-else />
</button> </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, 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; outline 0.2s ease-in-out;
span {
max-width: 10rem;
overflow: hidden;
text-overflow: ellipsis;
}
svg { svg {
width: 1em; width: 1em;
height: 1em; height: 1em;

View File

@@ -7,7 +7,7 @@
v-else-if=" v-else-if="
!['resourcepack', 'shader'].includes(type) && !['resourcepack', 'shader'].includes(type) &&
!(type === 'plugin' && search) && !(type === 'plugin' && search) &&
!categories.some((x) => $tag.loaderData.dataPackLoaders.includes(x)) !categories.some((x) => tags.loaderData.dataPackLoaders.includes(x))
" "
class="environment" class="environment"
> >
@@ -47,57 +47,52 @@
</template> </template>
</span> </span>
</template> </template>
<script> <script setup>
import InfoIcon from '~/assets/images/utils/info.svg' import InfoIcon from '~/assets/images/utils/info.svg'
import ClientIcon from '~/assets/images/utils/client.svg' import ClientIcon from '~/assets/images/utils/client.svg'
import GlobeIcon from '~/assets/images/utils/globe.svg' import GlobeIcon from '~/assets/images/utils/globe.svg'
import ServerIcon from '~/assets/images/utils/server.svg' import ServerIcon from '~/assets/images/utils/server.svg'
export default {
components: { defineProps({
InfoIcon, type: {
ClientIcon, type: String,
ServerIcon, default: 'mod',
GlobeIcon,
}, },
props: { serverSide: {
type: { type: String,
type: String, required: false,
default: 'mod', default: '',
}, },
serverSide: { clientSide: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
clientSide: { typeOnly: {
type: String, type: Boolean,
required: false, required: false,
default: '', default: false,
}, },
typeOnly: { alwaysShow: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
alwaysShow: { search: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
search: { categories: {
type: Boolean, type: Array,
required: false, required: false,
default: false, default() {
}, return []
categories: {
type: Array,
required: false,
default() {
return []
},
}, },
}, },
} })
const tags = useTags()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.environment { .environment {

View File

@@ -3,7 +3,7 @@
<div <div
:class="{ :class="{
shown: actuallyShown, shown: actuallyShown,
noblur: !$orElse($cosmetics.advancedRendering, true), noblur: !$orElse(cosmetics.advancedRendering, true),
}" }"
class="modal-overlay" class="modal-overlay"
@click="hide" @click="hide"
@@ -38,6 +38,11 @@ export default {
default: null, default: null,
}, },
}, },
setup() {
const cosmetics = useCosmetics()
return { cosmetics }
},
data() { data() {
return { return {
shown: false, shown: false,

View File

@@ -10,7 +10,7 @@
<Chips <Chips
id="project-type" id="project-type"
v-model="projectType" v-model="projectType"
:items="$tag.projectTypes.map((x) => x.display)" :items="tags.projectTypes.map((x) => x.display)"
/> />
<label for="name"> <label for="name">
<span class="label__title">Name<span class="required">*</span></span> <span class="label__title">Name<span class="required">*</span></span>
@@ -86,9 +86,14 @@ export default {
default: '', default: '',
}, },
}, },
setup() {
const tags = useTags()
return { tags }
},
data() { data() {
return { return {
projectType: this.$tag.projectTypes[0].display, projectType: this.tags.projectTypes[0].display,
name: '', name: '',
slug: '', slug: '',
description: '', description: '',
@@ -100,7 +105,7 @@ export default {
this.$refs.modal.hide() this.$refs.modal.hide()
}, },
getProjectType() { getProjectType() {
return this.$tag.projectTypes.find((x) => this.projectType === x.display) return this.tags.projectTypes.find((x) => this.projectType === x.display)
}, },
getClientSide() { getClientSide() {
switch (this.getProjectType().id) { switch (this.getProjectType().id) {
@@ -137,6 +142,8 @@ export default {
const formData = new FormData() const formData = new FormData()
const auth = await useAuth()
formData.append( formData.append(
'data', 'data',
JSON.stringify({ JSON.stringify({
@@ -148,8 +155,8 @@ export default {
initial_versions: [], initial_versions: [],
team_members: [ team_members: [
{ {
user_id: this.$auth.user.id, user_id: auth.value.user.id,
name: this.$auth.user.username, name: auth.value.user.username,
role: 'Owner', role: 'Owner',
}, },
], ],
@@ -167,7 +174,6 @@ export default {
body: formData, body: formData,
headers: { headers: {
'Content-Disposition': formData, 'Content-Disposition': formData,
Authorization: this.$auth.token,
}, },
}) })
@@ -193,7 +199,7 @@ export default {
stopLoading() stopLoading()
}, },
show() { show() {
this.projectType = this.$tag.projectTypes[0].display this.projectType = this.tags.projectTypes[0].display
this.name = '' this.name = ''
this.slug = '' this.slug = ''
this.description = '' this.description = ''

View File

@@ -113,7 +113,6 @@ export default {
await useBaseFetch(`project/${this.project.id}`, { await useBaseFetch(`project/${this.project.id}`, {
method: 'PATCH', method: 'PATCH',
body: data, body: data,
...this.$defaultHeaders(),
}) })
this.$refs.modal.hide() this.$refs.modal.hide()

View File

@@ -23,7 +23,7 @@
<Multiselect <Multiselect
id="report-type" id="report-type"
v-model="reportType" v-model="reportType"
:options="$tag.reportTypes" :options="tags.reportTypes"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)" :custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
:multiple="false" :multiple="false"
:searchable="false" :searchable="false"
@@ -82,6 +82,11 @@ export default {
default: '', default: '',
}, },
}, },
setup() {
const tags = useTags()
return { tags }
},
data() { data() {
return { return {
reportType: '', reportType: '',
@@ -110,7 +115,6 @@ export default {
await useBaseFetch('report', { await useBaseFetch('report', {
method: 'POST', method: 'POST',
body: data, body: data,
...this.$defaultHeaders(),
}) })
this.$refs.modal.hide() this.$refs.modal.hide()

View File

@@ -128,14 +128,15 @@ export default {
async proceed() { async proceed() {
startLoading() startLoading()
try { try {
await useBaseFetch(`user/${this.$auth.user.id}/payouts`, { const auth = await useAuth()
await useBaseFetch(`user/${auth.value.user.id}/payouts`, {
method: 'POST', method: 'POST',
body: { body: {
amount: Number(this.amount.replace('$', '')), amount: Number(this.amount.replace('$', '')),
}, },
...this.$defaultHeaders(),
}) })
await useAuth(this.$auth.token) await useAuth(auth.value.token)
this.$refs.modal.hide() this.$refs.modal.hide()
} catch (err) { } catch (err) {

View File

@@ -58,7 +58,7 @@
<nuxt-link :to="getProjectLink(project)" class="title-link"> <nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }} {{ project.title }}
</nuxt-link> </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" /> has been <Badge :type="notification.body.new_status" />
</template> </template>
<template v-else> <template v-else>
@@ -106,6 +106,7 @@
:raised="raised" :raised="raised"
:messages="getMessages()" :messages="getMessages()"
class="thread-summary" class="thread-summary"
:auth="auth"
/> />
<div v-else-if="type === 'project_update'" class="version-list"> <div v-else-if="type === 'project_update'" class="version-list">
<div <div
@@ -221,7 +222,7 @@
> >
<CheckIcon /> Mark as read <CheckIcon /> Mark as read
</button> </button>
<CopyCode v-if="$cosmetics.developerMode" :text="notification.id" /> <CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
</div> </div>
<div v-else class="input-group"> <div v-else class="input-group">
<nuxt-link <nuxt-link
@@ -253,7 +254,7 @@
> >
<CheckIcon /> Mark as read <CheckIcon /> Mark as read
</button> </button>
<CopyCode v-if="$cosmetics.developerMode" :text="notification.id" /> <CopyCode v-if="cosmetics.developerMode" :text="notification.id" />
</div> </div>
</div> </div>
</div> </div>
@@ -301,8 +302,15 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
auth: {
type: Object,
required: true,
},
}) })
const cosmetics = useCosmetics()
const tags = useTags()
const type = computed(() => const type = computed(() =>
!props.notification.body || props.notification.body.type === 'legacy_markdown' !props.notification.body || props.notification.body.type === 'legacy_markdown'
? null ? null
@@ -357,7 +365,6 @@ async function performAction(notification, actionIndex) {
if (actionIndex !== null) { if (actionIndex !== null) {
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, { await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
method: notification.actions[actionIndex].action_route[0].toUpperCase(), method: notification.actions[actionIndex].action_route[0].toUpperCase(),
...app.$defaultHeaders(),
}) })
} }
} catch (err) { } catch (err) {

View File

@@ -36,7 +36,7 @@
</p> </p>
<Categories <Categories
: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" :type="type"
class="tags" class="tags"
@@ -209,6 +209,11 @@ export default {
default: null, default: null,
}, },
}, },
setup() {
const tags = useTags()
return { tags }
},
computed: { computed: {
projectTypeDisplay() { projectTypeDisplay() {
return this.$getProjectTypeForDisplay(this.type, this.categories) return this.$getProjectTypeForDisplay(this.type, this.categories)

View File

@@ -1,5 +1,5 @@
<template> <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> <h2>Invitation to join project</h2>
<p> <p>
You've been invited be a member of this project with the role of '{{ currentMember.role }}'. You've been invited be a member of this project with the role of '{{ currentMember.role }}'.
@@ -15,10 +15,9 @@
</div> </div>
<div <div
v-if=" v-if="
$auth.user &&
currentMember && currentMember &&
nags.filter((x) => x.condition).length > 0 && 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" class="author-actions universal-card"
> >
@@ -155,6 +154,14 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
auth: {
type: Object,
required: true,
},
tags: {
type: Object,
required: true,
},
setProcessing: { setProcessing: {
type: Function, type: Function,
default() { 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, condition: true,
title: 'Resubmit for review', title: 'Resubmit for review',
id: 'resubmit-for-review', id: 'resubmit-for-review',
@@ -363,8 +370,8 @@ export default {
) )
}, },
showInvitation() { showInvitation() {
if (this.allMembers && this.$auth) { if (this.allMembers && this.auth) {
const member = this.allMembers.find((x) => x.user.id === this.$auth.user.id) const member = this.allMembers.find((x) => x.user.id === this.auth.user.id)
return member && !member.accepted return member && !member.accepted
} }
return false return false

View File

@@ -98,9 +98,10 @@ const props = defineProps({
}) })
const emit = defineEmits(['switch-page']) const emit = defineEmits(['switch-page'])
const data = useNuxtApp()
const route = useRoute() const route = useRoute()
const tags = useTags()
const tempLoaders = new Set() const tempLoaders = new Set()
let tempVersions = new Set() let tempVersions = new Set()
const tempReleaseChannels = new Set() const tempReleaseChannels = new Set()
@@ -119,7 +120,7 @@ tempVersions = Array.from(tempVersions)
const loaderFilters = shallowRef(Array.from(tempLoaders)) const loaderFilters = shallowRef(Array.from(tempLoaders))
const gameVersionFilters = shallowRef( 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 versionTypeFilters = shallowRef(Array.from(tempReleaseChannels))
const includeSnapshots = ref(route.query.s === 'true') const includeSnapshots = ref(route.query.s === 'true')

View File

@@ -63,10 +63,11 @@
class="thread-summary" class="thread-summary"
:raised="raised" :raised="raised"
:link="`/${moderation ? 'moderation' : 'dashboard'}/report/${report.id}`" :link="`/${moderation ? 'moderation' : 'dashboard'}/report/${report.id}`"
:auth="auth"
/> />
<div class="reporter-info"> <div class="reporter-info">
<ReportIcon class="inline-svg" /> Reported by <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"> <nuxt-link v-else :to="`/user/${report.reporterUser.username}`" class="iconified-link">
<Avatar <Avatar
:src="report.reporterUser.avatar_url" :src="report.reporterUser.avatar_url"
@@ -81,7 +82,7 @@
<span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{ <span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{
fromNow(report.created) fromNow(report.created)
}}</span> }}</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>
</div> </div>
</template> </template>
@@ -117,7 +118,13 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
auth: {
type: Object,
required: true,
},
}) })
const cosmetics = useCosmetics()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -7,11 +7,16 @@
:link-stack="breadcrumbsStack" :link-stack="breadcrumbsStack"
/> />
<h2>Report details</h2> <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>
<section class="universal-card"> <section class="universal-card">
<h2>Messages</h2> <h2>Messages</h2>
<ConversationThread :thread="thread" :report="report" :update-thread="updateThread" /> <ConversationThread
:thread="thread"
:report="report"
:update-thread="updateThread"
:auth="auth"
/>
</section> </section>
</div> </div>
</template> </template>
@@ -30,10 +35,12 @@ const props = defineProps({
type: Array, type: Array,
default: null, default: null,
}, },
auth: {
type: Object,
required: true,
},
}) })
const app = useNuxtApp()
const report = ref(null) const report = ref(null)
await fetchReport().then((result) => { await fetchReport().then((result) => {
@@ -41,7 +48,7 @@ await fetchReport().then((result) => {
}) })
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () => 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)) const thread = computed(() => addReportMessage(rawThread.value, report.value))
@@ -52,7 +59,7 @@ async function updateThread(newThread) {
async function fetchReport() { async function fetchReport() {
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () => 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, '') rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, '')
@@ -67,7 +74,7 @@ async function fetchReport() {
let users = [] let users = []
if (userIds.length > 0) { if (userIds.length > 0) {
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () => 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 users = usersVal.value
} }
@@ -75,7 +82,7 @@ async function fetchReport() {
let version = null let version = null
if (versionId) { if (versionId) {
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () => const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
useBaseFetch(`version/${versionId}`, app.$defaultHeaders()) useBaseFetch(`version/${versionId}`)
) )
version = versionVal.value version = versionVal.value
} }
@@ -89,7 +96,7 @@ async function fetchReport() {
let project = null let project = null
if (projectId) { if (projectId) {
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () => const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
useBaseFetch(`project/${projectId}`, app.$defaultHeaders()) useBaseFetch(`project/${projectId}`)
) )
project = projectVal.value project = projectVal.value
} }

View File

@@ -3,7 +3,7 @@
<ReportInfo <ReportInfo
v-for="report in reports.filter( v-for="report in reports.filter(
(x) => (x) =>
(moderation || x.reporterUser.id === $auth.user.id) && (moderation || x.reporterUser.id === auth.user.id) &&
(viewMode === 'open' ? x.open : !x.open) (viewMode === 'open' ? x.open : !x.open)
)" )"
:key="report.id" :key="report.id"
@@ -11,6 +11,7 @@
:thread="report.thread" :thread="report.thread"
:moderation="moderation" :moderation="moderation"
raised raised
:auth="auth"
class="universal-card recessed" class="universal-card recessed"
/> />
</template> </template>
@@ -24,16 +25,16 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
auth: {
type: Object,
required: true,
},
}) })
const app = useNuxtApp()
const viewMode = ref('open') const viewMode = ref('open')
const reports = ref([]) const reports = ref([])
let { data: rawReports } = await useAsyncData('report', () => let { data: rawReports } = await useAsyncData('report', () => useBaseFetch('report'))
useBaseFetch('report', app.$defaultHeaders())
)
rawReports = rawReports.value.map((report) => { rawReports = rawReports.value.map((report) => {
report.item_id = report.item_id.replace(/"/g, '') report.item_id = report.item_id.replace(/"/g, '')
@@ -53,13 +54,13 @@ const threadIds = [
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([ const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () => 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)}`, () => 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)}`, () => 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 projectIds = [...new Set(reportedProjects.concat(versionProjects))]
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () => 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) => { reports.value = rawReports.map((report) => {

View File

@@ -23,10 +23,15 @@ export default {
required: true, required: true,
}, },
}, },
setup() {
const tags = useTags()
return { tags }
},
computed: { computed: {
categoriesFiltered() { categoriesFiltered() {
return this.$tag.categories return this.tags.categories
.concat(this.$tag.loaders) .concat(this.tags.loaders)
.filter( .filter(
(x) => (x) =>
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type) this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type)

View File

@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
</Modal> </Modal>
<div v-if="$cosmetics.developerMode" class="thread-id"> <div v-if="cosmetics.developerMode" class="thread-id">
Thread ID: <CopyCode :text="thread.id" /> Thread ID: <CopyCode :text="thread.id" />
</div> </div>
<div v-if="sortedMessages.length > 0" class="messages universal-card recessed"> <div v-if="sortedMessages.length > 0" class="messages universal-card recessed">
@@ -77,7 +77,7 @@
> >
<SendIcon /> Send <SendIcon /> Send
</button> </button>
<template v-if="currentMember && !isStaff($auth.user)"> <template v-if="currentMember && !isStaff(auth.user)">
<template v-if="isRejected(project)"> <template v-if="isRejected(project)">
<button <button
v-if="replyBody" v-if="replyBody"
@@ -98,7 +98,7 @@
<div class="spacer"></div> <div class="spacer"></div>
<div class="input-group extra-options"> <div class="input-group extra-options">
<template v-if="report"> <template v-if="report">
<template v-if="isStaff($auth.user)"> <template v-if="isStaff(auth.user)">
<button <button
v-if="replyBody" v-if="replyBody"
class="iconified-button danger-button" class="iconified-button danger-button"
@@ -112,7 +112,7 @@
</template> </template>
</template> </template>
<template v-if="project"> <template v-if="project">
<template v-if="isStaff($auth.user)"> <template v-if="isStaff(auth.user)">
<button <button
v-if="replyBody" v-if="replyBody"
class="iconified-button brand-button" class="iconified-button brand-button"
@@ -216,8 +216,13 @@ const props = defineProps({
return null return null
}, },
}, },
auth: {
type: Object,
required: true,
},
}) })
const app = useNuxtApp() const app = useNuxtApp()
const cosmetics = useCosmetics()
const members = computed(() => { const members = computed(() => {
const members = {} const members = {}
@@ -250,7 +255,7 @@ async function updateThreadLocal() {
} }
let thread = null let thread = null
if (threadId) { if (threadId) {
thread = await useBaseFetch(`thread/${threadId}`, app.$defaultHeaders()) thread = await useBaseFetch(`thread/${threadId}`)
} }
props.updateThread(thread) props.updateThread(thread)
} }
@@ -265,7 +270,6 @@ async function sendReply(status = null) {
body: replyBody.value, body: replyBody.value,
}, },
}, },
...app.$defaultHeaders(),
}) })
replyBody.value = '' replyBody.value = ''
await updateThreadLocal() await updateThreadLocal()
@@ -293,7 +297,6 @@ async function closeReport(reply) {
body: { body: {
closed: true, closed: true,
}, },
...app.$defaultHeaders(),
}) })
await updateThreadLocal() await updateThreadLocal()
} catch (err) { } catch (err) {

View File

@@ -51,6 +51,10 @@ const props = defineProps({
return [] return []
}, },
}, },
auth: {
type: Object,
required: true,
},
}) })
const app = useNuxtApp() const app = useNuxtApp()
@@ -60,7 +64,7 @@ const members = computed(() => {
for (const member of props.thread.members) { for (const member of props.thread.members) {
members[member.id] = member members[member.id] = member
} }
members[app.$auth.user.id] = app.$auth.user members[props.auth.user.id] = props.auth.user
return members return members
}) })

View File

@@ -16,8 +16,12 @@ export const initAuth = async (oldToken = null) => {
const auth = { const auth = {
user: null, user: null,
token: '', token: '',
headers: {},
} }
if (oldToken === 'none') {
return auth
}
const route = useRoute() const route = useRoute()
const authCookie = useCookie('auth-token', { const authCookie = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 365 * 10, maxAge: 60 * 60 * 24 * 365 * 10,
@@ -31,33 +35,66 @@ export const initAuth = async (oldToken = null) => {
authCookie.value = oldToken authCookie.value = oldToken
} }
if (route.query.code) { if (route.query.code && !route.fullPath.includes('new_account=true')) {
authCookie.value = route.query.code authCookie.value = route.query.code
} }
if (authCookie.value) { if (authCookie.value) {
auth.token = authCookie.value auth.token = authCookie.value
try {
auth.user = await useBaseFetch('user', {
headers: {
Authorization: auth.token,
},
})
} catch {}
auth.headers = { if (!auth.token || !auth.token.startsWith('mra_')) {
headers: { return auth
Authorization: auth.token, }
},
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 return auth
} }
export const getAuthUrl = () => { export const getAuthUrl = (provider) => {
const config = useRuntimeConfig() const config = useRuntimeConfig()
const route = useRoute() 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}`
} }

View File

@@ -1,13 +1,19 @@
export const useBaseFetch = async (url, options = {}) => { export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
const config = useRuntimeConfig() const config = useRuntimeConfig()
const base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl 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 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) return await $fetch(`${base}${url}`, options)

View File

@@ -20,8 +20,8 @@ export const initUser = async () => {
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
const [notifications, follows] = await Promise.all([ const [notifications, follows] = await Promise.all([
useBaseFetch(`user/${auth.user.id}/notifications`, auth.headers), useBaseFetch(`user/${auth.user.id}/notifications`),
useBaseFetch(`user/${auth.user.id}/follows`, auth.headers), useBaseFetch(`user/${auth.user.id}/follows`),
]) ])
user.notifications = notifications user.notifications = notifications
@@ -41,7 +41,7 @@ export const initUserNotifs = async () => {
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
user.notifications = await useBaseFetch(`user/${auth.user.id}/notifications`, auth.headers) user.notifications = await useBaseFetch(`user/${auth.user.id}/notifications`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@@ -54,7 +54,7 @@ export const initUserFollows = async () => {
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`, auth.headers) user.follows = await useBaseFetch(`user/${auth.user.id}/follows`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@@ -67,7 +67,7 @@ export const initUserProjects = async () => {
if (auth.user && auth.user.id) { if (auth.user && auth.user.id) {
try { try {
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`, auth.headers) user.projects = await useBaseFetch(`user/${auth.user.id}/projects`)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@@ -75,7 +75,6 @@ export const initUserProjects = async () => {
} }
export const userFollowProject = async (project) => { export const userFollowProject = async (project) => {
const auth = (await useAuth()).value
const user = (await useUser()).value const user = (await useUser()).value
user.follows = user.follows.concat(project) user.follows = user.follows.concat(project)
@@ -84,13 +83,11 @@ export const userFollowProject = async (project) => {
setTimeout(() => { setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, { useBaseFetch(`project/${project.id}/follow`, {
method: 'POST', method: 'POST',
...auth.headers,
}) })
}) })
} }
export const userUnfollowProject = async (project) => { export const userUnfollowProject = async (project) => {
const auth = (await useAuth()).value
const user = (await useUser()).value const user = (await useUser()).value
user.follows = user.follows.filter((x) => x.id !== project.id) user.follows = user.follows.filter((x) => x.id !== project.id)
@@ -99,7 +96,6 @@ export const userUnfollowProject = async (project) => {
setTimeout(() => { setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, { useBaseFetch(`project/${project.id}/follow`, {
method: 'DELETE', method: 'DELETE',
...auth.headers,
}) })
}) })
} }
@@ -126,3 +122,45 @@ export const userReadNotifications = async (ids) => {
return x 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()
}

View File

@@ -2,13 +2,12 @@ import { useNuxtApp } from '#app'
import { userReadNotifications } from '~/composables/user.js' import { userReadNotifications } from '~/composables/user.js'
async function getBulk(type, ids) { async function getBulk(type, ids) {
const auth = (await useAuth()).value
if (ids.length === 0) { if (ids.length === 0) {
return [] return []
} }
const url = `${type}?ids=${JSON.stringify([...new Set(ids)])}` const url = `${type}?ids=${encodeURIComponent(JSON.stringify([...new Set(ids)]))}`
const { data: bulkFetch } = await useAsyncData(url, () => useBaseFetch(url, auth.headers)) const { data: bulkFetch } = await useAsyncData(url, () => useBaseFetch(url))
return bulkFetch.value return bulkFetch.value
} }
@@ -16,7 +15,7 @@ export async function fetchNotifications() {
try { try {
const auth = (await useAuth()).value const auth = (await useAuth()).value
const { data: notifications } = await useAsyncData(`user/${auth.user.id}/notifications`, () => 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 = [] const projectIds = []
@@ -161,12 +160,8 @@ export function groupNotifications(notifications, includeRead = false) {
export async function markAsRead(ids) { export async function markAsRead(ids) {
try { try {
const auth = (await useAuth()).value
await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, { await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, {
method: 'PATCH', method: 'PATCH',
headers: {
Authorization: auth.token,
},
}) })
await userReadNotifications(ids) await userReadNotifications(ids)
return (notifications) => { return (notifications) => {

View File

@@ -1,20 +1,21 @@
export const getProjectTypeForUrl = (type, categories) => { export const getProjectTypeForUrl = (type, categories) => {
const app = useNuxtApp() return getProjectTypeForUrlShorthand(type, categories)
return getProjectTypeForUrlShorthand(app, type, categories)
} }
export const getProjectTypeForUrlShorthand = (app, type, categories) => { export const getProjectTypeForUrlShorthand = (type, categories, overrideTags) => {
const tags = overrideTags ?? useTags().value
if (type === 'mod') { if (type === 'mod') {
const isMod = categories.some((category) => { const isMod = categories.some((category) => {
return app.$tag.loaderData.modLoaders.includes(category) return tags.loaderData.modLoaders.includes(category)
}) })
const isPlugin = categories.some((category) => { const isPlugin = categories.some((category) => {
return app.$tag.loaderData.allPluginLoaders.includes(category) return tags.loaderData.allPluginLoaders.includes(category)
}) })
const isDataPack = categories.some((category) => { const isDataPack = categories.some((category) => {
return app.$tag.loaderData.dataPackLoaders.includes(category) return tags.loaderData.dataPackLoaders.includes(category)
}) })
if (isDataPack) { if (isDataPack) {

View File

@@ -1,18 +1,14 @@
export const acceptTeamInvite = async (teamId) => { export const acceptTeamInvite = async (teamId) => {
const app = useNuxtApp()
await useBaseFetch(`team/${teamId}/join`, { await useBaseFetch(`team/${teamId}/join`, {
method: 'POST', method: 'POST',
...app.$defaultHeaders(),
}) })
} }
export const removeSelfFromTeam = async (teamId) => { export const removeSelfFromTeam = async (teamId) => {
const app = useNuxtApp() const auth = await useAuth()
await removeTeamMember(teamId, app.$auth.user.id) await removeTeamMember(teamId, auth.user.id)
} }
export const removeTeamMember = async (teamId, userId) => { export const removeTeamMember = async (teamId, userId) => {
const app = useNuxtApp()
await useBaseFetch(`team/${teamId}/members/${userId}`, { await useBaseFetch(`team/${teamId}/members/${userId}`, {
method: 'DELETE', method: 'DELETE',
...app.$defaultHeaders(),
}) })
} }

View File

@@ -1,5 +1,21 @@
<template> <template>
<div ref="main_page" class="layout" :class="{ 'expanded-mobile-nav': isBrowseMenuOpen }"> <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"> <header class="site-header" role="presentation">
<section class="navbar columns" role="navigation"> <section class="navbar columns" role="navigation">
<section class="logo column" role="presentation"> <section class="logo column" role="presentation">
@@ -25,7 +41,6 @@
<button <button
class="control-button button-transparent" class="control-button button-transparent"
title="Switch theme" title="Switch theme"
:disabled="isThemeSwitchOnHold"
@click="changeTheme" @click="changeTheme"
> >
<MoonIcon v-if="$colorMode.value === 'light'" aria-hidden="true" /> <MoonIcon v-if="$colorMode.value === 'light'" aria-hidden="true" />
@@ -80,7 +95,7 @@
<span class="title">Settings</span> <span class="title">Settings</span>
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
v-if="$tag.staffRoles.includes($auth.user.role)" v-if="tags.staffRoles.includes(auth.user.role)"
class="item button-transparent" class="item button-transparent"
to="/moderation" to="/moderation"
> >
@@ -88,21 +103,16 @@
<span class="title">Moderation</span> <span class="title">Moderation</span>
</NuxtLink> </NuxtLink>
<hr class="divider" /> <hr class="divider" />
<button class="item button-transparent" @click="logout()"> <button class="item button-transparent" @click="logoutUser()">
<LogOutIcon class="icon" /> <LogOutIcon class="icon" />
<span class="dropdown-item__text">Log out</span> <span class="dropdown-item__text">Log out</span>
</button> </button>
</div> </div>
</div> </div>
<section v-else class="auth-prompt"> <section v-else class="auth-prompt">
<a <nuxt-link class="iconified-button brand-button" to="/auth/sign-in">
:href="getAuthUrl()" <LogInIcon /> Sign in
class="log-in-button header-button brand-button" </nuxt-link>
rel="noopener nofollow"
>
<GitHubIcon aria-hidden="true" />
Sign in with GitHub</a
>
</section> </section>
</section> </section>
</section> </section>
@@ -150,19 +160,13 @@
<div>Visit your profile</div> <div>Visit your profile</div>
</div> </div>
</NuxtLink> </NuxtLink>
<a <nuxt-link v-else class="iconified-button brand-button" to="/auth/sign-in">
v-else <LogInIcon /> Sign in
class="iconified-button brand-button" </nuxt-link>
:href="getAuthUrl()"
rel="nofollow noopener"
>
<GitHubIcon aria-hidden="true" />
Sign in with GitHub
</a>
</div> </div>
<div class="links"> <div class="links">
<template v-if="auth.user"> <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" /> <LogOutIcon aria-hidden="true" />
Log out Log out
</button> </button>
@@ -187,7 +191,7 @@
<SettingsIcon aria-hidden="true" /> <SettingsIcon aria-hidden="true" />
Settings Settings
</NuxtLink> </NuxtLink>
<button class="iconified-button" :disabled="isThemeSwitchOnHold" @click="changeTheme"> <button class="iconified-button" @click="changeTheme">
<MoonIcon v-if="$colorMode.value === 'light'" class="icon" /> <MoonIcon v-if="$colorMode.value === 'light'" class="icon" />
<SunIcon v-else class="icon" /> <SunIcon v-else class="icon" />
<span class="dropdown-item__text">Change theme</span> <span class="dropdown-item__text">Change theme</span>
@@ -319,11 +323,7 @@
</a> </a>
</div> </div>
<div class="buttons"> <div class="buttons">
<button <button class="iconified-button raised-button" @click="changeTheme">
class="iconified-button raised-button"
:disabled="isThemeSwitchOnHold"
@click="changeTheme"
>
<MoonIcon v-if="$colorMode.value === 'light'" aria-hidden="true" /> <MoonIcon v-if="$colorMode.value === 'light'" aria-hidden="true" />
<SunIcon v-else aria-hidden="true" /> <SunIcon v-else aria-hidden="true" />
Change theme Change theme
@@ -340,6 +340,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { LogInIcon } from 'omorphia'
import HamburgerIcon from '~/assets/images/utils/hamburger.svg' import HamburgerIcon from '~/assets/images/utils/hamburger.svg'
import CrossIcon from '~/assets/images/utils/x.svg' import CrossIcon from '~/assets/images/utils/x.svg'
import SearchIcon from '~/assets/images/utils/search.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 HeartIcon from '~/assets/images/utils/heart.svg'
import ChartIcon from '~/assets/images/utils/chart.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 NavRow from '~/components/ui/NavRow.vue'
import ModalCreation from '~/components/ui/ModalCreation.vue' import ModalCreation from '~/components/ui/ModalCreation.vue'
import Avatar from '~/components/ui/Avatar.vue' import Avatar from '~/components/ui/Avatar.vue'
@@ -365,6 +365,8 @@ import Avatar from '~/components/ui/Avatar.vue'
const app = useNuxtApp() const app = useNuxtApp()
const auth = await useAuth() const auth = await useAuth()
const user = await useUser() const user = await useUser()
const cosmetics = useCosmetics()
const tags = useTags()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const route = useRoute() const route = useRoute()
@@ -383,9 +385,9 @@ let developerModeCounter = 0
function developerModeIncrement() { function developerModeIncrement() {
if (developerModeCounter >= 5) { if (developerModeCounter >= 5) {
app.$cosmetics.developerMode = !app.$cosmetics.developerMode cosmetics.value.developerMode = !cosmetics.value.developerMode
developerModeCounter = 0 developerModeCounter = 0
if (app.$cosmetics.developerMode) { if (cosmetics.value.developerMode) {
app.$notify({ app.$notify({
group: 'main', group: 'main',
title: 'Developer mode activated', title: 'Developer mode activated',
@@ -404,6 +406,10 @@ function developerModeIncrement() {
developerModeCounter++ developerModeCounter++
} }
} }
async function logoutUser() {
await logout()
}
</script> </script>
<script> <script>
export default defineNuxtComponent({ export default defineNuxtComponent({
@@ -412,7 +418,6 @@ export default defineNuxtComponent({
isDropdownOpen: false, isDropdownOpen: false,
isMobileMenuOpen: false, isMobileMenuOpen: false,
isBrowseMenuOpen: false, isBrowseMenuOpen: false,
isThemeSwitchOnHold: false,
registeredSkipLink: null, registeredSkipLink: null,
hideDropdown: false, hideDropdown: false,
navRoutes: [ navRoutes: [
@@ -463,12 +468,8 @@ export default defineNuxtComponent({
this.runAnalytics() this.runAnalytics()
}, },
}, },
async mounted() { mounted() {
this.runAnalytics() this.runAnalytics()
if (this.$route.query.code) {
await useAuth(this.$route.query.code)
window.history.replaceState(history.state, null, this.$route.path)
}
}, },
methods: { methods: {
runAnalytics() { runAnalytics() {
@@ -498,28 +499,8 @@ export default defineNuxtComponent({
this.isMobileMenuOpen = false 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() { changeTheme() {
this.isThemeSwitchOnHold = true
updateTheme(this.$colorMode.value === 'dark' ? 'light' : 'dark', 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>
<style src="vue-multiselect/dist/vue-multiselect.css"></style> <style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -1,7 +1,7 @@
export default defineNuxtRouteMiddleware(async () => { export default defineNuxtRouteMiddleware(async (_to, from) => {
const auth = await useAuth() const auth = await useAuth()
if (!auth.value.user) { if (!auth.value.user) {
return navigateTo(getAuthUrl(), { external: true }) return navigateTo(`/auth/sign-in?redirect=${encodeURIComponent(from.fullPath)}`)
} }
}) })

View File

@@ -294,12 +294,15 @@ export default defineNuxtConfig({
}, },
}, },
}, },
modules: ['@vintl/nuxt'], modules: ['@vintl/nuxt', '@nuxtjs/turnstile'],
vintl: { vintl: {
defaultLocale: 'en-US', defaultLocale: 'en-US',
storage: 'cookie', storage: 'cookie',
parserless: 'only-prod', parserless: 'only-prod',
}, },
turnstile: {
siteKey: '0x4AAAAAAAHWfmKCm7cUG869',
},
nitro: { nitro: {
moduleSideEffects: ['@vintl/compact-number/locale-data'], moduleSideEffects: ['@vintl/compact-number/locale-data'],
}, },

View File

@@ -14,6 +14,7 @@
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^6.1.2", "@formatjs/cli": "^6.1.2",
"@nuxtjs/eslint-config-typescript": "^12.0.0", "@nuxtjs/eslint-config-typescript": "^12.0.0",
"@nuxtjs/turnstile": "^0.5.0",
"@types/node": "^20.1.0", "@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8", "@typescript-eslint/parser": "^5.59.8",
@@ -44,6 +45,7 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"omorphia": "^0.4.31", "omorphia": "^0.4.31",
"qrcode.vue": "^3.4.0",
"vue-multiselect": "^3.0.0-alpha.2", "vue-multiselect": "^3.0.0-alpha.2",
"xss": "^1.0.14" "xss": "^1.0.14"
}, },

View File

@@ -106,6 +106,8 @@
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)" :toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
:all-members="allMembers" :all-members="allMembers"
:update-members="updateMembers" :update-members="updateMembers"
:auth="auth"
:tags="tags"
/> />
<NuxtPage <NuxtPage
v-model:project="project" v-model:project="project"
@@ -149,7 +151,7 @@
/> />
</Head> </Head>
<ModalModeration <ModalModeration
v-if="$auth.user" v-if="auth.user"
ref="modalModeration" ref="modalModeration"
:project="project" :project="project"
:status="moderationStatus" :status="moderationStatus"
@@ -161,7 +163,7 @@
</div> </div>
</Modal> </Modal>
<ModalReport <ModalReport
v-if="$auth.user" v-if="auth.user"
ref="modal_project_report" ref="modal_project_report"
:item-id="project.id" :item-id="project.id"
item-type="project" item-type="project"
@@ -169,7 +171,7 @@
<div <div
:class="{ :class="{
'normal-page': true, 'normal-page': true,
'alt-layout': $cosmetics.projectLayout, 'alt-layout': cosmetics.projectLayout,
}" }"
> >
<div class="normal-page__sidebar"> <div class="normal-page__sidebar">
@@ -229,7 +231,7 @@
class="categories" class="categories"
> >
<Badge <Badge
v-if="$auth.user && currentMember" v-if="auth.user && currentMember"
:type="project.status" :type="project.status"
class="status-badge" class="status-badge"
/> />
@@ -287,7 +289,7 @@
</div> </div>
<hr class="card-divider" /> <hr class="card-divider" />
<div class="input-group"> <div class="input-group">
<template v-if="$auth.user"> <template v-if="auth.user">
<button class="iconified-button" @click="$refs.modal_project_report.show()"> <button class="iconified-button" @click="$refs.modal_project_report.show()">
<ReportIcon aria-hidden="true" /> <ReportIcon aria-hidden="true" />
Report Report
@@ -344,7 +346,7 @@
/> />
<div class="buttons status-buttons"> <div class="buttons status-buttons">
<button <button
v-if="$tag.approvedStatuses.includes(project.status)" v-if="tags.approvedStatuses.includes(project.status)"
class="iconified-button" class="iconified-button"
@click="clearMessage" @click="clearMessage"
> >
@@ -354,14 +356,14 @@
</div> </div>
</div> </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" class="universal-card moderation-card"
> >
<h2>Moderation actions</h2> <h2>Moderation actions</h2>
<div class="input-stack"> <div class="input-stack">
<button <button
v-if=" v-if="
!$tag.approvedStatuses.includes(project.status) || project.status === 'processing' !tags.approvedStatuses.includes(project.status) || project.status === 'processing'
" "
class="iconified-button brand-button" class="iconified-button brand-button"
@click="openModerationModal(requestedStatus)" @click="openModerationModal(requestedStatus)"
@@ -372,9 +374,9 @@
</button> </button>
<button <button
v-if=" v-if="
$tag.approvedStatuses.includes(project.status) || tags.approvedStatuses.includes(project.status) ||
project.status === 'processing' || project.status === 'processing' ||
($tag.rejectedStatuses.includes(project.status) && project.status !== 'withheld') (tags.rejectedStatuses.includes(project.status) && project.status !== 'withheld')
" "
class="iconified-button danger-button" class="iconified-button danger-button"
@click="openModerationModal('withheld')" @click="openModerationModal('withheld')"
@@ -384,9 +386,9 @@
</button> </button>
<button <button
v-if=" v-if="
$tag.approvedStatuses.includes(project.status) || tags.approvedStatuses.includes(project.status) ||
project.status === 'processing' || project.status === 'processing' ||
($tag.rejectedStatuses.includes(project.status) && project.status !== 'rejected') (tags.rejectedStatuses.includes(project.status) && project.status !== 'rejected')
" "
class="iconified-button danger-button" class="iconified-button danger-button"
@click="openModerationModal('rejected')" @click="openModerationModal('rejected')"
@@ -422,6 +424,8 @@
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)" :toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
:all-members="allMembers" :all-members="allMembers"
:update-members="updateMembers" :update-members="updateMembers"
:auth="auth"
:tags="tags"
/> />
<div v-else-if="project.status === 'withheld'" class="card warning" aria-label="Warning"> <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 {{ project.title }} has been removed from search by Modrinth's moderators. Please use
@@ -447,7 +451,7 @@
Prism Launcher</a Prism Launcher</a
>. >.
</div> </div>
<Promotion v-if="$tag.approvedStatuses.includes(project.status)" /> <Promotion v-if="tags.approvedStatuses.includes(project.status)" />
<div class="navigation-card"> <div class="navigation-card">
<NavRow <NavRow
:links="[ :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 <nuxt-link
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`" :to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
class="iconified-button" class="iconified-button"
@@ -808,12 +812,15 @@ const data = useNuxtApp()
const route = useRoute() const route = useRoute()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const auth = await useAuth()
const user = await useUser() const user = await useUser()
const cosmetics = useCosmetics()
const tags = useTags()
if ( if (
!route.params.id || !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' route.params.type === 'project'
) )
) { ) {
@@ -833,28 +840,27 @@ try {
{ data: featuredVersions }, { data: featuredVersions },
{ data: versions }, { data: versions },
] = await Promise.all([ ] = await Promise.all([
useAsyncData( useAsyncData(`project/${route.params.id}`, () => useBaseFetch(`project/${route.params.id}`), {
`project/${route.params.id}`, transform: (project) => {
() => useBaseFetch(`project/${route.params.id}`, data.$defaultHeaders()), if (project) {
{ project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
transform: (project) => { project.project_type = data.$getProjectTypeForUrl(
if (project) { project.project_type,
project.actualProjectType = JSON.parse(JSON.stringify(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( useAsyncData(
`project/${route.params.id}/members`, `project/${route.params.id}/members`,
() => useBaseFetch(`project/${route.params.id}/members`, data.$defaultHeaders()), () => useBaseFetch(`project/${route.params.id}/members`),
{ {
transform: (members) => { transform: (members) => {
members.forEach((it, index) => { members.forEach((it, index) => {
@@ -867,13 +873,13 @@ try {
} }
), ),
useAsyncData(`project/${route.params.id}/dependencies`, () => 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`, () => 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`, () => 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 members = ref(allMembers.value.filter((x) => x.accepted))
const currentMember = ref( 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 ( if (
!currentMember.value && !currentMember.value &&
data.$auth.user && auth.value.user &&
data.$tag.staffRoles.includes(data.$auth.user.role) tags.value.staffRoles.includes(auth.value.user.role)
) { ) {
currentMember.value = { currentMember.value = {
team_id: project.team_id, team_id: project.team_id,
user: data.$auth.user, user: auth.value.user,
role: data.$auth.role, role: auth.value.role,
permissions: data.$auth.user.role === 'admin' ? 1023 : 12, permissions: auth.value.user.role === 'admin' ? 1023 : 12,
accepted: true, accepted: true,
payouts_split: 0, payouts_split: 0,
avatar_url: data.$auth.user.avatar_url, avatar_url: auth.value.user.avatar_url,
name: data.$auth.user.username, name: auth.value.user.username,
} }
} }
@@ -943,7 +949,7 @@ featuredVersions.value = versions.value.filter((version) => featuredIds.includes
featuredVersions.value.sort((a, b) => { featuredVersions.value.sort((a, b) => {
const aLatest = a.game_versions[a.game_versions.length - 1] const aLatest = a.game_versions[a.game_versions.length - 1]
const bLatest = b.game_versions[b.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) 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') const requestedStatus = computed(() => project.value.requested_status ?? 'approved')
async function resetProject() { 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)) newProject.actualProjectType = JSON.parse(JSON.stringify(newProject.project_type))
@@ -986,7 +992,6 @@ async function clearMessage() {
moderation_message: null, moderation_message: null,
moderation_message_body: null, moderation_message_body: null,
}, },
...data.$defaultHeaders(),
}) })
project.value.moderator_message = null project.value.moderator_message = null
@@ -1011,7 +1016,6 @@ async function setProcessing() {
body: { body: {
status: 'processing', status: 'processing',
}, },
...data.$defaultHeaders(),
}) })
project.value.status = 'processing' project.value.status = 'processing'
@@ -1048,7 +1052,6 @@ async function patchProject(resData, quiet = false) {
await useBaseFetch(`project/${project.value.id}`, { await useBaseFetch(`project/${project.value.id}`, {
method: 'PATCH', method: 'PATCH',
body: resData, body: resData,
...data.$defaultHeaders(),
}) })
for (const key in resData) { for (const key in resData) {
@@ -1099,7 +1102,6 @@ async function patchIcon(icon) {
{ {
method: 'PATCH', method: 'PATCH',
body: icon, body: icon,
...data.$defaultHeaders(),
} }
) )
await resetProject() await resetProject()
@@ -1136,7 +1138,7 @@ function openModerationModal(status) {
async function updateMembers() { async function updateMembers() {
allMembers.value = await useAsyncData( allMembers.value = await useAsyncData(
`project/${route.params.id}/members`, `project/${route.params.id}/members`,
() => useBaseFetch(`project/${route.params.id}/members`, data.$defaultHeaders()), () => useBaseFetch(`project/${route.params.id}/members`),
{ {
transform: (members) => { transform: (members) => {
members.forEach((it, index) => { members.forEach((it, index) => {

View File

@@ -8,7 +8,7 @@
<Meta name="og:description" :contcent="metaDescription" /> <Meta name="og:description" :contcent="metaDescription" />
</Head> </Head>
<Modal <Modal
v-if="$auth.user && currentMember" v-if="currentMember"
ref="modal_edit_item" ref="modal_edit_item"
:header="editIndex === -1 ? 'Upload gallery image' : 'Edit gallery item'" :header="editIndex === -1 ? 'Upload gallery image' : 'Edit gallery item'"
> >
@@ -127,7 +127,7 @@
</div> </div>
</Modal> </Modal>
<ModalConfirm <ModalConfirm
v-if="$auth.user && currentMember" v-if="currentMember"
ref="modal_confirm" ref="modal_confirm"
title="Are you sure you want to delete this gallery image?" title="Are you sure you want to delete this gallery image?"
description="This will remove this gallery image forever (like really forever)." description="This will remove this gallery image forever (like really forever)."
@@ -446,7 +446,6 @@ export default defineNuxtComponent({
await useBaseFetch(url, { await useBaseFetch(url, {
method: 'POST', method: 'POST',
body: this.editFile, body: this.editFile,
...this.$defaultHeaders(),
}) })
await this.updateProject() await this.updateProject()
@@ -484,7 +483,6 @@ export default defineNuxtComponent({
await useBaseFetch(url, { await useBaseFetch(url, {
method: 'PATCH', method: 'PATCH',
...this.$defaultHeaders(),
}) })
await this.updateProject() await this.updateProject()
@@ -511,7 +509,6 @@ export default defineNuxtComponent({
)}`, )}`,
{ {
method: 'DELETE', method: 'DELETE',
...this.$defaultHeaders(),
} }
) )
@@ -528,7 +525,7 @@ export default defineNuxtComponent({
stopLoading() stopLoading()
}, },
async updateProject() { 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)) project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))

View File

@@ -68,6 +68,7 @@
:project="project" :project="project"
:set-status="setStatus" :set-status="setStatus"
:current-member="currentMember" :current-member="currentMember"
:auth="auth"
/> />
</section> </section>
</div> </div>
@@ -104,9 +105,10 @@ const props = defineProps({
const emit = defineEmits(['update:project']) const emit = defineEmits(['update:project'])
const app = useNuxtApp() const app = useNuxtApp()
const auth = await useAuth()
const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () => 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) { async function setStatus(status) {
startLoading() startLoading()
@@ -117,12 +119,11 @@ async function setStatus(status) {
await useBaseFetch(`project/${props.project.id}`, { await useBaseFetch(`project/${props.project.id}`, {
method: 'PATCH', method: 'PATCH',
body: data, body: data,
...app.$defaultHeaders(),
}) })
const project = props.project const project = props.project
project.status = status project.status = status
emit('update:project', project) emit('update:project', project)
thread.value = await useBaseFetch(`thread/${thread.value.id}`, app.$defaultHeaders()) thread.value = await useBaseFetch(`thread/${thread.value.id}`)
} catch (err) { } catch (err) {
app.$notify({ app.$notify({
group: 'main', group: 'main',

View File

@@ -191,7 +191,7 @@
id="project-visibility" id="project-visibility"
v-model="visibility" v-model="visibility"
placeholder="Select one" placeholder="Select one"
:options="$tag.approvedStatuses" :options="tags.approvedStatuses"
:custom-label="(value) => $formatProjectStatus(value)" :custom-label="(value) => $formatProjectStatus(value)"
:searchable="false" :searchable="false"
:close-on-select="true" :close-on-select="true"
@@ -315,6 +315,11 @@ export default defineNuxtComponent({
}, },
}, },
}, },
setup() {
const tags = useTags()
return { tags }
},
data() { data() {
return { return {
name: this.project.title, name: this.project.title,
@@ -325,7 +330,7 @@ export default defineNuxtComponent({
clientSide: this.project.client_side, clientSide: this.project.client_side,
serverSide: this.project.server_side, serverSide: this.project.server_side,
deletedIcon: false, deletedIcon: false,
visibility: this.$tag.approvedStatuses.includes(this.project.status) visibility: this.tags.approvedStatuses.includes(this.project.status)
? this.project.status ? this.project.status
: this.project.requested_status, : this.project.requested_status,
} }
@@ -360,7 +365,7 @@ export default defineNuxtComponent({
if (this.serverSide !== this.project.server_side) { if (this.serverSide !== this.project.server_side) {
data.server_side = this.serverSide 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) { if (this.visibility !== this.project.status) {
data.status = this.visibility data.status = this.visibility
} }
@@ -376,7 +381,7 @@ export default defineNuxtComponent({
}, },
methods: { methods: {
hasModifiedVisibility() { hasModifiedVisibility() {
const originalVisibility = this.$tag.approvedStatuses.includes(this.project.status) const originalVisibility = this.tags.approvedStatuses.includes(this.project.status)
? this.project.status ? this.project.status
: this.project.requested_status : this.project.requested_status
@@ -407,7 +412,6 @@ export default defineNuxtComponent({
async deleteProject() { async deleteProject() {
await useBaseFetch(`project/${this.project.id}`, { await useBaseFetch(`project/${this.project.id}`, {
method: 'DELETE', method: 'DELETE',
...this.$defaultHeaders(),
}) })
await initUserProjects() await initUserProjects()
await this.$router.push('/dashboard/review') await this.$router.push('/dashboard/review')
@@ -426,7 +430,6 @@ export default defineNuxtComponent({
async deleteIcon() { async deleteIcon() {
await useBaseFetch(`project/${this.project.id}/icon`, { await useBaseFetch(`project/${this.project.id}/icon`, {
method: 'DELETE', method: 'DELETE',
...this.$defaultHeaders(),
}) })
await this.updateIcon() await this.updateIcon()
this.$notify({ this.$notify({

View File

@@ -96,7 +96,7 @@
<Multiselect <Multiselect
v-model="donationLink.platform" v-model="donationLink.platform"
placeholder="Select platform" placeholder="Select platform"
:options="$tag.donationPlatforms.map((x) => x.name)" :options="tags.donationPlatforms.map((x) => x.name)"
:searchable="false" :searchable="false"
:close-on-select="true" :close-on-select="true"
:show-labels="false" :show-labels="false"
@@ -155,6 +155,11 @@ export default defineNuxtComponent({
}, },
}, },
}, },
setup() {
const tags = useTags()
return { tags }
},
data() { data() {
const donationLinks = JSON.parse(JSON.stringify(this.project.donation_urls)) const donationLinks = JSON.parse(JSON.stringify(this.project.donation_urls))
donationLinks.push({ donationLinks.push({
@@ -195,7 +200,7 @@ export default defineNuxtComponent({
const donationLinks = this.donationLinks.filter((link) => link.url && link.platform) const donationLinks = this.donationLinks.filter((link) => link.url && link.platform)
donationLinks.forEach((link) => { donationLinks.forEach((link) => {
link.id = this.$tag.donationPlatforms.find( link.id = this.tags.donationPlatforms.find(
(platform) => platform.name === link.platform (platform) => platform.name === link.platform
).short ).short
}) })

View File

@@ -323,7 +323,6 @@ export default defineNuxtComponent({
await useBaseFetch(`team/${this.project.team}/members`, { await useBaseFetch(`team/${this.project.team}/members`, {
method: 'POST', method: 'POST',
body: data, body: data,
...this.$defaultHeaders(),
}) })
this.currentUsername = '' this.currentUsername = ''
await this.updateMembers() await this.updateMembers()
@@ -346,7 +345,6 @@ export default defineNuxtComponent({
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`, `team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
{ {
method: 'DELETE', method: 'DELETE',
...this.$defaultHeaders(),
} }
) )
await this.updateMembers() await this.updateMembers()
@@ -381,7 +379,6 @@ export default defineNuxtComponent({
{ {
method: 'PATCH', method: 'PATCH',
body: data, body: data,
...this.$defaultHeaders(),
} }
) )
await this.updateMembers() await this.updateMembers()
@@ -411,7 +408,6 @@ export default defineNuxtComponent({
body: { body: {
user_id: this.allTeamMembers[index].user.id, user_id: this.allTeamMembers[index].user.id,
}, },
...this.$defaultHeaders(),
}) })
await this.updateMembers() await this.updateMembers()
} catch (err) { } catch (err) {
@@ -426,9 +422,7 @@ export default defineNuxtComponent({
stopLoading() stopLoading()
}, },
async updateMembers() { async updateMembers() {
this.allTeamMembers = ( this.allTeamMembers = (await useBaseFetch(`team/${this.project.team}/members`)).map((it) => ({
await useBaseFetch(`team/${this.project.team}/members`, this.$defaultHeaders())
).map((it) => ({
avatar_url: it.user.avatar_url, avatar_url: it.user.avatar_url,
name: it.user.username, name: it.user.username,
oldRole: it.role, oldRole: it.role,

View File

@@ -152,13 +152,13 @@ export default defineNuxtComponent({
}, },
data() { data() {
return { return {
selectedTags: this.$sortedCategories.filter( selectedTags: this.$sortedCategories().filter(
(x) => (x) =>
x.project_type === this.project.actualProjectType && x.project_type === this.project.actualProjectType &&
(this.project.categories.includes(x.name) || (this.project.categories.includes(x.name) ||
this.project.additional_categories.includes(x.name)) this.project.additional_categories.includes(x.name))
), ),
featuredTags: this.$sortedCategories.filter( featuredTags: this.$sortedCategories().filter(
(x) => (x) =>
x.project_type === this.project.actualProjectType && x.project_type === this.project.actualProjectType &&
this.project.categories.includes(x.name) this.project.categories.includes(x.name)
@@ -168,7 +168,7 @@ export default defineNuxtComponent({
computed: { computed: {
categoryLists() { categoryLists() {
const lists = {} const lists = {}
this.$sortedCategories.forEach((x) => { this.$sortedCategories().forEach((x) => {
if (x.project_type === this.project.actualProjectType) { if (x.project_type === this.project.actualProjectType) {
const header = x.header const header = x.header
if (!lists[header]) { if (!lists[header]) {

View File

@@ -8,7 +8,7 @@
<Meta name="og:description" :content="metaDescription" /> <Meta name="og:description" :content="metaDescription" />
</Head> </Head>
<ModalConfirm <ModalConfirm
v-if="$auth.user && currentMember" v-if="currentMember"
ref="modal_confirm" ref="modal_confirm"
title="Are you sure you want to delete this version?" title="Are you sure you want to delete this version?"
description="This will remove this version forever (like really forever)." description="This will remove this version forever (like really forever)."
@@ -17,12 +17,12 @@
@proceed="deleteVersion()" @proceed="deleteVersion()"
/> />
<ModalReport <ModalReport
v-if="$auth.user" v-if="auth.user"
ref="modal_version_report" ref="modal_version_report"
:item-id="version.id" :item-id="version.id"
item-type="version" 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="modal-package-mod universal-labels">
<div class="markdown-body"> <div class="markdown-body">
<p> <p>
@@ -116,7 +116,7 @@
Create Create
</button> </button>
<nuxt-link <nuxt-link
v-if="$auth.user" v-if="auth.user"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`" :to="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`"
class="iconified-button" class="iconified-button"
> >
@@ -164,7 +164,7 @@
<ReportIcon aria-hidden="true" /> <ReportIcon aria-hidden="true" />
Report Report
</button> </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" /> <ReportIcon aria-hidden="true" />
Report Report
</a> </a>
@@ -181,7 +181,7 @@
<button <button
v-if=" v-if="
currentMember && currentMember &&
version.loaders.some((x) => $tag.loaderData.dataPackLoaders.includes(x)) version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))
" "
class="iconified-button" class="iconified-button"
@click="$refs.modal_package_mod.show()" @click="$refs.modal_package_mod.show()"
@@ -390,7 +390,7 @@
</span> </span>
<multiselect <multiselect
v-if=" v-if="
version.loaders.some((x) => $tag.loaderData.dataPackLoaders.includes(x)) && version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x)) &&
isEditing && isEditing &&
primaryFile.hashes.sha1 !== file.hashes.sha1 primaryFile.hashes.sha1 !== file.hashes.sha1
" "
@@ -457,7 +457,7 @@
<span class="file-size">({{ $formatBytes(file.size) }})</span> <span class="file-size">({{ $formatBytes(file.size) }})</span>
</span> </span>
<multiselect <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]" v-model="newFileTypes[index]"
class="raised-multiselect" class="raised-multiselect"
placeholder="Select file type" placeholder="Select file type"
@@ -484,7 +484,7 @@
</div> </div>
<div class="additional-files"> <div class="additional-files">
<h4>Upload additional files</h4> <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 Used for additional files such as required/optional resource packs
</span> </span>
<span v-else>Used for files such as sources or Javadocs.</span> <span v-else>Used for files such as sources or Javadocs.</span>
@@ -566,14 +566,14 @@
v-if="isEditing" v-if="isEditing"
v-model="version.loaders" v-model="version.loaders"
:options=" :options="
$tag.loaders tags.loaders
.filter((x) => .filter((x) =>
x.supported_project_types.includes(project.actualProjectType.toLowerCase()) x.supported_project_types.includes(project.actualProjectType.toLowerCase())
) )
.map((it) => it.name) .map((it) => it.name)
" "
:custom-label="(value) => $formatCategory(value)" :custom-label="(value) => $formatCategory(value)"
:loading="$tag.loaders.length === 0" :loading="tags.loaders.length === 0"
:multiple="true" :multiple="true"
:searchable="false" :searchable="false"
:show-no-results="false" :show-no-results="false"
@@ -593,12 +593,12 @@
v-model="version.game_versions" v-model="version.game_versions"
:options=" :options="
showSnapshots showSnapshots
? $tag.gameVersions.map((x) => x.version) ? tags.gameVersions.map((x) => x.version)
: $tag.gameVersions : tags.gameVersions
.filter((it) => it.version_type === 'release') .filter((it) => it.version_type === 'release')
.map((x) => x.version) .map((x) => x.version)
" "
:loading="$tag.gameVersions.length === 0" :loading="tags.gameVersions.length === 0"
:multiple="true" :multiple="true"
:searchable="true" :searchable="true"
:show-no-results="false" :show-no-results="false"
@@ -772,6 +772,9 @@ export default defineNuxtComponent({
const data = useNuxtApp() const data = useNuxtApp()
const route = useRoute() const route = useRoute()
const auth = await useAuth()
const tags = useTags()
const path = route.name.split('-') const path = route.name.split('-')
const mode = path[path.length - 1] const mode = path[path.length - 1]
@@ -828,7 +831,7 @@ export default defineNuxtComponent({
const inferredData = await inferVersionInfo( const inferredData = await inferVersionInfo(
replaceFile, replaceFile,
props.project, props.project,
data.$tag.gameVersions tags.value.gameVersions
) )
version = { version = {
@@ -895,6 +898,8 @@ export default defineNuxtComponent({
const order = ['required', 'optional', 'incompatible', 'embedded'] const order = ['required', 'optional', 'incompatible', 'embedded']
return { return {
auth,
tags,
fileTypes: ref(fileTypes), fileTypes: ref(fileTypes),
oldFileTypes: ref(oldFileTypes), oldFileTypes: ref(oldFileTypes),
isCreating: ref(isCreating), isCreating: ref(isCreating),
@@ -1110,7 +1115,6 @@ export default defineNuxtComponent({
body: formData, body: formData,
headers: { headers: {
'Content-Disposition': formData, 'Content-Disposition': formData,
Authorization: this.$auth.token,
}, },
}) })
} }
@@ -1135,13 +1139,11 @@ export default defineNuxtComponent({
} }
}), }),
}, },
...this.$defaultHeaders(),
}) })
for (const hash of this.deleteFiles) { for (const hash of this.deleteFiles) {
await useBaseFetch(`version_file/${hash}?version_id=${this.version.id}`, { await useBaseFetch(`version_file/${hash}?version_id=${this.version.id}`, {
method: 'DELETE', method: 'DELETE',
...this.$defaultHeaders(),
}) })
} }
@@ -1246,7 +1248,6 @@ export default defineNuxtComponent({
body: formData, body: formData,
headers: { headers: {
'Content-Disposition': formData, 'Content-Disposition': formData,
Authorization: this.$auth.token,
}, },
}) })
@@ -1263,7 +1264,6 @@ export default defineNuxtComponent({
await useBaseFetch(`version/${this.version.id}`, { await useBaseFetch(`version/${this.version.id}`, {
method: 'DELETE', method: 'DELETE',
...this.$defaultHeaders(),
}) })
await this.resetProjectVersions() await this.resetProjectVersions()
@@ -1279,7 +1279,7 @@ export default defineNuxtComponent({
this.version, this.version,
this.primaryFile, this.primaryFile,
this.members, this.members,
this.$tag.gameVersions, this.tags.gameVersions,
this.packageLoaders this.packageLoaders
) )
@@ -1324,12 +1324,9 @@ export default defineNuxtComponent({
}, },
async resetProjectVersions() { async resetProjectVersions() {
const [versions, featuredVersions, dependencies] = await Promise.all([ const [versions, featuredVersions, dependencies] = await Promise.all([
useBaseFetch(`project/${this.version.project_id}/version`, this.$defaultHeaders()), useBaseFetch(`project/${this.version.project_id}/version`),
useBaseFetch( useBaseFetch(`project/${this.version.project_id}/version?featured=true`),
`project/${this.version.project_id}/version?featured=true`, useBaseFetch(`project/${this.version.project_id}/dependencies`),
this.$defaultHeaders()
),
useBaseFetch(`project/${this.version.project_id}/dependencies`, this.$defaultHeaders()),
]) ])
const newCreatedVersions = this.$computeVersions(versions, this.members) const newCreatedVersions = this.$computeVersions(versions, this.members)

156
pages/auth.vue Normal file
View 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>

View 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
View 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
View 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>

View 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
View 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>

View File

@@ -1,12 +1,12 @@
<template> <template>
<div class="dashboard-overview"> <div class="dashboard-overview">
<section class="universal-card dashboard-header"> <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"> <div class="username">
<h1> <h1>
{{ $auth.user.username }} {{ auth.user.username }}
</h1> </h1>
<NuxtLink class="goto-link" :to="`/user/${$auth.user.username}`"> <NuxtLink class="goto-link" :to="`/user/${auth.user.username}`">
Visit your profile Visit your profile
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" /> <ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
</NuxtLink> </NuxtLink>
@@ -26,6 +26,7 @@
v-model:notifications="allNotifs" v-model:notifications="allNotifs"
class="universal-card recessed" class="universal-card recessed"
:notification="notification" :notification="notification"
:auth="auth"
raised raised
compact compact
/> />
@@ -116,14 +117,13 @@ useHead({
}) })
const auth = await useAuth() const auth = await useAuth()
const app = useNuxtApp()
const [{ data: projects }, { data: payouts }] = await Promise.all([ const [{ data: projects }, { data: payouts }] = await Promise.all([
useAsyncData(`user/${auth.value.user.id}/projects`, () => 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`, () => useAsyncData(`user/${auth.value.user.id}/payouts`, () =>
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders()) useBaseFetch(`user/${auth.value.user.id}/payouts`)
), ),
]) ])

View File

@@ -36,6 +36,7 @@
v-model:notifications="allNotifs" v-model:notifications="allNotifs"
class="universal-card recessed" class="universal-card recessed"
:notification="notification" :notification="notification"
:auth="auth"
raised raised
/> />
</template> </template>
@@ -55,6 +56,8 @@ useHead({
title: 'Notifications - Modrinth', title: 'Notifications - Modrinth',
}) })
const auth = await useAuth()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -459,7 +459,6 @@ export default defineNuxtComponent({
{ {
method: 'PATCH', method: 'PATCH',
body: baseData, body: baseData,
...this.$defaultHeaders(),
} }
) )

View File

@@ -1,5 +1,6 @@
<template> <template>
<ReportView <ReportView
:auth="auth"
:report-id="route.params.id" :report-id="route.params.id"
:breadcrumbs-stack="[{ href: '/dashboard/reports', label: 'Active reports' }]" :breadcrumbs-stack="[{ href: '/dashboard/reports', label: 'Active reports' }]"
/> />
@@ -8,6 +9,7 @@
import ReportView from '~/components/ui/report/ReportView.vue' import ReportView from '~/components/ui/report/ReportView.vue'
const route = useRoute() const route = useRoute()
const auth = await useAuth()
useHead({ useHead({
title: `Report ${route.params.id} - Modrinth`, title: `Report ${route.params.id} - Modrinth`,

View File

@@ -2,13 +2,14 @@
<div> <div>
<section class="universal-card"> <section class="universal-card">
<h2>Reports you've filed</h2> <h2>Reports you've filed</h2>
<ReportsList /> <ReportsList :auth="auth" />
</section> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
import ReportsList from '~/components/ui/report/ReportsList.vue' import ReportsList from '~/components/ui/report/ReportsList.vue'
const auth = await useAuth()
useHead({ useHead({
title: 'Active reports - Modrinth', title: 'Active reports - Modrinth',
}) })

View File

@@ -39,10 +39,9 @@ useHead({
}) })
const auth = await useAuth() const auth = await useAuth()
const app = useNuxtApp()
const { data: payouts } = await useAsyncData(`user/${auth.value.user.id}/payouts`, () => 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -7,7 +7,7 @@
<div class="animate-strong"> <div class="animate-strong">
<span> <span>
<strong <strong
v-for="projectType in $tag.projectTypes" v-for="projectType in tags.projectTypes"
:key="projectType.id" :key="projectType.id"
class="main-header-strong" class="main-header-strong"
> >
@@ -23,14 +23,14 @@
</h2> </h2>
<div class="button-group"> <div class="button-group">
<nuxt-link to="/mods" class="iconified-button brand-button"> Discover mods </nuxt-link> <nuxt-link to="/mods" class="iconified-button brand-button"> Discover mods </nuxt-link>
<a <nuxt-link
v-if="!$auth.user" v-if="!auth.user"
:href="getAuthUrl()" to="sign-up"
class="iconified-button outline-button" class="iconified-button outline-button"
rel="noopener nofollow" rel="noopener nofollow"
> >
Sign up Sign up
</a> </nuxt-link>
<nuxt-link v-else to="/dashboard/projects" class="iconified-button outline-button"> <nuxt-link v-else to="/dashboard/projects" class="iconified-button outline-button">
Go to dashboard Go to dashboard
</nuxt-link> </nuxt-link>
@@ -530,6 +530,9 @@ import homepageProjects from '~/generated/homepage.json'
const searchQuery = ref('better') const searchQuery = ref('better')
const sortType = ref('relevance') const sortType = ref('relevance')
const auth = await useAuth()
const tags = useTags()
const [{ data: searchProjects, refresh: updateSearchProjects }, { data: notifications }] = const [{ data: searchProjects, refresh: updateSearchProjects }, { data: notifications }] =
await Promise.all([ await Promise.all([
useAsyncData( useAsyncData(
@@ -1277,9 +1280,8 @@ const rows = shallowRef([
font-size: 1.625rem; font-size: 1.625rem;
} }
padding: 12rem 1rem; margin-top: -4rem;
// Magic number to cover header (space in rem header occupies) padding: 11.25rem 1rem 12rem;
margin-top: -5.75rem;
} }
.users-section-outer { .users-section-outer {

View File

@@ -38,9 +38,5 @@ useHead({
title: 'Staff overview - Modrinth', title: 'Staff overview - Modrinth',
}) })
const app = useNuxtApp() const { data: stats } = await useAsyncData('statistics', () => useBaseFetch('statistics'))
const { data: stats } = await useAsyncData('statistics', () =>
useBaseFetch('statistics', app.$defaultHeaders())
)
</script> </script>

View File

@@ -8,6 +8,7 @@
:key="thread.id" :key="thread.id"
:thread="thread" :thread="thread"
:link="getLink(thread)" :link="getLink(thread)"
:auth="auth"
/> />
</section> </section>
</div> </div>
@@ -19,11 +20,8 @@ useHead({
title: 'Moderation inbox - Modrinth', title: 'Moderation inbox - Modrinth',
}) })
const app = useNuxtApp() const auth = await useAuth()
const { data: inbox } = await useAsyncData('thread/inbox', () => useBaseFetch('thread/inbox'))
const { data: inbox } = await useAsyncData('thread/inbox', () =>
useBaseFetch('thread/inbox', app.$defaultHeaders())
)
function getLink(thread) { function getLink(thread) {
if (thread.report_id) { if (thread.report_id) {

View File

@@ -1,5 +1,6 @@
<template> <template>
<ReportView <ReportView
:auth="auth"
:report-id="route.params.id" :report-id="route.params.id"
:breadcrumbs-stack="[{ href: '/moderation/reports', label: 'Reports' }]" :breadcrumbs-stack="[{ href: '/moderation/reports', label: 'Reports' }]"
/> />
@@ -7,6 +8,7 @@
<script setup> <script setup>
import ReportView from '~/components/ui/report/ReportView.vue' import ReportView from '~/components/ui/report/ReportView.vue'
const auth = await useAuth()
const route = useRoute() const route = useRoute()
useHead({ useHead({

View File

@@ -2,13 +2,14 @@
<div> <div>
<section class="universal-card"> <section class="universal-card">
<h2>Reports</h2> <h2>Reports</h2>
<ReportsList moderation /> <ReportsList :auth="auth" moderation />
</section> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
import ReportsList from '~/components/ui/report/ReportsList.vue' import ReportsList from '~/components/ui/report/ReportsList.vue'
const auth = await useAuth()
useHead({ useHead({
title: 'Reports - Modrinth', title: 'Reports - Modrinth',
}) })

View File

@@ -103,7 +103,7 @@ const TIME_24H = 86400000
const TIME_48H = TIME_24H * 2 const TIME_48H = TIME_24H * 2
const { data: projects } = await useAsyncData('moderation/projects?count=1000', () => 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 members = ref([])
const projectType = ref('all') const projectType = ref('all')
@@ -145,37 +145,30 @@ const projectTypes = computed(() => {
if (projects.value) { if (projects.value) {
const teamIds = projects.value.map((x) => x.team) const teamIds = projects.value.map((x) => x.team)
await useAsyncData( const url = `teams?ids=${encodeURIComponent(JSON.stringify(teamIds))}`
'teams?ids=' + JSON.stringify(teamIds), const { data: result } = await useAsyncData(url, () => useBaseFetch(url))
() => useBaseFetch('teams?ids=' + JSON.stringify(teamIds), app.$defaultHeaders()),
{
transform: (result) => {
if (result) {
members.value = result
projects.value = projects.value.map((project) => { if (result.value) {
project.owner = members.value members.value = result.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
})
}
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -3,7 +3,7 @@
:class="{ :class="{
'search-page': true, 'search-page': true,
'normal-page': true, 'normal-page': true,
'alt-layout': $cosmetics.searchLayout, 'alt-layout': cosmetics.searchLayout,
}" }"
> >
<Head> <Head>
@@ -65,7 +65,7 @@
> >
<h3 <h3
v-if=" 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 .length > 0
" "
class="sidebar-menu-heading" class="sidebar-menu-heading"
@@ -73,7 +73,7 @@
Loaders Loaders
</h3> </h3>
<SearchFilter <SearchFilter
v-for="loader in $tag.loaders.filter((x) => { v-for="loader in tags.loaders.filter((x) => {
if ( if (
projectType.id === 'mod' && projectType.id === 'mod' &&
!showAllLoaders && !showAllLoaders &&
@@ -83,11 +83,11 @@
) { ) {
return false return false
} else if (projectType.id === 'mod' && showAllLoaders) { } 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') { } else if (projectType.id === 'plugin') {
return $tag.loaderData.pluginLoaders.includes(x.name) return tags.loaderData.pluginLoaders.includes(x.name)
} else if (projectType.id === 'datapack') { } else if (projectType.id === 'datapack') {
return $tag.loaderData.dataPackLoaders.includes(x.name) return tags.loaderData.dataPackLoaders.includes(x.name)
} else { } else {
return x.supported_project_types.includes(projectType.actual) return x.supported_project_types.includes(projectType.actual)
} }
@@ -113,7 +113,7 @@
<section v-if="projectType.id === 'plugin'" aria-label="Platform loader filters"> <section v-if="projectType.id === 'plugin'" aria-label="Platform loader filters">
<h3 <h3
v-if=" 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 .length > 0
" "
class="sidebar-menu-heading" class="sidebar-menu-heading"
@@ -121,8 +121,8 @@
Proxies Proxies
</h3> </h3>
<SearchFilter <SearchFilter
v-for="loader in $tag.loaders.filter((x) => v-for="loader in tags.loaders.filter((x) =>
$tag.loaderData.pluginPlatformLoaders.includes(x.name) tags.loaderData.pluginPlatformLoaders.includes(x.name)
)" )"
:key="loader.name" :key="loader.name"
ref="platformFilters" ref="platformFilters"
@@ -167,8 +167,8 @@
v-model="selectedVersions" v-model="selectedVersions"
:options=" :options="
showSnapshots showSnapshots
? $tag.gameVersions.map((x) => x.version) ? tags.gameVersions.map((x) => x.version)
: $tag.gameVersions : tags.gameVersions
.filter((it) => it.version_type === 'release') .filter((it) => it.version_type === 'release')
.map((x) => x.version) .map((x) => x.version)
" "
@@ -195,7 +195,7 @@
</aside> </aside>
<section class="normal-page__content"> <section class="normal-page__content">
<div <div
v-if="projectType.id === 'modpack' && $orElse($cosmetics.modpacksAlphaNotice, true)" v-if="projectType.id === 'modpack' && $orElse(cosmetics.modpacksAlphaNotice, true)"
class="card information" class="card information"
aria-label="Information" aria-label="Information"
> >
@@ -267,7 +267,7 @@
v-model="maxResults" v-model="maxResults"
placeholder="Select one" placeholder="Select one"
class="labeled-control__control" class="labeled-control__control"
:options="maxResultsForView[$cosmetics.searchDisplayMode[projectType.id]]" :options="maxResultsForView[cosmetics.searchDisplayMode[projectType.id]]"
:searchable="false" :searchable="false"
:close-on-select="true" :close-on-select="true"
:show-labels="false" :show-labels="false"
@@ -276,13 +276,13 @@
/> />
</div> </div>
<button <button
v-tooltip="$capitalizeString($cosmetics.searchDisplayMode[projectType.id]) + ' view'" v-tooltip="$capitalizeString(cosmetics.searchDisplayMode[projectType.id]) + ' view'"
:aria-label="$capitalizeString($cosmetics.searchDisplayMode[projectType.id]) + ' view'" :aria-label="$capitalizeString(cosmetics.searchDisplayMode[projectType.id]) + ' view'"
class="square-button" class="square-button"
@click="cycleSearchDisplayMode()" @click="cycleSearchDisplayMode()"
> >
<GridIcon v-if="$cosmetics.searchDisplayMode[projectType.id] === 'grid'" /> <GridIcon v-if="cosmetics.searchDisplayMode[projectType.id] === 'grid'" />
<ImageIcon v-else-if="$cosmetics.searchDisplayMode[projectType.id] === 'gallery'" /> <ImageIcon v-else-if="cosmetics.searchDisplayMode[projectType.id] === 'gallery'" />
<ListIcon v-else /> <ListIcon v-else />
</button> </button>
</div> </div>
@@ -302,7 +302,7 @@
<div <div
id="search-results" id="search-results"
class="project-list" class="project-list"
:class="'display-mode--' + $cosmetics.searchDisplayMode[projectType.id]" :class="'display-mode--' + cosmetics.searchDisplayMode[projectType.id]"
role="list" role="list"
aria-label="Search results" aria-label="Search results"
> >
@@ -310,7 +310,7 @@
v-for="result in results?.hits" v-for="result in results?.hits"
:id="result.slug ? result.slug : result.project_id" :id="result.slug ? result.slug : result.project_id"
:key="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]" :featured-image="result.featured_gallery ? result.featured_gallery : result.gallery[0]"
:type="result.project_type" :type="result.project_type"
:author="result.author" :author="result.author"
@@ -367,6 +367,9 @@ const showAllLoaders = ref(false)
const data = useNuxtApp() const data = useNuxtApp()
const route = useRoute() const route = useRoute()
const cosmetics = useCosmetics()
const tags = useTags()
const query = ref('') const query = ref('')
const facets = ref([]) const facets = ref([])
const orFacets = ref([]) const orFacets = ref([])
@@ -444,7 +447,7 @@ if (route.query.o) {
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1 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) (x) => x.id === route.path.substring(1, route.path.length - 1)
) )
@@ -481,15 +484,15 @@ const {
formattedFacets.push(orFacets.value) formattedFacets.push(orFacets.value)
} else if (projectType.value.id === 'plugin') { } else if (projectType.value.id === 'plugin') {
formattedFacets.push( 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') { } else if (projectType.value.id === 'mod') {
formattedFacets.push( 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') { } else if (projectType.value.id === 'datapack') {
formattedFacets.push( 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 categoriesMap = computed(() => {
const categories = {} const categories = {}
for (const category of data.$sortedCategories) { for (const category of data.$sortedCategories()) {
if (categories[category.header]) { if (categories[category.header]) {
categories[category.header].push(category) categories[category.header].push(category)
} else { } else {
@@ -747,9 +750,9 @@ function onSearchChangeToTop(newPageNumber) {
} }
function cycleSearchDisplayMode() { function cycleSearchDisplayMode() {
data.$cosmetics.searchDisplayMode[projectType.value.id] = data.$cycleValue( cosmetics.value.searchDisplayMode[projectType.value.id] = data.$cycleValue(
data.$cosmetics.searchDisplayMode[projectType.value.id], cosmetics.value.searchDisplayMode[projectType.value.id],
data.$tag.projectViewModes tags.value.projectViewModes
) )
saveCosmetics() saveCosmetics()
setClosestMaxResults() setClosestMaxResults()
@@ -775,7 +778,7 @@ function onMaxResultsChange(newPageNumber) {
} }
function setClosestMaxResults() { 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 maxResultsOptions = maxResultsForView.value[view] ?? [20]
const currentMax = maxResults.value const currentMax = maxResults.value
if (!maxResultsOptions.includes(currentMax)) { if (!maxResultsOptions.includes(currentMax)) {

View File

@@ -7,11 +7,17 @@
<NavStackItem link="/settings" label="Appearance"> <NavStackItem link="/settings" label="Appearance">
<PaintbrushIcon /> <PaintbrushIcon />
</NavStackItem> </NavStackItem>
<template v-if="$auth.user"> <template v-if="auth.user">
<h3>User settings</h3> <h3>User settings</h3>
<NavStackItem link="/settings/account" label="Account"> <NavStackItem link="/settings/account" label="Account">
<UserIcon /> <UserIcon />
</NavStackItem> </NavStackItem>
<NavStackItem link="/settings/pats" label="PATs">
<KeyIcon />
</NavStackItem>
<NavStackItem link="/settings/sessions" label="Sessions">
<ShieldIcon />
</NavStackItem>
<NavStackItem link="/settings/monetization" label="Monetization"> <NavStackItem link="/settings/monetization" label="Monetization">
<CurrencyIcon /> <CurrencyIcon />
</NavStackItem> </NavStackItem>
@@ -31,8 +37,11 @@ import NavStackItem from '~/components/ui/NavStackItem.vue'
import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg' import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg'
import UserIcon from '~/assets/images/utils/user.svg' import UserIcon from '~/assets/images/utils/user.svg'
import CurrencyIcon from '~/assets/images/utils/currency.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 route = useRoute()
const auth = await useAuth()
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -9,54 +9,259 @@
:has-to-type="true" :has-to-type="true"
@proceed="deleteAccount" @proceed="deleteAccount"
/> />
<Modal ref="changeEmailModal" :header="`${auth.user.email ? 'Change' : 'Add'} email`">
<Modal ref="modal_revoke_token" header="Revoke your Modrinth token"> <div class="universal-modal">
<div class="modal-revoke-token markdown-body"> <p>Your account information is not displayed publicly.</p>
<p> <label for="email-input"><span class="label__title">Email address</span> </label>
Revoking your Modrinth token can have unintended consequences. Please be aware that the <input
following could break: id="email-input"
</p> v-model="email"
<ul> maxlength="2048"
<li>Any application that uses your token to access the API.</li> type="email"
<li>Gradle - if Minotaur is given a incorrect token, your Gradle builds could fail.</li> :placeholder="`Enter your email address...`"
<li> />
GitHub - if you use a GitHub action that uses the Modrinth API, it will cause errors. <div class="input-group push-right">
</li> <button class="iconified-button" @click="$refs.changeEmailModal.hide()">
</ul> <XIcon />
<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 />
Cancel Cancel
</button> </button>
<button class="iconified-button brand-button" @click="logout"> <button
<RightArrowIcon /> type="button"
Log out 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> </button>
</div> </div>
</div> </div>
</Modal> </Modal>
<section class="universal-card"> <section class="universal-card">
<h2>User profile</h2> <h2>User profile</h2>
<p>Visit your user profile to edit your profile information.</p> <p>Visit your user profile to edit your profile information.</p>
@@ -66,53 +271,83 @@
</section> </section>
<section class="universal-card"> <section class="universal-card">
<h2>Account information</h2> <h2>Account security</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>
<section class="universal-card"> <div class="adjacent-input">
<h2>Authorization token</h2> <label for="theme-selector">
<p> <span class="label__title">Email</span>
Your authorization token can be used with the Modrinth API, the Minotaur Gradle plugin, and <span class="label__description">Changes the email associated with your account.</span>
other applications that interact with Modrinth's API. Be sure to keep this secret! </label>
</p> <div>
<div class="input-group"> <button class="iconified-button" @click="$refs.changeEmailModal.show()">
<button type="button" class="iconified-button" value="Copy to clipboard" @click="copyToken"> <template v-if="auth.user.email">
<template v-if="copied"> <EditIcon />
<CheckIcon /> Change email
Copied token to clipboard </template>
</template> <template v-else>
<template v-else> <CopyIcon />Copy token to clipboard </template> <PlusIcon />
</button> Add email
<button type="button" class="iconified-button" @click="$refs.modal_revoke_token.show()"> </template>
<SlashIcon /> </button>
Revoke token </div>
</button> </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> </div>
</section> </section>
@@ -134,128 +369,273 @@
</div> </div>
</template> </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 ModalConfirm from '~/components/ui/ModalConfirm.vue'
import Modal from '~/components/ui/Modal.vue' import Modal from '~/components/ui/Modal.vue'
import CrossIcon from '~/assets/images/utils/x.svg' useHead({
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg' title: 'Account settings - Modrinth',
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'
export default defineNuxtComponent({ definePageMeta({
components: { middleware: 'auth',
Modal, })
ModalConfirm,
CrossIcon, const data = useNuxtApp()
RightArrowIcon, const auth = await useAuth()
CheckIcon,
SaveIcon, const changeEmailModal = ref()
UserIcon, const email = ref(auth.value.user.email)
CopyIcon, async function saveEmail() {
TrashIcon, if (!email.value) {
SlashIcon, return
}, }
async setup() {
definePageMeta({ startLoading()
middleware: 'auth', 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 { id: 'gitlab',
copied: false, display: 'GitLab',
email: this.auth.user.email, icon: GitLabIcon,
showKnownErrors: false,
}
}, },
head: { {
title: 'Account settings - Modrinth', id: 'steam',
display: 'Steam',
icon: SteamIcon,
}, },
methods: { {
async copyToken() { id: 'discord',
this.copied = true display: 'Discord',
await navigator.clipboard.writeText(this.auth.token) icon: DiscordIcon,
},
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: '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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.modal-revoke-token { canvas {
padding: var(--spacing-card-bg); margin: 0 auto;
border-radius: var(--size-rounded-card);
}
.button-group { .table-row {
width: fit-content; grid-template-columns: 1fr 10rem;
margin-left: auto;
span {
display: flex;
align-items: center;
margin: auto 0;
svg {
width: 1.25rem;
height: 1.25rem;
margin-right: 0.35rem;
}
} }
} }
</style> </style>

View File

@@ -34,7 +34,7 @@
</label> </label>
<input <input
id="search-layout-toggle" id="search-layout-toggle"
v-model="$cosmetics.searchLayout" v-model="cosmetics.searchLayout"
class="switch stylized-toggle" class="switch stylized-toggle"
type="checkbox" type="checkbox"
@change="saveCosmetics" @change="saveCosmetics"
@@ -49,7 +49,7 @@
</label> </label>
<input <input
id="project-layout-toggle" id="project-layout-toggle"
v-model="$cosmetics.projectLayout" v-model="cosmetics.projectLayout"
class="switch stylized-toggle" class="switch stylized-toggle"
type="checkbox" type="checkbox"
@change="saveCosmetics" @change="saveCosmetics"
@@ -71,8 +71,8 @@
</label> </label>
<Multiselect <Multiselect
:id="projectType + '-search-display-mode'" :id="projectType + '-search-display-mode'"
v-model="$cosmetics.searchDisplayMode[projectType.id]" v-model="cosmetics.searchDisplayMode[projectType.id]"
:options="$tag.projectViewModes" :options="tags.projectViewModes"
:custom-label="$capitalizeString" :custom-label="$capitalizeString"
:searchable="false" :searchable="false"
:close-on-select="true" :close-on-select="true"
@@ -94,7 +94,7 @@
</label> </label>
<input <input
id="advanced-rendering" id="advanced-rendering"
v-model="$cosmetics.advancedRendering" v-model="cosmetics.advancedRendering"
class="switch stylized-toggle" class="switch stylized-toggle"
type="checkbox" type="checkbox"
@change="saveCosmetics" @change="saveCosmetics"
@@ -107,7 +107,7 @@
</label> </label>
<input <input
id="modpacks-alpha-notice" id="modpacks-alpha-notice"
v-model="$cosmetics.modpacksAlphaNotice" v-model="cosmetics.modpacksAlphaNotice"
class="switch stylized-toggle" class="switch stylized-toggle"
type="checkbox" type="checkbox"
@change="saveCosmetics" @change="saveCosmetics"
@@ -124,7 +124,7 @@
</label> </label>
<input <input
id="external-links-new-tab" id="external-links-new-tab"
v-model="$cosmetics.externalLinksNewTab" v-model="cosmetics.externalLinksNewTab"
class="switch stylized-toggle" class="switch stylized-toggle"
type="checkbox" type="checkbox"
@change="saveCosmetics" @change="saveCosmetics"
@@ -141,9 +141,15 @@ export default defineNuxtComponent({
components: { components: {
Multiselect, Multiselect,
}, },
setup() {
const cosmetics = useCosmetics()
const tags = useTags()
return { cosmetics, tags }
},
data() { data() {
return { return {
searchDisplayMode: this.$cosmetics.searchDisplayMode, searchDisplayMode: this.cosmetics.searchDisplayMode,
} }
}, },
head: { head: {
@@ -151,7 +157,7 @@ export default defineNuxtComponent({
}, },
computed: { computed: {
listTypes() { listTypes() {
const types = this.$tag.projectTypes.map((type) => { const types = this.tags.projectTypes.map((type) => {
return { return {
id: type.id, id: type.id,
name: this.$formatProjectType(type.id) + ' search', name: this.$formatProjectType(type.id) + ' search',

View File

@@ -184,7 +184,6 @@ export default defineNuxtComponent({
await useBaseFetch(`user/${this.auth.user.id}`, { await useBaseFetch(`user/${this.auth.user.id}`, {
method: 'PATCH', method: 'PATCH',
body: data, body: data,
...this.$defaultHeaders(),
}) })
await useAuth(this.auth.token) await useAuth(this.auth.token)

296
pages/settings/pats.vue Normal file
View 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>

View 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>

View File

@@ -46,7 +46,7 @@
<UploadIcon /> <UploadIcon />
</FileInput> </FileInput>
<button <button
v-else-if="$auth.user && $auth.user.id === user.id" v-else-if="auth.user && auth.user.id === user.id"
class="iconified-button" class="iconified-button"
@click="isEditing = true" @click="isEditing = true"
> >
@@ -54,7 +54,7 @@
Edit Edit
</button> </button>
<button <button
v-else-if="$auth.user" v-else-if="auth.user"
class="iconified-button" class="iconified-button"
@click="$refs.modal_report.show()" @click="$refs.modal_report.show()"
> >
@@ -81,7 +81,7 @@
@click=" @click="
() => { () => {
isEditing = false isEditing = false
user = JSON.parse(JSON.stringify($auth.user)) user = JSON.parse(JSON.stringify(auth.user))
previewImage = null previewImage = null
icon = null icon = null
} }
@@ -96,7 +96,7 @@
</template> </template>
<template v-else> <template v-else>
<div class="sidebar__item"> <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" /> <Badge v-else-if="projects.length > 0" type="creator" />
</div> </div>
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span> <span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
@@ -129,16 +129,6 @@
<UserIcon class="secondary-stat__icon" aria-hidden="true" /> <UserIcon class="secondary-stat__icon" aria-hidden="true" />
<span class="secondary-stat__text"> User ID: <CopyCode :text="user.id" /> </span> <span class="secondary-stat__text"> User ID: <CopyCode :text="user.id" /> </span>
</div> </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> </template>
</div> </div>
</div> </div>
@@ -161,7 +151,7 @@
/> />
<div class="input-group"> <div class="input-group">
<NuxtLink <NuxtLink
v-if="$auth.user && $auth.user.id === user.id" v-if="auth.user && auth.user.id === user.id"
class="iconified-button" class="iconified-button"
to="/dashboard/projects" to="/dashboard/projects"
> >
@@ -169,13 +159,13 @@
Manage projects Manage projects
</NuxtLink> </NuxtLink>
<button <button
v-tooltip="$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'" v-tooltip="$capitalizeString(cosmetics.searchDisplayMode.user) + ' view'"
:aria-label="$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'" :aria-label="$capitalizeString(cosmetics.searchDisplayMode.user) + ' view'"
class="square-button" class="square-button"
@click="cycleSearchDisplayMode()" @click="cycleSearchDisplayMode()"
> >
<GridIcon v-if="$cosmetics.searchDisplayMode.user === 'grid'" /> <GridIcon v-if="cosmetics.searchDisplayMode.user === 'grid'" />
<ImageIcon v-else-if="$cosmetics.searchDisplayMode.user === 'gallery'" /> <ImageIcon v-else-if="cosmetics.searchDisplayMode.user === 'gallery'" />
<ListIcon v-else /> <ListIcon v-else />
</button> </button>
</div> </div>
@@ -183,7 +173,7 @@
<div <div
v-if="projects.length > 0" v-if="projects.length > 0"
class="project-list" class="project-list"
:class="'display-mode--' + $cosmetics.searchDisplayMode.user" :class="'display-mode--' + cosmetics.searchDisplayMode.user"
> >
<ProjectCard <ProjectCard
v-for="project in (route.params.projectType !== undefined v-for="project in (route.params.projectType !== undefined
@@ -199,7 +189,7 @@
:id="project.slug || project.id" :id="project.slug || project.id"
:key="project.id" :key="project.id"
:name="project.title" :name="project.title"
:display="$cosmetics.searchDisplayMode.user" :display="cosmetics.searchDisplayMode.user"
:featured-image=" :featured-image="
project.gallery project.gallery
.slice() .slice()
@@ -216,7 +206,7 @@
:client-side="project.client_side" :client-side="project.client_side"
:server-side="project.server_side" :server-side="project.server_side"
:status=" :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 ? project.status
: null : null
" "
@@ -226,7 +216,7 @@
</div> </div>
<div v-else class="error"> <div v-else class="error">
<UpToDate class="icon" /><br /> <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 /> You don't have any projects.<br />
Would you like to Would you like to
<a class="link" @click.prevent="$refs.modal_creation.show()"> create one</a>? <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 Badge from '~/components/ui/Badge.vue'
import Promotion from '~/components/ads/Promotion.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 ReportIcon from '~/assets/images/utils/report.svg'
import SunriseIcon from '~/assets/images/utils/sunrise.svg' import SunriseIcon from '~/assets/images/utils/sunrise.svg'
import DownloadIcon from '~/assets/images/utils/download.svg' import DownloadIcon from '~/assets/images/utils/download.svg'
@@ -266,23 +255,25 @@ import Avatar from '~/components/ui/Avatar.vue'
const data = useNuxtApp() const data = useNuxtApp()
const route = useRoute() const route = useRoute()
const auth = await useAuth()
const cosmetics = useCosmetics()
const tags = useTags()
let user, projects let user, projects
try { try {
;[{ data: user }, { data: projects }] = await Promise.all([ ;[{ data: user }, { data: projects }] = await Promise.all([
useAsyncData(`user/${route.params.id}`, () => useAsyncData(`user/${route.params.id}`, () => useBaseFetch(`user/${route.params.id}`)),
useBaseFetch(`user/${route.params.id}`, data.$defaultHeaders())
),
useAsyncData( useAsyncData(
`user/${route.params.id}/projects`, `user/${route.params.id}/projects`,
() => useBaseFetch(`user/${route.params.id}/projects`, data.$defaultHeaders()), () => useBaseFetch(`user/${route.params.id}/projects`),
{ {
transform: (projects) => { transform: (projects) => {
for (const project of projects) { for (const project of projects) {
project.categories = project.categories.concat(project.loaders) project.categories = project.categories.concat(project.loaders)
project.project_type = data.$getProjectTypeForUrl( project.project_type = data.$getProjectTypeForUrl(
project.project_type, 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) { if (user.value.username !== route.params.id) {
await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 }) await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 })
} }
@@ -369,13 +354,12 @@ async function saveChanges() {
try { try {
if (icon.value) { if (icon.value) {
await useBaseFetch( 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] icon.value.type.split('/')[icon.value.type.split('/').length - 1]
}`, }`,
{ {
method: 'PATCH', method: 'PATCH',
body: icon.value, body: icon.value,
...data.$defaultHeaders(),
} }
) )
} }
@@ -384,16 +368,15 @@ async function saveChanges() {
email: user.value.email, email: user.value.email,
bio: user.value.bio, 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 reqData.username = user.value.username
} }
await useBaseFetch(`user/${data.$auth.user.id}`, { await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'PATCH', method: 'PATCH',
body: reqData, body: reqData,
...data.$defaultHeaders(),
}) })
await useAuth(data.$auth.token) await useAuth(auth.value.token)
isEditing.value = false isEditing.value = false
} catch (err) { } catch (err) {
@@ -409,9 +392,9 @@ async function saveChanges() {
} }
function cycleSearchDisplayMode() { function cycleSearchDisplayMode() {
data.$cosmetics.searchDisplayMode.user = data.$cycleValue( cosmetics.value.searchDisplayMode.user = data.$cycleValue(
data.$cosmetics.searchDisplayMode.user, cosmetics.value.searchDisplayMode.user,
data.$tag.projectViewModes tags.value.projectViewModes
) )
saveCosmetics() saveCosmetics()
} }
@@ -504,10 +487,6 @@ export default defineNuxtComponent({
cursor: default; cursor: default;
} }
.github-button {
display: inline-flex;
}
.inputs { .inputs {
margin-bottom: 1rem; margin-bottom: 1rem;

View File

@@ -1,4 +1,6 @@
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin(async (nuxtApp) => {
await useAuth()
await useUser()
const themeStore = useTheme() const themeStore = useTheme()
nuxtApp.hook('app:mounted', () => { nuxtApp.hook('app:mounted', () => {

View File

@@ -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))
})

View File

@@ -1,29 +1,10 @@
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js' import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
export default defineNuxtPlugin((nuxtApp) => { 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('formatNumber', formatNumber)
nuxtApp.provide('capitalizeString', capitalizeString) nuxtApp.provide('capitalizeString', capitalizeString)
nuxtApp.provide('formatMoney', formatMoney) 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('orElse', (first, otherwise) => first ?? otherwise)
nuxtApp.provide('external', () => { nuxtApp.provide('external', () => {
const cosmeticsStore = useCosmetics().value 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)) .sort((a, b) => nuxtApp.$dayjs(b.date_published) - nuxtApp.$dayjs(a.date_published))
}) })
nuxtApp.provide('getProjectTypeForDisplay', (type, categories) => { nuxtApp.provide('getProjectTypeForDisplay', (type, categories) => {
const tagStore = useTags()
if (type === 'mod') { if (type === 'mod') {
const isPlugin = categories.some((category) => { const isPlugin = categories.some((category) => {
return tagStore.loaderData.allPluginLoaders.includes(category) return tagStore.value.loaderData.allPluginLoaders.includes(category)
}) })
const isMod = categories.some((category) => { const isMod = categories.some((category) => {
return tagStore.loaderData.modLoaders.includes(category) return tagStore.value.loaderData.modLoaders.includes(category)
}) })
const isDataPack = categories.some((category) => { const isDataPack = categories.some((category) => {
return tagStore.loaderData.dataPackLoaders.includes(category) return tagStore.value.loaderData.dataPackLoaders.includes(category)
}) })
if (isMod && isPlugin && isDataPack) { if (isMod && isPlugin && isDataPack) {
@@ -123,25 +106,29 @@ export default defineNuxtPlugin((nuxtApp) => {
return type return type
}) })
nuxtApp.provide('getProjectTypeForUrl', (type, loaders) => nuxtApp.provide('getProjectTypeForUrl', (type, loaders, tags) =>
getProjectTypeForUrlShorthand(nuxtApp, type, loaders) getProjectTypeForUrlShorthand(type, loaders, tags)
) )
nuxtApp.provide('cycleValue', cycleValue) nuxtApp.provide('cycleValue', cycleValue)
const sortedCategories = tagStore.categories.slice().sort((a, b) => { nuxtApp.provide('sortedCategories', () => {
const headerCompare = a.header.localeCompare(b.header) const tagStore = useTags()
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 tagStore.value.categories.slice().sort((a, b) => {
} const headerCompare = a.header.localeCompare(b.header)
return 0 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) => { export const formatNumber = (number, abbreviate = true) => {
const x = +number const x = +number
@@ -257,8 +244,9 @@ export const formatProjectStatus = (name) => {
return capitalizeString(name) return capitalizeString(name)
} }
export const formatVersions = (versionArray, tag) => { export const formatVersions = (versionArray) => {
const allVersions = tag.gameVersions.slice().reverse() const tag = useTags()
const allVersions = tag.value.gameVersions.slice().reverse()
const allReleases = allVersions.filter((x) => x.version_type === 'release') const allReleases = allVersions.filter((x) => x.version_type === 'release')
const intervals = [] const intervals = []

25
pnpm-lock.yaml generated
View File

@@ -34,6 +34,9 @@ dependencies:
omorphia: omorphia:
specifier: ^0.4.31 specifier: ^0.4.31
version: 0.4.31 version: 0.4.31
qrcode.vue:
specifier: ^3.4.0
version: 3.4.0(vue@3.3.4)
vue-multiselect: vue-multiselect:
specifier: ^3.0.0-alpha.2 specifier: ^3.0.0-alpha.2
version: 3.0.0-alpha.2 version: 3.0.0-alpha.2
@@ -48,6 +51,9 @@ devDependencies:
'@nuxtjs/eslint-config-typescript': '@nuxtjs/eslint-config-typescript':
specifier: ^12.0.0 specifier: ^12.0.0
version: 12.0.0(eslint@8.41.0)(typescript@5.0.4) version: 12.0.0(eslint@8.41.0)(typescript@5.0.4)
'@nuxtjs/turnstile':
specifier: ^0.5.0
version: 0.5.0
'@types/node': '@types/node':
specifier: ^20.1.0 specifier: ^20.1.0
version: 20.1.0 version: 20.1.0
@@ -1357,6 +1363,17 @@ packages:
- supports-color - supports-color
dev: true 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: /@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -6024,6 +6041,14 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true 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: /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true