mirror of
https://github.com/PhaseII-eAmusement-Network/PhaseWeb3-Vue.git
synced 2026-09-07 00:35:15 -05:00
Add in various stuff i've been working on in the background, i want to get my git changes slimmed down
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
VITE_APP_VERSION="3.1.2"
|
||||
VITE_APP_VERSION="3.1.3"
|
||||
VITE_API_URL="http://localhost:8000/"
|
||||
VITE_API_KEY="your_api_key_should_be_here"
|
||||
VITE_ASSET_PATH="/assets"
|
||||
VITE_CDN_PATH="https://cdn.phaseii.network/file/PhaseII"
|
||||
VITE_GAME_ASSET_PATH="https://cdn.phaseii.network/file/PhaseII/game-assets"
|
||||
VITE_DISCORD_OAUTH_URL="https://discord.com/oauth2/authorize?client_id=947985989421395988&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2F%23%2Fprofile%2Fintegrate%2Fdiscord&scope=identify"
|
||||
VITE_TACHI_OAUTH_URL="https://kamai.tachi.ac/oauth/request-auth?clientID=CIb72d1ff81fcfdc001ead8bde3a0ae1a7e0663a37"
|
||||
VITE_DISCORD_URL="https://discord.gg/mNmjpX33cd"
|
||||
VITE_DOCS_URL="https://docs.phaseii.network"
|
||||
VITE_DOCS_URL="https://docs.phaseii.network"
|
||||
VITE_UNITY_URL="http://localhost:5174"
|
||||
@@ -1,9 +1,11 @@
|
||||
VITE_APP_VERSION="3.1.2"
|
||||
VITE_APP_VERSION="3.1.3"
|
||||
VITE_API_URL="https://restfulsleep.phaseii.network"
|
||||
VITE_API_KEY="your_api_key_should_be_here"
|
||||
VITE_ASSET_PATH="https://cdn.phaseii.network/file/PhaseII/web-assets"
|
||||
VITE_CDN_PATH="https://cdn.phaseii.network/file/PhaseII"
|
||||
VITE_GAME_ASSET_PATH="https://cdn.phaseii.network/file/PhaseII/game-assets"
|
||||
VITE_DISCORD_OAUTH_URL="https://discord.com/oauth2/authorize?client_id=947985989421395988&response_type=code&redirect_uri=https%3A%2F%2Fweb3.phaseii.network%2Fprofile%2Fintegrate%2Fdiscord&scope=identify"
|
||||
VITE_TACHI_OAUTH_URL="https://kamai.tachi.ac/oauth/request-auth?clientID=CIce4260e1939ceed11c8e48ee857a3aef2a87ba56"
|
||||
VITE_DISCORD_URL="https://discord.gg/mNmjpX33cd"
|
||||
VITE_DOCS_URL="https://docs.phaseii.network"
|
||||
VITE_DOCS_URL="https://docs.phaseii.network"
|
||||
VITE_UNITY_URL="https://unity.phaseii.network"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "phaseweb3",
|
||||
"version": "3.1.2",
|
||||
"version": "3.1.3",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
|
||||
BIN
public/assets/card/unity.webp
Normal file
BIN
public/assets/card/unity.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
@@ -48,5 +48,6 @@
|
||||
"3.1.0-RC": ["- (Feature) Add onboarding page", "- (Minor) Add BaseImage component", "- (Minor) Add BaseAccordion component, - (Admin) Finish admin event log page"],
|
||||
"3.1.0": ["- (Major) WebUI V3 full release"],
|
||||
"3.1.1": ["- (Admin) Add more admin stats for users", "- (Minor) Add new greetings"],
|
||||
"3.1.2": ["- (Major) Add initial code for oAuth pages", "- (Major) Add redirect to target page after login", "- (Minor) Add quick links to navbar", "- (Minor) Add new greetings", "- (Minor) Replace login page background", "- (Minor) Add animation to loading modal", "- (Bugfix) Refactor loading modal", "- (Bugfix) Refactor overlay layer", "- (Bugfix) Optimize main layouts", "- (Bugfix) Fix loading states around site", "- (Bugfix) Optimize news read api calls", "- (Minor) Add simple auth checkers to router, blocking access at webui level"]
|
||||
"3.1.2": ["- (Major) Add initial code for oAuth pages", "- (Major) Add redirect to target page after login", "- (Minor) Add quick links to navbar", "- (Minor) Add new greetings", "- (Minor) Replace login page background", "- (Minor) Add animation to loading modal", "- (Bugfix) Refactor loading modal", "- (Bugfix) Refactor overlay layer", "- (Bugfix) Optimize main layouts", "- (Bugfix) Fix loading states around site", "- (Bugfix) Optimize news read api calls", "- (Minor) Add simple auth checkers to router, blocking access at webui level"],
|
||||
"3.1.3": ["- (Major) Add better error popups, add new buttons on errors", "- (Major) Finish initial authorization page for internal apps", "- (Minor) Add more greetings", "- (Bugfix) Fix overlay layer transparency when saving", "- (Bugfix) Fix remaining missing colors"]
|
||||
}
|
||||
14
src/App.vue
14
src/App.vue
@@ -24,7 +24,19 @@ watch(isActive, (val) => {
|
||||
if (!isActive.value) {
|
||||
showModal.value = false;
|
||||
}
|
||||
}, 250);
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
|
||||
watch(errorCode, (val) => {
|
||||
if (val) {
|
||||
showModal.value = true;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (!errorCode.value) {
|
||||
showModal.value = false;
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,7 @@ export const colorsBgLight = {
|
||||
info: "bg-blue-700 border-blue-500 text-white",
|
||||
sakura: "bg-pink-700 border-pink-500 text-white",
|
||||
gold: "bg-amber-700 border-amber-500 text-white",
|
||||
slight_danger: "bg-red-600 border-red-500",
|
||||
};
|
||||
|
||||
export const colorsText = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from "vue-router";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import BaseButtons from "@/components/BaseButtons.vue";
|
||||
import { useMainStore } from "@/stores/main";
|
||||
import { PhCloudCheck, PhCloudX, PhShrimp } from "@phosphor-icons/vue";
|
||||
|
||||
@@ -32,6 +33,18 @@ function hotReload() {
|
||||
$router.go();
|
||||
return null;
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
$router.go(-1);
|
||||
return null;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
mainStore.errorCode = null;
|
||||
mainStore.decrementLoading();
|
||||
mainStore.decrementSaving();
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -107,15 +120,29 @@ function hotReload() {
|
||||
<h1 class="text-lg md:text-xl">
|
||||
Error: <span class="text-red-500">{{ errorCode }}</span>
|
||||
</h1>
|
||||
<div>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Reload"
|
||||
:small="false"
|
||||
@click="hotReload()"
|
||||
/>
|
||||
</div>
|
||||
<BaseButtons>
|
||||
<div class="flex justify-center w-full">
|
||||
<BaseButton
|
||||
color="warning"
|
||||
label="Go Back"
|
||||
:small="false"
|
||||
@click="goBack()"
|
||||
/>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="success"
|
||||
label="Reload"
|
||||
:small="false"
|
||||
@click="hotReload()"
|
||||
/>
|
||||
<BaseButton
|
||||
color="info"
|
||||
label="Close"
|
||||
:small="false"
|
||||
@click="closeModal()"
|
||||
/>
|
||||
</div>
|
||||
</BaseButtons>
|
||||
</div>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useStyleStore } from "@/stores/style.js";
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
zIndex: {
|
||||
type: String,
|
||||
default: "z-50",
|
||||
@@ -27,14 +28,20 @@ const overlayClick = (event) => {
|
||||
};
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
|
||||
const transparentClass = computed(() => {
|
||||
return props.transparent
|
||||
? "dark:from-gray-700/90 dark:via-gray-900/90 dark:to-gray-700/90"
|
||||
: "dark:from-gray-700 dark:via-gray-900 dark:to-gray-700";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="overlay-fade">
|
||||
<div
|
||||
v-show="active"
|
||||
:class="[type, zIndex]"
|
||||
class="fixed inset-0 flex flex-col items-center justify-center bg-linear-to-tr dark:from-gray-700 dark:via-gray-900 dark:to-gray-700 transform"
|
||||
:class="[type, zIndex, transparentClass]"
|
||||
class="fixed inset-0 flex flex-col items-center justify-center bg-linear-to-tr transform"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
|
||||
@@ -12,6 +12,7 @@ export class ProfileCustomizations {
|
||||
{ id: "loveplus", label: "LovePlus" },
|
||||
{ id: "paint", label: "i eat paint" },
|
||||
{ id: "shrimp", label: "Shrimp" },
|
||||
{ id: "unity", label: "Unity" },
|
||||
];
|
||||
|
||||
static get cardList() {
|
||||
|
||||
9
src/constants/developer/apps.js
Normal file
9
src/constants/developer/apps.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const InternalApps = {
|
||||
unity: {
|
||||
id: "unity",
|
||||
name: "Unity",
|
||||
image: "https://cdn.phaseii.network/file/PhaseII/web-assets/unity.png",
|
||||
about:
|
||||
"Unity is PhaseII's developer platform. It allows you to create and manage applications that can connect to PhaseII accounts and data.",
|
||||
},
|
||||
};
|
||||
@@ -655,5 +655,25 @@
|
||||
"author": "superaustin7",
|
||||
"header": "Welcome to jubeat, <username>",
|
||||
"comment": "THAT WON'T MAKE IT IN TIME FOR THIS SCHEDULE."
|
||||
},
|
||||
{
|
||||
"author": "Anonymous",
|
||||
"header": "Explore Every Avenue, <username>.",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"author": "Anonymous",
|
||||
"header": "Welcome to PhaseII Web Solutions, <username>.",
|
||||
"comment": "Now offering a 99% SLA. Excludes pickaxe related incidents."
|
||||
},
|
||||
{
|
||||
"author": "yonokid",
|
||||
"header": "Something might happen, <username>",
|
||||
"comment": "If you would please consult the graph."
|
||||
},
|
||||
{
|
||||
"author": "superaustin7",
|
||||
"header": "THE <username> HAS AWAKENED",
|
||||
"comment": "BE VERY SCARED"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -30,6 +30,7 @@ import { gameData } from "@/constants";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
const DISCORD_URL = import.meta.env.VITE_DISCORD_URL;
|
||||
const DOCS_URL = import.meta.env.VITE_DOCS_URL;
|
||||
const UNITY_URL = import.meta.env.VITE_UNITY_URL;
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -174,7 +175,8 @@ const menuAside = computed(() => {
|
||||
sideMenu.push({
|
||||
label: "Developer Portal",
|
||||
icon: PhCode,
|
||||
to: "/developer",
|
||||
href: UNITY_URL,
|
||||
target: "blank",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -452,26 +452,6 @@ const routes = [
|
||||
hotReload: true, // disables Hot Reload
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresDev: true,
|
||||
title: "Developer Portal",
|
||||
},
|
||||
path: "/developer",
|
||||
name: "Developer Portal",
|
||||
component: () => import("@/views/Developer/PortalView.vue"),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresDev: true,
|
||||
title: "App Registration",
|
||||
},
|
||||
path: "/developer/register",
|
||||
name: "App Registration",
|
||||
component: () => import("@/views/Developer/RegisterView.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -321,3 +321,27 @@ export async function APIDeleteUserSessions() {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIGetAuthClient(clientId) {
|
||||
const mainStore = useMainStore();
|
||||
|
||||
try {
|
||||
const data = await mainStore.callApi(`/oauth/client/${clientId}`);
|
||||
return data?.data;
|
||||
} catch (error) {
|
||||
console.log("Error fetching client:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIPostAuthClient(clientId) {
|
||||
const mainStore = useMainStore();
|
||||
|
||||
try {
|
||||
const data = await mainStore.callApi(`/oauth/client/${clientId}`, "POST");
|
||||
return data?.data;
|
||||
} catch (error) {
|
||||
console.log("Error posting client:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,8 @@ export const useMainStore = defineStore("main", {
|
||||
this.incrementSaving();
|
||||
}
|
||||
|
||||
this.errorCode = null;
|
||||
|
||||
const baseHeaders = {
|
||||
"App-Auth-Key": apiKey,
|
||||
};
|
||||
@@ -158,11 +160,12 @@ export const useMainStore = defineStore("main", {
|
||||
this.errorCode = error.message || "Network Error";
|
||||
throw error;
|
||||
} finally {
|
||||
// Always decrement in finally block
|
||||
if (method === "GET") {
|
||||
this.decrementLoading();
|
||||
} else if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
this.decrementSaving();
|
||||
if (this.errorCode === null) {
|
||||
if (method === "GET") {
|
||||
this.decrementLoading();
|
||||
} else if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
this.decrementSaving();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,34 +1,56 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { reactive } from "vue";
|
||||
import { useMainStore } from "@/stores/main.js";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import LayoutGuest from "@/layouts/LayoutGuest.vue";
|
||||
import { APIGetAuthClient, APIPostAuthClient } from "@/stores/api/account.js";
|
||||
import { InternalApps } from "@/constants/developer/apps.js";
|
||||
|
||||
const router = useRouter();
|
||||
const mainStore = useMainStore();
|
||||
const route = useRoute();
|
||||
const clientId = route.query.client_id;
|
||||
const clientData = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
spinin: false,
|
||||
onMounted(async () => {
|
||||
if (clientId) {
|
||||
await getClient(clientId);
|
||||
} else {
|
||||
console.error("No client_id provided in query parameters.");
|
||||
}
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
const response = await mainStore.createUserSession();
|
||||
if (response) {
|
||||
router.push("/");
|
||||
async function getClient(id) {
|
||||
try {
|
||||
const data = await APIGetAuthClient(id);
|
||||
if (data.internal && InternalApps[id]) {
|
||||
clientData.value = InternalApps[id];
|
||||
clientData.value.internal = true;
|
||||
clientData.value.callbackUrl = data.callbackUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch client data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
const response = await APIPostAuthClient(clientId);
|
||||
if (!response.code) {
|
||||
console.error("Authorization failed");
|
||||
}
|
||||
|
||||
window.location.href =
|
||||
clientData.value.callbackUrl + "?code=" + response.code;
|
||||
};
|
||||
|
||||
const appInfo = {
|
||||
name: "Edi",
|
||||
image: "/edi512x512_2.png",
|
||||
manager: "Lumen",
|
||||
about:
|
||||
"Edi is a DDR score tracking app that offers a direct connection to your PhaseII account.",
|
||||
intents: ["user", "account", "webhook_scores"],
|
||||
internal: false,
|
||||
};
|
||||
// const appInfo = {
|
||||
// name: "Edi",
|
||||
// image: "/edi512x512_2.png",
|
||||
// manager: "Lumen",
|
||||
// about:
|
||||
// "Edi is a DDR score tracking app that offers a direct connection to your PhaseII account.",
|
||||
// intents: ["user", "account", "webhook_scores"],
|
||||
// internal: false,
|
||||
// };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,7 +58,6 @@ const appInfo = {
|
||||
<div class="flex md:min-h-screen md:items-center md:justify-center">
|
||||
<CardBox
|
||||
class="w-full md:w-auto rounded-none md:rounded-xl md:drop-shadow-xl"
|
||||
:class="form.spinin ? 'animate-spin' : 'animate-none'"
|
||||
has-table
|
||||
is-auth
|
||||
>
|
||||
@@ -47,10 +68,7 @@ const appInfo = {
|
||||
<div class="flex flex-col items-center text-wrap">
|
||||
<img src="/favicon.png" class="rounded-full shadow-lg mb-2" />
|
||||
<h1 class="text-xl"><samp>PhaseII</samp></h1>
|
||||
<button
|
||||
class="text-sm text-gray-700 dark:text-white/75 hover:cursor-pointer"
|
||||
@click="form.spinin = !form.spinin"
|
||||
>
|
||||
<button class="text-sm text-gray-700 dark:text-white/75">
|
||||
Spinnin' since 2021
|
||||
</button>
|
||||
</div>
|
||||
@@ -60,50 +78,67 @@ const appInfo = {
|
||||
<div class="md:border-r" />
|
||||
<form @submit.prevent="submit()">
|
||||
<div class="flex flex-col items-center text-wrap">
|
||||
<h1 class="text-lg md:text-xl mb-2">
|
||||
Integrate with <span class="font-bold">{{ appInfo.name }}</span>
|
||||
</h1>
|
||||
<template v-if="!clientData?.internal">
|
||||
<h1 class="text-lg md:text-xl mb-2">
|
||||
Integrate with
|
||||
<span class="font-bold">{{ clientData?.name }}</span>
|
||||
</h1>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1 class="text-lg md:text-xl mb-2">
|
||||
Log in to
|
||||
<span class="font-bold">{{ clientData?.name }}</span>
|
||||
</h1>
|
||||
</template>
|
||||
<img
|
||||
:src="appInfo.image"
|
||||
:src="clientData?.image"
|
||||
width="75"
|
||||
class="rounded-full shadow-lg mb-2"
|
||||
/>
|
||||
<p class="text-md max-w-md wrap-break-word text-center">
|
||||
{{ appInfo.about }}
|
||||
{{ clientData?.about }}
|
||||
</p>
|
||||
|
||||
<hr class="border-r my-2 w-full" />
|
||||
|
||||
<h2 class="text-md md:text-lg">
|
||||
<span class="font-bold">{{ appInfo.name }}</span> will have
|
||||
access to:
|
||||
</h2>
|
||||
<ul class="text-sm md:text-md text-center">
|
||||
<li v-for="intent of appInfo.intents" :v-key="intent">
|
||||
- {{ intent }}
|
||||
</li>
|
||||
</ul>
|
||||
<template v-if="!clientData?.internal">
|
||||
<h2 class="text-md md:text-lg">
|
||||
<span class="font-bold">{{ clientData?.name }}</span> will
|
||||
have access to:
|
||||
</h2>
|
||||
<ul class="text-sm md:text-md text-center">
|
||||
<li v-for="intent of clientData?.intents" :v-key="intent">
|
||||
- {{ intent }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-4">
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<BaseButton label="Authorize" color="success" type="submit" />
|
||||
</div>
|
||||
<hr class="border-t my-4 w-full" />
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<p
|
||||
v-if="!appInfo.internal"
|
||||
class="text-sm max-w-md wrap-break-word text-center mb-2"
|
||||
>
|
||||
Please note that
|
||||
<span class="font-bold">{{ appInfo.name }}</span> is not a
|
||||
<samp>PhaseII</samp> provided service, and that any issues
|
||||
should be directed to the appropriate channels.
|
||||
</p>
|
||||
<p class="text-sm max-w-md wrap-break-word text-center">
|
||||
<span class="font-bold">{{ appInfo.name }}</span> is managed by
|
||||
<span class="font-bold">{{ appInfo.manager }}</span>
|
||||
</p>
|
||||
<template v-if="!clientData?.internal">
|
||||
<p class="text-sm max-w-md wrap-break-word text-center mb-2">
|
||||
Please note that
|
||||
<span class="font-bold">{{ clientData?.name }}</span> is not a
|
||||
<samp>PhaseII</samp> provided service, and that any issues
|
||||
should be directed to the appropriate channels.
|
||||
</p>
|
||||
<p class="text-sm max-w-md wrap-break-word text-center">
|
||||
<span class="font-bold">{{ clientData?.name }}</span> is
|
||||
managed by
|
||||
<span class="font-bold">{{ clientData?.manager }}</span>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="text-sm max-w-md wrap-break-word text-center">
|
||||
This is an internal application provided by
|
||||
<samp>PhaseII</samp>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<hr class="border-t my-4 w-full" />
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from "vue";
|
||||
import { PhCloud, PhCode } from "@phosphor-icons/vue";
|
||||
import { dashCode } from "@/constants/userData.js";
|
||||
import SectionMain from "@/components/SectionMain.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import UserCard from "@/components/UserCard.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import FormControl from "@/components/FormControl.vue";
|
||||
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
|
||||
import SectionTitleLine from "@/components/SectionTitleLine.vue";
|
||||
const DOCS_URL = import.meta.env.VITE_DOCS_URL;
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
alert("Copied to clipboard!");
|
||||
})
|
||||
.catch(() => {
|
||||
alert("Failed to copy to clipboard!");
|
||||
});
|
||||
};
|
||||
|
||||
const appInfo = {
|
||||
name: "Edi",
|
||||
image: "/edi512x512_2.png",
|
||||
manager: "Lumen",
|
||||
about:
|
||||
"Edi is a DDR score tracking app that offers a direct connection to your PhaseII account.",
|
||||
intents: ["user", "account", "webhook_scores"],
|
||||
webhooks: ["scores"],
|
||||
useOAuth: true,
|
||||
internal: false,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutAuthenticated>
|
||||
<SectionMain>
|
||||
<UserCard class="mb-6" use-small even-smaller />
|
||||
|
||||
<SectionTitleLine :icon="PhCode" title="Developer Portal" main />
|
||||
<CardBox class="row-span-2 mb-6">
|
||||
<h1 class="text-xl md:text-2xl">
|
||||
Welcome to the <samp>PhaseII</samp> Developer Portal!
|
||||
</h1>
|
||||
<h2 class="text-lg">You're gonna do great 😄</h2>
|
||||
|
||||
<p>
|
||||
If you're just now getting started, please read the
|
||||
<a
|
||||
class="text-blue-400 hover:text-blue-600"
|
||||
:href="`${DOCS_URL}/developer`"
|
||||
target="_blank"
|
||||
>
|
||||
developer docs
|
||||
</a>
|
||||
</p>
|
||||
<hr class="border-r my-2 w-full" />
|
||||
<p>Create or manage your applications below</p>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine :icon="PhCloud" title="My Applications" main>
|
||||
<BaseButton
|
||||
label="Add Application"
|
||||
color="success"
|
||||
to="/developer/register"
|
||||
/>
|
||||
</SectionTitleLine>
|
||||
<CardBox class="row-span-2 mb-6">
|
||||
<div class="flex">
|
||||
<div
|
||||
class="flex flex-col justify-center p-4 rounded-xl bg-slate-700 hover:drop-shadow-xl transition-all"
|
||||
>
|
||||
<div class="w-full flex justify-center">
|
||||
<img
|
||||
:src="appInfo.image"
|
||||
width="75"
|
||||
class="rounded-full shadow-lg mb-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2 text-center">
|
||||
<h1 class="text-lg md:text-xl font-bold">
|
||||
{{ appInfo.name }}
|
||||
</h1>
|
||||
<h2 v-if="appInfo.useOAuth">OAuth Enabled</h2>
|
||||
<h2>{{ appInfo.webhooks?.length || 0 }} Webhook(s)</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
@@ -1,344 +0,0 @@
|
||||
<script setup>
|
||||
import { reactive } from "vue";
|
||||
import {
|
||||
PhCloudArrowUp,
|
||||
PhFileImage,
|
||||
PhKey,
|
||||
PhLinkSimple,
|
||||
PhStorefront,
|
||||
PhTextbox,
|
||||
PhWebhooksLogo,
|
||||
} from "@phosphor-icons/vue";
|
||||
import SectionMain from "@/components/SectionMain.vue";
|
||||
import UserCard from "@/components/UserCard.vue";
|
||||
import PillTag from "@/components/PillTag.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import FormCheckRadio from "@/components/FormCheckRadio.vue";
|
||||
import FormFilePicker from "@/components/FormFilePicker.vue";
|
||||
import FormControl from "@/components/FormControl.vue";
|
||||
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
|
||||
import SectionTitleLine from "@/components/SectionTitleLine.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { useMainStore } from "@/stores/main";
|
||||
import { applicationIntents, applicationWebhooks } from "@/constants/developer";
|
||||
|
||||
const mainStore = useMainStore();
|
||||
|
||||
const appForm = reactive({
|
||||
name: "",
|
||||
about: "",
|
||||
image: null,
|
||||
useOAuth: false,
|
||||
callbackUri: null,
|
||||
intents: [],
|
||||
useWebhooks: false,
|
||||
webhookList: [],
|
||||
});
|
||||
|
||||
const initWebhook = {
|
||||
type: "network.news",
|
||||
name: "",
|
||||
endpoint: "",
|
||||
authKey: "",
|
||||
fields: {},
|
||||
};
|
||||
|
||||
const newWebhook = reactive({
|
||||
...initWebhook,
|
||||
});
|
||||
|
||||
const sortedArcades = mainStore.userArcades.map((arcade) => ({
|
||||
label: arcade.name,
|
||||
id: arcade.id,
|
||||
}));
|
||||
|
||||
function isValidUrl(value, { requireHttps = true } = {}) {
|
||||
if (!value || typeof value !== "string") return false;
|
||||
|
||||
try {
|
||||
const url = new URL(value);
|
||||
|
||||
if (!["http:", "https:"].includes(url.protocol)) return false;
|
||||
if (requireHttps && url.protocol !== "https:") return false;
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidAuthKey(key) {
|
||||
return typeof key === "string" && key.length >= 40;
|
||||
}
|
||||
|
||||
async function addWebhook() {
|
||||
const tmpWebhook = JSON.parse(
|
||||
JSON.stringify({
|
||||
type: newWebhook.type,
|
||||
name: newWebhook.name,
|
||||
endpoint: newWebhook.endpoint,
|
||||
authKey: newWebhook.authKey,
|
||||
fields: newWebhook.fields,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!tmpWebhook.name?.trim()) {
|
||||
return alert("Webhook name is required.");
|
||||
}
|
||||
if (!isValidUrl(tmpWebhook.endpoint)) {
|
||||
return alert("Webhook endpoint must be a valid HTTPS URL.");
|
||||
}
|
||||
if (!isValidAuthKey(tmpWebhook.authKey)) {
|
||||
return alert("Auth key must be at least 40 characters.");
|
||||
}
|
||||
|
||||
const webhookMeta = applicationWebhooks.find((w) => w.id === tmpWebhook.type);
|
||||
if (webhookMeta?.fields?.includes("arcadeId")) {
|
||||
if (!tmpWebhook.fields.arcadeId) {
|
||||
return alert("Arcade selection is required.");
|
||||
}
|
||||
} else {
|
||||
tmpWebhook.fields = {};
|
||||
}
|
||||
|
||||
appForm.webhookList.push(tmpWebhook);
|
||||
Object.assign(newWebhook, initWebhook);
|
||||
}
|
||||
|
||||
function removeWebhook(authKey) {
|
||||
appForm.webhookList = appForm.webhookList.filter(
|
||||
(webhook) => webhook.authKey !== authKey,
|
||||
);
|
||||
}
|
||||
|
||||
async function requestApp() {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutAuthenticated>
|
||||
<SectionMain>
|
||||
<UserCard class="mb-6" use-small even-smaller />
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="PhCloudArrowUp"
|
||||
title="Create an Application"
|
||||
main
|
||||
/>
|
||||
<CardBox class="row-span-2 mb-6">
|
||||
<PillTag color="info" label="General" class="mb-2" />
|
||||
<FormField label="Name">
|
||||
<FormControl
|
||||
v-model="appForm.name"
|
||||
:icon="PhTextbox"
|
||||
name="name"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="About">
|
||||
<FormControl
|
||||
v-model="appForm.about"
|
||||
name="about"
|
||||
type="textarea"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="App Logo">
|
||||
<FormFilePicker
|
||||
v-model="appForm.image"
|
||||
label="Image Upload"
|
||||
accept="image/*"
|
||||
:icon="PhFileImage"
|
||||
/>
|
||||
</FormField>
|
||||
</CardBox>
|
||||
<CardBox class="row-span-2 mb-6">
|
||||
<PillTag color="warning" label="OAuth2.0" class="mb-2" />
|
||||
<FormField label="Enable OAuth">
|
||||
<FormCheckRadio
|
||||
v-model="appForm.useOAuth"
|
||||
name="useOAuth"
|
||||
:model-value="appForm.useOAuth"
|
||||
:input-value="appForm.useOAuth"
|
||||
type="switch"
|
||||
/>
|
||||
</FormField>
|
||||
<template v-if="appForm.useOAuth">
|
||||
<FormField
|
||||
label="Callback URI"
|
||||
help="The URL that the user will be redirected to from the authorization page (ex: input of https://mycoolapp.my.domain/callback/url will redirect user to https://address/callback/url?code= )"
|
||||
>
|
||||
<FormControl
|
||||
v-model="appForm.callbackUri"
|
||||
:icon="PhLinkSimple"
|
||||
name="callbackUri"
|
||||
placeholder="https://my.domain/callback/url"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Intents"
|
||||
help="Select what you'd like your app to do"
|
||||
>
|
||||
<template v-for="intent in applicationIntents" :key="intent.id">
|
||||
<div class="grid md:flex justify-between">
|
||||
<FormCheckRadio
|
||||
v-model="appForm.intents"
|
||||
:name="'intents'"
|
||||
:label="intent.label"
|
||||
:input-value="intent.id"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="text-slate-400 hover:text-slate-200 font-thin transition-all delay-0"
|
||||
>
|
||||
{{ intent.tip ?? "" }}
|
||||
</span>
|
||||
</div>
|
||||
<hr class="border-r my-3 w-full" />
|
||||
</template>
|
||||
</FormField>
|
||||
</template>
|
||||
</CardBox>
|
||||
<CardBox class="row-span-2 mb-6">
|
||||
<PillTag color="success" label="Webhooks" class="mb-2" />
|
||||
<FormField
|
||||
label="Enable Application Webhooks"
|
||||
help="These are different from user webhooks!"
|
||||
>
|
||||
<FormCheckRadio
|
||||
v-model="appForm.useWebhooks"
|
||||
name="useWebhooks"
|
||||
:model-value="appForm.useWebhooks"
|
||||
:input-value="appForm.useWebhooks"
|
||||
type="switch"
|
||||
/>
|
||||
</FormField>
|
||||
<template v-if="appForm.useWebhooks">
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<form class="h-full" @submit.prevent="addWebhook()">
|
||||
<PillTag color="info" label="New Webhook" class="mb-4" />
|
||||
<FormField
|
||||
label="Webhook Type"
|
||||
:help="
|
||||
applicationWebhooks.find(
|
||||
(webhook) => webhook.id === newWebhook.type,
|
||||
)?.tip
|
||||
"
|
||||
>
|
||||
<FormControl
|
||||
v-model="newWebhook.type"
|
||||
name="type"
|
||||
:icon="PhWebhooksLogo"
|
||||
:options="applicationWebhooks"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Name">
|
||||
<FormControl
|
||||
v-model="newWebhook.name"
|
||||
name="name"
|
||||
:icon="PhTextbox"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="API Endpoint"
|
||||
help="The URL that the Webhook server will contact"
|
||||
>
|
||||
<FormControl
|
||||
v-model="newWebhook.endpoint"
|
||||
name="name"
|
||||
placeholder="https://my.domain/endpoint/url"
|
||||
:icon="PhLinkSimple"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="API Auth Key"
|
||||
help="Sent as headers x-api-key, x-webhook-key, authorization"
|
||||
>
|
||||
<FormControl
|
||||
v-model="newWebhook.authKey"
|
||||
name="authKey"
|
||||
:icon="PhKey"
|
||||
required
|
||||
:minlength="40"
|
||||
/>
|
||||
</FormField>
|
||||
<template
|
||||
v-for="field of applicationWebhooks.find(
|
||||
(webhookData) => webhookData.id === newWebhook.type,
|
||||
)?.fields"
|
||||
>
|
||||
<template v-if="field === 'arcadeId'">
|
||||
<FormField
|
||||
label="Arcade"
|
||||
help="The arcade that this Webhook will track"
|
||||
>
|
||||
<FormControl
|
||||
v-model="newWebhook.fields.arcadeId"
|
||||
name="arcadeId"
|
||||
:icon="PhStorefront"
|
||||
:options="sortedArcades"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<BaseButton color="success" type="submit" label="Add Webhook" />
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<PillTag color="warning" label="Queue" class="mb-4" />
|
||||
<div class="grid gap-4">
|
||||
<CardBox
|
||||
v-for="webhook of appForm.webhookList"
|
||||
:key="webhook.type"
|
||||
color-prop="bg-slate-800 dark:bg-slate-800"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="m-[-5px]">
|
||||
<PillTag
|
||||
color="info"
|
||||
:label="webhook.type"
|
||||
class="mb-4"
|
||||
small
|
||||
/>
|
||||
<h1 class="text-md font-bold">{{ webhook.name }}</h1>
|
||||
<samp
|
||||
class="text-sm font-mono p-1 bg-slate-900 rounded-lg"
|
||||
>
|
||||
{{ webhook.endpoint }}
|
||||
</samp>
|
||||
<h2
|
||||
v-for="(value, field) of webhook.fields"
|
||||
class="text-sm font-mono mt-2"
|
||||
>
|
||||
{{ field }}: {{ value }}
|
||||
</h2>
|
||||
</div>
|
||||
<BaseButton
|
||||
color="danger"
|
||||
label="Remove"
|
||||
@click="removeWebhook(webhook.authKey)"
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardBox>
|
||||
<BaseButton
|
||||
:small="false"
|
||||
label="Submit"
|
||||
color="success"
|
||||
@click="console.log(appForm)"
|
||||
/>
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
Reference in New Issue
Block a user