chore: rewrite /login page for OAuth, write createUser for the database

pull/1/head
Francesco Minnocci 2 years ago
parent 2901a1f899
commit 9ae74fa649
Signed by: BachoSeven
GPG Key ID: 2BE4AB7FDAD828A4

@ -1,3 +1,5 @@
// TODO: move to `shared/` and distinguish client and server (fix imports)
export function prependBaseUrl(route: string) { export function prependBaseUrl(route: string) {
return import.meta.env.BASE_URL + (route.startsWith('/') ? route.substring(1) : route) return import.meta.env.BASE_URL + (route.startsWith('/') ? route.substring(1) : route)
} }

@ -29,6 +29,8 @@ type UserFunctionsHook = () => {
logout: () => Promise<void> logout: () => Promise<void>
} }
// TODO: remove login as it is deprecated
export const useUserFunctions: UserFunctionsHook = () => { export const useUserFunctions: UserFunctionsHook = () => {
const login = async (username: string) => { const login = async (username: string) => {
await fetch(prependBaseUrl(`/api/login`), { await fetch(prependBaseUrl(`/api/login`), {

@ -1,37 +1,13 @@
import { route } from 'preact-router'
import { useState } from 'preact/hooks'
import { prependBaseUrl } from '../api.js' import { prependBaseUrl } from '../api.js'
import { Header } from '../components/Header.jsx' import { Header } from '../components/Header.jsx'
import { useUserFunctions } from '../hooks/useCurrentUser.js'
export const LoginPage = () => { export const LoginPage = () => {
const [username, setUsername] = useState('')
const { login } = useUserFunctions()
const handleLogin = async () => {
await login(username)
route(prependBaseUrl('/'))
}
return ( return (
<> <>
<Header /> <Header />
<main class="page-login"> <main class="page-login">
<div class="subtitle">Accedi</div>
<div class="form">
<label for="login-username">Username</label>
<input
id="login-username"
type="text"
value={username}
onInput={e => setUsername(e.target instanceof HTMLInputElement ? e.target.value : '')}
onKeyDown={e => e.key === 'Enter' && handleLogin()}
/>
<div class="fill"> <div class="fill">
<button onClick={handleLogin}>Accedi</button> <a class="button" role="button" href={prependBaseUrl('/auth/redirect')} data-native>Accedi con Ateneo</a>
</div>
</div> </div>
</main> </main>
</> </>

@ -119,7 +119,7 @@ input[type='text'] {
} }
} }
button { button, .button {
cursor: pointer; cursor: pointer;
font-family: 'Lato'; font-family: 'Lato';

@ -14,8 +14,6 @@ import { RenderFunction } from './shared/ssr'
// Load ".env" // Load ".env"
dotenv.config() dotenv.config()
// const HTML_ROUTES = ['/', '/login', '/problem/:id', '/u/:uid', '/admin', '/profile', '/scores']
const config = { const config = {
isDevelopment: process.env.MODE === 'development', isDevelopment: process.env.MODE === 'development',
port: process.env.PORT || 3000, port: process.env.PORT || 3000,
@ -26,13 +24,6 @@ if (config.isDevelopment) {
console.log(`[Config] PORT = ${config.port}`) console.log(`[Config] PORT = ${config.port}`)
} }
// function mountIndexHtmlRoutes(r: Router, serveIndexHtml: Handler) {
// for (const route of HTML_ROUTES) {
// console.log(`[Server] Mounted index html for "${route}"`)
// r.get(route, serveIndexHtml)
// }
// }
async function createDevRouter() { async function createDevRouter() {
const r = express.Router() const r = express.Router()

@ -1,8 +1,11 @@
import { Router } from 'express' import { Router } from 'express'
import { AuthorizationCode } from 'simple-oauth2' import { AuthorizationCode } from 'simple-oauth2'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { DatabaseConnection, getUser, createUser } from './db/database'
import { UserId } from '../shared/model'
import { SessionService } from './routes'
export function setupOauth(r: Router) { export function setupOauth(r: Router, db: DatabaseConnection, sessions: SessionService) {
const config = { const config = {
client: { client: {
id: process.env.OAUTH_CLIENT_ID ?? '', id: process.env.OAUTH_CLIENT_ID ?? '',
@ -55,8 +58,29 @@ export function setupOauth(r: Router) {
}) })
).json() ).json()
// TODO: call to db && login type UserInfo = { name: string; email: string }
return res.status(200).json(userInfo)
// Parse user info into a sensible struct
const authUser = {
id: (userInfo as UserInfo).email.split('@')[0] as UserId,
fullName: (userInfo as UserInfo).name
.split(' ')
.map(s => s.substring(0, 1) + s.substring(1).toLowerCase())
.join(' '),
}
const user = await getUser(db, authUser.id)
if (!user) {
await createUser(db, {
id: authUser.id,
fullName: authUser.fullName,
role: 'student',
})
}
res.cookie('sid', sessions.createSession(authUser.id), { maxAge: 1000 * 60 * 60 * 24 * 7 })
return res.status(200).redirect('/')
} catch (error) { } catch (error) {
console.error('Access Token Error', error.message) console.error('Access Token Error', error.message)
return res.status(500).redirect(`/error?message=${encodeURIComponent('Autenticazione fallita')}`) return res.status(500).redirect(`/error?message=${encodeURIComponent('Autenticazione fallita')}`)

@ -2,7 +2,7 @@ import crypto from 'crypto'
import { readFile, writeFile, access, constants } from 'fs/promises' import { readFile, writeFile, access, constants } from 'fs/promises'
import { MetadataProps as MetaProps, Problem, ProblemId, Solution, SolutionId, User } from '../../shared/model' import { MetadataProps as MetaProps, Problem, ProblemId, Solution, SolutionId, User, UserId, UserRole } from '../../shared/model'
function once<T extends (...args: any) => any>(fn: T, message: string): T { function once<T extends (...args: any) => any>(fn: T, message: string): T {
let flag = false let flag = false
@ -53,7 +53,7 @@ function createMutex(): Mutex {
return { lock } return { lock }
} }
type DatabaseConnection = { export type DatabaseConnection = {
path: string path: string
initialValue: Database initialValue: Database
mu: Mutex mu: Mutex
@ -110,6 +110,11 @@ export const getUser: (db: DatabaseConnection, id: string) => Promise<User | nul
return state.users[id] ?? null return state.users[id] ?? null
}) })
export const createUser: (db: DatabaseConnection, user: User) => Promise<void> = (db, user) =>
withDatabase(db, state => {
state.users[user.id] = user
})
// //
// Problems // Problems
// //

@ -40,11 +40,16 @@ import { initialDatabaseValue } from './db/example-data'
import { validateObjectKeys } from '../shared/utils' import { validateObjectKeys } from '../shared/utils'
import { setupOauth } from './auth' import { setupOauth } from './auth'
export async function createApiRouter() {
type SessionId = Opaque<string, string, 'session'> type SessionId = Opaque<string, string, 'session'>
export interface SessionService {
createSession(userId: UserId): SessionId
getUserForSession(sid: SessionId): UserId | null
}
export async function createApiRouter() {
const sessionStore: Record<SessionId, UserId> = {} const sessionStore: Record<SessionId, UserId> = {}
const sessions = { const sessions: SessionService = {
createSession(userId: UserId) { createSession(userId: UserId) {
const sid = crypto.randomBytes(10).toString('hex') as SessionId const sid = crypto.randomBytes(10).toString('hex') as SessionId
sessionStore[sid] = userId sessionStore[sid] = userId
@ -73,7 +78,7 @@ export async function createApiRouter() {
const authRouter: Router = express.Router() const authRouter: Router = express.Router()
setupOauth(authRouter) setupOauth(authRouter, db, sessions)
r.use('/auth', authRouter) r.use('/auth', authRouter)
r.get('/api/status', (req, res) => { r.get('/api/status', (req, res) => {
@ -84,18 +89,18 @@ export async function createApiRouter() {
res.json(await getRequestUser(req)) res.json(await getRequestUser(req))
}) })
r.post('/api/login', async (req, res) => { // r.post('/api/login', async (req, res) => {
const { id, name } = req.body // const { id, name } = req.body
const user = await getUser(db, id) // const user = await getUser(db, id)
if (!user) { // if (!user) {
res.sendStatus(StatusCodes.FORBIDDEN) // res.sendStatus(StatusCodes.FORBIDDEN)
return // return
} // }
res.cookie('sid', sessions.createSession(id), { maxAge: 1000 * 60 * 60 * 24 * 7 }) // res.cookie('sid', sessions.createSession(id), { maxAge: 1000 * 60 * 60 * 24 * 7 })
res.json({ status: 'ok' }) // res.json({ status: 'ok' })
}) // })
r.post('/api/logout', (req, res) => { r.post('/api/logout', (req, res) => {
res.cookie('sid', '', { expires: new Date() }) res.cookie('sid', '', { expires: new Date() })

@ -20,6 +20,7 @@ export type UserId = Id<User>
export type User = { export type User = {
id: UserId id: UserId
fullName: string
role: UserRole role: UserRole
} }

Loading…
Cancel
Save