Skip to content

Commit

Permalink
Add fake login form (#3)
Browse files Browse the repository at this point in the history
remove GIGS_MANAGABLE_USER_EMAIL in favour of email login form;
add fake login form storing email in a cookie;
add auth protection: only allow going to /checkout and /backoffice if logged in
  • Loading branch information
gigsanna authored Apr 22, 2024
1 parent e1abce0 commit 2b488ab
Show file tree
Hide file tree
Showing 18 changed files with 673 additions and 26 deletions.
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
GIGS_PROJECT=
GIGS_API_KEY=
GIGS_MANAGABLE_USER_EMAIL=
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Connect Sessions Buy & Manage example

This application showcase how Connect Sessions can be used to buy and manag phone plans through Connect from your existing application. This is a developer reference.
This application showcase how Connect Sessions can be used to buy and manage phone plans through Connect from your existing application. This is a developer reference.

To get the most out of this, please read the [associated guide](https://developers.gigs.com/docs/api/805ba2c145553-example-purchasing-and-managing-subscriptions-using-connect-sessions).

Expand All @@ -20,7 +20,7 @@ In order to run this example locally, you need:
- an existing Project with Connect enabled and at least one plan and one add-on configured
- a valid API key

To setup the example:
To set up the example:

**Clone the repository**

Expand All @@ -45,7 +45,6 @@ Set the required environment variables:

- `GIGS_PROJECT`: The name of your project (the yourproject part from your yourproject.gigs.com Connect url)
- `GIGS_API_KEY`: Your API key
- `GIGS_MANAGABLE_USER_EMAIL`: The desired email of your user (it does not have to exist in your project yet, but you **should have access to the emails**)

**Start the app**

Expand Down
13 changes: 13 additions & 0 deletions app/(manage)/backoffice/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { auth } from '@/lib/applicationMocks'
import { LoginForm } from '@/components/LoginForm'

type Props = {
children: React.ReactNode
}

const Layout = async ({ children }: Props) => {
const currentUser = auth.getUser()
return currentUser.email ? <>{children}</> : <LoginForm />
}

export default Layout
13 changes: 13 additions & 0 deletions app/(purchase)/checkout/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { auth } from '@/lib/applicationMocks'
import { LoginForm } from '@/components/LoginForm'

type Props = {
children: React.ReactNode
}

const Layout = async ({ children }: Props) => {
const currentUser = auth.getUser()
return currentUser.email ? <>{children}</> : <LoginForm />
}

export default Layout
10 changes: 3 additions & 7 deletions app/(purchase)/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
} from '@/components/ui/carousel'
import { getPlans } from '@/lib/api'
import { envVarsPresent } from '@/lib/utils'
import { UserCircle } from 'lucide-react'
import Image from 'next/image'
import { redirect } from 'next/navigation'
import { UserButton } from '@/components/UserButton'
import { auth } from '@/lib/applicationMocks'

const CheckoutPage = async () => {
if (!envVarsPresent()) {
Expand All @@ -32,12 +33,7 @@ const CheckoutPage = async () => {
src="/tigr-logo.webp"
width={100}
/>
<div className="flex items-center gap-2">
<div className="flex gap-2 text-gray-700 dark:text-gray-300">
<UserCircle />
{process.env.GIGS_MANAGABLE_USER_EMAIL}
</div>
</div>
<UserButton user={auth.getUser().email} />
</header>
<main className="mx-auto max-w-6xl px-12 py-6 ">
<h1 className="mb-24 mt-6 text-center text-3xl font-bold">
Expand Down
11 changes: 3 additions & 8 deletions app/setup-error/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Link from 'next/link'
const SetupErrorPage = () => {
const gigsProject = process.env.GIGS_PROJECT
const gigsAPIKey = process.env.GIGS_API_KEY
const gigsUser = process.env.GIGS_MANAGABLE_USER_EMAIL

return (
<div className="flex h-screen items-center justify-center bg-ottoman-100">
Expand All @@ -25,17 +24,13 @@ const SetupErrorPage = () => {
</a>
<div className="my-12 flex flex-col gap-4 self-center bg-gray-100 p-8 font-mono">
<div className="flex items-center gap-2">
<PresentMissinIcon present={!!gigsProject} />
<PresentMissingIcon present={!!gigsProject} />
GIGS_PROJECT
</div>
<div className="flex items-center gap-2">
<PresentMissinIcon present={!!gigsAPIKey} />
<PresentMissingIcon present={!!gigsAPIKey} />
GIGS_API_KEY
</div>
<div className="flex items-center gap-2">
<PresentMissinIcon present={!!gigsUser} />
GIGS_MANAGABLE_USER_EMAIL
</div>
</div>

<Link
Expand All @@ -49,7 +44,7 @@ const SetupErrorPage = () => {
)
}

const PresentMissinIcon = ({ present }: { present: boolean }) => {
const PresentMissingIcon = ({ present }: { present: boolean }) => {
if (present) {
return <Check className="h-4 w-4 text-green-600" />
}
Expand Down
28 changes: 28 additions & 0 deletions components/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import { Button } from '@/components/ui/button'
import { login } from '@/lib/actions'
import { Input } from '@/components/ui/input'

export const LoginForm = () => {
return (
<div className="flex min-h-screen items-center justify-center">
<form role="form" action={login} className="w-96 space-y-6">
<div className="space-y-2">
<label htmlFor="email" className="">
Enter your mail
</label>
<Input
name="email"
type="email"
id="email"
required
placeholder="example@gigs.com"
autoFocus
/>
</div>
<Button type="submit">Log In</Button>
</form>
</div>
)
}
6 changes: 5 additions & 1 deletion components/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Link from 'next/link'
import Image from 'next/image'
import { Phone, ShoppingCart, Smartphone, User2 } from 'lucide-react'

import { SideNavLogoutButton } from '@/components/SideNavLogoutButton'

export const SideNav = () => {
return (
<div className="flex h-full max-h-screen flex-col gap-2">
Expand All @@ -15,7 +17,7 @@ export const SideNav = () => {
/>
</div>
<div className="flex-1 overflow-auto py-2">
<nav className="grid items-start gap-2 px-4 text-sm font-medium">
<nav className="flex h-full flex-col items-stretch gap-2 px-4 text-sm font-medium">
<Link
className="flex cursor-not-allowed items-center gap-3 rounded-lg px-3 py-2 text-gray-500 transition-all hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50"
href="#"
Expand Down Expand Up @@ -44,6 +46,8 @@ export const SideNav = () => {
<Phone className="h-4 w-4" />
Phone Plans
</Link>

<SideNavLogoutButton />
</nav>
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions components/SideNavLogoutButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'

import { logout } from '@/lib/actions'
import { LogOut } from 'lucide-react'

export const SideNavLogoutButton = () => {
return (
<button
className="mt-auto flex items-center gap-3 rounded-lg px-3 py-2 text-gray-900 transition-all hover:bg-gray-50 hover:text-gray-900 dark:text-gray-50 dark:hover:bg-gray-800 dark:hover:text-gray-50"
onClick={() => logout()}
>
<LogOut className="h-4 w-4" />
Log out
</button>
)
}
43 changes: 43 additions & 0 deletions components/UserButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client'

import { logout } from '@/lib/actions'
import { UserCircle, LogOut } from 'lucide-react'

import {
Dropdown,
DropdownContent,
DropdownItem,
DropdownPortal,
DropdownTrigger,
} from '@/components/ui/dropdown'

type UserButtonProps = {
user: string | undefined
}

export const UserButton = ({ user }: UserButtonProps) => {
if (!user) {
return null
}

return (
<Dropdown>
<DropdownTrigger className="outline-none">
<div className="flex gap-2 text-gray-700 dark:text-gray-300">
<UserCircle />
{user}
</div>
</DropdownTrigger>
<DropdownPortal>
<DropdownContent align="end" sideOffset={5}>
<DropdownItem onSelect={() => logout()} className="text-end">
<div className="flex gap-2">
<LogOut />
Log out
</div>
</DropdownItem>
</DropdownContent>
</DropdownPortal>
</Dropdown>
)
}
57 changes: 57 additions & 0 deletions components/ui/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client'

import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'

import { cn } from '@/lib/utils'

const Dropdown = DropdownMenuPrimitive.Root

const DropdownTrigger = DropdownMenuPrimitive.Trigger

const DropdownPortal = DropdownMenuPrimitive.Portal

const DropdownContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.Content
ref={ref}
className={cn(
'border border-neutral-200 bg-white px-4 py-2 shadow-lg duration-200 dark:border-neutral-800 dark:bg-neutral-950 sm:rounded-lg',
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-top-1/2',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-top-1/2',
className,
)}
{...props}
>
{children}
</DropdownMenuPrimitive.Content>
))
DropdownContent.displayName = DropdownMenuPrimitive.Content.displayName

const DropdownItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'cursor-default select-none rounded-md px-2 py-2 text-sm outline-none',
'text-gray-700 focus:bg-gray-50 dark:text-gray-300 dark:focus:bg-gray-900',
className,
)}
{...props}
>
{children}
</DropdownMenuPrimitive.Item>
))
DropdownItem.displayName = DropdownMenuPrimitive.Item.displayName

export {
Dropdown,
DropdownTrigger,
DropdownContent,
DropdownPortal,
DropdownItem,
}
26 changes: 26 additions & 0 deletions components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react'

import { cn } from '@/lib/utils'

export const Input = React.forwardRef<
HTMLInputElement,
React.InputHTMLAttributes<HTMLInputElement>
>((props, ref) => {
const { className, ...inputProps } = props

return (
<input
ref={ref}
className={cn(
'block w-full rounded-lg border text-left text-base transition',
'font-medium placeholder:text-gray-500 focus:border-gray-600 focus:ring-1 focus:ring-gray-600',
'h-[52px] px-2 py-4 outline-none',
'border-gray-400 bg-white text-gray-800',
'[&:not(:placeholder-shown)]:invalid:border-rose-400 [&:not(:placeholder-shown)]:invalid:focus:border-rose-600' +
'[&:not(:placeholder-shown)]:invalid:ring-rose-600 [&:not(:placeholder-shown)]:invalid:focus:ring-rose-600',
className,
)}
{...inputProps}
/>
)
})
13 changes: 12 additions & 1 deletion lib/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use server'

import { createConnectSession, findUser } from './api'
import { auth } from './applicationMocks'
import { auth, resetUserEmail, setUserEmail } from './applicationMocks'
import { ConnectSessionParams } from './schemas/connectSession'

export const checkoutCurrentUserWithPlan = async (planId: string) => {
Expand Down Expand Up @@ -111,3 +111,14 @@ export const checkoutAddon = async (

return await createConnectSession(connectSession)
}

export const login = (formData: FormData) => {
const email = formData.get('email') as string

// perform an actual authentication call here
setUserEmail(email)
}

export const logout = () => {
resetUserEmail()
}
4 changes: 2 additions & 2 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type ApiCollectionResponse<T> = {
data?: T[]
}

type ApiItemResonse<T> = {
type ApiItemResponse<T> = {
error?: string
data?: T
}
Expand Down Expand Up @@ -119,7 +119,7 @@ export const getAddons = async (

export const createConnectSession = async (
connectSession: ConnectSessionParams,
): Promise<ApiItemResonse<ConnectSessionResponse>> => {
): Promise<ApiItemResponse<ConnectSessionResponse>> => {
const options: RequestInit = {
method: 'POST',
headers,
Expand Down
11 changes: 10 additions & 1 deletion lib/applicationMocks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { cookies } from 'next/headers'

const loginCookie = 'connect_session_example_user'

export const setUserEmail = (email: string) =>
cookies().set(loginCookie, email, { maxAge: 3600 * 24, sameSite: 'strict' })
export const getUserEmail = () => cookies().get(loginCookie)?.value ?? ''
export const resetUserEmail = () => cookies().delete(loginCookie)

export const auth = {
getUser: () => ({
email: process.env.GIGS_MANAGABLE_USER_EMAIL!,
email: getUserEmail(),
birthday: '1991-07-22',
fullName: 'John Doe',
}),
Expand Down
3 changes: 1 addition & 2 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export type ElementOf<T> = T extends readonly (infer E)[] ? E : never
export const envVarsPresent = () => {
const gigsProject = process.env.GIGS_PROJECT
const gigsAPIKey = process.env.GIGS_API_KEY
const gigsUser = process.env.GIGS_MANAGABLE_USER_EMAIL

return !!gigsProject && !!gigsAPIKey && !!gigsUser
return !!gigsProject && !!gigsAPIKey
}
Loading

0 comments on commit 2b488ab

Please sign in to comment.