Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix getCurrentUser type to get rid of red squiggles #6329

Merged
merged 8 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions __fixtures__/test-project/api/src/functions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ export const handler = async (
// the database. Returning anything truthy will automatically log the user
// in. Return `false` otherwise, and in the Reset Password page redirect the
// user to the login page.
handler: (user) => {
return !!user
handler: (_user) => {
return true
},

// If `false` then the new password MUST be different from the current one
Expand Down
8 changes: 6 additions & 2 deletions __fixtures__/test-project/api/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DbAuthSession } from '@redwoodjs/api'
import type { Decoded } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

import { db } from './db'
Expand All @@ -20,7 +20,11 @@ import { db } from './db'
* fields to the `select` object below once you've decided they are safe to be
* seen if someone were to open the Web Inspector in their browser.
*/
export const getCurrentUser = async (session: DbAuthSession<number>) => {
export const getCurrentUser = async (session: Decoded) => {
if (!session || typeof session.id !== 'number') {
return null
}

return await db.user.findUnique({
where: { id: session.id },
select: { id: true, roles: true, email: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const standard = defineScenario<Prisma.PostCreateArgs>({
body: 'String',
author: {
create: {
email: 'String5205455',
email: 'String2239255',
hashedPassword: 'String',
fullName: 'String',
salt: 'String',
Expand All @@ -24,7 +24,7 @@ export const standard = defineScenario<Prisma.PostCreateArgs>({
body: 'String',
author: {
create: {
email: 'String4790139',
email: 'String6221063',
hashedPassword: 'String',
fullName: 'String',
salt: 'String',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ export const standard = defineScenario<Prisma.UserCreateArgs>({
user: {
one: {
data: {
email: 'String2936724',
email: 'String8714307',
hashedPassword: 'String',
fullName: 'String',
salt: 'String',
},
},
two: {
data: {
email: 'String512848',
email: 'String7793588',
hashedPassword: 'String',
fullName: 'String',
salt: 'String',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ const ForgotPasswordPage = () => {
}
}, [isAuthenticated])

const usernameRef = useRef<HTMLInputElement>()
const usernameRef = useRef<HTMLInputElement>(null)
useEffect(() => {
usernameRef.current.focus()
usernameRef?.current?.focus()
}, [])

const onSubmit = async (data) => {
const onSubmit = async (data: { username: string }) => {
const response = await forgotPassword(data.username)

if (response.error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { navigate, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'

const ResetPasswordPage = ({ resetToken }) => {
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
useAuth()
const [enabled, setEnabled] = useState(true)
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/auth/decoders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface Req {
context: LambdaContext
}

type Decoded = null | string | Record<string, unknown>
export type Decoded = null | Record<string, unknown>
dac09 marked this conversation as resolved.
Show resolved Hide resolved

const typesToDecoders: Record<
SupportedAuthTypes,
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'

import type { SupportedAuthTypes } from '@redwoodjs/auth'

import type { DbAuthSession } from '../functions/dbAuth/DbAuthHandler'
import { Decoded, decodeToken } from './decoders'

import { decodeToken } from './decoders'
export type { Decoded } from './decoders'

// This is shared by `@redwoodjs/web`
const AUTH_PROVIDER_HEADER = 'auth-provider'
Expand Down Expand Up @@ -41,7 +41,7 @@ export const parseAuthorizationHeader = (
}

export type AuthContextPayload = [
string | Record<string, unknown> | null | DbAuthSession,
Decoded,
{ type: SupportedAuthTypes } & AuthorizationHeader,
{ event: APIGatewayProxyEvent; context: LambdaContext }
]
Expand Down
65 changes: 52 additions & 13 deletions packages/api/src/auth/parseJWT.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
const appMetadata = (token: {
decoded: { [index: string]: Record<string, any> }
import { Decoded } from './decoders'

interface DecodedWithRoles extends Record<string, unknown> {
roles: string | string[]
}

interface DecodedWithMetadata extends Record<string, unknown> {
[key: string]: Record<string, unknown>
}

interface TokenWithRoles {
decoded: DecodedWithRoles
}

interface TokenWithMetadata {
decoded: DecodedWithMetadata
namespace?: string
}): any => {
}

function isTokenWithRoles(token: {
decoded: Decoded
}): token is TokenWithRoles {
return !!(token.decoded as DecodedWithRoles)?.roles
}

function isTokenWithMetadata(token: {
decoded: Decoded
namespace?: string
}): token is TokenWithMetadata {
const claim = token.namespace
? `${token.namespace}/app_metadata`
: 'app_metadata'
return token.decoded?.[claim] || {}
return !!(token.decoded as DecodedWithMetadata)?.[claim]
}

const appMetadata = (token: { decoded: Decoded; namespace?: string }): any => {
if (typeof token.decoded === 'string') {
return {}
}

if (isTokenWithMetadata(token)) {
const claim = token.namespace
? `${token.namespace}/app_metadata`
: 'app_metadata'
return token.decoded?.[claim]
}

return {}
}

const roles = (token: {
decoded: { [index: string]: Record<string, any> }
decoded: Decoded
namespace?: string
}): any => {
}): string | string[] => {
if (isTokenWithRoles(token)) {
return token.decoded.roles
}

const metadata = appMetadata(token)
return (
token.decoded?.roles ||
metadata?.roles ||
metadata.authorization?.roles ||
[]
)
return metadata?.roles || metadata.authorization?.roles || []
}

export const parseJWT = (token: {
decoded: { [index: string]: Record<string, any> }
decoded: Decoded
namespace?: string
}): any => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ const ForgotPasswordPage = () => {
}
}, [isAuthenticated])

const usernameRef = useRef<HTMLInputElement>()
const usernameRef = useRef<HTMLInputElement>(null)
useEffect(() => {
usernameRef.current.focus()
usernameRef?.current?.focus()
}, [])

const onSubmit = async (data) => {
const onSubmit = async (data: { username: string }) => {
const response = await forgotPassword(data.username)

if (response.error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { navigate, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'

const ResetPasswordPage = ({ resetToken }) => {
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
useAuth()
const [enabled, setEnabled] = useState(true)
Expand Down
22 changes: 10 additions & 12 deletions packages/cli/src/commands/setup/auth/templates/auth.ts.template
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseJWT } from '@redwoodjs/api'
import { parseJWT, Decoded } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

/**
Expand All @@ -12,11 +12,6 @@ type RedwoodUser = Record<string, unknown> & { roles?: string[] }
* an optional collection of roles used by requireAuth() to check
* if the user is authenticated or has role-based access
*
* @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null.
* @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type
* @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker
* such as headers and cookies, and the context information about the invocation such as IP Address
*
* !! BEWARE !! Anything returned from this function will be available to the
* client--it becomes the content of `currentUser` on the web side (as well as
* `context.currentUser` on the api side). You should carefully add additional
Expand All @@ -25,15 +20,18 @@ type RedwoodUser = Record<string, unknown> & { roles?: string[] }
*
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
*
* @param decoded - The decoded access token containing user info and JWT
* claims like `sub`. Note, this could be null.
* @param { token, SupportedAuthTypes type } - The access token itself as well
* as the auth provider type
* @param { APIGatewayEvent event, Context context } - An optional object which
* contains information from the invoker such as headers and cookies, and the
* context information about the invocation such as IP Address
* @returns RedwoodUser
*/
export const getCurrentUser = async (
decoded,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ token, type },
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ event, context }
): Promise<RedwoodUser> => {
decoded: Decoded
): Promise<RedwoodUser | null> => {
if (!decoded) {
return null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseJWT } from '@redwoodjs/api'
import { parseJWT, Decoded } from '@redwoodjs/api'
Tobbe marked this conversation as resolved.
Show resolved Hide resolved
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

/**
Expand All @@ -12,11 +12,6 @@ type RedwoodUser = Record<string, unknown> & { roles?: string[] }
* an optional collection of roles used by requireAuth() to check
* if the user is authenticated or has role-based access
*
* @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null.
* @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type
* @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker
* such as headers and cookies, and the context information about the invocation such as IP Address
*
* !! BEWARE !! Anything returned from this function will be available to the
* client--it becomes the content of `currentUser` on the web side (as well as
* `context.currentUser` on the api side). You should carefully add additional
Expand All @@ -25,15 +20,18 @@ type RedwoodUser = Record<string, unknown> & { roles?: string[] }
*
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
*
* @param decoded - The decoded access token containing user info and JWT
* claims like `sub`. Note, this could be null.
* @param { token, SupportedAuthTypes type } - The access token itself as well
* as the auth provider type
* @param { APIGatewayEvent event, Context context } - An optional object which
* contains information from the invoker such as headers and cookies, and the
* context information about the invocation such as IP Address
* @returns RedwoodUser
*/
export const getCurrentUser = async (
decoded,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ token, type },
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ event, context }
): Promise<RedwoodUser> => {
decoded: Decoded
): Promise<RedwoodUser | null> => {
if (!decoded) {
return null
}
Expand Down Expand Up @@ -75,7 +73,7 @@ export const hasRole = (roles: AllowedRoles): boolean => {
return false
}

const currentUserRoles = context.currentUser?.roles
const currentUserRoles = context.currentUser?.roles

if (typeof roles === 'string') {
if (typeof currentUserRoles === 'string') {
Expand All @@ -95,9 +93,7 @@ export const hasRole = (roles: AllowedRoles): boolean => {
)
} else if (typeof currentUserRoles === 'string') {
// roles to check is an array, currentUser.roles is a string
return roles.some(
(allowedRole) => currentUserRoles === allowedRole
)
return roles.some((allowedRole) => currentUserRoles === allowedRole)
}
}

Expand All @@ -110,12 +106,12 @@ export const hasRole = (roles: AllowedRoles): boolean => {
* whether or not they are assigned a role, and optionally raise an
* error if they're not.
*
* @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
* @param roles?: {@link AllowedRoles} - When checking role membership, these roles grant access.
*
* @returns - If the currentUser is authenticated (and assigned one of the given roles)
*
* @throws {@link AuthenticationError} - If the currentUser is not authenticated
* @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
* @throws {@link ForbiddenError} - If the currentUser is not allowed due to role permissions
*
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
*/
Expand Down
Loading