Skip to content

Commit

Permalink
feat: ✨ Implement proper login and logout using UI
Browse files Browse the repository at this point in the history
  • Loading branch information
CPlusPatch committed Apr 27, 2024
1 parent e0c41bb commit 3c8093a
Show file tree
Hide file tree
Showing 23 changed files with 273 additions and 193 deletions.
27 changes: 27 additions & 0 deletions app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@ useServerSeoMeta({
// Use SSR-safe IDs for Headless UI
provideHeadlessUseId(() => useId());
const code = useRequestURL().searchParams.get("code");
if (code) {
const client = useMegalodon();
const appData = useAppData();
const tokenData = useTokenData();
if (appData.value) {
client.value
?.fetchAccessToken(
appData.value.client_id,
appData.value.client_secret,
code,
new URL("/", useRequestURL().origin).toString(),
)
.then((res) => {
tokenData.value = res;
// Remove code from URL
window.history.replaceState(
{},
document.title,
window.location.pathname,
);
});
}
}
</script>

<style>
Expand Down
13 changes: 10 additions & 3 deletions components/buttons/base.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<template>
<button v-bind="$props" type="button"
class="rounded-md duration-200 hover:shadow disabled:opacity-70 disabled:cursor-not-allowed px-3 py-2 text-sm font-semibold text-white shadow-sm">
<button v-bind="$props" type="button" :disabled="loading"
:class="['rounded-md duration-200 relative hover:shadow disabled:opacity-70 content-none disabled:cursor-not-allowed px-3 py-2 text-sm font-semibold text-white shadow-sm', loading && '[&>*]:invisible']">
<div v-if="loading" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 !visible">
<Icon name="tabler:loader-2" class="animate-spin w-5 h-5" />
</div>
<slot />
</button>
</template>
Expand All @@ -10,7 +13,11 @@ import type { ButtonHTMLAttributes } from "vue";
interface Props extends /* @vue-ignore */ ButtonHTMLAttributes {}
defineProps<Props>();
defineProps<
Props & {
loading?: boolean;
}
>();
</script>

<style></style>
8 changes: 6 additions & 2 deletions components/buttons/secondary.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<ButtonsBase class="bg-white/10 hover:bg-white/20">
<ButtonsBase class="bg-white/10 hover:bg-white/20" :loading="loading">
<slot />
</ButtonsBase>
</template>
Expand All @@ -9,7 +9,11 @@ import type { ButtonHTMLAttributes } from "vue";
interface Props extends /* @vue-ignore */ ButtonHTMLAttributes {}
defineProps<Props>();
defineProps<
Props & {
loading?: boolean;
}
>();
</script>

<style></style>
78 changes: 70 additions & 8 deletions components/sidebars/navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
<div class="flex flex-col gap-3 mt-auto">
<h3 class="font-semibold text-gray-300 text-xs uppercase opacity-0 group-hover:opacity-100 duration-200">
Account</h3>
<NuxtLink href="/about/apps">
<ButtonsBase
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 overflow-hidden h-12 w-full duration-200">
<Icon name="tabler:login" class="shrink-0 text-2xl" />
<span class="pr-28 line-clamp-1">Sign In</span>
</ButtonsBase>
</NuxtLink>
<NuxtLink href="/register">
<ButtonsBase v-if="tokenData" @click="signOut().finally(() => loadingAuth = false)" :loading="loadingAuth"
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 overflow-hidden h-12 w-full duration-200">
<Icon name="tabler:logout" class="shrink-0 text-2xl" />
<span class="pr-28 line-clamp-1">Sign Out</span>
</ButtonsBase>
<ButtonsBase v-else @click="signIn().finally(() => loadingAuth = false)" :loading="loadingAuth"
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 overflow-hidden h-12 w-full duration-200">
<Icon name="tabler:login" class="shrink-0 text-2xl" />
<span class="pr-28 line-clamp-1">Sign In</span>
</ButtonsBase>
<NuxtLink href="/register" v-if="!tokenData">
<ButtonsBase
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 overflow-hidden h-12 w-full duration-200">
<Icon name="tabler:certificate" class="shrink-0 text-2xl" />
Expand All @@ -52,4 +55,63 @@ const timelines = ref([
icon: "tabler:home",
},
]);
const loadingAuth = ref(false);
const appData = useAppData();
const tokenData = useTokenData();
const client = useMegalodon();
const signIn = async () => {
loadingAuth.value = true;
const output = await client.value?.createApp("Lysand", {
scopes: ["read", "write", "follow", "push"],
redirect_uris: new URL("/", useRequestURL().origin).toString(),
website: useBaseUrl().value,
});
if (!output) {
alert("Failed to create app");
return;
}
appData.value = output;
const url = await client.value?.generateAuthUrl(
output.client_id,
output.client_secret,
{
scope: ["read", "write", "follow", "push"],
redirect_uri: new URL("/", useRequestURL().origin).toString(),
},
);
if (!url) {
alert("Failed to generate auth URL");
return;
}
window.location.href = url;
};
const signOut = async () => {
loadingAuth.value = true;
if (!appData.value || !tokenData.value) {
console.error("No app or token data to sign out");
return;
}
// Don't do anything on error, as Lysand doesn't implement the revoke endpoint yet
await client.value
?.revokeToken(
appData.value.client_id,
tokenData.value.access_token,
tokenData.value.access_token,
)
.catch(() => {});
tokenData.value = null;
};
</script>
2 changes: 1 addition & 1 deletion components/social-elements/notes/header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
</template>

<script lang="ts" setup>
import type { Status } from '~/types/mastodon/status';
import type { Status } from "~/types/mastodon/status";
const props = defineProps<{
note?: Status;
Expand Down
25 changes: 18 additions & 7 deletions components/social-elements/notes/note.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,30 @@ const props = defineProps<{
// Handle reblogs
const note = computed(() => props.note?.reblog ?? props.note);
const noteClosed = ref(note.value?.sensitive || !!note.value?.spoiler_text || false);
const noteClosed = ref(
note.value?.sensitive || !!note.value?.spoiler_text || false,
);
const { copy } = useClipboard();
const client = useMegalodon();
const mentions = await useResolveMentions(note.value?.mentions ?? [], client);
const eventualReblogAccountName = props.note?.reblog ? (useParsedContent(props.note?.account.display_name, props.note?.account.emojis, mentions.value)).value : null;
const mentions = await useResolveMentions(
note.value?.mentions ?? [],
client.value,
);
const eventualReblogAccountName = props.note?.reblog
? useParsedContent(
props.note?.account.display_name,
props.note?.account.emojis,
mentions.value,
).value
: null;
const content =
note.value && process.client
? useParsedContent(
note.value.content,
note.value.emojis,
mentions.value,
)
note.value.content,
note.value.emojis,
mentions.value,
)
: "";
const numberFormat = (number = 0) =>
new Intl.NumberFormat(undefined, {
Expand Down
58 changes: 31 additions & 27 deletions components/social-elements/notifications/notif.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,52 @@
</template>

<script lang="ts" setup>
import type { Notification } from '~/types/mastodon/notification';
import type { Notification } from "~/types/mastodon/notification";
const props = defineProps<{
notification?: Notification;
}>();
const accountName = useParsedContent(props.notification?.account?.display_name ?? '', props.notification?.account?.emojis ?? [], []);
const accountName = useParsedContent(
props.notification?.account?.display_name ?? "",
props.notification?.account?.emojis ?? [],
[],
);
const text = computed(() => {
if (!props.notification) return '';
if (!props.notification) return "";
switch (props.notification.type) {
case 'mention':
return 'mentioned you';
case 'reblog':
return 'reblogged your note';
case 'favourite':
return 'liked your note';
case 'follow':
return 'followed you';
case 'follow_request':
return 'requested to follow you';
case "mention":
return "mentioned you";
case "reblog":
return "reblogged your note";
case "favourite":
return "liked your note";
case "follow":
return "followed you";
case "follow_request":
return "requested to follow you";
default:
console.error('Unknown notification type', props.notification.type)
return '';
console.error("Unknown notification type", props.notification.type);
return "";
}
});
const icon = computed(() => {
if (!props.notification) return '';
if (!props.notification) return "";
switch (props.notification.type) {
case 'mention':
return 'tabler:at';
case 'reblog':
return 'tabler:repeat';
case 'favourite':
return 'tabler:heart';
case 'follow':
return 'tabler:plus';
case 'follow_request':
return 'tabler:plus';
case "mention":
return "tabler:at";
case "reblog":
return "tabler:repeat";
case "favourite":
return "tabler:heart";
case "follow":
return "tabler:plus";
case "follow_request":
return "tabler:plus";
default:
return '';
return "";
}
});
</script>
36 changes: 17 additions & 19 deletions components/social-elements/users/Account.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,29 +115,27 @@ watch(
skeleton,
async () => {
if (skeleton.value) return;
parsedNote.value = (
parsedNote.value =
useParsedContent(
props.account?.note ?? "",
props.account?.emojis ?? [],
[],
)
).value ?? "";
parsedFields.value = props.account?.fields.map((field) => ({
name: (
useParsedContent(
field.name,
props.account?.emojis ?? [],
[],
)
).value ?? "",
value: (
useParsedContent(
field.value,
props.account?.emojis ?? [],
[],
)
).value ?? "",
})) ?? [];
).value ?? "";
parsedFields.value =
props.account?.fields.map((field) => ({
name:
useParsedContent(
field.name,
props.account?.emojis ?? [],
[],
).value ?? "",
value:
useParsedContent(
field.value,
props.account?.emojis ?? [],
[],
).value ?? "",
})) ?? [];
},
{
immediate: true,
Expand Down
6 changes: 3 additions & 3 deletions components/timelines/Notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
</template>

<script lang="ts" setup>
const access_token = useLocalStorage("lysand:access_token", null);
const client = useMegalodon(access_token);
const tokenData = useTokenData();
const client = useMegalodon(tokenData);
const isLoading = ref(true);
const timelineParameters = ref({});
const hasReachedEnd = ref(false);
const { timeline, loadNext, loadPrev } = useNotificationTimeline(
client,
client.value,
timelineParameters,
);
const skeleton = ref<HTMLSpanElement | null>(null);
Expand Down
2 changes: 1 addition & 1 deletion components/timelines/Public.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const isLoading = ref(true);
const timelineParameters = ref({});
const hasReachedEnd = ref(false);
const { timeline, loadNext, loadPrev } = usePublicTimeline(
client,
client.value,
timelineParameters,
);
const skeleton = ref<HTMLSpanElement | null>(null);
Expand Down
8 changes: 8 additions & 0 deletions composables/AccessToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { StorageSerializers } from "@vueuse/core";
import type { OAuth } from "megalodon";

export const useTokenData = () => {
return useLocalStorage<OAuth.TokenData | null>("lysand:token_data", null, {
serializer: StorageSerializers.object,
});
};
Loading

0 comments on commit 3c8093a

Please sign in to comment.