This commit is contained in:
Hayden 2022-08-30 18:11:36 -08:00
parent 682774c9ce
commit 630fe83de5
5 changed files with 184 additions and 146 deletions

View file

@ -1,9 +1,10 @@
{ {
"arrowParens": "avoid", "arrowParens": "avoid",
"semi": true, "semi": true,
"tabWidth": 4, "tabWidth": 2,
"useTabs": true, "useTabs": false,
"vueIndentScriptAndStyle": true, "vueIndentScriptAndStyle": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5" "trailingComma": "es5",
"printWidth": 120
} }

View file

@ -1,18 +1,22 @@
import { BaseAPI, UrlBuilder } from './base'; import { BaseAPI, UrlBuilder } from './base';
export type Result<T> = { export type Result<T> = {
item: T; item: T;
}; };
export type User = { export type User = {
name: string; name: string;
email: string; email: string;
isSuperuser: boolean; isSuperuser: boolean;
id: number; id: number;
}; };
export class UserApi extends BaseAPI { export class UserApi extends BaseAPI {
public self() { public self() {
return this.http.get<Result<User>>(UrlBuilder('/users/self')); return this.http.get<Result<User>>(UrlBuilder('/users/self'));
} }
public logout() {
return this.http.post<object, void>(UrlBuilder('/users/logout'), {});
}
} }

View file

@ -1,95 +1,97 @@
export enum Method { export enum Method {
GET = 'GET', GET = 'GET',
POST = 'POST', POST = 'POST',
PUT = 'PUT', PUT = 'PUT',
DELETE = 'DELETE', DELETE = 'DELETE',
} }
export interface TResponse<T> { export interface TResponse<T> {
status: number; status: number;
error: boolean; error: boolean;
data: T; data: T;
response: Response; response: Response;
} }
export class Requests { export class Requests {
private baseUrl: string; private baseUrl: string;
private token: () => string; private token: () => string;
private headers: Record<string, string> = {}; private headers: Record<string, string> = {};
private logger?: (response: Response) => void; private logger?: (response: Response) => void;
private url(rest: string): string { private url(rest: string): string {
return this.baseUrl + rest; return this.baseUrl + rest;
} }
constructor( constructor(
baseUrl: string, baseUrl: string,
token: string | (() => string) = '', token: string | (() => string) = '',
headers: Record<string, string> = {}, headers: Record<string, string> = {},
logger?: (response: Response) => void logger?: (response: Response) => void
) { ) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.token = typeof token === 'string' ? () => token : token; this.token = typeof token === 'string' ? () => token : token;
this.headers = headers; this.headers = headers;
this.logger = logger; this.logger = logger;
} }
public get<T>(url: string): Promise<TResponse<T>> { public get<T>(url: string): Promise<TResponse<T>> {
return this.do<T>(Method.GET, url); return this.do<T>(Method.GET, url);
} }
public post<T, U>(url: string, payload: T): Promise<TResponse<U>> { public post<T, U>(url: string, payload: T): Promise<TResponse<U>> {
return this.do<U>(Method.POST, url, payload); return this.do<U>(Method.POST, url, payload);
} }
public put<T, U>(url: string, payload: T): Promise<TResponse<U>> { public put<T, U>(url: string, payload: T): Promise<TResponse<U>> {
return this.do<U>(Method.PUT, url, payload); return this.do<U>(Method.PUT, url, payload);
} }
public delete<T>(url: string): Promise<TResponse<T>> { public delete<T>(url: string): Promise<TResponse<T>> {
return this.do<T>(Method.DELETE, url); return this.do<T>(Method.DELETE, url);
} }
private methodSupportsBody(method: Method): boolean { private methodSupportsBody(method: Method): boolean {
return method === Method.POST || method === Method.PUT; return method === Method.POST || method === Method.PUT;
} }
private async do<T>( private async do<T>(method: Method, url: string, payload: Object = {}): Promise<TResponse<T>> {
method: Method, const args: RequestInit = {
url: string, method,
payload: Object = {} headers: {
): Promise<TResponse<T>> { 'Content-Type': 'application/json',
const args: RequestInit = { ...this.headers,
method, },
headers: { };
'Content-Type': 'application/json',
...this.headers,
},
};
const token = this.token(); const token = this.token();
if (token !== '' && args.headers !== undefined) { if (token !== '' && args.headers !== undefined) {
// @ts-expect-error -- headers is always defined at this point // @ts-expect-error -- headers is always defined at this point
args.headers['Authorization'] = token; args.headers['Authorization'] = token;
} }
if (this.methodSupportsBody(method)) { if (this.methodSupportsBody(method)) {
args.body = JSON.stringify(payload); args.body = JSON.stringify(payload);
} }
const response = await fetch(this.url(url), args); const response = await fetch(this.url(url), args);
if (this.logger) { if (this.logger) {
this.logger(response); this.logger(response);
} }
const data = await response.json(); const data: T = await (async () => {
try {
return await response.json();
} catch (e) {
return {} as T;
}
})();
return { return {
status: response.status, status: response.status,
error: !response.ok, error: !response.ok,
data, data,
response, response,
}; };
} }
} }

