Skip to content

Commit

Permalink
added login feature utilizing the grafbase and query using graphql
Browse files Browse the repository at this point in the history
  • Loading branch information
jasondev01 committed Sep 9, 2023
1 parent 06d74fa commit 9f26216
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ next-env.d.ts

#text
grafbase.config.text

#environment
.env
7 changes: 7 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import NextAuth from "next-auth";

import { authOptions } from "@/lib/session";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST }
46 changes: 40 additions & 6 deletions components/AuthProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import React from 'react'
'use client'

import { getProviders, signIn } from 'next-auth/react'
import { useState, useEffect } from 'react'

type Provider = {
id: string
name: string
type: string
signinUrl: string
callbackUrl: string
signinUrlParams?: Record<string, string> | null;
}

type Providers = Record<string, Provider>

const AuthProviders = () => {
return (
<div>
AuthProvider
</div>
)
const [ providers, setProviders ] = useState<Providers | null>(null)

useEffect(() => {
const fetchProviders = async () => {
const res = await getProviders()
console.log('res', res)
setProviders(res)
}
fetchProviders()
}, [])

if(providers) {
return (
<div>
{Object.values(providers).map((provider: Provider, i) => (
<button
key={i}
onClick={() => signIn(provider?.id)}
>
{provider.id}
</button>
))}
</div>
)
}
}

export default AuthProviders
14 changes: 10 additions & 4 deletions components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { NavLinks } from '@/constants'
import Image from 'next/image'
import Link from 'next/link'
import AuthProviders from './AuthProviders'
import { getCurrentUser } from '@/lib/session'
import ProfileMenu from './ProfileMenu'

