User Management

User management logic can be implemented in 3 ways:

  1. The logic is implemented in the frontend / webapp and a cloud function acts as an authorization proxy to the automatically created GraphQL Backend

    • Example Code

      import { gql } from "https://deno.land/x/graphql_tag@0.0.1/mod.ts"
      import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"
      
      
      const gqlApi = "http://localhost:4185/dbs/<TODO>/graphql"
      
      
      interface User {
        id: number
        email: string
        name: string
        password_hash?: string
      }
      
      
      async function saveUserWithToken(user: User, token: string) {
        const response = await fetch(gqlApi, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            query: `
              mutation UpdateSessionToken(
                $user: users_set_input!,
                $usersFilter: users_filter!
              ) {
                update_users(filter: $usersFilter, set: $user) {
                  affected_rows
                }
              }
            `,
            variables: {
              usersFilter: {
                email: {
                  eq: user.email,
                },
              },
              user: {
                email: user.email,
                session_token: token,
              },
            },
          }),
        })
      
      
        if (response.ok) {
          return { data: { sessionToken: token } }
        } else {
          return { errors: [{ message: "Failed to save session" }] }
        }
      }
      
      
      async function loadUserFromToken(
        bearerToken: string,
      ): Promise<User | null> {
        const token = bearerToken.replace(/^Bearer /, "")
        const response = await fetch(gqlApi, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            query: `
                query GetUserForToken($userFilter: users_filter) {
                  users(filter: $userFilter) {
                    rowid
                    email
                    first_name
                    last_name
                    session_token
                  }
                }
              `,
            variables: {
              userFilter: { session_token: { eq: token } },
            },
          }),
        })
      
      
        if (response.ok) {
          const jsonResponse = await response.json()
          const user = jsonResponse.data?.users?.[0]
          if (user) {
            user.id = user.rowid
            user.name = user.first_name + " " + user.last_name
            delete user.session_token
            return user
          }
        } else {
          console.error(response)
        }
      
      
        return null
      }
      
      
      async function loadUsers(): Promise<User[]> {
        // Password hash: BCrypt
        // https://gchq.github.io/CyberChef/#recipe=Bcrypt(10)
      
      
        const response = await fetch(gqlApi, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            query: `
            query GetUsers {
              users {
                email
                password_hash
              }
            }
          `,
          }),
        })
      
      
        if (response.ok) {
          const jsonResponse = await response.json()
          return jsonResponse?.data?.users
        } else {
          console.error(response)
          return []
        }
      }
      
      
      interface GQLResponse {
        data?: Record<string, any>
        errors?: { message: string }[]
      }
      
      
      export default async function (context): Promise<GQLResponse> {
        const gqlQuery = context.data.query
        const queryName = gql`
          ${gqlQuery}
        `?.definitions[0]?.name?.value
      
      
        if (queryName === "LoginRequest") {
          const argsObj = gql`
            ${gqlQuery}
          `?.definitions[0]?.selectionSet?.selections[0]?.arguments
          const email = argsObj?.find((arg) => arg.name.value === "email")?.value
            ?.value
          const password = argsObj?.find((arg) => arg.name.value === "password")
            ?.value?.value
          // const rememberMe = argsObj
          //  ?.find(arg => arg.name.value === "rememberMe")
          //  ?.value
          //  ?.value
      
      
          if (email && password) {
            const users = await loadUsers()
            const user = users.find((user) => user.email === email)
      
      
            if (user && bcrypt.compareSync(password, user?.password_hash)) {
              user.name = ""
              const randomToken = Math.random().toString(36).substring(2)
              const saveResult = await saveUserWithToken(user, randomToken)
              return saveResult
            } else {
              return {
                errors: [
                  {
                    message:
                      "User does not exist or an incorrect password was specified",
                  },
                ],
              }
            }
          }
        }
      
      
        if (queryName === "UserRequest") {
          return { data: await loadUserFromToken(context.headers.Authorization) }
        }
      
      
        const response = await fetch(gqlApi, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ query: gqlQuery }),
        })
      
      
        if (response.ok) {
          const jsonResponse = await response.json()
          return jsonResponse
        } else {
          return {
            errors: [{ message: "Failed to fetch data" }],
          }
        }
      }
      
  2. The whole logic is implemented in cloud functions which provide their own JSON API. The database access can then either be implemented by making GraphQL requests to the GraphQL resolver or by directly accessing the SQLite database. (Use the example code from 1. and extend it with business logic functions that write directly to the database as seen in the functions documentation page)

  3. The logic is implemented in an additional external server (written in any language) which provides a JSON or GraphQL API to your APP and calls Airsequel’s GraphQL API to access the database.

Here is an architecture diagram of the involved components:

Screenshot 2023-12-26 at 11.39.47.png