-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: setup authentication (TT-1755) (#2)
* feat: add auth provider and auth routes * add simple header to display logged in user and logout button * use env variables from k8s secrets in deployment * add .env.example * exclude AuthProvider from StrictMode in local development to prevent auth problems * fix main page layout * run lint --fix * Apply suggestions
- Loading branch information
1 parent
c2ca7e1
commit 1659eea
Showing
19 changed files
with
507 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
NEXT_PUBLIC_BASE_PATH=/ammo | ||
NEXT_PUBLIC_KEYCLOAK_BASE_URL=https://your-keycloak-url.org | ||
NEXT_PUBLIC_KEYCLOAK_REALM=your-realm | ||
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=your-client-id | ||
IMAGE_API_URL=https://your-image-server-example.com | ||
AUTH_API_PATH=https://your-auth-server-example.com |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,25 @@ | ||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). | ||
# AMMO | ||
**A**lt **M**ulig **MO**ttak (AMMO) er en web app for behandling av materiale i produksjonsløype for tekst. | ||
|
||
## Getting Started | ||
## Lokalt oppsett | ||
For å kjøre lokalt må du sette de nødvendige miljøvariablene: | ||
```bash | ||
cp .env.example .env.local | ||
``` | ||
|
||
First, run the development server: | ||
| Variabelnavn | Standardverdi | Beskrivelse | | ||
|--------------------------------|---------------|------------------------------| | ||
| NEXT_PUBLIC_BASE_PATH | /ammo | Base path for applikasjonen | | ||
| NEXT_PUBLIC_KEYCLOAK_BASE_URL | _N/A_ | URL til keycloak | | ||
| NEXT_PUBLIC_KEYCLOAK_REALM | _N/A_ | Keycloak-realmen | | ||
| NEXT_PUBLIC_KEYCLOAK_CLIENT_ID | _N/A_ | Keycloak-klienten | | ||
| AUTH_API_PATH | _N/A_ | Sti til autentiserings-APIet | | ||
| IMAGE_API_URL | _N/A_ | Sti til bilde-APIet | | ||
|
||
Deretter må du kjøre følgende kommandoer: | ||
```bash | ||
npm install | ||
npm run dev | ||
# or | ||
yarn dev | ||
# or | ||
pnpm dev | ||
# or | ||
bun dev | ||
``` | ||
|
||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | ||
|
||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. | ||
|
||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. | ||
|
||
## Learn More | ||
|
||
To learn more about Next.js, take a look at the following resources: | ||
|
||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | ||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | ||
|
||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! | ||
|
||
## Deploy on Vercel | ||
|
||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | ||
|
||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. | ||
Applikasjonen finner du nå i nettleseren på [http://localhost:3000/ammo](http://localhost:3000/ammo). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
'use client'; | ||
|
||
import {createContext, useCallback, useContext, useEffect, useState} from 'react'; | ||
import {useRouter} from 'next/navigation'; | ||
import keycloakConfig from '@/lib/keycloak'; | ||
import {User} from '@/models/UserToken'; | ||
import {refresh, signIn, signOut} from '@/services/auth.data'; | ||
|
||
interface IAuthContext { | ||
authenticated: boolean; | ||
user?: User; | ||
logout?: () => void; | ||
} | ||
|
||
const AuthContext = createContext<IAuthContext>({ | ||
authenticated: false, | ||
logout: () => {} | ||
}); | ||
|
||
export const AuthProvider = ({children}: { children: React.ReactNode }) => { | ||
const router = useRouter(); | ||
|
||
const [authenticated, setAuthenticated] = useState<boolean>(false); | ||
const [user, setUser] = useState<User>(); | ||
const [intervalId, setIntervalId] = useState<number>(); | ||
|
||
const handleNotAuthenticated = useCallback(() => { | ||
setAuthenticated(false); | ||
setUser(undefined); | ||
if (intervalId) { | ||
clearInterval(intervalId); | ||
} | ||
let currentUrl = window.location.href; | ||
// The app dislikes being redirected to a sub-path, redirecting to root to avoid issues | ||
currentUrl = currentUrl.replace(/\/ammo.*/, '/ammo'); | ||
currentUrl = encodeURIComponent(currentUrl); | ||
|
||
window.location.assign(`${keycloakConfig.url}/realms/${keycloakConfig.realm}/protocol/openid-connect/auth` + | ||
`?client_id=${keycloakConfig.clientId}&redirect_uri=${currentUrl}&response_type=code&scope=openid`); | ||
}, [intervalId]); | ||
|
||
useEffect(() => { | ||
const codeInParams = new URLSearchParams(window.location.search).get('code'); | ||
if (codeInParams) { | ||
const redirectUrl = new URLSearchParams({redirectUrl: trimRedirectUrl(window.location.href)}).toString(); | ||
void signIn(codeInParams, redirectUrl).then((token: User) => { | ||
handleIsAuthenticated(token); | ||
router.push('/'); | ||
}).catch((e: Error) => { | ||
console.error('Failed to sign in: ', e.message); | ||
handleNotAuthenticated(); | ||
}); | ||
} else if (user) { | ||
if (user.expires && new Date(user.expires) > new Date()) { | ||
handleIsAuthenticated(user); | ||
} | ||
} else { | ||
handleNotAuthenticated(); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
const handleIsAuthenticated = (newUser: User) => { | ||
if (newUser) { | ||
setUser(newUser); | ||
setAuthenticated(true); | ||
} | ||
}; | ||
|
||
const refreshToken = useCallback(async () => { | ||
return refresh(); | ||
}, []); | ||
|
||
const setIntervalToRefreshAccessToken = useCallback(async () => { | ||
if (user?.expires && !intervalId) { | ||
const expiryTime = new Date(user?.expires).getTime() - Date.now(); | ||
if (expiryTime < 1000 * 60 * 4.75) { | ||
await refreshToken(); | ||
} | ||
setIntervalId(window.setInterval(() => { | ||
void refreshToken().then((newUser: User) => { | ||
handleIsAuthenticated(newUser); | ||
}) | ||
.catch((e: Error) => { | ||
console.error('Failed to refresh token: ', e.message); | ||
handleNotAuthenticated(); | ||
}); | ||
}, (1000 * 60 * 4.75))); // Refresh every 4.75 minutes (fifteen seconds before expiry) | ||
} | ||
}, [handleNotAuthenticated, intervalId, refreshToken, user?.expires]); | ||
|
||
useEffect(() => { | ||
void setIntervalToRefreshAccessToken(); | ||
}, [setIntervalToRefreshAccessToken]); | ||
|
||
const trimRedirectUrl= (returnUrl: string): string => { | ||
returnUrl = returnUrl.split('?')[0]; | ||
if (returnUrl.at(-1) === '/') { | ||
returnUrl = returnUrl.slice(0, -1); | ||
} | ||
return returnUrl; | ||
}; | ||
|
||
const logout = async () => { | ||
await signOut() | ||
.then(() => { | ||
handleNotAuthenticated(); | ||
}); | ||
}; | ||
|
||
return ( | ||
<AuthContext.Provider | ||
value={{ | ||
authenticated, | ||
user, | ||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||
logout | ||
}} | ||
> | ||
{children} | ||
</AuthContext.Provider> | ||
); | ||
}; | ||
|
||
export const useAuth = () => useContext<IAuthContext>(AuthContext); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import {NextResponse} from 'next/server'; | ||
import {User, UserToken} from '@/models/UserToken'; | ||
import {getRefreshToken, setUserCookie} from '@/utils/cookieUtils'; | ||
|
||
// POST api/auth/refresh | ||
export async function POST(): Promise<NextResponse> { | ||
const refreshToken = getRefreshToken(); | ||
if (!refreshToken) { | ||
return NextResponse.json({error: 'No user token found'}, {status: 401}); | ||
} | ||
|
||
const data = await fetch(`${process.env.AUTH_API_PATH}/refresh`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: refreshToken | ||
}); | ||
|
||
const newToken = await data.json() as UserToken; | ||
|
||
if (!newToken || !newToken.name || !newToken.expires) { | ||
return NextResponse.json({error: 'Failed to refresh token'}, {status: 500}); | ||
} | ||
|
||
setUserCookie(newToken); | ||
|
||
const user: User = {name: newToken.name, expires: newToken.expires}; | ||
return NextResponse.json(user, {status: 200}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import {NextRequest, NextResponse} from 'next/server'; | ||
import {User, UserToken} from '@/models/UserToken'; | ||
import {ProblemDetail} from '@/models/ProblemDetail'; | ||
import {setUserCookie} from '@/utils/cookieUtils'; | ||
|
||
interface LoginRequest { | ||
code: string; | ||
redirectUrl: string; | ||
} | ||
|
||
// POST api/auth/signin | ||
export async function POST(req: NextRequest): Promise<NextResponse> { | ||
const {code, redirectUrl} = await req.json() as LoginRequest; | ||
const data = await fetch(`${process.env.AUTH_API_PATH}/login?${redirectUrl}`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: code | ||
}) | ||
.then(async response => { | ||
if (!response.ok) { | ||
const problemDetail = await response.json() as ProblemDetail; | ||
return NextResponse.json({error: problemDetail.detail}, {status: problemDetail.status}); | ||
} | ||
return response; | ||
}); | ||
|
||
if (data instanceof NextResponse) { | ||
return data; | ||
} | ||
|
||
const userToken = await data.json() as UserToken; | ||
|
||
if (!userToken || !userToken.name || !userToken.expires) { | ||
return NextResponse.json({error: 'Failed to authenticate'}, {status: 500}); | ||
} | ||
|
||
setUserCookie(userToken); | ||
|
||
const user: User = {name: userToken.name, expires: userToken.expires}; | ||
return NextResponse.json(user, {status: 200}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import {NextResponse} from 'next/server'; | ||
import {deleteUserToken, getRefreshToken} from '@/utils/cookieUtils'; | ||
|
||
// POST api/auth/signout | ||
export async function POST(): Promise<NextResponse> { | ||
const refreshToken = getRefreshToken(); | ||
if (!refreshToken) { | ||
return NextResponse.json({error: 'No user token found'}, {status: 401}); | ||
} | ||
|
||
return await fetch(`${process.env.AUTH_API_PATH}/logout`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: refreshToken | ||
}).then(res => { | ||
if (!res.ok) { | ||
return NextResponse.json({error: 'Failed to logout'}, {status: res.status}); | ||
} | ||
deleteUserToken(); | ||
return NextResponse.json({message: 'Logged out successfully'}, {status: 200}); | ||
}).catch((error: Error) => { | ||
return NextResponse.json({error: `Failed to logout: ${error.message}`}, {status: 500}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,17 @@ | ||
'use client'; | ||
|
||
import {AuthProvider} from '@/app/AuthProvider'; | ||
import {NextUIProvider} from '@nextui-org/react'; | ||
import React from 'react'; | ||
|
||
export function Providers({children}: { children: React.ReactNode }) { | ||
return ( | ||
<NextUIProvider> | ||
{children} | ||
</NextUIProvider> | ||
<AuthProvider> | ||
<React.StrictMode> | ||
<NextUIProvider> | ||
{children} | ||
</NextUIProvider> | ||
</React.StrictMode> | ||
</AuthProvider> | ||
); | ||
} | ||
} |
Oops, something went wrong.