const Navbar = () => {
const session = {}
const Navbar = async () => {
const session = await getCurrentUser()

console.log(session)

return (
<nav className="flexBetween navbar">
Expand All @@ -31,9 +35,11 @@ const Navbar = () => {
</ul>
</div>
<div className='flexCenter gap-4'>
{session ? (
{session?.user ? (
<>
UserPhoto
<ProfileMenu
session={session}
/>

<Link href='/create-project'>
Share Work
Expand Down
82 changes: 82 additions & 0 deletions components/ProfileMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client"

import Link from "next/link";
import Image from "next/image";
import { signOut } from "next-auth/react";
import { Fragment, useState } from "react";
import { Menu, Transition } from "@headlessui/react";

import { SessionInterface } from "@/common.types";

const ProfileMenu = ({ session }: { session: SessionInterface }) => {
const [ openModal, setOpenModal ] = useState(false);
console.log(session)
return (
<div className="flexCenter z-10 flex-col relative">
<Menu as="div">
<Menu.Button className="flexCenter" onMouseEnter={() => setOpenModal(true)} >
{session?.user?.image && (
<Image
src={session.user.image}
width={40}
height={40}
className="rounded-full"
alt="user profile image"
/>
)}
</Menu.Button>

<Transition
show={openModal}
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="flexStart profile_menu-items"
onMouseLeave={() => setOpenModal(false)}
>
<div className="flex flex-col items-center gap-y-4">
{session?.user?.image && (
<Image
src={session?.user?.image}
className="rounded-full"
width={80}
height={80}
alt="profile Image"
/>
)}
<p className="font-semibold">{session?.user?.name}</p>
</div>

<div className="flex flex-col gap-3 pt-10 items-start w-full">
<Menu.Item>
<Link href={`/profile/${session?.user?.id}`} className="text-sm">Work Preferences</Link>
</Menu.Item>
<Menu.Item>
<Link href={`/profile/${session?.user?.id}`} className="text-sm">Settings</Link>
</Menu.Item>
<Menu.Item>
<Link href={`/profile/${session?.user?.id}`} className="text-sm">Profile</Link>
</Menu.Item>
</div>
<div className="w-full flexStart border-t border-nav-border mt-5 pt-5">
<Menu.Item>
<button type="button" className="text-sm" onClick={() => signOut()}>
Sign out
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
)
}

export default ProfileMenu
2 changes: 1 addition & 1 deletion grafbase/.env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# KEY=VALUE
NEXTAUTH_SECRET=0+VO7GwqqB+EfNoU+4ezrgrFyqOVeOr8Whp/m1X/cdQ=
26 changes: 21 additions & 5 deletions grafbase/grafbase.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { g, auth, config } from '@grafbase/sdk'

// @ts-ignore
const User = g.model('User', {
name: g.string().length({ min: 2, max: 20 }),
name: g.string().length({ min: 2, max: 100 }),
email: g.string().unique(),
avatarUrl: g.url(),
description: g.string().optional(),
description: g.string().length({ min: 2, max: 1000 }).optional(),
githubUrl: g.url().optional(),
linkedInUrl: g.url().optional(),
linkedinUrl: g.url().optional(),
projects: g.relation(() => Project).list().optional(), // user can create many project
})
}).auth((rules) => {
rules.public().read()
}) // try different method later (rules) => rules.public().read())

// @ts-ignore
const Project = g.model('Project', {
title: g.string().length({ min: 3 }),
description: g.string(),
Expand All @@ -19,8 +23,20 @@ const Project = g.model('Project', {
githubUrl: g.url(),
category: g.string().search(),
createdBy: g.relation(() => User) // belongs to a user
}).auth((rules) => {
rules.public().read(),
rules.private().create().delete().update()
})

const jwt = auth.JWT({
issuer: 'grafbase',
secret: g.env('NEXTAUTH_SECRET')
})

export default config({
schema: g
schema: g,
auth: {
providers: [jwt],
rules: (rules) => rules.private(),
}
})
30 changes: 30 additions & 0 deletions grapql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const getUserQuery = `
query GetUser($email: String!) {
user(by: { email: $email }) {
id
name
email
avatarUrl
description
githubUrl
linkedinUrl
}
}
`;


export const createUserMutation = `
mutation CreateUser($input: UserCreateInput!) {
userCreate(input: $input) {
user {
name
email
avatarUrl
description
githubUrl
linkedinUrl
id
}
}
}
`
34 changes: 34 additions & 0 deletions lib/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createUserMutation, getUserQuery } from "@/grapql";
import { GraphQLClient } from "graphql-request";

const isProduction = process.env.NODE_ENV === 'production'
const apiUrl = isProduction ? process.env.NEXT_PUBLIC_GRAFBASE_API_URL || '' : 'http://127.0.0.1:4000/graphql'
const apiKey = isProduction ? process.env.NEXT_PUBLIC_GRAFBASE_API_KEY || '' : 'letmein'
const serverUrl = isProduction ? process.env.NEXT_PUBLIC_SERVER_URL : 'http://localhost:3000'

const client = new GraphQLClient(apiUrl)

const makeGraphQLRequest = async (query: string, variables = {}) => {
try {
// client req
return await client.request(query, variables)

} catch (error) {
throw error;
}
}

export const getUser = (email: string) => {
client.setHeader('x-api-key', apiKey)
return makeGraphQLRequest(getUserQuery, { email })
}

export const createUser = (name: string, email: string, avatarUrl: string) => {
client.setHeader('x-api-key', apiKey)
const variables = {
input: {
name, email, avatarUrl
}
}
return makeGraphQLRequest(createUserMutation, variables)
}
81 changes: 81 additions & 0 deletions lib/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { getServerSession } from "next-auth/next";
import { NextAuthOptions, User } from "next-auth";
import { AdapterUser } from "next-auth/adapters";
import GoogleProvider from 'next-auth/providers/google';
import jsonwebtoken from 'jsonwebtoken'
import { JWT } from 'next-auth/jwt';
import { SessionInterface, UserProfile } from "@/common.types";
import { createUser, getUser } from "./actions";

export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!, // could be an undefined
clientSecret: process.env.GOOGLE_CLIENT_SECRET!, // could be an undefined
})
],
jwt: {
encode: ({ secret, token }) => {
const encodedToken = jsonwebtoken.sign({
...token,
iss: 'grafbase',
exp: Math.floor(Date.now() / 1000) + 60 * 60
}, secret);
return encodedToken;
},
decode: async ({ secret, token }) => {
const decodedToken = jsonwebtoken.verify(token!, secret);

return decodedToken as JWT;
}
},
theme: {
colorScheme: 'light',
logo: '/logo.svg'
},
callbacks: {
async session({ session }) {
const email = session?.user?.email as string;

try {
const data = await getUser(email) as { user?: UserProfile }

const newSession = {
...session,
user: {
...session?.user,
...data?.user
}
}
return newSession;
} catch (error) {
console.log('Error retrieving user data', error)
return session; // need to always return a session or something
}
},
async signIn({ user }: { user: AdapterUser | User }) {
try {
const userExist = await getUser(user?.email as string) as { user?: UserProfile }

if(!userExist.user) {
await createUser(
user.name as string,
user.email as string,
user.image as string
);
}

return true
} catch(error: any) {
console.log(error)
return false
}
}
}
}

export async function getCurrentUser() {
const session = await getServerSession(authOptions) as SessionInterface;

return session;
}
6 changes: 5 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
images: {
domains: ['lh3.googleusercontent.com'],
}
}

module.exports = nextConfig
Loading

0 comments on commit 9f26216

Please sign in to comment.