chore: rewrite /login page for OAuth, write createUser for the database
This commit is contained in:
@@ -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="fill">
|
||||||
<div class="form">
|
<a class="button" role="button" href={prependBaseUrl('/auth/redirect')} data-native>Accedi con Ateneo</a>
|
||||||
<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">
|
|
||||||
<button onClick={handleLogin}>Accedi</button>
|
|
||||||
</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()
|
||||||
|
|
||||||
|
|||||||
+27
-3
@@ -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
|
||||||
//
|
//
|
||||||
|
|||||||
+19
-14
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user