Skip to content

Commit

Permalink
refactor: adopt useConfig hook
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxSchaefer committed Aug 13, 2023
1 parent f60faf9 commit 08f8066
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 187 deletions.
2 changes: 1 addition & 1 deletion lib/hooks/useConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Context, Dispatch, PropsWithChildren, SetStateAction } from 'react'
import { createContext, useContext, useState } from 'react'

type ConfigContextValue<T = unknown> = {
type ConfigContextValue<T> = {
config: T,
setConfig: Dispatch<SetStateAction<T>>
}
Expand Down
4 changes: 2 additions & 2 deletions tasks/components/MobileInterceptor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useTranslation } from '@helpwave/common/hooks/useTranslation'
import Link from 'next/link'
import HelpwaveLogo from '@helpwave/common/icons/HelpwaveRect'
import type { NextPage } from 'next'
import { getConfig } from '../utils/config'
import { Span } from '@helpwave/common/components/Span'
import { useConfig } from '../hooks/useConfig'

type MobileInterceptorTranslation = {
pleaseDownloadApp: string,
Expand Down Expand Up @@ -34,7 +34,7 @@ const defaultMobileInterceptorTranslation = {
*/
const MobileInterceptor: NextPage = ({ language }: PropsWithLanguage<MobileInterceptorTranslation>) => {
const translation = useTranslation(language, defaultMobileInterceptorTranslation)
const config = getConfig()
const { config } = useConfig()
const playStoreLink = config.appstoreLinks.playStore
const appstoreLink = config.appstoreLinks.appStore
return (
Expand Down
5 changes: 2 additions & 3 deletions tasks/components/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import { LanguageModal } from '@helpwave/common/components/modals/LanguageModal'
import type { PropsWithLanguage } from '@helpwave/common/hooks/useTranslation'
import { useTranslation } from '@helpwave/common/hooks/useTranslation'
import type { Languages } from '@helpwave/common/hooks/useLanguage'
import { getConfig } from '../utils/config'
import { useRouter } from 'next/router'

const config = getConfig()
import { useConfig } from '../hooks/useConfig'

type UserMenuTranslation = {
profile: string,
Expand Down Expand Up @@ -48,6 +46,7 @@ export const UserMenu = ({
const [isLanguageModalOpen, setLanguageModalOpen] = useState(false)
const { user, signOut } = useAuth()
const router = useRouter()
const { config } = useConfig()

if (!user) return null

Expand Down
31 changes: 15 additions & 16 deletions tasks/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { useEffect, useState } from 'react'
import { z } from 'zod'
import Cookies from 'js-cookie'
import { getConfig } from '../utils/config'
import { getAuthorizationUrl } from '../utils/oauth'

const config = getConfig()
import { useConfig } from './useConfig'

export const COOKIE_ID_TOKEN_KEY = 'id-token'
export const COOKIE_REFRESH_TOKEN_KEY = 'refresh-token'
Expand All @@ -31,21 +29,15 @@ const parseJwtPayload = (token: string) => {
return JSON.parse(decodedPayload)
}

const tokenToUser = (token: string): User | null => {
let decoded: string
if (config.fakeTokenEnable) {
decoded = JSON.parse(Buffer.from(config.fakeToken, 'base64').toString())
} else {
decoded = parseJwtPayload(token)
}
const parseFakeTokenPayload = (token: string) => JSON.parse(Buffer.from(token, 'base64').toString())

const tokenToUser = (token: string, parseTokenFnc: (token: string) => string): User | null => {
const decoded = parseTokenFnc(token)
const parsed = IdTokenClaimsSchema.safeParse(decoded)
return parsed.success ? parsed.data : null
}

const isJwtExpired = (token: string) => {
if (config.fakeTokenEnable) {
return tokenToUser(token) === null
}
const payloadBase64 = token.split('.')[1]
const decodedPayload = Buffer.from(payloadBase64, 'base64').toString()
const parsedPayload = JSON.parse(decodedPayload)
Expand All @@ -65,6 +57,9 @@ const isJwtExpired = (token: string) => {
export const useAuth = () => {
const [user, setUser] = useState<User>()
const [idToken, setIdToken] = useState<string>()
const { config } = useConfig()

const parseTokenFnc = config.fakeTokenEnable ? parseFakeTokenPayload : parseJwtPayload

const signOut = () => {
Cookies.remove(COOKIE_ID_TOKEN_KEY)
Expand All @@ -76,12 +71,16 @@ export const useAuth = () => {
try {
const idToken = Cookies.get(COOKIE_ID_TOKEN_KEY)

const idTokenValid = idToken !== undefined && !isJwtExpired(idToken)
const idTokenValid = idToken !== undefined && (
config.fakeTokenEnable
? tokenToUser(idToken, parseTokenFnc) !== null
: !isJwtExpired(idToken)
)

if (!idTokenValid) Cookies.remove(COOKIE_ID_TOKEN_KEY)

if (idTokenValid) {
const user = tokenToUser(idToken)
const user = tokenToUser(idToken, parseTokenFnc)
if (!user) throw new Error('Cannot parse idToken to user')
setUser(user)
setIdToken(idToken)
Expand All @@ -92,7 +91,7 @@ export const useAuth = () => {
}

// Both tokens are invalid. User needs to sign in again.
getAuthorizationUrl()
getAuthorizationUrl(config)
.then((url) => {
// Store current href into localStorage. Will be used by /auth/callback.
window.localStorage.setItem(LOCALSTORAGE_HREF_AFTER_AUTH_KEY, window.location.href)
Expand Down
63 changes: 61 additions & 2 deletions tasks/hooks/useConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,66 @@
import z from 'zod'
import { createConfig } from '@helpwave/common/hooks/useConfig'

export type Config = {
api: string
const fakeTokenSchema = z.object({
sub: z.string().uuid(),
name: z.string(),
nickname: z.string(),
email: z.string().email(),
organizations: z.string().array()
}).default({
sub: '18159713-5d4e-4ad5-94ad-fbb6bb147984',
name: 'Max Mustermann',
nickname: 'max.mustermann',
email: '[email protected]',
organizations: [
'3b25c6f5-4705-4074-9fc6-a50c28eba406'
]
})

export const configSchema = z.object({
NEXT_PUBLIC_API_URL: z.string().url().default('https://api.helpwave.de'),
NEXT_PUBLIC_MOCK: z.literal('true').or(z.literal('false')).optional(),
NEXT_PUBLIC_REQUEST_LOGGING: z.literal('true').or(z.literal('false')).optional(),
NEXT_PUBLIC_PLAYSTORE_LINK: z.string().url().default('https://play.google.com/store/apps'),
NEXT_PUBLIC_APPSTORE_LINK: z.string().url().default('https://www.apple.com/de/app-store/'),
NEXT_PUBLIC_OAUTH_ISSUER_URL: z.string().url().default('https://auth.helpwave.de'),
NEXT_PUBLIC_OAUTH_REDIRECT_URI: z.string().url().default('https://tasks.helpwave.de/auth/callback'),
NEXT_PUBLIC_OAUTH_CLIENT_ID: z.string().default('425f8b8d-c786-4ff7-b2bf-e52f505fb588'),
NEXT_PUBLIC_OAUTH_SCOPES: z.string().default('openid,offline_access,email,nickname,name,organizations'),
NEXT_PUBLIC_FAKE_TOKEN_ENABLE: z.literal('true').or(z.literal('false')).default('false'),
NEXT_PUBLIC_FAKE_TOKEN: fakeTokenSchema,
}).transform((obj) => ({
apiUrl: obj.NEXT_PUBLIC_API_URL,
mock: obj.NEXT_PUBLIC_MOCK,
requestLogging: obj.NEXT_PUBLIC_REQUEST_LOGGING === 'true',
appstoreLinks: {
playStore: obj.NEXT_PUBLIC_PLAYSTORE_LINK,
appStore: obj.NEXT_PUBLIC_APPSTORE_LINK,
},
oauth: {
issuerUrl: obj.NEXT_PUBLIC_OAUTH_ISSUER_URL,
redirectUri: obj.NEXT_PUBLIC_OAUTH_REDIRECT_URI,
clientId: obj.NEXT_PUBLIC_OAUTH_CLIENT_ID,
scopes: obj.NEXT_PUBLIC_OAUTH_SCOPES.split(',').map((scope) => scope.trim())
},
fakeTokenEnable: obj.NEXT_PUBLIC_FAKE_TOKEN_ENABLE === 'true',
fakeToken: Buffer.from(JSON.stringify(obj.NEXT_PUBLIC_FAKE_TOKEN)).toString('base64'),
}))

export const parseConfigFromEnvironmentVariablesWithNextPublicPrefix = () => {
// Filter environment variables that starts with "NEXT_PUBLIC_"
const possibleEnvironmentVariables = Object.keys(process.env).reduce<Record<string, string|undefined>>((acc, key) => {
if (key.startsWith('NEXT_PUBLIC_')) {
acc[key] = process.env[key]
}
return acc
}, {})

const config = configSchema.safeParse(possibleEnvironmentVariables)
if (!config.success) throw new Error(`Invalid environment variables:\n${config.error}`)
return config.data
}

export type Config = z.output<typeof configSchema>

export const { ConfigProvider, useConfig } = createConfig<Config>()
7 changes: 3 additions & 4 deletions tasks/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import MobileInterceptor from '../components/MobileInterceptor'
import titleWrapper from '../utils/titleWrapper'
import type { Config } from '../hooks/useConfig'
import { ConfigProvider } from '../hooks/useConfig'
import { ConfigProvider, parseConfigFromEnvironmentVariablesWithNextPublicPrefix } from '../hooks/useConfig'
import { useEffect } from 'react';

const inter = Inter({
subsets: ['latin'],
Expand Down Expand Up @@ -58,9 +59,7 @@ function MyApp({ Component, pageProps, config }: AppProps & AppOwnProps) {
// TODO: Adopting the AppRouter https://nextjs.org/docs/pages/building-your-application/routing/custom-app#getinitialprops-with-app
MyApp.getInitialProps = async (context: AppContext): Promise<AppInitialProps & AppOwnProps> => {
const ctx = await App.getInitialProps(context)
const config: Config = {
api: process.env.API || '123'
}
const config = parseConfigFromEnvironmentVariablesWithNextPublicPrefix()
return { ...ctx, config }
}

Expand Down
5 changes: 4 additions & 1 deletion tasks/pages/auth/callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { handleCodeExchange } from '../../utils/oauth'
import { COOKIE_ID_TOKEN_KEY, LOCALSTORAGE_HREF_AFTER_AUTH_KEY } from '../../hooks/useAuth'
import Cookies from 'js-cookie'
import { useEffect } from 'react'
import { useConfig } from '../../hooks/useConfig'

const AuthCallback: NextPage = () => {
const { config } = useConfig()

useEffect(() => {
handleCodeExchange().then((tokens) => {
handleCodeExchange(config).then((tokens) => {
Cookies.set(COOKIE_ID_TOKEN_KEY, tokens.id_token)

const hrefAfterAuth = window.localStorage.getItem(LOCALSTORAGE_HREF_AFTER_AUTH_KEY)
Expand Down
48 changes: 0 additions & 48 deletions tasks/utils/api.ts

This file was deleted.

89 changes: 0 additions & 89 deletions tasks/utils/config.ts

This file was deleted.

14 changes: 10 additions & 4 deletions tasks/utils/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ import {
TaskTemplateServicePromiseClient
} from '@helpwave/proto-ts/proto/services/task_svc/v1/task_template_svc_grpc_web_pb'
import { TaskServicePromiseClient } from '@helpwave/proto-ts/proto/services/task_svc/v1/task_svc_grpc_web_pb'
import { OrganizationServicePromiseClient } from '@helpwave/proto-ts/proto/services/user_svc/v1/organization_svc_grpc_web_pb'
import { getConfig } from './config'
import {
OrganizationServicePromiseClient
} from '@helpwave/proto-ts/proto/services/user_svc/v1/organization_svc_grpc_web_pb'

// TODO: RESOLVE BEFORE MERGE dont hardcode
const config = {
apiUrl: 'https://staging.api.helpwave.de'
}

const taskSvcBaseUrl = `${getConfig().apiUrl}/task-svc`
const userSvcBaseUrl = `${getConfig().apiUrl}/user-svc`
const taskSvcBaseUrl = `${config.apiUrl}/task-svc`
const userSvcBaseUrl = `${config.apiUrl}/user-svc`

// TODO: Implement something like a service registry
export const wardService = new WardServicePromiseClient(taskSvcBaseUrl)
Expand Down
Loading

0 comments on commit 08f8066

Please sign in to comment.