Skip to content

Latest commit

 

History

History
753 lines (578 loc) · 16.3 KB

README.md

File metadata and controls

753 lines (578 loc) · 16.3 KB

Doc

What Is AuthPC

AuthPC is a utility library that combines both

  • Authentication (Auth)
  • Better Solid'S RPC (PC)

Supports

  • AuthPC currently supports two Auth Providers: Clerk & AuthJS.
  • AuthPC currently supports two Validators: Zod & ValiBot.

AuthPC also allows you to throw type-safe errors, redirect the user, modify headers, set cookies and most importantly, you can choose to use either GET (allowing the use of HTTP cache-control headers) or POST.

That means you can create server actions completly type safe, cached, auth/session protected and all in simple function declaration, many might think but wait this is very simple to achieve in other frameworks, what took you so long to create this one, In this doc i'm actually going to explain the behind the scenes of AuthPC.

Everything here is type-safe, it also includes middlewares and allowed to be imported to any file thanks to the advance babel plugin.

Install

First install both the plugin & the authpc package

pnpm install @solid-mediakit/authpc@latest @solid-mediakit/authpc-plugin@latest @tanstack/solid-query@latest

App Config

Wrap your entire config with the withAuthPC method for typesafety:

import { withAuthPC } from '@solid-mediakit/authpc-plugin'

const config = withAuthPC({
  ssr: true,
})

// this is important: otherwise you cannot access the session$ property
declare module '@solid-mediakit/authpc' {
  interface Settings {
    config: typeof config
  }
}

export default config

Note

To use any auth provider, you need to follow the guides bellow, this is optional you don't have to use any auth provider with this library, if you don't use one, you will not be able to access the session$ property. Also if you don't use any auth provider, you need to wrap your app with the AuthPCProvider

  • I Want To Use Auth By Using AuthJS - here
  • I Want To Use Auth By Using Clerk - here
  • I Don't Use Any Auth - here

API

createCaller

Use this method to interact with the api, you can choose between a query or a mutation (default is query) and also choose to use either GET/POST as the request method (default is POST).

No Schema

// server.ts
import { createCaller } from '@solid-mediakit/authpc'
import * as v from 'valibot'

const mySchema = v.object({ name: v.string() })

const myServerQuery = createCaller(
  ({ session$ }) => {
    console.log(session$, event$.request.headers.get('user-agent'))
    return 'Who do i say hey to'
  },
  {
    method: 'GET',
  },
)

// client.tsx
import { myServerQuery } from './server'
const query = myServerQuery()

Zod

// server.ts
import { createCaller } from '@solid-mediakit/authpc'
import { z } from 'zod'

const mySchema = z.object({ name: z.string() })

export const myServerQuery = createCaller(
  mySchema,
  ({ input$, session$, event$ }) => {
    console.log(session$, event$.request.headers.get('user-agent'))
    return `Hey there ${input$.name}`
  },
  {
    method: 'GET',
  },
)

// client.tsx
import { myServerQuery } from './server'
const query = myServerQuery(() => ({ name: 'Demo' }))

Valibot

// server.ts
import { createCaller } from '@solid-mediakit/authpc'
import * as v from 'valibot'

const mySchema = v.object({ name: v.string() })

export const myServerQuery = createCaller(
  mySchema,
  ({ input$, session$, event$ }) => {
    console.log(session$, event$.request.headers.get('user-agent'))
    return `Hey there ${input$.name}`
  },
  {
    method: 'GET',
  },
)

// client.tsx
import { myServerQuery } from './server'
const query = myServerQuery(() => ({ name: 'Demo' }))

Error Handling

Errors are thrown using the AuthPClientError class. When the server function has a schema defined, you can use the isValidationError to get the issues.

import { createEffect } from 'solid-js'
import { myServerQuery } from './server'

const MyClient = () => {
  const query = myServerQuery(() => ({ name: 'Demo' }))

  createEffect(() => {
    if (query.error) {
      if (query.error.isValidationError()) {
        query.error.cause.fieldErrors.name // string[]
      } else {
        console.error('What is this', query.error.cause)
      }
    }
  })

  return (...)
}

export default MyClient

Methods

You can choose any method (GET || POST), regardless of the function type (default is POST)

GET

import { createCaller, response$ } from '@solid-mediakit/authpc'

export const getRequest = createCaller(
  () => {
    return response$(
      { iSetTheHeader: true },
      { headers: { 'cache-control': 'max-age=60' } },
    )
  },
  {
    method: 'GET',
  },
)

POST

This is the default so you don't have to mention it

import { createCaller, response$ } from '@solid-mediakit/authpc'

export const postRequest = createCaller(
  () => {
    return response$({ iSetTheHeader: true }, { headers: { 'X-Testing': '1' } })
  },
  {
    method: 'POST',
  },
)

export const postRequest2 = createCaller(() => {
  return response$({ iSetTheHeader: true }, { headers: { 'X-Testing': '1' } })
})

Function Types

