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

Adds ability for developers to authenticate users in the local environment #108

Merged
merged 21 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d142fe3
chore: add boilerplate localauth function
as1729 Feb 9, 2024
2b524de
chore: add an env var for auth providers
as1729 Feb 13, 2024
5c8b727
chore: add library needed for testing
as1729 Feb 13, 2024
02338f6
feat: add login page that is compatible with local auth
as1729 Feb 13, 2024
5b5ad04
feat: adds implementation of /localAuth endpoint
as1729 Feb 14, 2024
bd80d19
feat: adds localAuth implementation for web
as1729 Feb 14, 2024
1fd212b
chore: ensure instantiation of the correct auth provider based on env…
as1729 Feb 14, 2024
e3c2f51
fix: login page uses the correct typing
as1729 Feb 14, 2024
4eda476
feat: add localAuth implementation on the API
as1729 Feb 14, 2024
dec6fc6
chore: adds useful seed data to be able to test localAuth functionality
as1729 Feb 14, 2024
157fe01
chore: adds auth-provider environment variable for staging
as1729 Feb 14, 2024
aeac354
fix: linting error unnecessary import
as1729 Feb 14, 2024
c1404b8
fix: terraform format
as1729 Feb 14, 2024
3e090a0
fix: addresses comments
as1729 Feb 15, 2024
cc265d9
fix: addresses comments re redundant checks of auth provider and envi…
as1729 Feb 15, 2024
c85e1ff
fix: removes localAuth function
as1729 Feb 22, 2024
1233b88
fix: linting issue
as1729 Feb 22, 2024
f42561d
fix: unused import
as1729 Feb 22, 2024
be48b55
Merge branch 'main' into as/add-local-auth-module
as1729 Mar 1, 2024
0f35d9f
Apply suggestions from code review
as1729 Mar 1, 2024
741b0a7
Merge branch 'main' into as/add-local-auth-module
as1729 Mar 1, 2024
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
3 changes: 3 additions & 0 deletions .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ DD_RUM_SESSION_REPLAY_SAMPLE_RATE=20
DD_RUM_TRACK_USER_INTERACTIONS=true
DD_RUM_TRACK_RESOURCES=true
DD_RUM_TRACK_LONG_TASKS=true

# Auth provider environment variables
AUTH_PROVIDER=local
17 changes: 13 additions & 4 deletions api/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
import { Decoded } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

import { db } from 'src/lib/db'

