Skip to content

Commit

Permalink
Enable features requiring authentication (#89)
Browse files Browse the repository at this point in the history
Closes #81
  • Loading branch information
andrew-codes authored Jan 10, 2024
2 parents aec6a84 + b743740 commit 26e7ce1
Show file tree
Hide file tree
Showing 22 changed files with 246 additions and 14 deletions.
53 changes: 53 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified .yarn/install-state.gz
Binary file not shown.
6 changes: 6 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ packageExtensions:
'remix-island@*':
dependencies:
'@remix-run/server-runtime': '*'
'remix-auth@*':
dependencies:
'@remix-run/server-runtime': '*'
'remix-auth-form@*':
dependencies:
'@remix-run/server-runtime': '*'
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ Use the docker [packaged image](https://github.com/andrew-codes/playnite-web/pkg
| DB_USERNAME | Username to access database | Optional, only required if disabled anonymous access |
| DB_PASSWORD | Password to access database | Optional, only required if disabled anonymous access |
| DEBUG | `"playnite-web/*"` | Optional, for troubleshooting; send logs to STDIO |
| USERNAME | | Username used to login |
| PASSWORD | | Password value used to login |
| SECRET | | Secret used to protect credentials |

### Post Deployment Steps

Expand Down
6 changes: 5 additions & 1 deletion apps/playnite-web/local.env
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
NODE_ENV=development

DEBUG="playnite-web/*"
DEBUG="playnite-web-app/*"

DB_HOST=localhost
DB_PORT=27017
DB_USERNAME=local
DB_PASSWORD=dev

SECRET="some secret"
USERNAME="username"
PASSWORD="password"
2 changes: 2 additions & 0 deletions apps/playnite-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"react-dom": "^18.2.0",
"react-redux": "^9.0.4",
"react-use-dimensions": "^1.2.1",
"remix-auth": "^3.6.0",
"remix-auth-form": "^1.4.0",
"remix-island": "^0.1.2",
"remix-routes": "^1.5.1",
"styled-components": "^6.1.6"
Expand Down
5 changes: 4 additions & 1 deletion apps/playnite-web/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "yarn concurrently \"remix-routes -w\" \"yarn cross-env DEBUG='playnite-web-app/*' yarn remix dev -c 'node server.mjs'\""
"commands": [
"yarn remix-routes -w",
"bash -c 'set -o allexport && source local.env && set +o allexport && yarn remix dev -c \"node server.mjs\"'"
]
}
},
"test/components": {
Expand Down
6 changes: 1 addition & 5 deletions apps/playnite-web/server.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { createRequestHandler } from '@remix-run/express'
import { broadcastDevReady } from '@remix-run/node'
import createDebugger from 'debug'
import dotenv from 'dotenv'
import express from 'express'
import path from 'path'
import * as build from './build/index.js'

const debug = createDebugger('playnite-web-app/server')

dotenv.config({ path: path.join(process.cwd(), 'local.env'), override: true })

const { PORT } = process.env
const port = PORT ? parseInt(PORT, 10) : 3000

Expand All @@ -23,5 +19,5 @@ app.listen(port, () => {
debug('sending dev-ready')
broadcastDevReady(build)
}
debug(`App listening on http://localhost:$PORT`)
debug(`App listening on http://localhost:${port}`)
})
27 changes: 27 additions & 0 deletions apps/playnite-web/src/api/auth/auth.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import createDebugger from 'debug'
import { Authenticator } from 'remix-auth'
import { FormStrategy } from 'remix-auth-form'
import { sessionStorage } from './session.server'

const { USERNAME, PASSWORD } = process.env
const debug = createDebugger('playnite-web-app/auth.server')

const authenticator = new Authenticator<{
username: string
}>(sessionStorage)

authenticator.use(
new FormStrategy(async ({ form }) => {
let username = form.get('username')
let password = form.get('password')
if (USERNAME !== username || PASSWORD !== password) {
debug('Invalid credentials', { username, password })
throw new Error('Invalid credentials')
}

return { username }
}),
'user-pass',
)

export { authenticator }
16 changes: 16 additions & 0 deletions apps/playnite-web/src/api/auth/session.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// app/services/session.server.ts
import { createCookieSessionStorage } from '@remix-run/node'

// export the whole sessionStorage object
const sessionStorage = createCookieSessionStorage({
cookie: {
name: '_session', // use any name you want here
sameSite: 'strict',
path: '/',
httpOnly: true,
secrets: [process.env.SECRET ?? 'secret'],
secure: process.env.NODE_ENV === 'production',
},
})

export { sessionStorage }
28 changes: 28 additions & 0 deletions apps/playnite-web/src/api/client/state/authSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createSlice } from '@reduxjs/toolkit'
import _ from 'lodash'

const { merge } = _

const initialState = {
isAuthenticated: false,
}

const authSlice = createSlice({
name: 'auth',
initialState,
selectors: {
getIsAuthenticated: (state) => state.isAuthenticated,
},
reducers: {
signedIn(state, action) {
return merge(state, { isAuthenticated: true })
},
signedOut(state, action) {
return merge(state, { isAuthenticated: false })
},
},
})

export const { reducer } = authSlice
export const { signedIn, signedOut } = authSlice.actions
export const { getIsAuthenticated } = authSlice.selectors
6 changes: 5 additions & 1 deletion apps/playnite-web/src/api/client/state/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { combineReducers } from '@reduxjs/toolkit'
import * as authSlice from './authSlice'
import * as layoutSlice from './layoutSlice'

