Skip to content

Instantly share code, notes, and snippets.

@acidjazz
Created February 8, 2022 08:04
Show Gist options
  • Select an option

  • Save acidjazz/5d6a6a041090e9c5206c9919a2a9fb79 to your computer and use it in GitHub Desktop.

Select an option

Save acidjazz/5d6a6a041090e9c5206c9919a2a9fb79 to your computer and use it in GitHub Desktop.

Revisions

  1. acidjazz created this gist Feb 8, 2022.
    214 changes: 214 additions & 0 deletions api.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,214 @@
    import { FetchError, FetchOptions, SearchParams } from 'ohmyfetch'
    import { reactive, ref } from '@vue/reactivity'
    import { IncomingMessage, ServerResponse } from 'http'
    import { useCookie } from 'h3'
    import { TailvueToast } from 'tailvue'
    import { Router } from 'vue-router'
    import Cookies from 'universal-cookie'

    export interface UserLogin {
    token: string
    user: models.User
    provider: string
    error?: string
    action?: LoginAction
    }

    export interface AuthConfig {
    fetchOptions: FetchOptions
    req?: IncomingMessage
    res?: ServerResponse
    redirect: {
    logout: string
    login: undefined|string
    }
    }

    export interface LoginAction {
    action: string
    url: string
    }

    const authConfigDefaults:AuthConfig = {
    fetchOptions: {},
    req: undefined,
    redirect: {
    logout: '/',
    login: undefined,
    },
    }

    export default class Api {

    public token = ref<string|undefined>(undefined)
    private cookies:Cookies = new Cookies();
    public config: AuthConfig
    public $user = reactive<models.User|Record<string, unknown>>({})
    public $toast:TailvueToast
    public loggedIn = ref<boolean>(false)
    public modal = ref<boolean>(false)
    public redirect = ref<boolean>(false)
    public action = ref<null|LoginAction>(null)
    public callback = undefined

    constructor(config: AuthConfig, toast: TailvueToast) {
    this.$toast = toast
    this.config = { ...authConfigDefaults,...config }
    this.checkUser()
    }

    on(redirect: boolean, action: LoginAction|null) {
    this.redirect.value = redirect
    this.modal.value = true
    this.action.value = action
    }

    off() {
    this.modal.value = false
    }

    checkUser() {
    this.token.value = this.getToken()
    if (this.token.value) {
    this.loggedIn.value = true
    this.setUser().then()
    }
    else this.loggedIn.value = false
    }

    async login (result: UserLogin): Promise<undefined|string> {
    this.loggedIn.value = true
    this.token.value = result.token
    Object.assign(this.$user, result.user)
    this.cookies.set('token', this.token.value, { path: '/', maxAge: 60*60*24*30 })
    this.$toast.show({ type: 'success', message: 'Login Successful', timeout: 1 })
    if (result.action && result.action.action === 'redirect') return result.action.url
    if (this.callback) this.callback()
    return this.config.redirect.login
    }

    private getToken(): string {
    if (this.config.req) return useCookie(this.config.req, 'token')
    return this.cookies.get('token')
    }

    private fetchOptions(params?: SearchParams, method = 'GET'): FetchOptions {
    const fetchOptions = this.config.fetchOptions
    fetchOptions.headers = {
    Accept: 'application/json',
    Authorization: `Bearer ${this.token.value}`,
    }
    fetchOptions.method = method
    delete this.config.fetchOptions.body
    delete this.config.fetchOptions.params
    if (params)
    if (method === 'POST' || method === 'PUT')
    this.config.fetchOptions.body = params
    else
    this.config.fetchOptions.params = params
    return this.config.fetchOptions
    }

    private async setUser(): Promise<void> {
    try {
    const result = await $fetch<api.MetApiResponse & { data: models.User }>('/me', this.fetchOptions())
    Object.assign(this.$user, result.data)
    } catch (e) {
    await this.invalidate()
    }
    }

    public async index <Results>(endpoint: string, params?: SearchParams): Promise<api.MetApiResults & { data: Results }> {
    try {
    return await $fetch<api.MetApiResults & { data: Results }>(endpoint, this.fetchOptions(params))
    } catch (error) {
    await this.toastError(error)
    }
    }

    public async get <Result>(endpoint: string, params?: SearchParams): Promise<api.MetApiResponse & { data: Result }> {
    try {
    return await $fetch<api.MetApiResponse & { data: Result }>(endpoint, this.fetchOptions(params))
    } catch (error) {
    await this.toastError(error)
    }
    }

    public async update (endpoint: string, params?: SearchParams): Promise<api.MetApiResponse> {
    try {
    return (await $fetch<api.MetApiResults & { data: api.MetApiResponse}>(endpoint, this.fetchOptions(params, 'PUT'))).data
    } catch (error) {
    await this.toastError(error)
    }
    }

    public async store <Result>(endpoint: string, params?: SearchParams): Promise<api.MetApiResponse & { data: Result }> {
    try {
    return (await $fetch<api.MetApiResults & { data: api.MetApiResponse & { data: Result } }>(endpoint, this.fetchOptions(params, 'POST'))).data
    } catch (error) {
    await this.toastError(error)
    }
    }

    public async delete (endpoint: string, params?: SearchParams): Promise<api.MetApiResponse> {
    try {
    return (await $fetch<api.MetApiResults & { data: api.MetApiResponse}>(endpoint, this.fetchOptions(params, 'DELETE'))).data
    } catch (error) {
    await this.toastError(error)
    }
    }

    public async attempt (token: string | string[]): Promise<UserLogin> {
    try {
    return (await $fetch<api.MetApiResponse & { data: UserLogin }>('/login', this.fetchOptions({ token }, 'POST'))).data
    } catch (error) {
    await this.toastError(error)
    }
    }

    private async toastError (error: FetchError): Promise<void> {

    if (error.response?.status === 401)
    return await this.invalidate()

    if (!this.$toast) throw error

    if (error.response._data && error.response._data.errors)
    for (const err of error.response._data.errors)
    this.$toast.show({
    type: 'danger',
    message: err.detail ?? err.message ?? '',
    timeout: 12,
    })

    if (error.response?.status === 403)
    return this.$toast.show({
    type: 'denied',
    message: error.response._data.message,
    timeout: 0,
    })

    if (error.response._data.exception)
    this.$toast.show({
    type: 'danger',
    message: `<b>[${error.response._data.exception}]</b> <br /> ${error.response._data.message} <br /> <a href="phpstorm://open?file=/${error.response._data.file}&line=${error.response._data.line}">${error.response._data.file}:${error.response._data.line}</a>`,
    timeout: 0,
    })
    }

    public async logout (router: Router): Promise<void> {
    const response = (await $fetch<api.MetApiResults>('/logout', this.fetchOptions()))
    this.$toast.show(Object.assign(response.data, { timeout: 1 }))
    await this.invalidate(router)
    }

    public async invalidate (router?: Router): Promise<void> {
    this.token.value = undefined
    this.loggedIn.value = false
    Object.assign(this.$user, {})
    this.cookies.remove('token')
    if (router) await router.push(this.config.redirect.logout)
    else if (process.client) document.location.href = this.config.redirect.logout
    }

    }
    33 changes: 33 additions & 0 deletions apiPlugin.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,33 @@
    import { defineNuxtPlugin, useNuxtApp, useRuntimeConfig } from '#app'
    import Api from '~/lib/api'

    export default defineNuxtPlugin((nuxtApp) => {
    const config = useRuntimeConfig()
    const { $toast } = useNuxtApp()
    nuxtApp.provide('api',
    new Api({
    req: nuxtApp.ssrContext?.req,
    res: nuxtApp.ssrContext?.res,
    fetchOptions: {
    baseURL: config.apiURL,
    },
    redirect: {
    logout: '/',
    login: '/home',
    },
    }, $toast),
    )
    })

    declare module '#app' {
    interface NuxtApp {
    $api: Api
    }
    }

    declare module '@vue/runtime-core' {
    interface ComponentCustomProperties {
    $api: Api
    }
    }