In addition to methods, you can also choose a function type (the function type will not affect the request method).

query

This is the default, you don't have to specify this:

import { createCaller } from '@solid-mediakit/authpc'

const thisIsAQuery = createCaller(() => {
  return 1
})

const alsoIsAQuery = createCaller(
  () => {
    return 1
  },
  {
    type: 'query',
  },
)

// client side
const query = thisIsAQuery()
query.data // number

mutation

You can either specify {type: 'action'} or use the createAction method

import { createCaller, createAction } from '@solid-mediakit/authpc'

const thisIsAMutation = createCaller(
  () => {
    return 1
  },
  {
    type: 'action',
  },
)

const alsoIsAMutation = createAction(() => {
  return 1
})

// client side
const mutation = thisIsAMutation()
mutation.mutate()

.use

This method allows you create a reuseable caller that contains your middlewares. You can combine multiple callers / middlewares and import them to different files, take a look at this example.

file1.ts

In this file we create a caller with a custom middleware and then export it so it could be use in other files as-well.

import { createCaller } from '@solid-mediakit/authpc'

export const withMw1 = createCaller.use(() => {
  return {
    myFile1: 1,
  }
})

export const action1 = withMw1(({ ctx$ }) => {
  return `hey ${ctx$.myFile1} `
})

This Transforms To

file2.ts

In this file we can actually import the caller we created and then add more middlewares to it or use it as is.

import { withMw1 } from './file1'

export const withMw2 = withMw1.use(({ ctx$ }) => {
  return {
    ...ctx$,
    myFile2: 2,
  }
})

export const action2 = withMw2(({ ctx$ }) => {
  return `hey ${ctx$.myFile1} ${ctx$.myFile2}`
})

Utils

AuthPC contains many utils out of the box, like redirection, erroring, setting cookies, etc.

redirect$

Use this function to redirect the user (this will not affect the function type):

import { createCaller, redirect$ } from '@solid-mediakit/authpc'

let redirect = false

export const myQuery = createCaller(() => {
  if (redirect) {
    return redirect$('/login')
  }
  return 'yes'
})

error$

Use this function to throw an error on the user side (this will not affect the function type):

import { createCaller, error$ } from '@solid-mediakit/authpc'

let shouldError = false

export const myQuery = createCaller(() => {
  if (shouldError) {
    return error$$('Why did i error')
  }
  return 'yes'
})

response$

Use this function to return data & modify headers (the return type is also infered from the data passed to response$):

import { createCaller, response$ } from '@solid-mediakit/authpc'

let setHeader = false

export const myQuery = createCaller(() => {
  if (setHeader) {
    return response$(1, {
      headers: { this: 'that' },
    })
  }
  return 'yes'
})

// respone type: number | string

Optimistic Updates

Similiar to tRPC, each query function has a useUtils method, so lets say we import a server action we created using createCaller called serverQuery1

import { serverQuery1, serverMutation1 } from '~/server/etc'

const MyComponent = () => {
  const listPostQuery = serverQuery1()
  const serverQueryUtils = serverQuery1.useUtils()

  const postCreate = serverMutation1(() => ({
    async onMutate(newPost) {
      // Cancel outgoing fetches (so they don't overwrite our optimistic update)
      await serverQueryUtils.cancel()

      // Get the data from the queryCache
      const prevData = serverQueryUtils.getData()

      // Optimistically update the data with our new post
      serverQueryUtils.setData(undefined, (old) => [...old, newPost])

      // Return the previous data so we can revert if something goes wrong
      return { prevData }
    },
    onError(err, newPost, ctx) {
      // If the mutation fails, use the context-value from onMutate
      serverQueryUtils.setData(undefined, ctx.prevData)
    },
    onSettled() {
      // Sync with server once mutation has settled
      serverQueryUtils.invalidate()
    },
  }))
}

Setting up auth

As mentioned, you can choose to use either Clerk or AuthJS.

Clerk

First Install The Dependencies

Install

pnpm install clerk-solidjs@latest

Env Variables

Make sure to have this in your .env

VITE_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=

Wrap Your App With ClerkProvider

src/app.tsx should be something like:

// @refresh reload
import './app.css'
import { MetaProvider, Title } from '@solidjs/meta'
import { Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { Suspense } from 'solid-js'
import { QueryClient } from '@tanstack/solid-query'
import { AuthPCProvider } from '@solid-mediakit/authpc'
import { ClerkProvider } from 'clerk-solidjs'

export default function App() {
  const queryClient = new QueryClient()
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>SolidStart - Basic</Title>
          <ClerkProvider
            publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}
          >
            <AuthPCProvider queryClient={queryClient}>
              <Suspense>{props.children}</Suspense>
            </AuthPCProvider>
          </ClerkProvider>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  )
}

Creating A Middleware

Head over to src/middleware.ts and make sure its something like:

import { createMiddleware } from '@solidjs/start/middleware'
import { clerkMiddleware } from 'clerk-solidjs/start/server'

