inject defaults + cleanup

This commit is contained in:
Hayden 2022-09-03 22:19:13 -08:00
parent 9b46ea7874
commit 5f589f95b8
8 changed files with 212 additions and 85 deletions

View file

@ -87,8 +87,21 @@ func mwStripTrailingSlash(next http.Handler) http.Handler {
}) })
} }
type StatusRecorder struct {
http.ResponseWriter
Status int
}
func (r *StatusRecorder) WriteHeader(status int) {
r.Status = status
r.ResponseWriter.WriteHeader(status)
}
func (a *app) mwStructLogger(next http.Handler) http.Handler { func (a *app) mwStructLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
record := &StatusRecorder{ResponseWriter: w, Status: http.StatusOK}
next.ServeHTTP(record, r)
scheme := "http" scheme := "http"
if r.TLS != nil { if r.TLS != nil {
scheme = "https" scheme = "https"
@ -101,25 +114,35 @@ func (a *app) mwStructLogger(next http.Handler) http.Handler {
Str("url", url). Str("url", url).
Str("method", r.Method). Str("method", r.Method).
Str("remote_addr", r.RemoteAddr). Str("remote_addr", r.RemoteAddr).
Msgf("[%s] %s", r.Method, url) Int("status", record.Status).
next.ServeHTTP(w, r) Msg(url)
}) })
} }
func (a *app) mwSummaryLogger(next http.Handler) http.Handler { func (a *app) mwSummaryLogger(next http.Handler) http.Handler {
bold := func(s string) string { bold := func(s string) string { return "\033[1m" + s + "\033[0m" }
return "\033[1m" + s + "\033[0m" orange := func(s string) string { return "\033[33m" + s + "\033[0m" }
} aqua := func(s string) string { return "\033[36m" + s + "\033[0m" }
red := func(s string) string { return "\033[31m" + s + "\033[0m" }
green := func(s string) string { return "\033[32m" + s + "\033[0m" }
pink := func(s string) string { fmtCode := func(code int) string {
return "\033[35m" + s + "\033[0m" switch {
} case code >= 500:
return red(fmt.Sprintf("%d", code))
aqua := func(s string) string { case code >= 400:
return "\033[36m" + s + "\033[0m" return orange(fmt.Sprintf("%d", code))
case code >= 300:
return aqua(fmt.Sprintf("%d", code))
default:
return green(fmt.Sprintf("%d", code))
}
} }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
record := &StatusRecorder{ResponseWriter: w, Status: http.StatusOK}
next.ServeHTTP(record, r) // Blocks until the next handler returns.
scheme := "http" scheme := "http"
if r.TLS != nil { if r.TLS != nil {
scheme = "https" scheme = "https"
@ -127,7 +150,11 @@ func (a *app) mwSummaryLogger(next http.Handler) http.Handler {
url := fmt.Sprintf("%s://%s%s %s", scheme, r.Host, r.RequestURI, r.Proto) url := fmt.Sprintf("%s://%s%s %s", scheme, r.Host, r.RequestURI, r.Proto)
log.Info().Msgf("%s %s", bold(pink("["+r.Method+"]")), aqua(url)) log.Info().
next.ServeHTTP(w, r) Msgf("%s %s %s",
bold(orange(""+r.Method+"")),
aqua(url),
bold(fmtCode(record.Status)),
)
}) })
} }

View file

@ -23,6 +23,8 @@ type UserService struct {
repos *repo.AllRepos repos *repo.AllRepos
} }
// RegisterUser creates a new user and group in the data with the provided data. It also bootstraps the user's group
// with default Labels and Locations.
func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistration) (*types.UserOut, error) { func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistration) (*types.UserOut, error) {
group, err := svc.repos.Groups.Create(ctx, data.GroupName) group, err := svc.repos.Groups.Create(ctx, data.GroupName)
if err != nil { if err != nil {
@ -38,7 +40,26 @@ func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistr
GroupID: group.ID, GroupID: group.ID,
} }
return mappers.ToOutUser(svc.repos.Users.Create(ctx, usrCreate)) usr, err := svc.repos.Users.Create(ctx, usrCreate)
if err != nil {
return &types.UserOut{}, err
}
for _, label := range defaultLabels() {
_, err := svc.repos.Labels.Create(ctx, group.ID, label)
if err != nil {
return &types.UserOut{}, err
}
}
for _, location := range defaultLocations() {
_, err := svc.repos.Locations.Create(ctx, group.ID, location)
if err != nil {
return &types.UserOut{}, err
}
}
return mappers.ToOutUser(usr, nil)
} }
// GetSelf returns the user that is currently logged in based of the token provided within // GetSelf returns the user that is currently logged in based of the token provided within

