fix: api client, auth workflow

This commit is contained in:
L-Nafaryus 2024-07-08 16:26:35 +05:00
parent aef6c2b541
commit ec41110e0b
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
14 changed files with 199 additions and 124 deletions

View File

@ -1,4 +1,4 @@
import { client, type ResponseError, handle_error } from "@/api/client"; import { api_client, type ResponseError, handle_error } from "@/client";
export interface UserCredentials { export interface UserCredentials {
name: string, name: string,
@ -7,17 +7,16 @@ export interface UserCredentials {
} }
export async function signup(body: UserCredentials): Promise<null | ResponseError> { export async function signup(body: UserCredentials): Promise<null | ResponseError> {
return await client.post("/auth/signup", JSON.stringify(body)) return await api_client.post("/auth/signup", JSON.stringify(body))
.catch(handle_error); .catch(handle_error);
} }
export async function signin(body: UserCredentials): Promise<null | ResponseError> { export async function signin(body: UserCredentials): Promise<null | ResponseError> {
return await client.post("/auth/signin", JSON.stringify(body)) return await api_client.post("/auth/signin", JSON.stringify(body))
.catch(handle_error); .catch(handle_error);
} }
export async function signout(): Promise<null | ResponseError> { export async function signout(): Promise<null | ResponseError> {
return await client.post("/auth/signout") return await api_client.get("/auth/signout")
.catch(handle_error); .catch(handle_error);
} }

View File

@ -1,2 +1,3 @@
export * as auth from "@/api/auth"; export * as auth from "@/api/auth";
export * as user from "@/api/user"; export * as user from "@/api/user";
export * as repository from "@/api/repository";

View File

@ -0,0 +1,23 @@
import { api_client, type ResponseError, handle_error } from "@/client";
export interface RepositoryInfo {
id: number,
capacity: number,
used?: number
}
export async function info(): Promise<RepositoryInfo | ResponseError> {
return await api_client.get("/repository")
.then(async response => { return Promise.resolve<RepositoryInfo>(response.data); })
.catch(handle_error);
}
export async function create(): Promise<null | ResponseError> {
return await api_client.post("/repository")
.catch(handle_error);
}
export async function remove(): Promise<null | ResponseError> {
return await api_client.delete("/repository")
.catch(handle_error);
}

View File

@ -1,4 +1,4 @@
import { client, upload_client, resources_client, type ResponseError, handle_error } from "@/api/client"; import { api_client, type ResponseError, handle_error } from "@/client";
export interface UserCredentials { export interface UserCredentials {
name: string, name: string,
@ -6,77 +6,30 @@ export interface UserCredentials {
email?: string email?: string
} }
export async function signup(body: UserCredentials): Promise<null | ResponseError> { export interface UserInfo {
return await client.post("/auth/signup", JSON.stringify(body)) id: string,
.catch(handle_error); name: string,
lower_name: string,
full_name?: string,
email?: string,
is_email_private: boolean,
must_change_password: boolean,
login_type: string,
created: number,
updated: number,
last_login?: number,
is_active: boolean,
is_admin: boolean,
avatar?: string
} }
export async function signin(body: UserCredentials): Promise<null | ResponseError> {
return await client.post("/auth/signin", JSON.stringify(body))
.catch(handle_error);
}
export type Image = string | ArrayBuffer; export type Image = string | ArrayBuffer;
export async function register(body: NewUser): Promise<User | ResponseError> { export async function info(): Promise<UserInfo | ResponseError> {
return await client.post("/user/register", JSON.stringify(body)) return await api_client.get("/user")
.then(async response => { return Promise.resolve<User>(response.data); }) .then(async response => { return Promise.resolve<UserInfo>(response.data); })
.catch(handle_error); .catch(handle_error);
} }
export async function login(body: LoginUser): Promise<User | ResponseError> {
return await client.post("/user/login", JSON.stringify(body))
.then(async response => { return Promise.resolve<User>(response.data); })
.catch(handle_error);
}
export async function remove(body: RemoveUser): Promise<null | ResponseError> {
return await client.post("/user/remove", JSON.stringify(body))
.then(async () => { return Promise.resolve(null); })
.catch(handle_error);
}
export async function logout(): Promise<null | ResponseError> {
return await client.get("/user/logout")
.then(async () => { return Promise.resolve(null); })
.catch(handle_error);
}
export async function current(): Promise<User | ResponseError> {
return await client.get("/user/current")
.then(async response => { return Promise.resolve<User>(response.data); })
.catch(handle_error);
}
export async function avatar(file: FormData, progress?: any): Promise<null | ResponseError> {
return await upload_client.post("/user/avatar", file, {
onUploadProgress: progress ?? null,
//headers: { "Accept-Encoding": "gzip" }
})
.then(async () => { return Promise.resolve(null); })
.catch(handle_error);
}
export async function get_avatar(avatar: string): Promise<Image | null | ResponseError> {
return await resources_client.get("/avatars/".concat(avatar))
.then(async response => {
return new Promise<Image | null>((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsDataURL(response.data);
})
})
.catch(handle_error);
}
export async function profile(login: string): Promise<User | ResponseError> {
return await client.get("/user/".concat(login))
.then(async response => { return Promise.resolve<User>(response.data); })
.catch(handle_error);
}

View File

@ -22,11 +22,15 @@
} }
.button { .button {
@apply pt-2 pb-2 pl-5 pr-5 rounded bg-ctp-mantle hover:bg-ctp-base text-ctp-blue; @apply pt-2 pb-2 pl-5 pr-5 rounded bg-ctp-mantle hover:bg-ctp-base text-ctp-blue cursor-pointer;
} }
.link-button { .link-button {
@apply button text-ctp-green; @apply button text-ctp-green cursor-pointer;
}
.hline {
@apply border-t border-ctp-overlay0 ml-0 mr-0;
} }
h1 { h1 {

View File

@ -30,7 +30,7 @@ export function handle_error(error: AxiosError): Promise<ResponseError> {
const debug = import.meta.hot; const debug = import.meta.hot;
export const client: AxiosInstance = axios.create({ export const api_client: AxiosInstance = axios.create({
baseURL: debug ? "http://localhost:54601/api" : "/api", baseURL: debug ? "http://localhost:54601/api" : "/api",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -38,17 +38,8 @@ export const client: AxiosInstance = axios.create({
withCredentials: true, withCredentials: true,
}); });
export const upload_client: AxiosInstance = axios.create({
baseURL: debug ? "http://localhost:54601/api" : "/api",
headers: {
"Content-Type": "multipart/form-data"
},
withCredentials: true,
});
export const resources_client: AxiosInstance = axios.create({ export const resources_client: AxiosInstance = axios.create({
baseURL: debug ? "http://localhost:54601/resources" : "/resources", baseURL: debug ? "http://localhost:54601/resources" : "/resources",
responseType: "blob" responseType: "blob"
}); });
export default client;

View File

@ -1 +1,2 @@
export * as api from "@/api"; export * as api from "@/api";
export * as resources from "@/resources";

View File

@ -0,0 +1,23 @@
import { resources_client, type ResponseError, handle_error } from "@/client";
export type Image = string | ArrayBuffer;
export async function avatars(avatar_id: string, format?: string): Promise<Image | null | ResponseError> {
return await resources_client.get("/avatars/".concat(avatar_id), { params: { format: format ? format : "png" }})
.then(async response => {
return new Promise<Image | null>((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsDataURL(response.data);
})
})
.catch(handle_error);
}

View File

@ -1,18 +1,16 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import { user } from "@/api";
import { useUserStore } from "@/stores"; import { useUserStore } from "@/stores";
import { api, resources } from "@";
async function check_authorized(): Promise<boolean> { async function check_authorized(): Promise<boolean> {
const userStore = useUserStore(); const userStore = useUserStore();
// TODO: add timer return await api.user.info()
return await user.current() .then(async user_info => { userStore.info = user_info; })
.then(async user => { userStore.current = user; })
.then(async () => { .then(async () => {
if (userStore.current.avatar?.length) { if (!userStore.avatar && userStore.info.avatar) {
await user.get_avatar(userStore.current.avatar) await resources.avatars(userStore.info.avatar)
.then(async avatar => { userStore.avatar = avatar; }) .then(async avatar => { userStore.avatar = avatar; })
} }
}) })
@ -69,8 +67,8 @@ const router = createRouter({
] ]
}, },
{ {
path: "/:user", name: "profile", beforeEnter: [bypass_auth], path: "/:user/repository", name: "repository", beforeEnter: [required_auth],
component: () => import("@/views/user/Profile.vue") component: () => import("@/views/Repository.vue")
}, },
{ {
path: "/admin/settings", name: "settings", beforeEnter: [required_auth, required_admin], path: "/admin/settings", name: "settings", beforeEnter: [required_auth, required_admin],

View File

@ -2,17 +2,18 @@ import { defineStore } from "pinia";
import { ref, type Ref } from "vue"; import { ref, type Ref } from "vue";
import { user } from "@/api"; import { user } from "@/api";
import { resources } from "@";
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore("user", () => {
const current: Ref<user.User | null> = ref(null); const info: Ref<user.UserInfo | null> = ref(null);
const avatar: Ref<Blob | null> = ref(null); const avatar: Ref<resources.Image | null> = ref(null);
function clear() { function clear() {
current.value = null; info.value = null;
avatar.value = null; avatar.value = null;
} }
return { current, avatar, clear }; return { info, avatar, clear };
}); });
export const useMiscStore = defineStore("misc", () => { export const useMiscStore = defineStore("misc", () => {

View File

@ -5,14 +5,15 @@ import DropdownMenu from "@/components/DropdownMenu.vue";
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import router from "@/router"; import router from "@/router";
import { user } from "@/api"; import { user, auth } from "@/api";
import { resources } from "@";
import { useUserStore } from "@/stores"; import { useUserStore } from "@/stores";
const userStore = useUserStore(); const userStore = useUserStore();
const error = ref(null); const error = ref(null);
async function signout() { async function signout() {
await user.logout() await auth.signout()
.then(async () => { .then(async () => {
userStore.clear(); userStore.clear();
router.push({ path: "/" }); router.push({ path: "/" });
@ -28,36 +29,36 @@ async function signout() {
<template #left> <template #left>
<RouterLink class="link-button" to="/">Home</RouterLink> <RouterLink class="link-button" to="/">Home</RouterLink>
</template> </template>
<template #right> <template #right v-if="userStore.info">
<DropdownMenu v-if="userStore.current"> <RouterLink v-if="userStore.info.is_admin" :to="{ name: 'settings' }" class="link-button">Settings
</RouterLink>
<DropdownMenu>
<template #button> <template #button>
<div class="pl-3 pr-3 flex gap-2 items-center rounded hover:bg-zinc-600 cursor-pointer"> <div class="link-button flex">
<div class="max-w-8" v-if="userStore.avatar"><img :src="userStore.avatar"></div> <span>{{ userStore.info.name }}</span>
<span class="flex min-w-9 min-h-9 items-center">{{userStore.current.login }}</span> <div class="max-w-6 ml-4" v-if="userStore.avatar">
<img :src="userStore.avatar">
</div>
</div> </div>
</template> </template>
<template #content> <template #content>
<div <div
class="absolute z-20 flex flex-col left-auto right-0 mt-4 bg-zinc-700 border rounded border-zinc-500 mr-3"> class="absolute z-20 flex flex-col left-auto right-0 mt-4 mr-3 bg-ctp-mantle border rounded border-ctp-overlay0">
<RouterLink :to="{ name: 'profile', params: { user: userStore.current.login } }" <RouterLink :to="{ name: 'repository', params: { user: userStore.info.lower_name } }"
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600"> class="button">
Profile</RouterLink> Repository</RouterLink>
<RouterLink :to="{ name: 'prefs' }" <RouterLink :to="{ name: 'prefs' }" class="button">
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600">
Preferencies</RouterLink> Preferencies</RouterLink>
<div class="border-t border-zinc-500 ml-0 mr-0"></div>
<RouterLink v-if="userStore.current.is_admin" :to="{ name: 'settings' }" <div class="hline"></div>
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600"> <div @click="signout" class="button">
Settings</RouterLink>
<div class="border-t border-zinc-500 ml-0 mr-0"></div>
<div @click="signout"
class="flex min-w-7 pl-5 pr-5 pt-1 pb-1 hover:bg-zinc-600 cursor-pointer">
Sign Out</div> Sign Out</div>
</div> </div>
</template> </template>
</DropdownMenu> </DropdownMenu>
</template>
<RouterLink v-if="!userStore.current" class="link-button" to="/user/login">Sign In</RouterLink> <template #right v-else>
<RouterLink class="link-button" to="/auth/signin">Sign In</RouterLink>
</template> </template>
</NavBar> </NavBar>
@ -68,8 +69,7 @@ async function signout() {
<div class="relative overflow-hidden h-full "> <div class="relative overflow-hidden h-full ">
</div> </div>
<footer <footer class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
<a href="/">Made with glove by Elnafo, 2024</a> <a href="/">Made with glove by Elnafo, 2024</a>
<div> <div>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/Error.vue";
import { ref, onMounted, watch } from "vue";
import { onBeforeRouteUpdate, useRoute } from "vue-router"
import { repository } from "@/api";
import { useUserStore } from "@/stores";
import router from "@/router";
const route = useRoute();
const userStore = useUserStore();
const error = ref<string>(null);
const repository_info = ref(null);
const is_created = ref(null);
onMounted(async () => {
await repository.info()
.then(async _repository_info => {
is_created.value = true;
repository_info.value = _repository_info;
})
.catch(err => {
is_created.value = false;
});
});
async function create_repository() {
await repository.create()
.then(async () => {
await repository.info()
.then(async _repository_info => {
repository_info.value = _repository_info;
})
.catch(err => {
error.value = err;
});
is_created.value = true;
})
.catch(err => {
error.value = err;
})
}
function round_size(size: number, mesure: string) {
if (mesure == "GB") {
return size / 8 / 1024 / 1024;
} else if (mesure == "MB") {
return size / 8 / 1024;
}
}
function size_procent() {
return Math.round(repository_info.value.used / repository_info.value.capacity) * 100;
}
</script>
<template>
<Base>
<Error v-if="error">{{ error }}</Error>
<section v-if="is_created">
<div class="flex items-center">
<div class="w-full rounded-full h-2.5 bg-ctp-surface0">
<div class="bg-ctp-lavender h-2.5 rounded-full" :style="{ width: size_procent() + '%' }"></div>
</div>
<span class="min-w-32 text-center">{{ round_size(repository_info.used, "GB") }} / {{
round_size(repository_info.capacity, "GB") }} GB</span>
</div>
</section>
<section v-else>
<p>It looks like you don't have a repository yet...</p>
<div class="flex justify-center mt-8">
<button @click="create_repository" class="button">+ Create repository</button>
</div>
</section>
</Base>
</template>

View File

@ -40,7 +40,7 @@ async function signin() {
await api.auth.signin(body) await api.auth.signin(body)
.then(async () => { .then(async () => {
//userStore.current = user; //userStore.info = user_info;
router.push({ path: "/" }); router.push({ path: "/" });
}) })
.catch(err => { error.value = err.message; }); .catch(err => { error.value = err.message; });
@ -62,7 +62,7 @@ async function signin() {
</div> </div>
<div class="mb-5 flex justify-between items-center"> <div class="mb-5 flex justify-between items-center">
<button @click="signin" class="button">Sign In</button> <button @click="signin" class="button">Sign In</button>
<button @click="$router.push('/user/register')" class="button">Sign Up</button> <button @click="$router.push('/auth/signup')" class="button">Sign Up</button>
</div> </div>
</form> </form>
<Error v-if="error">{{ error }}</Error> <Error v-if="error">{{ error }}</Error>

View File

@ -18,7 +18,7 @@ async function signup() {
return; return;
} }
await api.auth.signup({ name: login.value, password: password.value, email: email.value }) await api.auth.signup({ name: login.value, password: password.value, email: email.value })
.then(async user => { router.push({ path: "/user/login" }); }) .then(async user => { router.push({ path: "/auth/signin" }); })
.catch(err => { error.value = err.message; }); .catch(err => { error.value = err.message; });
}; };
</script> </script>