Skip to content

Commit

Permalink
Merge branch 'master' into endre_tavlenavn
Browse files Browse the repository at this point in the history
  • Loading branch information
natashatikhonova authored Aug 23, 2023
2 parents 9e9cbe9 + d7c2757 commit f90c902
Show file tree
Hide file tree
Showing 27 changed files with 1,615 additions and 110 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/deploy_next_tavla_staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- master
paths:
- "next-tavla/**"
env:
FIREBASE_CONFIG: ${{ secrets.FIREBASE_CONFIG }}

jobs:
deploy-staging:
Expand Down
3 changes: 2 additions & 1 deletion next-tavla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@dnd-kit/modifiers": "6.0.1",
"@dnd-kit/sortable": "7.0.2",
"@dnd-kit/utilities": "3.2.1",
"@entur/alert": "0.14.3",
"@entur/alert": "0.14.9",
"@entur/button": "3.0.8",
"@entur/chip": "0.6.39",
"@entur/dropdown": "4.0.9",
Expand All @@ -43,6 +43,7 @@
"@firebase/database-types": "0.10.4",
"classnames": "2.3.2",
"firebase": "9.18.0",
"firebase-admin": "11.10.1",
"graphql": "16.6.0",
"lodash": "4.17.21",
"nanoid": "4.0.2",
Expand Down
32 changes: 32 additions & 0 deletions next-tavla/pages/api/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { initializeAdminApp } from 'Admin/utils/firebase'
import { auth } from 'firebase-admin'
import { NextApiRequest, NextApiResponse } from 'next'

initializeAdminApp()

export default async function handler(
request: NextApiRequest,
response: NextApiResponse,
) {
const authorization = request.headers.authorization
if (authorization?.startsWith('Bearer ')) {
const idToken = authorization.split('Bearer ')[1] ?? ''
const decodedToken = await auth().verifyIdToken(idToken)

if (decodedToken) {
const expiresIn = 60 * 60 * 24 * 10 // Ten days
const sessionCookie = await auth().createSessionCookie(idToken, {
expiresIn,
})

response.setHeader(
'Set-Cookie',
`session=${sessionCookie};HttpOnly;Max-Age=${expiresIn};Secure;SameSite=Strict;Path=/;`,
)
return response
.status(200)
.json({ message: 'Successfully logged in!' })
}
}
return response.status(400).json({ error: 'Could not log in!' })
}
9 changes: 9 additions & 0 deletions next-tavla/pages/api/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
request: NextApiRequest,
response: NextApiResponse,
) {
response.setHeader('Set-Cookie', `session="";Max-Age=-1;Path=/`)
return response.status(200).json({ message: 'Successfully logged out!' })
}
20 changes: 18 additions & 2 deletions next-tavla/pages/edit/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ import classes from 'styles/pages/admin.module.css'
import { Contrast } from '@entur/layout'
import { upgradeSettings } from 'utils/converters'
import { ToastProvider } from '@entur/alert'
import { IncomingNextMessage } from 'types/next'
import { verifySession } from 'Admin/utils/firebase'
import { DecodedIdToken } from 'firebase-admin/lib/auth/token-verifier'