View file

@ -0,0 +1,55 @@
package services
import "github.com/hay-kot/content/backend/internal/types"
func defaultLocations() []types.LocationCreate {
return []types.LocationCreate{
{
Name: "Living Room",
},
{
Name: "Garage",
},
{
Name: "Kitchen",
},
{
Name: "Bedroom",
},
{
Name: "Bathroom",
},
{
Name: "Office",
},
{
Name: "Attic",
},
{
Name: "Basement",
},
}
}
func defaultLabels() []types.LabelCreate {
return []types.LabelCreate{
{
Name: "Appliances",
},
{
Name: "IOT",
},
{
Name: "Electronics",
},
{
Name: "Servers",
},
{
Name: "General",
},
{
Name: "Important",
},
}
}

View file

@ -1,23 +1,31 @@
import { PublicApi } from "~~/lib/api/public"; import { PublicApi } from '~~/lib/api/public';
import { UserApi } from "~~/lib/api/user"; import { UserApi } from '~~/lib/api/user';
import { Requests } from "~~/lib/requests"; import { Requests } from '~~/lib/requests';
import { useAuthStore } from "~~/stores/auth"; import { useAuthStore } from '~~/stores/auth';
async function ApiDebugger(r: Response) { function ApiDebugger(r: Response) {
console.table({ console.table({
"Request Url": r.url, 'Request Url': r.url,
"Response Status": r.status, 'Response Status': r.status,
"Response Status Text": r.statusText, 'Response Status Text': r.statusText,
}); });
} }
export function usePublicApi(): PublicApi { export function usePublicApi(): PublicApi {
const requests = new Requests("", "", {}, ApiDebugger); const requests = new Requests('', '', {});
return new PublicApi(requests); return new PublicApi(requests);
} }
export function useUserApi(): UserApi { export function useUserApi(): UserApi {
const authStore = useAuthStore(); const authStore = useAuthStore();
const requests = new Requests("", () => authStore.token, {}, ApiDebugger);
const requests = new Requests('', () => authStore.token, {});
requests.addResponseInterceptor(ApiDebugger);
requests.addResponseInterceptor(r => {
if (r.status === 401) {
authStore.clearSession();
}
});
return new UserApi(requests); return new UserApi(requests);
} }

View file