const reducer = combineReducers({ layout: layoutSlice.reducer })
const reducer = combineReducers({
layout: layoutSlice.reducer,
auth: authSlice.reducer,
})

export { reducer }
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const getDbClient = (connectionOptions?: DbConnectionOptions): MongoClient => {
)

if (!username && !password) {
debug(`No username or password provided; connecting without auth`)
client = new MongoClient(`mongodb://${host}:${port}`)
} else {
client = new MongoClient(`mongodb://${host}:${port}`, {
Expand Down
5 changes: 5 additions & 0 deletions apps/playnite-web/src/components/WithNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FC, PropsWithChildren } from 'react'
import { useSelector } from 'react-redux'
import { styled } from 'styled-components'
import { getIsAuthenticated } from '../api/client/state/authSlice'
import { getIsMobile } from '../api/client/state/layoutSlice'

const Layout = styled.div<{ mobile: boolean }>`
Expand Down Expand Up @@ -77,12 +78,16 @@ const WithNavigation: FC<PropsWithChildren & { Toolbar?: FC }> = ({
}) => {
const isMobile = useSelector(getIsMobile)

const isAuthenticated = useSelector(getIsAuthenticated)

return (
<Layout mobile={isMobile}>
<GlobalNavigation mobile={isMobile}>
<a href={`/`}>On Deck</a>
{Toolbar && <Toolbar />}
<a href={`/browse`}>Browse</a>
{!isAuthenticated && <a href={`/login`}>Sign In</a>}
{isAuthenticated && <a href={`/logout`}>Logout</a>}
</GlobalNavigation>
{children}
</Layout>
Expand Down
13 changes: 12 additions & 1 deletion apps/playnite-web/src/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { FC } from 'react'
import { Provider } from 'react-redux'
import { createHead } from 'remix-island'
import { createGlobalStyle } from 'styled-components'
import { authenticator } from './api/auth/auth.server'
import { reducer } from './api/client/state'
import { signedIn, signedOut } from './api/client/state/authSlice'
import { layoutDetermined } from './api/client/state/layoutSlice'
import inferredLayout from './api/server/layout'

Expand All @@ -38,10 +40,13 @@ async function loader({ request }: LoaderFunctionArgs) {

const isMobile = request.headers.get('user-agent')?.includes('Mobile')

const user = await authenticator.isAuthenticated(request)

return json({
isMobile,
gameWidth,
gameHeight,
user,
})
}

Expand All @@ -67,14 +72,20 @@ body {
`

const App: FC<{}> = () => {
const { isMobile, gameWidth, gameHeight } = useLoaderData<{
const { isMobile, gameWidth, gameHeight, user } = useLoaderData<{
isMobile: boolean
gameWidth: number
gameHeight: number
user?: any
}>()

const store = configureStore({ reducer })
store.dispatch(layoutDetermined({ isMobile, gameWidth, gameHeight }))
if (!!user) {
store.dispatch(signedIn({ payload: null }))
} else {
store.dispatch(signedOut({ payload: null }))
}

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion apps/playnite-web/src/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useLoaderData } from '@remix-run/react'
import { useSelector } from 'react-redux'
import { styled } from 'styled-components'
import { getGameDimensions } from '../api/client/state/layoutSlice'
import PlayniteApi from '../api/server/playnite'
import PlayniteApi from '../api/server/playnite/index.server'
import type { Game, Playlist } from '../api/server/playnite/types'
import GameList from '../components/GameList.js'
import GameListItem from '../components/GameListItem'
Expand Down
13 changes: 10 additions & 3 deletions apps/playnite-web/src/routes/browse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { useCallback, useEffect, useReducer } from 'react'
import { useSelector } from 'react-redux'
import useDimensions from 'react-use-dimensions'
import { styled } from 'styled-components'
import { authenticator } from '../api/auth/auth.server'
import { getGameDimensions } from '../api/client/state/layoutSlice'
import PlayniteApi from '../api/server/playnite'
import PlayniteApi from '../api/server/playnite/index.server'
import type { Game } from '../api/server/playnite/types'
import GameList from '../components/GameList'
import GameListItem from '../components/GameListItem'
import Search from '../components/Search'
import WithNavigation from '../components/WithNavigation'

const { debounce, merge } = _
const { debounce } = _

async function loader({ request }: LoaderFunctionArgs) {
const api = new PlayniteApi()
Expand All @@ -32,7 +33,10 @@ async function loader({ request }: LoaderFunctionArgs) {
return 0
})

const user = await authenticator.isAuthenticated(request)

return json({
user,
games,
})
}
Expand Down Expand Up @@ -100,7 +104,10 @@ function Index() {
[debouncedSearch, height, ref, search.query],
)

const { games } = useLoaderData() as unknown as { games: Game[] }
const { games } = useLoaderData() as unknown as {
games: Game[]
}

const handleFilter = useCallback(
(game: Game) => game.name.toLowerCase().includes(search.query),
[search.query],
Expand Down
2 changes: 1 addition & 1 deletion apps/playnite-web/src/routes/coverArt.$oid.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LoaderFunctionArgs } from '@remix-run/node'
import createDebugger from 'debug'
import { $params } from 'remix-routes'
import PlayniteApi from '../api/server/playnite'
import Oid from '../api/server/playnite/Oid'
import PlayniteApi from '../api/server/playnite/index.server'

const debug = createDebugger('playnite-web-app/route/coverArt')

Expand Down
Loading

0 comments on commit 26e7ce1

Please sign in to comment.