Sort members list #45
20 changed files with 231 additions and 34 deletions
|
@ -23,3 +23,17 @@ steps:
|
|||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
|
||||
- name: container-build-and-publish (staging)
|
||||
image: docker
|
||||
commands:
|
||||
- docker login --username radical --password $PASSWORD git.gorb.app
|
||||
- docker buildx build --platform linux/amd64,linux/arm64 --rm --push -t git.gorb.app/gorb/frontend:staging .
|
||||
environment:
|
||||
PASSWORD:
|
||||
from_secret: docker_password
|
||||
volumes:
|
||||
- /var/run/podman/podman.sock:/var/run/docker.sock
|
||||
when:
|
||||
- branch: staging
|
||||
event: push
|
||||
|
|
|
@ -26,9 +26,7 @@
|
|||
<script lang="ts" setup>
|
||||
const { fetchFriends } = useApi();
|
||||
|
||||
const friends = await fetchFriends().then((response) => {
|
||||
return response.sort((a, b) => getDisplayName(a).localeCompare(getDisplayName(b)))
|
||||
})
|
||||
const friends = sortUsers(await fetchFriends())
|
||||
|
||||
const props = defineProps<{
|
||||
variant: string
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
<script lang="ts" setup>
|
||||
import type { UserResponse } from '~/types/interfaces';
|
||||
|
||||
const { fetchMembers } = useApi();
|
||||
|
||||
const props = defineProps<{
|
||||
user: UserResponse
|
||||
}>();
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div @click="props.callback()" class="button" :class="props.variant + '-button'">
|
||||
<button @click="props.callback ? props.callback() : null" class="button" :class="props.variant + '-button'">
|
||||
{{ props.text }}
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
const props = defineProps<{
|
||||
text: string,
|
||||
callback: CallableFunction,
|
||||
callback?: CallableFunction,
|
||||
variant?: "normal" | "scary" | "neutral",
|
||||
}>();
|
||||
|
||||
|
@ -28,6 +28,8 @@ const props = defineProps<{
|
|||
border-radius: 0.7rem;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
|
|
|
@ -1,24 +1,36 @@
|
|||
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse, UserResponse } from "~/types/interfaces";
|
||||
|
||||
function ensureIsArray(list: any) {
|
||||
if (Array.isArray(list)) {
|
||||
return list
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const useApi = () => {
|
||||
async function fetchGuilds(): Promise<GuildResponse[] | undefined> {
|
||||
return await fetchWithApi(`/guilds`);
|
||||
async function fetchGuilds(): Promise<GuildResponse[]> {
|
||||
return ensureIsArray(await fetchWithApi(`/guilds`));
|
||||
}
|
||||
|
||||
async function fetchGuild(guildId: string): Promise<GuildResponse | undefined> {
|
||||
return await fetchWithApi(`/guilds/${guildId}`);
|
||||
}
|
||||
|
||||
async function fetchChannels(guildId: string): Promise<ChannelResponse[] | undefined> {
|
||||
return await fetchWithApi(`/guilds/${guildId}/channels`);
|
||||
async function fetchMyGuilds(): Promise<GuildResponse[]> {
|
||||
return ensureIsArray(await fetchWithApi(`/me/guilds`));
|
||||
}
|
||||
|
||||
async function fetchChannels(guildId: string): Promise<ChannelResponse[]> {
|
||||
return ensureIsArray(await fetchWithApi(`/guilds/${guildId}/channels`));
|
||||
}
|
||||
|
||||
async function fetchChannel(channelId: string): Promise<ChannelResponse | undefined> {
|
||||
return await fetchWithApi(`/channels/${channelId}`)
|
||||
}
|
||||
|
||||
async function fetchMembers(guildId: string): Promise<GuildMemberResponse[] | undefined> {
|
||||
return await fetchWithApi(`/guilds/${guildId}/members`);
|
||||
async function fetchMembers(guildId: string): Promise<GuildMemberResponse[]> {
|
||||
return ensureIsArray(await fetchWithApi(`/guilds/${guildId}/members`));
|
||||
}
|
||||
|
||||
async function fetchMember(guildId: string, memberId: string): Promise<GuildMemberResponse | undefined> {
|
||||
|
@ -34,12 +46,7 @@ export const useApi = () => {
|
|||
}
|
||||
|
||||
async function fetchFriends(): Promise<UserResponse[]> {
|
||||
const response = await fetchWithApi('/me/friends')
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return ensureIsArray(await fetchWithApi('/me/friends'));
|
||||
}
|
||||
|
||||
async function addFriend(username: string): Promise<void> {
|
||||
|
@ -79,9 +86,18 @@ export const useApi = () => {
|
|||
await fetchWithApi("/auth/verify-email", { method: "POST", body: { email } });
|
||||
}
|
||||
|
||||
async function sendPasswordResetEmail(identifier: string): Promise<void> {
|
||||
await fetchWithApi("/auth/reset-password", { method: "GET", query: { identifier } });
|
||||
}
|
||||
|
||||
async function resetPassword(password: string, token: string) {
|
||||
await fetchWithApi("/auth/reset-password", { method: "POST", body: { password, token } });
|
||||
}
|
||||
|
||||
return {
|
||||
fetchGuilds,
|
||||
fetchGuild,
|
||||
fetchMyGuilds,
|
||||
fetchChannels,
|
||||
fetchChannel,
|
||||
fetchMembers,
|
||||
|
@ -97,6 +113,8 @@ export const useApi = () => {
|
|||
joinGuild,
|
||||
createChannel,
|
||||
fetchInstanceStats,
|
||||
sendVerificationEmail
|
||||
sendVerificationEmail,
|
||||
sendPasswordResetEmail,
|
||||
resetPassword
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
</div>
|
||||
<div v-else id="auth-form-container">
|
||||
<slot />
|
||||
<div v-if="!['/recover', '/reset-password'].includes(route.path)">Forgot password? Recover <NuxtLink href="/recover">here</NuxtLink>!</div>
|
||||
</div>
|
||||
<div v-if="instanceUrl">
|
||||
Instance URL is set to <span style="color: var(--primary-color);">{{ instanceUrl }}</span>
|
||||
|
@ -36,7 +37,11 @@ const apiVersion = useRuntimeConfig().public.apiVersion;
|
|||
const apiBase = useCookie("api_base");
|
||||
const registrationEnabled = useState("registrationEnabled", () => true);
|
||||
|
||||
const auth = useAuth();
|
||||
const route = useRoute();
|
||||
|
||||
const query = route.query as Record<string, string>;
|
||||
const searchParams = new URLSearchParams(query);
|
||||
searchParams.delete("token");
|
||||
|
||||
onMounted(async () => {
|
||||
instanceUrl.value = useCookie("instance_url").value;
|
||||
|
@ -111,6 +116,7 @@ const form = reactive({
|
|||
#auth-form-container form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
margin-top: 10dvh;
|
||||
gap: 1em;
|
||||
|
|
|
@ -95,7 +95,7 @@ const options = [
|
|||
if (invite.length == 6) {
|
||||
try {
|
||||
const joinedGuild = await api.joinGuild(invite);
|
||||
guilds?.push(joinedGuild);
|
||||
guilds.push(joinedGuild);
|
||||
return await navigateTo(`/servers/${joinedGuild.uuid}`);
|
||||
} catch (error) {
|
||||
alert(`Couldn't use invite: ${error}`);
|
||||
|
@ -151,7 +151,7 @@ const options = [
|
|||
}
|
||||
];
|
||||
|
||||
const guilds: GuildResponse[] | undefined = await fetchWithApi("/me/guilds");
|
||||
const guilds = await api.fetchMyGuilds();
|
||||
|
||||
function createDropdown() {
|
||||
const dropdown = h(GuildDropdown, { options });
|
||||
|
|
|
@ -16,7 +16,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (["/login", "/register"].includes(to.path) && !Object.keys(to.query).includes("special")) {
|
||||
if (["/login", "/register", "/recover", "/reset-password"].includes(to.path) && !Object.keys(to.query).includes("special")) {
|
||||
console.log("path is login or register");
|
||||
const apiBase = useCookie("api_base");
|
||||
console.log("apiBase gotten:", apiBase.value);
|
||||
|
|
|
@ -5,10 +5,10 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||
|
||||
const guildId = to.params.serverId as string;
|
||||
|
||||
const channels: ChannelResponse[] | undefined = await fetchChannels(guildId);
|
||||
const channels: ChannelResponse[] = await fetchChannels(guildId);
|
||||
console.log("channels:", channels);
|
||||
|
||||
if (channels && channels.length > 0) {
|
||||
if (channels.length > 0) {
|
||||
console.log("wah");
|
||||
return await navigateTo(`/servers/${guildId}/channels/${channels[0].uuid}`, { replace: true });
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ const form = reactive({
|
|||
|
||||
const query = useRoute().query as Record<string, string>;
|
||||
const searchParams = new URLSearchParams(query);
|
||||
searchParams.delete("token");
|
||||
|
||||
const registrationEnabled = ref<boolean>(true);
|
||||
const apiBase = useCookie("api_base");
|
||||
|
@ -48,7 +49,7 @@ if (apiBase.value) {
|
|||
}
|
||||
}
|
||||
|
||||
const registerUrl = `/register?${searchParams}`
|
||||
const registerUrl = `/register?${searchParams}`;
|
||||
|
||||
const { login } = useAuth();
|
||||
|
||||
|
|
89
pages/recover.vue
Normal file
89
pages/recover.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div v-if="errorValue">{{ errorValue }}</div>
|
||||
<form v-if="!emailFormSent" @submit.prevent="sendEmail">
|
||||
<div>
|
||||
<label for="identifier">Email or username</label>
|
||||
<br>
|
||||
<input type="text" name="identifier" id="identifier" v-model="emailForm.identifier">
|
||||
</div>
|
||||
<div>
|
||||
<Button type="submit" text="Send email" variant="normal" />
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
If an account with that username or email exists, an email will be sent to it shortly.
|
||||
</div>
|
||||
<div v-if="registrationEnabled">
|
||||
Don't have an account? <NuxtLink :href="registerUrl">Register</NuxtLink> one!
|
||||
</div>
|
||||
<div>
|
||||
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Button from '~/components/UserInterface/Button.vue';
|
||||
|
||||
const emailForm = reactive({
|
||||
identifier: ""
|
||||
});
|
||||
|
||||
const emailFormSent = ref(false);
|
||||
|
||||
const passwordForm = reactive({
|
||||
password: ""
|
||||
});
|
||||
|
||||
const errorValue = ref<string>();
|
||||
|
||||
const registrationEnabled = ref<boolean>(true);
|
||||
const apiBase = useCookie("api_base");
|
||||
|
||||
const query = useRoute().query as Record<string, string>;
|
||||
const searchParams = new URLSearchParams(query);
|
||||
|
||||
const token = ref(searchParams.get("token"))
|
||||
searchParams.delete("token");
|
||||
const { resetPassword } = useApi();
|
||||
|
||||
const registerUrl = `/register?${searchParams}`;
|
||||
const loginUrl = `/login?${searchParams}`;
|
||||
|
||||
if (apiBase.value) {
|
||||
console.log("apiBase:", apiBase.value);
|
||||
const stats = await useApi().fetchInstanceStats(apiBase.value);
|
||||
if (stats) {
|
||||
registrationEnabled.value = stats.registration_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
const { sendPasswordResetEmail } = useApi();
|
||||
|
||||
async function sendEmail() {
|
||||
try {
|
||||
await sendPasswordResetEmail(emailForm.identifier);
|
||||
emailFormSent.value = true;
|
||||
} catch (error) {
|
||||
errorValue.value = (error as any).toString();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPassword() {
|
||||
try {
|
||||
console.log("pass:", passwordForm.password);
|
||||
const hashedPass = await hashPassword(passwordForm.password)
|
||||
console.log("hashed pass:", hashedPass);
|
||||
await resetPassword(hashedPass, token.value!);
|
||||
return await navigateTo(`/login?${searchParams}`);
|
||||
} catch (error) {
|
||||
errorValue.value = (error as any).toString();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
|
@ -86,6 +86,7 @@ const auth = useAuth();
|
|||
const loggedIn = ref(await auth.getUser());
|
||||
|
||||
const query = new URLSearchParams(useRoute().query as Record<string, string>);
|
||||
query.delete("token");
|
||||
|
||||
const user = await useAuth().getUser();
|
||||
|
||||
|
|
56
pages/reset-password.vue
Normal file
56
pages/reset-password.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div v-if="errorValue">{{ errorValue }}</div>
|
||||
<form @submit.prevent="sendPassword">
|
||||
<div>
|
||||
<label for="password">Password</label>
|
||||
<br>
|
||||
<input type="password" name="password" id="password" v-model="passwordForm.password">
|
||||
</div>
|
||||
<div>
|
||||
<Button type="submit" text="Submit" variant="normal" />
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Button from '~/components/UserInterface/Button.vue';
|
||||
|
||||
const query = useRoute().query as Record<string, string>;
|
||||
const searchParams = new URLSearchParams(query);
|
||||
|
||||
const loginUrl = `/login?${searchParams}`;
|
||||
|
||||
const token = ref(searchParams.get("token"))
|
||||
|
||||
if (!token.value) await navigateTo("/login");
|
||||
|
||||
const passwordForm = reactive({
|
||||
password: ""
|
||||
});
|
||||
|
||||
const errorValue = ref<string>();
|
||||
|
||||
const { resetPassword } = useApi();
|
||||
|
||||
async function sendPassword() {
|
||||
try {
|
||||
console.log("pass:", passwordForm.password);
|
||||
const hashedPass = await hashPassword(passwordForm.password)
|
||||
console.log("hashed pass:", hashedPass);
|
||||
await resetPassword(hashedPass, token.value!);
|
||||
return await navigateTo("/login?");
|
||||
} catch (error) {
|
||||
errorValue.value = (error as any).toString();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
|
@ -64,7 +64,7 @@ onActivated(async () => {
|
|||
});
|
||||
|
||||
async function setArrayVariables() {
|
||||
members.value = await fetchMembers(route.params.serverId as string);
|
||||
members.value = sortMembers(await fetchMembers(route.params.serverId as string))
|
||||
const guildUrl = `guilds/${route.params.serverId}`;
|
||||
channels.value = await fetchWithApi(`${guildUrl}/channels`);
|
||||
console.log("channels:", channels.value);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { GuildMemberResponse, UserResponse } from "~/types/interfaces";
|
||||
|
||||
export function getDisplayName(user: UserResponse, member?: GuildMemberResponse): string {
|
||||
export default (user: UserResponse, member?: GuildMemberResponse): string => {
|
||||
if (member?.nickname) return member.nickname
|
||||
if (user.display_name) return user.display_name
|
||||
return user.username
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export async function hashPassword(password: string) {
|
||||
export default async (password: string) => {
|
||||
const encodedPass = new TextEncoder().encode(password);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-384", encodedPass);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
7
utils/sortMembers.ts
Normal file
7
utils/sortMembers.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { GuildMemberResponse } from "~/types/interfaces";
|
||||
|
||||
export default (members: GuildMemberResponse[]): GuildMemberResponse[] => {
|
||||
twig marked this conversation as resolved
Outdated
|
||||
return members.sort((a, b) => {
|
||||
return getDisplayName(a.user, a).localeCompare(getDisplayName(b.user, b))
|
||||
})
|
||||
}
|
7
utils/sortUsers.ts
Normal file
7
utils/sortUsers.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { UserResponse } from "~/types/interfaces";
|
||||
|
||||
export default (users: UserResponse[]): UserResponse[] => {
|
||||
twig marked this conversation as resolved
Outdated
sauceyred
commented
No need to name function here, name is derived from file name No need to name function here, name is derived from file name
|
||||
return users.sort((a, b) => {
|
||||
return getDisplayName(a).localeCompare(getDisplayName(b))
|
||||
})
|
||||
}
|
3
utils/validateUsername.ts
Normal file
3
utils/validateUsername.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default (username: string) => {
|
||||
return /^[\w.-]+$/.test(username);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export function validateUsername(username: string) {
|
||||
return /^[\w.-]+$/.test(username);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue
No need to name function here, name is derived from file name