export async function getServerSideProps({
params,
req,
}: {
params: { id: string }
req: IncomingNextMessage
}) {
const { id } = params

const session = req.cookies['session']
const user = await verifySession(session)
const settings: TSettings | undefined = await getBoardSettings(id)

if (!settings) {
Expand All @@ -26,18 +33,27 @@ export async function getServerSideProps({

return {
props: {
user: user,
settings: convertedSettings,
id,
},
}
}

function AdminPage({ settings, id }: { settings: TSettings; id: string }) {
function AdminPage({
user,
settings,
id,
}: {
user: DecodedIdToken | null
settings: TSettings
id: string
}) {
return (
<Contrast className={classes.root}>
<ToastProvider>
<Header />
<Edit initialSettings={settings} documentId={id} />
<Edit initialSettings={settings} documentId={id} user={user} />
</ToastProvider>
</Contrast>
)
Expand Down
4 changes: 2 additions & 2 deletions next-tavla/src/Admin/scenarios/AddTile/AddTile.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Contrast } from '@entur/layout'
import React, { useReducer } from 'react'
import { geocoder_endpoint } from 'src/Shared/assets/environmentConfig'
import { TilesOverview } from '../TilesOverview'
import { AddTile } from './index'
import { SettingsDispatchContext } from 'Admin/utils/contexts'
import { settingsReducer } from '../Edit/reducer'
import { ToastProvider } from '@entur/alert'
import { GEOCODER_ENDPOINT } from 'assets/env'

describe('<AddTile />', () => {
const TestComponent = () => {
Expand All @@ -30,7 +30,7 @@ describe('<AddTile />', () => {

it('can add a stop place tile', () => {
cy.mount(<TestComponent />)
cy.intercept(`${geocoder_endpoint}/autocomplete?*`, {
cy.intercept(`${GEOCODER_ENDPOINT}/autocomplete?*`, {
fixture: 'graphql/geocoder.json',
})

Expand Down
6 changes: 6 additions & 0 deletions next-tavla/src/Admin/scenarios/Edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ import { CopyIcon, SaveIcon } from '@entur/icons'
import { SecondaryLink } from 'components/SecondaryLink'
import { useToast } from '@entur/alert'
import BoardTitle from '../BoardTitle'
import { Login } from '../Login'
import { DecodedIdToken } from 'firebase-admin/lib/auth/token-verifier'

function Edit({
initialSettings,
documentId,
user,
}: {
initialSettings: TSettings
documentId: string
user: DecodedIdToken | null
}) {
const [settings, dispatch] = useReducer(settingsReducer, initialSettings)
const { addToast } = useToast()
Expand Down Expand Up @@ -51,6 +55,8 @@ function Edit({
Lagre tavla
<SaveIcon />
</PrimaryButton>

<Login user={user} />
</div>
</div>
<AddTile />
Expand Down
67 changes: 67 additions & 0 deletions next-tavla/src/Admin/scenarios/Login/components/CreateUser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { PrimaryButton } from '@entur/button'
import { TextField } from '@entur/form'
import { Heading3 } from '@entur/typography'
import musk from 'assets/illustrations/Musk.png'
import { FirebaseError } from 'firebase/app'
import Image from 'next/image'
import { SyntheticEvent } from 'react'

import { useAuth } from '../hooks/useAuth'
import { useFirebaseAuthError } from '../hooks/useFirebaseAuthError'
import { UserError } from './UserError'

function CreateUser() {
const { createUser } = useAuth()
const { error, setError, getTextFieldPropsForType } = useFirebaseAuthError()

const submitCreateUser = async (event: SyntheticEvent) => {
event.preventDefault()

const data = event.currentTarget as unknown as {
email: HTMLInputElement
password: HTMLInputElement
repeat_password: HTMLInputElement
}

const email = data.email.value
const password = data.password.value
const repeatPassword = data.repeat_password.value

try {
await createUser(email, password, repeatPassword)
} catch (error) {
if (error instanceof FirebaseError) setError(error)
}
}

return (
<div>
<Image src={musk} alt="illustration" className="h-50 w-50" />
<Heading3>Logg inn med e-post</Heading3>
<form className="flexColumn" onSubmit={submitCreateUser}>
<TextField
name="email"
label="E-post"
type="email"
{...getTextFieldPropsForType('email')}
/>
<TextField
name="password"
label="Passord"
type="password"
{...getTextFieldPropsForType('password')}
/>
<TextField
name="repeat_password"
label="Gjenta passord"
type="password"
{...getTextFieldPropsForType('repeat_password')}
/>
<UserError error={error} />
<PrimaryButton type="submit">Opprett ny bruker</PrimaryButton>
</form>
</div>
)
}

export { CreateUser }
62 changes: 62 additions & 0 deletions next-tavla/src/Admin/scenarios/Login/components/Email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { PrimaryButton } from '@entur/button'
import { TextField } from '@entur/form'
import { Heading3 } from '@entur/typography'
import musk from 'assets/illustrations/Musk.png'
import { FirebaseError } from 'firebase/app'
import Image from 'next/image'
import { SyntheticEvent } from 'react'
import { useAuth } from '../hooks/useAuth'
import { useFirebaseAuthError } from '../hooks/useFirebaseAuthError'
import { UserError } from './UserError'

function Email() {
const { error, setError, getTextFieldPropsForType } = useFirebaseAuthError()
const { login } = useAuth()

const submitEmailLogin = async (event: SyntheticEvent) => {
event.preventDefault()

const data = event.currentTarget as unknown as {
email: HTMLInputElement
password: HTMLInputElement
}

const email = data.email.value
const password = data.password.value

try {
await login(email, password)
} catch (error: unknown) {
if (error instanceof FirebaseError) {
setError(error)
}
}
}

return (
<div>
<Image src={musk} alt="illustration" className="h-50 w-50" />
<Heading3>Logg inn med e-post</Heading3>
<form className="flexColumn" onSubmit={submitEmailLogin}>
<TextField
name="email"
label="E-post"
type="email"
{...getTextFieldPropsForType('email')}
/>
<TextField
name="password"
label="Passord"
type="password"
{...getTextFieldPropsForType('password')}
/>

<UserError error={error} />

<PrimaryButton type="submit">Logg inn</PrimaryButton>
</form>
</div>
)
}

export { Email }
27 changes: 27 additions & 0 deletions next-tavla/src/Admin/scenarios/Login/components/Start.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PrimaryButton, SecondaryButton } from '@entur/button'
import { Heading3, Paragraph } from '@entur/typography'
import { TLoginPage } from 'Admin/types/login'
import musk from 'assets/illustrations/Musk.png'
import Image from 'next/image'

function Start({ pushPage }: { pushPage: (page: TLoginPage) => void }) {
return (
<div>
<Image src={musk} alt="illustration" className="h-50 w-50" />
<Heading3>Logg inn for å fortsette</Heading3>
<Paragraph>
Logg inn for å få tilgang til å opprette og administrere tavler.
</Paragraph>
<div className="flexColumn">
<PrimaryButton onClick={() => pushPage('email')}>
Logg inn med e-post
</PrimaryButton>
<SecondaryButton onClick={() => pushPage('create')}>
Opprett ny bruker
</SecondaryButton>
</div>
</div>
)
}

export { Start }
9 changes: 9 additions & 0 deletions next-tavla/src/Admin/scenarios/Login/components/UserError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SmallAlertBox } from '@entur/alert'
import { TAuthError } from 'Admin/types/login'

function UserError({ error }: { error?: TAuthError }) {
if (!error || error.type !== 'user') return null
return <SmallAlertBox variant="error">{error.value}</SmallAlertBox>
}

export { UserError }
52 changes: 52 additions & 0 deletions next-tavla/src/Admin/scenarios/Login/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { fetchWithIdToken } from 'Admin/utils'
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from 'firebase/auth'
import { auth } from 'utils/firebase'

import { FirebaseError } from 'firebase/app'
import { useRouter } from 'next/router'

function useAuth() {
const router = useRouter()

const login = async (email: string, password: string) => {
const credential = await signInWithEmailAndPassword(
auth,
email,
password,
)
await fetchWithIdToken('/api/login', await credential.user.getIdToken())
router.reload()
}

const logout = async () => {
await fetch('/api/logout')
router.reload()
}

const createUser = async (
email: string,
password: string,
repeatPassword: string,
) => {
if (password !== repeatPassword)
throw new FirebaseError(
'auth/password-no-match',
'passwords does not match',
)

const credential = await createUserWithEmailAndPassword(
auth,
email,
password,
)
await fetchWithIdToken('/api/login', await credential.user.getIdToken())
router.reload()
}

return { login, logout, createUser }
}

export { useAuth }
Loading

0 comments on commit f90c902

Please sign in to comment.