Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/run-ci-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,6 @@ jobs:
- name: Prepare frontend environment
run: |
touch frontend/.env
echo "GITHUB_CLIENT_ID=${{ secrets.GITHUB_CLIENT_ID }}" >> frontend/.env
echo "GITHUB_CLIENT_SECRET=${{ secrets.GITHUB_CLIENT_SECRET }}" >> frontend/.env
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> frontend/.env
echo "NEXTAUTH_URL=${{ secrets.VITE_API_URL }}" >> frontend/.env
echo "NEXT_PUBLIC_API_URL=${{ secrets.VITE_API_URL }}" >> frontend/.env
Expand All @@ -309,6 +307,8 @@ jobs:
echo "NEXT_PUBLIC_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }}" >> frontend/.env
echo "NEXT_SERVER_CSRF_URL=${{ secrets.NEXT_SERVER_CSRF_URL }}" >> frontend/.env
echo "NEXT_SERVER_GRAPHQL_URL=${{ secrets.NEXT_SERVER_GRAPHQL_URL }}" >> frontend/.env
echo "NEXT_SERVER_GITHUB_CLIENT_ID=${{ secrets.GITHUB_CLIENT_ID }}" >> frontend/.env
echo "NEXT_SERVER_GITHUB_CLIENT_SECRET=${{ secrets.GITHUB_CLIENT_SECRET }}" >> frontend/.env

- name: Build frontend image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
Expand Down Expand Up @@ -490,8 +490,6 @@ jobs:
- name: Prepare frontend environment
run: |
touch frontend/.env
echo "GITHUB_CLIENT_ID=${{ secrets.GITHUB_CLIENT_ID }}" >> frontend/.env
echo "GITHUB_CLIENT_SECRET=${{ secrets.GITHUB_CLIENT_SECRET }}" >> frontend/.env
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> frontend/.env
echo "NEXTAUTH_URL=${{ secrets.VITE_API_URL }}" >> frontend/.env
echo "NEXT_PUBLIC_API_URL=${{ secrets.VITE_API_URL }}" >> frontend/.env
Expand All @@ -504,6 +502,8 @@ jobs:
echo "NEXT_PUBLIC_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }}" >> frontend/.env
echo "NEXT_SERVER_CSRF_URL=${{ secrets.NEXT_SERVER_CSRF_URL }}" >> frontend/.env
echo "NEXT_SERVER_GRAPHQL_URL=${{ secrets.NEXT_SERVER_GRAPHQL_URL }}" >> frontend/.env
echo "NEXT_SERVER_GITHUB_CLIENT_ID=${{ secrets.GITHUB_CLIENT_ID }}" >> frontend/.env
echo "NEXT_SERVER_GITHUB_CLIENT_SECRET=${{ secrets.GITHUB_CLIENT_SECRET }}" >> frontend/.env

- name: Build frontend image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
Expand Down
2 changes: 1 addition & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1/
NEXT_PUBLIC_CSRF_URL=http://localhost:8000/csrf/
Expand All @@ -12,4 +13,3 @@ NEXT_SERVER_DISABLE_SSR=false
NEXT_SERVER_GRAPHQL_URL=http://backend:8000/graphql/
NEXT_SERVER_GITHUB_CLIENT_ID=
NEXT_SERVER_GITHUB_CLIENT_SECRET=
NEXT_SERVER_NEXTAUTH_SECRET=
8 changes: 0 additions & 8 deletions frontend/__tests__/e2e/pages/Login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ test.describe('Login Page', () => {
body: JSON.stringify(mockHomeData),
})
})