View file

@ -1,57 +1,73 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUserApi } from '@/composables/use-api'; import { useUserApi } from '@/composables/use-api';
useHead({ import { useAuthStore } from '@/store/auth';
title: 'Homebox | Home', useHead({
}); title: 'Homebox | Home',
});
const links = [ const api = useUserApi();
{
name: 'Home',
href: '/home',
},
{
name: 'Logout',
href: '/logout',
last: true,
},
];
const api = useUserApi();
const user = ref({}); const user = ref({});
onMounted(async () => { onMounted(async () => {
const { data } = await api.self(); const { data } = await api.self();
if (data) { if (data) {
user.value = data.item; user.value = data.item;
} }
}); });
const authStore = useAuthStore();
const router = useRouter();
async function logout() {
const { error } = await authStore.logout(api);
if (error) {
console.error(error);
return;
}
router.push('/');
}
const links = [
{
name: 'Home',
href: '/home',
},
{
name: 'Logout',
action: logout,
last: true,
},
];
</script> </script>
<template> <template>
<section class="max-w-7xl mx-auto"> <section class="max-w-7xl mx-auto">
<header class="sm:px-6 py-2 lg:p-14 sm:py-6"> <header class="sm:px-6 py-2 lg:p-14 sm:py-6">
<h2 <h2 class="mt-1 text-4xl font-bold tracking-tight text-gray-200 sm:text-5xl lg:text-6xl">Homebox</h2>
class="mt-1 text-4xl font-bold tracking-tight text-gray-200 sm:text-5xl lg:text-6xl" <div class="ml-1 text-lg text-gray-400 space-x-2">
> <template v-for="link in links">
Homebox <router-link
</h2> v-if="!link.action"
<div class="ml-1 text-lg text-gray-400 space-x-2 italic"> class="hover:text-base-content transition-color duration-200 italic"
<template v-for="link in links"> :to="link.href"
<router-link >
class="hover:text-base-content transition-color duration-200" {{ link.name }}
:to="link.href" </router-link>
> <button v-else @click="link.action" class="hover:text-base-content transition-color duration-200 italic">
{{ link.name }} {{ link.name }}
</router-link> </button>
<span v-if="!link.last"> / </span> <span v-if="!link.last"> / </span>
</template> </template>
</div> </div>
</header> </header>
</section> </section>
<section class="max-w-7xl mx-auto sm:px-6 lg:px-14"> <section class="max-w-7xl mx-auto sm:px-6 lg:px-14">
{{ user }} {{ user }}
</section> </section>
</template> </template>
<route lang="yaml"> <route lang="yaml">

View file

@ -1,21 +1,36 @@
import { UserApi } from '@/api/user';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () => ({ state: () => ({
token: useLocalStorage('pinia/auth/token', ''), token: useLocalStorage('pinia/auth/token', ''),
expires: useLocalStorage('pinia/auth/expires', ''), expires: useLocalStorage('pinia/auth/expires', ''),
}), }),
getters: { getters: {
isTokenExpired: state => { isTokenExpired: state => {
if (!state.expires) { if (!state.expires) {
return true; return true;
} }
if (typeof state.expires === 'string') { if (typeof state.expires === 'string') {
return new Date(state.expires) < new Date(); return new Date(state.expires) < new Date();
} }
return state.expires < new Date(); return state.expires < new Date();
}, },
}, },
actions: {
async logout(api: UserApi) {
const result = await api.logout();
if (result.error) {
return result;
}
this.token = '';
this.expires = '';
return result;
},
},
}); });