diff --git a/app/Main.hs b/app/Main.hs index 0aa8595..0842fb9 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -5,16 +5,16 @@ module Main where -import Data.ByteString.Char8 qualified as C8 import Data.ByteString qualified as BS +import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Lazy qualified as BL +import Data.List (isSuffixOf) import Effectful import Network.HTTP.Types qualified as HTTP import Network.Wai qualified as Wai import Network.Wai.Handler.Warp qualified as Warp import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (takeDirectory) -import Data.List (isSuffixOf) import Sis.Database import Sis.Page.Activity @@ -88,3 +88,6 @@ router RDashboard = runPage Sis.Page.Dashboard.page router RChores = runPage Sis.Page.Chores.page router RHousehold = runPage Sis.Page.Household.page router RActivity = runPage Sis.Page.Activity.page +router RSeed = do + seed + redirect (routeUri RDashboard) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8375833 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,56 @@ +{ + "name": "sis", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "playwright": "^1.61.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2fe1cd5 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "playwright": "^1.61.1" + } +} diff --git a/src/Sis/Database.hs b/src/Sis/Database.hs index 1dcf778..8d95210 100644 --- a/src/Sis/Database.hs +++ b/src/Sis/Database.hs @@ -51,6 +51,10 @@ import System.FilePath (takeDirectory) import Text.Read (readMaybe) import Sis.Types +import Sis.Auth (hashPassword) +import Crypto.Random.Entropy (getEntropy) +import Data.ByteString.Base64 qualified as B64 +import Data.Text.Encoding qualified as TE ---------------------------------------------------------------------- -- Effect definition @@ -369,14 +373,12 @@ generateRecurringDates period startDate fromDate toDate = go (max startDate from next PeriodDaily = addDays 1; next PeriodWeekly = addDays 7; next PeriodMonthly = addGregorianMonthsClip 1 hashPasswordIO :: Text -> IO Text -hashPasswordIO password = do - -- Simple hashing stub — we'll use crypton via Auth.hs in the real implementation - pure password +hashPasswordIO = hashPassword generateTokenIO :: IO Text generateTokenIO = do - -- Simple token stub - pure "dummy-token" + bytes <- getEntropy 32 + pure $ TE.decodeUtf8 $ B64.encode bytes ---------------------------------------------------------------------- -- Database open helper diff --git a/src/Sis/Page/Activity.hs b/src/Sis/Page/Activity.hs index c954166..aeeb275 100644 --- a/src/Sis/Page/Activity.hs +++ b/src/Sis/Page/Activity.hs @@ -98,7 +98,7 @@ page = do case mSession of Nothing -> do redirect (routeUri RLogin) - pure $ hyper ActivityPage $ el "Redirecting..." + Just us -> do hhs <- getUserHouseholds (UserId (usUserId us)) case hhs of diff --git a/src/Sis/Page/Chores.hs b/src/Sis/Page/Chores.hs index 8f84d98..b7aa36a 100644 --- a/src/Sis/Page/Chores.hs +++ b/src/Sis/Page/Chores.hs @@ -95,7 +95,7 @@ page = do case mSession of Nothing -> do redirect (routeUri RLogin) - pure $ hyper ChoresPage $ el "Redirecting..." + Just us -> do hhs <- getUserHouseholds (UserId (usUserId us)) case hhs of diff --git a/src/Sis/Page/Dashboard.hs b/src/Sis/Page/Dashboard.hs index b21d114..9b83350 100644 --- a/src/Sis/Page/Dashboard.hs +++ b/src/Sis/Page/Dashboard.hs @@ -111,7 +111,7 @@ page = do case mSession of Nothing -> do redirect (routeUri RLogin) - pure $ hyper DashboardPage $ el "Redirecting..." + Just us -> do today <- liftIO (utctDay <$> getCurrentTime) hhs <- getUserHouseholds (UserId (usUserId us)) diff --git a/src/Sis/Page/Household.hs b/src/Sis/Page/Household.hs index bfd0b0c..ba50b41 100644 --- a/src/Sis/Page/Household.hs +++ b/src/Sis/Page/Household.hs @@ -120,7 +120,7 @@ page = do case mSession of Nothing -> do redirect (routeUri RLogin) - pure $ hyper HouseholdPage $ el "Redirecting..." + Just us -> do hhs <- getUserHouseholds (UserId (usUserId us)) case hhs of diff --git a/src/Sis/Page/Login.hs b/src/Sis/Page/Login.hs index 5648a37..f777415 100644 --- a/src/Sis/Page/Login.hs +++ b/src/Sis/Page/Login.hs @@ -41,10 +41,19 @@ instance (DB :> es, IOE :> es) => HyperView LoginPage es where Just u | verifyPassword (lfPassword formData') (userPasswordHash u) -> do saveSession (UserSession (unUserId (userId u))) - redirect (routeUri RDashboard) - pure $ hyper LoginPage $ el "Redirecting..." + pure (loginSuccessView) + _ -> pure (loginView (Just "Invalid email or password")) - update Noop = pure $ hyper LoginPage $ loginView Nothing + update Noop = pure (loginView Nothing) + +loginSuccessView :: View LoginPage () +loginSuccessView = do + el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto;text-align:center" $ do + el @ att "class" nbBoxClass @ att "style" "padding:2rem" $ do + el @ att "class" nbFontHeading1Class $ text "Logged In!" + el @ att "style" "margin-top:1rem;margin-bottom:1rem" $ text "You are now logged in." + route RDashboard $ text "Go to Dashboard" + loginView :: Maybe Text -> View LoginPage () loginView mError = do el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto" $ do @@ -71,5 +80,5 @@ page = do case mSession of Just _ -> do redirect (routeUri RDashboard) - pure $ hyper LoginPage $ el "Redirecting..." + Nothing -> pure $ hyper LoginPage $ loginView Nothing diff --git a/src/Sis/Page/Signup.hs b/src/Sis/Page/Signup.hs index 12b34eb..cb88db7 100644 --- a/src/Sis/Page/Signup.hs +++ b/src/Sis/Page/Signup.hs @@ -49,8 +49,16 @@ instance (DB :> es, IOE :> es) => HyperView SignupPage es where pwHash <- liftIO (hashPassword (sfPassword form)) uid <- createUser (sfDisplayName form) (sfEmail form) pwHash saveSession (UserSession (unUserId uid)) - redirect (routeUri RDashboard) - pure $ hyper SignupPage $ el "Redirecting..." + pure (signupSuccessView) + +signupSuccessView :: View SignupPage () +signupSuccessView = do + el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto;text-align:center" $ do + el @ att "class" nbBoxClass @ att "style" "padding:2rem" $ do + el @ att "class" nbFontHeading1Class $ text "Account Created!" + el @ att "style" "margin-top:1rem;margin-bottom:1rem" $ text "Your account has been created." + route RDashboard $ text "Go to Dashboard" + signupView :: Maybe Text -> View SignupPage () signupView mError = do el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto" $ do @@ -81,5 +89,5 @@ page = do case mSession of Just _ -> do redirect (routeUri RDashboard) - pure $ hyper SignupPage $ el "Redirecting..." + Nothing -> pure $ hyper SignupPage $ signupView Nothing diff --git a/src/Sis/Route.hs b/src/Sis/Route.hs index 81c0ecc..e2445e3 100644 --- a/src/Sis/Route.hs +++ b/src/Sis/Route.hs @@ -14,6 +14,7 @@ data AppRoute | RChores | RHousehold | RActivity + | RSeed deriving stock (Eq, Generic, Show) instance Route AppRoute where diff --git a/test-login.js b/test-login.js new file mode 100644 index 0000000..7479f3e --- /dev/null +++ b/test-login.js @@ -0,0 +1,47 @@ +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + + // Collect ALL console messages + page.on('console', msg => console.log('CONSOLE:', msg.type(), msg.text())); + page.on('pageerror', err => console.log('PAGE ERROR:', err.message)); + + // Listen for network responses + page.on('response', resp => { + if (resp.url().includes('/rlogin') || resp.url().includes('websocket')) { + console.log('RESPONSE:', resp.status(), resp.url().substring(0, 50)); + } + }); + + await page.goto('http://localhost:8080/rlogin', { waitUntil: 'networkidle', timeout: 10000 }); + + // Check the HTML + const html = await page.content(); + console.log('Has "Welcome":', html.includes('Welcome')); + console.log('Has "lfEmail":', html.includes('lfEmail')); + console.log('Has form action:', html.includes('data-onsubmit')); + + await page.fill('input[name="lfEmail"]', 'alice@demo.com'); + await page.fill('input[name="lfPassword"]', 'password123'); + + console.log('Clicking submit...'); + await page.click('button[type="submit"]'); + + // Wait to see what happens + await page.waitForTimeout(4000); + + const newHtml = await page.content(); + console.log('After submit URL:', page.url()); + console.log('Has "Invalid":', newHtml.includes('Invalid')); + console.log('Has "Logged In":', newHtml.includes('Logged In')); + console.log('Has "Redirecting":', newHtml.includes('Redirecting')); + console.log('Has "Dashboard":', newHtml.includes('Dashboard')); + + // Check cookies + const cookies = await page.context().cookies(); + console.log('Cookies:', JSON.stringify(cookies.map(c => c.name + '=' + c.value))); + + await browser.close(); +})().catch(e => { console.error('FAIL:', e.message); process.exit(1); }); diff --git a/test-login.mjs b/test-login.mjs new file mode 100644 index 0000000..9ed1beb --- /dev/null +++ b/test-login.mjs @@ -0,0 +1,58 @@ +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + + // Collect console and page errors + const errors = []; + page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); + page.on('pageerror', err => errors.push(err.message)); + + // Test 1: Load login page + console.log("=== Test 1: Load login page ==="); + await page.goto('http://localhost:8080/rlogin', { waitUntil: 'networkidle' }); + const title = await page.title(); + console.log("Title:", title); + const hasForm = await page.$('form') !== null; + console.log("Has form:", hasForm); + const hasWelcome = (await page.content()).includes('Welcome Back'); + console.log("Has Welcome:", hasWelcome); + + if (errors.length > 0) { + console.log("Errors on load:", errors); + } + + // Test 2: Fill and submit login form + console.log("\n=== Test 2: Submit login form ==="); + await page.fill('input[name="lfEmail"]', 'alice@demo.com'); + await page.fill('input[name="lfPassword"]', 'password123'); + await page.check('input[name="lfRemember"]'); + + // Listen for response + const [response] = await Promise.all([ + page.waitForResponse(resp => resp.url().includes('/rlogin'), { timeout: 5000 }).catch(() => null), + page.click('button[type="submit"]') + ]); + + console.log("Response status:", response ? response.status() : 'no response'); + + // Wait for navigation or content change + await page.waitForTimeout(2000); + const content = await page.content(); + console.log("Page contains 'Dashboard':", content.includes('Dashboard') || content.includes('Overdue')); + console.log("Page contains error:", content.includes('Invalid') || content.includes('error')); + + if (errors.length > 0) { + console.log("Errors after submit:", errors); + } + + // Test 3: Try signup + console.log("\n=== Test 3: Load signup page ==="); + await page.goto('http://localhost:8080/rsignup', { waitUntil: 'networkidle' }); + const hasCreate = (await page.content()).includes('Create Account'); + console.log("Has Create Account:", hasCreate); + + await browser.close(); + console.log("\nDone."); +})().catch(e => { console.error("Test failed:", e.message); process.exit(1); });