Files
AstralRinth/apps/frontend/src/pages/auth/authorize.vue
Evan Song abec2e48d4 Add TailwindCSS (#1252)
* Setup TailwindCSS

* Fully setup configuration

* Refactor some tailwind variables
2024-07-06 20:57:32 -07:00

366 lines
8.6 KiB
Vue

<template>
<div>
<div v-if="error" class="oauth-items">
<div>
<h1>{{ formatMessage(commonMessages.errorLabel) }}</h1>
</div>
<p>
<span>{{ error.data.error }}: </span>
{{ error.data.description }}
</p>
</div>
<div v-else class="oauth-items">
<div class="connected-items">
<div class="profile-pics">
<Avatar size="md" :src="app.icon_url" />
<!-- <img class="profile-pic" :src="app.icon_url" alt="User profile picture" /> -->
<div class="connection-indicator"></div>
<Avatar size="md" circle :src="auth.user.avatar_url" />
<!-- <img class="profile-pic" :src="auth.user.avatar_url" alt="User profile picture" /> -->
</div>
</div>
<div class="title">
<h1>{{ formatMessage(messages.title, { appName: app.name }) }}</h1>
</div>
<div class="auth-info">
<div class="scope-heading">
<IntlFormatted
:message-id="messages.appInfo"
:values="{
appName: app.name,
creator: createdBy.username,
}"
>
<template #strong="{ children }">
<strong>
<component :is="() => normalizeChildren(children)" />
</strong>
</template>
<template #creator-link="{ children }">
<nuxt-link class="text-link" :to="'/user/' + createdBy.id">
<component :is="() => normalizeChildren(children)" />
</nuxt-link>
</template>
</IntlFormatted>
</div>
<div class="scope-items">
<div v-for="scopeItem in scopeDefinitions" :key="scopeItem">
<div class="scope-item">
<div class="scope-icon">
<CheckIcon />
</div>
{{ scopeItem }}
</div>
</div>
</div>
</div>
<div class="button-row">
<Button class="wide-button" large :action="onReject" :disabled="pending">
<XIcon />
{{ formatMessage(messages.decline) }}
</Button>
<Button class="wide-button" color="primary" large :action="onAuthorize" :disabled="pending">
<CheckIcon />
{{ formatMessage(messages.authorize) }}
</Button>
</div>
<div class="redirection-notice">
<p class="redirect-instructions">
<IntlFormatted :message-id="messages.redirectUrl" :values="{ url: redirectUri }">
<template #redirect-url="{ children }">
<span class="redirect-url">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</p>
</div>
</div>
</div>
</template>
<script setup>
import { Button, Avatar } from "@modrinth/ui";
import { XIcon, CheckIcon } from "@modrinth/assets";
import { useBaseFetch } from "@/composables/fetch.js";
import { useAuth } from "@/composables/auth.js";
import { useScopes } from "@/composables/auth/scopes.ts";
const { formatMessage } = useVIntl();
const messages = defineMessages({
appInfo: {
id: "auth.authorize.app-info",
defaultMessage:
"<strong>{appName}</strong> by <creator-link>{creator}</creator-link> will be able to:",
},
authorize: {
id: "auth.authorize.action.authorize",
defaultMessage: "Authorize",
},
decline: {
id: "auth.authorize.action.decline",
defaultMessage: "Decline",
},
noRedirectUrlError: {
id: "auth.authorize.error.no-redirect-url",
defaultMessage: "No redirect location found in response",
},
redirectUrl: {
id: "auth.authorize.redirect-url",
defaultMessage: "You will be redirected to <redirect-url>{url}</redirect-url>",
},
title: {
id: "auth.authorize.authorize-app-name",
defaultMessage: "Authorize {appName}",
},
});
const data = useNuxtApp();
const router = useNativeRoute();
const auth = await useAuth();
const { scopesToDefinitions } = useScopes();
const clientId = router.query?.client_id || false;
const redirectUri = router.query?.redirect_uri || false;
const scope = router.query?.scope || false;
const state = router.query?.state || false;
const getFlowIdAuthorization = async () => {
const query = {
client_id: clientId,
redirect_uri: redirectUri,
scope,
};
if (state) {
query.state = state;
}
const authorization = await useBaseFetch("oauth/authorize", {
method: "GET",
internal: true,
query,
}); // This will contain the flow_id and oauth_client_id for accepting the oauth on behalf of the user
if (typeof authorization === "string") {
await navigateTo(authorization, {
external: true,
});
}
return authorization;
};
const {
data: authorizationData,
pending,
error,
} = await useAsyncData("authorization", getFlowIdAuthorization);
const { data: app } = await useAsyncData("oauth/app/" + clientId, () =>
useBaseFetch("oauth/app/" + clientId, {
method: "GET",
internal: true,
}),
);
const scopeDefinitions = scopesToDefinitions(
BigInt(authorizationData.value?.requested_scopes || 0),
);
const { data: createdBy } = await useAsyncData("user/" + app.value.created_by, () =>
useBaseFetch("user/" + app.value.created_by, {
method: "GET",
apiVersion: 3,
}),
);
const onAuthorize = async () => {
try {
const res = await useBaseFetch("oauth/accept", {
method: "POST",
internal: true,
body: {
flow: authorizationData.value.flow_id,
},
});
if (typeof res === "string") {
navigateTo(res, {
external: true,
});
return;
}
throw new Error(formatMessage(messages.noRedirectUrlError));
} catch (error) {
data.$notify({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: "error",
});
}
};
const onReject = async () => {
try {
const res = await useBaseFetch("oauth/reject", {
method: "POST",
body: {
flow: authorizationData.value.flow_id,
},
});
if (typeof res === "string") {
navigateTo(res, {
external: true,
});
return;
}
throw new Error(formatMessage(messages.noRedirectUrlError));
} catch (error) {
data.$notify({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: "error",
});
}
};
definePageMeta({
middleware: "auth",
});
</script>
<style scoped lang="scss">
.oauth-items {
display: flex;
flex-direction: column;
gap: var(--gap-xl);
}
.scope-items {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
}
.scope-item {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
}
.scope-icon {
display: flex;
color: var(--color-raised-bg);
background-color: var(--color-green);
aspect-ratio: 1;
border-radius: 50%;
padding: var(--gap-xs);
}
.title {
margin-inline: auto;
h1 {
margin-bottom: 0 !important;
}
}
.redirection-notice {
display: flex;
flex-direction: column;
gap: var(--gap-xs);
text-align: center;
.redirect-instructions {
font-size: var(--font-size-sm);
}
.redirect-url {
font-weight: bold;
}
}
.wide-button {
width: 100% !important;
}
.button-row {
display: flex;
flex-direction: row;
gap: var(--gap-xs);
justify-content: center;
}
.auth-info {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
}
.scope-heading {
margin-bottom: var(--gap-sm);
}
.profile-pics {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
.connection-indicator {
// Make sure the text sits in the middle and is centered.
// Make the text large, and make sure it's not selectable.
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
user-select: none;
color: var(--color-primary);
}
}
.profile-pic {
width: 6rem;
height: 6rem;
border-radius: 50%;
margin: 0 1rem;
}
.dotted-border-line {
width: 75%;
border: 0.1rem dashed var(--color-divider);
}
.connected-items {
// Display dotted-border-line under profile-pics and centered behind them
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
margin-top: 1rem;
// Display profile-pics on top of dotted-border-line
.profile-pics {
position: relative;
z-index: 2;
}
// Display dotted-border-line behind profile-pics
.dotted-border-line {
position: absolute;
z-index: 1;
}
}
</style>