export default createMiddleware({
  onRequest: [
    clerkMiddleware({
      publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
      secretKey: process.env.CLERK_SECRET_KEY,
    }),
  ],
})

Modifying app.config.ts

This is it, you just need to modify app.config.ts

import { withAuthPC } from '@solid-mediakit/authpc-plugin'

const config = withAuthPC(
  {
    ssr: true,
  },
  {
    auth: 'clerk',
    authCfg: {
      middleware: './src/middleware.ts',
      protectedMessage: 'You need to sign in first',
    },
  },
)

export default config

declare module '@solid-mediakit/authpc' {
  interface Settings {
    config: typeof config
  }
}

AuthJS

First Install The Dependencies

Install

pnpm install @auth/core@0.35.0 @solid-mediakit/auth@latest

Env Variables

Make sure to have this in your .env

VITE_AUTH_PATH=/api/auth
DISCORD_ID=
DISCORD_SECRET=

Create Config

After installing, head over to server/auth.ts and create the AuthJS config:

import { type SolidAuthConfig } from '@solid-mediakit/auth'
import Discord from '@auth/core/providers/discord'

declare module '@auth/core/types' {
  export interface Session {
    user: {} & DefaultSession['user']
  }
}

export const authOpts: SolidAuthConfig = {
  providers: [
    Discord({
      clientId: process.env.DISCORD_ID,
      clientSecret: process.env.DISCORD_SECRET,
    }),
  ],
  debug: false,
  basePath: import.meta.env.VITE_AUTH_PATH,
}

Create Auth API Endpoint

Go to src/routes/api/auth/[...solidauth].ts:

import { SolidAuth } from '@solid-mediakit/auth'
import { authOpts } from '~/server/auth'

export const { GET, POST } = SolidAuth(authOpts)

Wrap Your App With SessionProvider

src/app.tsx should be something like:

// @refresh reload
import './app.css'
import { MetaProvider, Title } from '@solidjs/meta'
import { Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { Suspense } from 'solid-js'
import { QueryClient } from '@tanstack/solid-query'
import { AuthPCProvider } from '@solid-mediakit/authpc'
import { SessionProvider } from '@solid-mediakit/auth/client'

export default function App() {
  const queryClient = new QueryClient()
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>SolidStart - Basic</Title>
          <SessionProvider>
            <AuthPCProvider queryClient={queryClient}>
              <Suspense>{props.children}</Suspense>
            </AuthPCProvider>
          </SessionProvider>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  )
}

Modifying app.config.ts

This is it, you just need to modify app.config.ts

import { withAuthPC } from '@solid-mediakit/authpc-plugin'

const config = withAuthPC(
  {
    ssr: true,
  },
  {
    auth: 'authjs',
    authCfg: {
      source: '~/server/auth',
      configName: 'authOpts',
      protectedMessage: 'You need to sign in first',
    },
  },
)

export default config

declare module '@solid-mediakit/authpc' {
  interface Settings {
    config: typeof config
  }
}

AuthPCProvider

src/app.tsx should be something like:

// @refresh reload
import './app.css'
import { MetaProvider, Title } from '@solidjs/meta'
import { Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { Suspense } from 'solid-js'
import { QueryClient } from '@tanstack/solid-query'
import { AuthPCProvider } from '@solid-mediakit/authpc'

export default function App() {
  const queryClient = new QueryClient()
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>SolidStart - Basic</Title>
          <AuthPCProvider queryClient={queryClient}>
            <Suspense>{props.children}</Suspense>
          </AuthPCProvider>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  )
}

Transforms

file1

File1 Transforms To:

import { createCaller, callMiddleware$ } from '@solid-mediakit/authpc'

export const withMw1 = createCaller

export const action1 = createCaller(
  async ({ input$: _$$payload }) => {
    'use server'
    const ctx$ = await callMiddleware$(_$$event, _$$withMw1_mws)
    if (ctx$ instanceof Response) return ctx$
    return `hey ${ctx$.myFile1} `
  },
  {
    protected: false,
    key: 'action1',
    method: 'POST',
    type: 'query',
  },
)

export const _$$withMw1_mws = [
  () => {
    return {
      myFile1: 1,
    }
  },
]

file2

File2 Transforms To:

import { createCaller, callMiddleware$ } from '@solid-mediakit/authpc'
import { withMw1, _$$withMw1_mws } from './file1'

export const withMw2 = withMw1

export const action2 = createCaller(
  async ({ input$: _$$payload }) => {
    'use server'
    const ctx$ = await callMiddleware$(_$$event, _$$withMw2_mws)
    if (ctx$ instanceof Response) return ctx$
    return `hey ${ctx$.myFile1} ${ctx$.myFile2}`
  },
  {
    protected: false,
    key: 'action2',
    method: 'POST',
    type: 'query',
  },
)

export const _$$withMw2_mws = [
  ..._$$withMw1_mws,
  ({ ctx$ }) => {
    return {
      ...ctx$,
      myFile2: 2,
    }
  },
]```