@ -5,6 +5,9 @@ export enum Method {
DELETE = 'DELETE', DELETE = 'DELETE',
} }
export type RequestInterceptor = (r: Response) => void;
export type ResponseInterceptor = (r: Response) => void;
export interface TResponse<T> { export interface TResponse<T> {
status: number; status: number;
error: boolean; error: boolean;
@ -16,22 +19,24 @@ 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 responseInterceptors: ResponseInterceptor[] = [];
addResponseInterceptor(interceptor: ResponseInterceptor) {
this.responseInterceptors.push(interceptor);
}
private callResponseInterceptors(response: Response) {
this.responseInterceptors.forEach(i => i(response));
}
private url(rest: string): string { private url(rest: string): string {
return this.baseUrl + rest; return this.baseUrl + rest;
} }
constructor( constructor(baseUrl: string, token: string | (() => string) = '', headers: Record<string, string> = {}) {
baseUrl: string,
token: string | (() => string) = '',
headers: Record<string, string> = {},
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;
} }
public get<T>(url: string): Promise<TResponse<T>> { public get<T>(url: string): Promise<TResponse<T>> {
@ -73,10 +78,7 @@ export class Requests {
} }
const response = await fetch(this.url(url), args); const response = await fetch(this.url(url), args);
this.callResponseInterceptors(response);
if (this.logger) {
this.logger(response);
}
const data: T = await (async () => { const data: T = await (async () => {
if (response.status === 204) { if (response.status === 204) {

View file

@ -44,7 +44,7 @@
</script> </script>
<template> <template>
<BaseContainer class="space-y-16"> <BaseContainer class="space-y-16 pb-16">
<section aria-labelledby="profile-overview-title" class="mt-8"> <section aria-labelledby="profile-overview-title" class="mt-8">
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white shadow">
<h2 class="sr-only" id="profile-overview-title">Profile Overview</h2> <h2 class="sr-only" id="profile-overview-title">Profile Overview</h2>

View file

@ -47,7 +47,7 @@
loading.value = true; loading.value = true;
// Print Values of registerFields // Print Values of registerFields
const { data, error } = await api.register({ const { error } = await api.register({
user: { user: {
name: registerFields[0].value, name: registerFields[0].value,
email: registerFields[1].value, email: registerFields[1].value,
@ -58,11 +58,14 @@
if (error) { if (error) {
toast.error('Problem registering user'); toast.error('Problem registering user');
} else { return;
toast.success('User registered');
} }
toast.success('User registered');
loading.value = false; loading.value = false;
loginFields[0].value = registerFields[1].value;
registerForm.value = false;
} }
const loginFields = [ const loginFields = [
@ -116,55 +119,57 @@
<p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Shit.</p> <p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Shit.</p>
</header> </header>
<div class="grid p-6 sm:place-items-center min-h-[50vh]"> <div class="grid p-6 sm:place-items-center min-h-[50vh]">
<Transition name="slide-fade"> <div>
<form v-if="registerForm" @submit.prevent="registerUser"> <Transition name="slide-fade">
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl"> <form v-if="registerForm" @submit.prevent="registerUser">
<div class="card-body"> <div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
<h2 class="card-title">Register</h2> <div class="card-body">
<TextField <h2 class="card-title">Register</h2>
v-for="field in registerFields" <TextField
v-model="field.value" v-for="field in registerFields"
:label="field.label" v-model="field.value"
:key="field.label" :label="field.label"
:type="field.type" :key="field.label"
/> :type="field.type"
<div class="card-actions justify-end"> />
<button <div class="card-actions justify-end">
type="submit" <button
class="btn btn-primary mt-2" type="submit"
:class="loading ? 'loading' : ''" class="btn btn-primary mt-2"
:disabled="loading" :class="loading ? 'loading' : ''"
> :disabled="loading"
Register >
</button> Register
</button>
</div>
</div> </div>
</div> </div>
</div> </form>
</form> <form v-else @submit.prevent="login">
<form v-else @submit.prevent="login"> <div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl"> <div class="card-body">
<div class="card-body"> <h2 class="card-title">Login</h2>
<h2 class="card-title">Login</h2> <TextField
<TextField v-for="field in loginFields"
v-for="field in loginFields" v-model="field.value"
v-model="field.value" :label="field.label"
:label="field.label" :key="field.label"
:key="field.label" :type="field.type"
:type="field.type" />
/> <div class="card-actions justify-end mt-2">
<div class="card-actions justify-end mt-2"> <button type="submit" class="btn btn-primary" :class="loading ? 'loading' : ''" :disabled="loading">
<button type="submit" class="btn btn-primary" :class="loading ? 'loading' : ''" :disabled="loading"> Login
Login </button>
</button> </div>
</div> </div>
</div> </div>
</div> </form>
</form> </Transition>
</Transition> <div class="text-center mt-4">
<div class="text-center mt-2"> <button @click="toggleLogin" class="text-primary-content text-lg">
<button @click="toggleLogin"> {{ registerForm ? 'Already a User? Login' : 'Not a User? Register' }}
{{ registerForm ? 'Already a User? Login' : 'Not a User? Register' }} </button>
</button> </div>
</div> </div>
</div> </div>
<div class="min-w-full absolute bottom-0 z-[-1]"> <div class="min-w-full absolute bottom-0 z-[-1]">

View file

@ -33,5 +33,14 @@ export const useAuthStore = defineStore('auth', {
return result; return result;
}, },
/**
* clearSession is used when the user cannot be logged out via the API and
* must clear it's local session, usually when a 401 is received.
*/
clearSession() {
this.token = '';
this.expires = '';
navigateTo('/');
},
}, },
}); });