await page.context().addCookies([
{
name: 'csrftoken',
Expand All @@ -30,13 +29,6 @@ test.describe('Login Page', () => {
await expect(button).toBeVisible()
})

test('displays loading spinner when logging in', async ({ page }) => {
await page.goto('/login')

const button = page.getByRole('button', { name: /sign in with github/i })
await button.click()
await expect(page).toHaveURL(/github\.com/)
})
test('shows spinner while loading session', async ({ page }) => {
await page.route('**/api/auth/session', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 500))
Expand Down
3 changes: 0 additions & 3 deletions frontend/__tests__/unit/pages/Login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ jest.mock('next-auth/react', () => ({
useSession: jest.fn(),
signIn: jest.fn(),
}))

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
}))

jest.mock('@heroui/toast', () => ({
addToast: jest.fn(),
}))
Expand All @@ -37,7 +35,6 @@ describe('LoginPage', () => {
;(useSession as jest.Mock).mockReturnValue({ status: 'authenticated' })

render(<LoginPage />)

expect(screen.getByText(/Redirecting/i)).toBeInTheDocument()
expect(addToast).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down
24 changes: 16 additions & 8 deletions frontend/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import {
NEXTAUTH_URL,
} from 'utils/credentials'

const authOptions = {
providers: [
const providers = []

if (GITHUB_CLIENT_ID && GITHUB_CLIENT_SECRET) {
providers.push(
GitHubProvider({
clientId: GITHUB_CLIENT_ID!,
clientSecret: GITHUB_CLIENT_SECRET!,
}),
],
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
})
)
}

const authOptions = {
providers,
session: {
strategy: 'jwt' as const,
},
Expand Down Expand Up @@ -48,14 +54,16 @@ const authOptions = {
},

async jwt({ token, account }) {
if (account) {
if (account?.access_token) {
token.accessToken = account.access_token
}
return token
},

async session({ session, token }) {
session.accessToken = token.accessToken
if (token?.accessToken) {
session.accessToken = token.accessToken
}
return session
},
},
Expand Down
75 changes: 52 additions & 23 deletions frontend/src/components/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,82 @@
'use client'

import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Skeleton } from '@heroui/react'
import Image from 'next/image'
import { useSession, signIn, signOut } from 'next-auth/react'
import { useEffect, useId, useRef, useState } from 'react'
import { userAuthStatus } from 'utils/constants'

export default function UserMenu() {
const { data: session, status } = useSession()
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const dropdownId = useId()

// Close on outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])

if (status === userAuthStatus.LOADING) {
return (
<div className="flex h-10 w-10 items-center justify-center">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="animate-pulse h-10 w-10 rounded-full bg-gray-300 dark:bg-slate-700" />
</div>
)
}

if (!session) {
return (
<button
onClick={() => signIn('github', { callbackUrl: '/', prompt: 'login' })}
className="group relative flex h-10 w-full cursor-pointer items-center justify-center gap-2 overflow-hidden whitespace-pre rounded-md bg-[#87a1bc] px-4 py-2 text-sm font-medium text-black hover:ring-1 hover:ring-[#b0c7de] hover:ring-offset-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 dark:bg-slate-900 dark:text-white dark:hover:bg-slate-900/90 dark:hover:ring-[#46576b] md:flex"
className="group relative flex h-10 w-full cursor-pointer items-center justify-center gap-2 overflow-hidden whitespace-pre rounded-md bg-[#87a1bc] px-4 py-2 text-sm font-medium text-black hover:ring-1 hover:ring-[#b0c7de] hover:ring-offset-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 dark:bg-slate-900 dark:text-white dark:hover:bg-slate-900/90 dark:hover:ring-[#46576b]"
>
Sign in
</button>
)
}

return (
<Dropdown className="bg-owasp-blue dark:bg-slate-800">
<DropdownTrigger>
<Image
src={session.user?.image || '/default-avatar.png'}
height={40}
width={40}
alt="User avatar"
className="h-10 w-10 cursor-pointer rounded-full object-cover"
/>
</DropdownTrigger>
<div ref={dropdownRef} className="relative flex items-center justify-center">
<button
onClick={() => setIsOpen((prev) => !prev)}
aria-expanded={isOpen}
aria-haspopup="true"
aria-controls={dropdownId}
className="w-auto focus:outline-none"
>
<div className="h-10 w-10 overflow-hidden rounded-full">
<Image
src={session.user?.image || '/default-avatar.png'}
height={40}
width={40}
alt="User avatar"
className="h-full w-full object-cover"
/>
</div>
</button>

<DropdownMenu className="w-48" variant="bordered">
<DropdownItem
key={'sign-out'}
disableAnimation
onClick={() => signOut({ callbackUrl: '/' })}
className="relative flex h-10 w-full cursor-pointer items-center justify-center gap-2 whitespace-pre rounded-md bg-[#87a1bc] px-4 py-2 text-sm font-medium text-black focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 dark:bg-slate-900 dark:text-white"
{isOpen && (
<div
id={dropdownId}
className="absolute right-0 top-full z-20 mt-2 w-48 overflow-hidden rounded-md bg-white shadow-lg dark:bg-slate-800"
>
Sign out
</DropdownItem>
</DropdownMenu>
</Dropdown>
<button
onClick={() => {
signOut({ callbackUrl: '/' })
setIsOpen(false)
}}
className="block w-full px-4 py-2 text-left text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white"
>
Sign out
</button>
</div>
)}
</div>
)
}
4 changes: 2 additions & 2 deletions frontend/src/utils/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ export const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID
export const IDX_URL = process.env.NEXT_PUBLIC_IDX_URL
export const RELEASE_VERSION = process.env.NEXT_PUBLIC_RELEASE_VERSION
export const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN
export const NEXTAUTH_SECRET = process.env.NEXT_SERVER_NEXTAUTH_SECRET
export const NEXTAUTH_URL = process.env.NEXT_SERVER_NEXTAUTH_URL
export const NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET
export const NEXTAUTH_URL = process.env.NEXTAUTH_URL