/**
* Represents the user attributes returned by the decoding the
* Authentication provider's JWT together with an optional list of roles.
Expand Down Expand Up @@ -35,9 +37,17 @@ type RedwoodUser = Record<string, unknown> & { roles?: string[] }
* @returns RedwoodUser
*/
export const getCurrentUser = async (
decoded: Decoded
decoded: Decoded,
{ token }: { token: string }
): Promise<RedwoodUser | null> => {
console.log(decoded)
// Verify that the request is coming from the local development environment
// and is only being processed within the local environment
if (process.env.AUTH_PROVIDER === 'local') {
const user = await db.user.findFirst({
where: { email: token },
})
return user
}
return {
id: 1,
organizationId: 1,
Expand Down Expand Up @@ -73,8 +83,7 @@ export const hasRole = (roles: AllowedRoles): boolean => {
if (!isAuthenticated()) {
return false
}

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

if (typeof roles === 'string') {
if (typeof currentUserRoles === 'string') {
Expand Down
63 changes: 35 additions & 28 deletions scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,45 @@
// Seeds automatically with `yarn rw prisma migrate dev` and `yarn rw prisma migrate reset`
//

const organization: Prisma.OrganizationCreateArgs['data'] =
await db.organization.create({
data: {
name: 'Example Organization',
},
})

const agency: Prisma.AgencyCreateArgs['data'] = await db.agency.create({
data: {
name: 'Example Agency',
code: 'EA',
organizationId: organization.id,
},
const organization: Prisma.OrganizationCreateArgs['data'] = {
name: 'US Digital Response',
}
const organizationRecord = await db.organization.create({
data: organization,
})

const users: Prisma.UserCreateManyInput[] = [
const mainAgency: Prisma.AgencyCreateArgs['data'] = {
name: 'Main Agency',
abbreviation: 'MAUSDR',
code: 'MAUSDR',
organizationId: organizationRecord.id,
}
const mainAgencyRecord = await db.agency.create({ data: mainAgency })

const users: Prisma.UserCreateArgs['data'][] = [
{
email: '[email protected]',
name: 'Organization Staff',
role: 'ORGANIZATION_STAFF',
agencyId: agency.id,
email: '[email protected]',
name: 'USDR Admin',
agencyId: mainAgencyRecord.id,
organizationId: organizationRecord.id,
as1729 marked this conversation as resolved.
Show resolved Hide resolved
role: 'USDR_ADMIN',
},
{
email: '[email protected]',
email: '[email protected]',
name: 'Organization Admin',
agencyId: mainAgencyRecord.id,
organizationId: organizationRecord.id,
as1729 marked this conversation as resolved.
Show resolved Hide resolved
role: 'ORGANIZATION_ADMIN',
agencyId: agency.id,
},
{
email: '[email protected]',
name: 'USDR Admin',
role: 'USDR_ADMIN',
agencyId: agency.id,
email: '[email protected]',
name: 'Organization Staff',
agencyId: mainAgencyRecord.id,
organizationId: organizationRecord.id,
as1729 marked this conversation as resolved.
Show resolved Hide resolved
role: 'ORGANIZATION_STAFF',
},
]

await db.user.createMany({
data: users,
})
const userRecord = await db.user.create({ data: users[0] })
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved

const inputTemplates: Prisma.InputTemplateCreateArgs['data'][] = [
{
Expand All @@ -66,6 +66,13 @@

// Note: if using PostgreSQL, using `createMany` to insert multiple records is much faster
// @see: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany
await Promise.all(
users.map(async (data: Prisma.UserCreateArgs['data']) => {
const record = await db.user.create({ data })
console.log(record)
})
)

as1729 marked this conversation as resolved.
Show resolved Hide resolved
await Promise.all(
//
// Change to match your data model and seeding needs
Expand Down
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"devDependencies": {
"@redwoodjs/vite": "6.4.2",
"@testing-library/react": "^14.2.1",
"@types/node": "^20.10.4",
"@types/react": "18.2.39",
"@types/react-dom": "18.2.17",
Expand Down
19 changes: 14 additions & 5 deletions web/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createAuthentication } from '@redwoodjs/auth'

import {
createLocalAuthImplementation,
localAuthClient,
} from 'src/auth/localAuth'

// If you're integrating with an auth service provider you should delete this interface.
// Instead you should import the type from their auth client sdk.
export interface AuthClient {
Expand Down Expand Up @@ -53,11 +58,15 @@ const client = {
}

function createAuth() {
const authImplementation = createAuthImplementation(client)

// You can pass custom provider hooks here if you need to as a second
// argument. See the Redwood framework source code for how that's used
return createAuthentication(authImplementation)
if (process.env.AUTH_PROVIDER == 'local') {
const authImplementation = createLocalAuthImplementation(localAuthClient)
return createAuthentication(authImplementation)
} else {
const authImplementation = createAuthImplementation(client)
// You can pass custom provider hooks here if you need to as a second
// argument. See the Redwood framework source code for how that's used
return createAuthentication(authImplementation)
}
}

// This is where most of the integration work will take place. You should keep
Expand Down
82 changes: 82 additions & 0 deletions web/src/auth/localAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { SubmitHandler } from '@redwoodjs/forms'

import { users } from 'src/lib/seeds'

// If you're integrating with an auth service provider you should delete this interface.
// Instead you should import the type from their auth client sdk.
export interface AuthClient {
login: () => User
logout: () => void
signup: () => User
getToken: () => string
getUserMetadata: () => User | null
}

export interface LoginEventInterface {
email: string
}

export interface LocalAuthClient {
login: (event: SubmitHandler<LoginEventInterface>) => void
logout: () => Promise<void>
signup: () => null
getToken: () => Promise<string>
getUserMetadata: () => Promise<User | null>
}

interface User {
name: string
email: string
role: string
agencyId: number
organizationId: number
createdAt: string
updatedAt: string
}

export const localAuthClient = {
logout: async () => {
localStorage.removeItem('local_auth_token')
window.location.href = '/login'
},
getToken: async () => {
return localStorage.getItem('local_auth_token')
},
getUserMetadata: async () => {
if (!localStorage.getItem('local_auth_token')) {
return null
}

const user: User | null = users.find(
(u) => u.email === localStorage.getItem('local_auth_token')
)

return user
},
login: async (event) => {
let token
if (event.user === 'manual') {
token = event.email
} else {
token = event.user
}
localStorage.setItem('local_auth_token', token)
window.location.href = '/'
},
signup: () => {
console.log('implemented via GraphQL directly from the user creation page')
return null
},
}

export function createLocalAuthImplementation(client: LocalAuthClient) {
return {
type: 'local',
client,
login: async (e: SubmitHandler<LoginEventInterface>) => client.login(e),
logout: async () => client.logout(),
signup: async () => client.signup(),
getToken: async () => client.getToken(),
getUserMetadata: async () => client.getUserMetadata(),
}
}
47 changes: 47 additions & 0 deletions web/src/lib/seeds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export const users = [
{
email: '[email protected]',
name: 'USDR Admin',
role: 'USDR_ADMIN',
agency: {
name: 'Main Agency',
abbreviation: 'MAUSDR',
code: 'MAUSDR',
organizationId: 1,
},
agencyId: 1, // TO_DEPRECATE
organizationId: 1, // TO_DEPRECATE
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
email: '[email protected]',
name: 'Organization Admin',
role: 'ORGANIZATION_ADMIN',
agency: {
name: 'Main Agency',
abbreviation: 'MAUSDR',
code: 'MAUSDR',
organizationId: 1,
},
agencyId: 1, // TO_DEPRECATE
organizationId: 1, // TO_DEPRECATE
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
email: '[email protected]',
name: 'Organization Staff',
role: 'ORGANIZATION_STAFF',
agency: {
name: 'Main Agency',
abbreviation: 'MAUSDR',
code: 'MAUSDR',
organizationId: 1,
},
agencyId: 1, // TO_DEPRECATE
organizationId: 1, // TO_DEPRECATE
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
]
14 changes: 13 additions & 1 deletion web/src/pages/LoginPage/LoginPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { screen } from '@testing-library/react'

import { render } from '@redwoodjs/testing/web'

import LoginPage from './LoginPage'
Expand All @@ -6,9 +8,19 @@ import LoginPage from './LoginPage'
// https://redwoodjs.com/docs/testing#testing-pages-layouts

describe('LoginPage', () => {
it('renders successfully', () => {
it('renders successfully for local auth', () => {
expect(() => {
render(<LoginPage />)
}).not.toThrow()
})
it('renders a login button for local auth', () => {
render(<LoginPage />)
expect(screen.getByRole('button', { name: 'Login' })).toBeInTheDocument()
})
it('renders an empty page when valid auth provider is missing', () => {
process.env.AUTH_PROVIDER = 'no-auth'
render(<LoginPage />)
const submitButton = screen.queryByText('submit')
expect(submitButton).not.toBeInTheDocument()
})
})
Loading
Loading