fix: login/signup flow working with session-based auth
- Fixed hashPasswordIO to use real PBKDF2 hashing from Sis.Auth - Added seed route (rseed) for demo data initialization - Replaced redirect() in update functions with success views (redirect uses throwError_ which broke with effectful 2.4) - Added loginSuccessView and signupSuccessView with dashboard links - Patched Hyperbole Socket.hs to parse WebSocket form body into Form params - Patched Hyperbole Wai.hs to include TargetViewId in response metadata - Fixed View/Page type mismatches (hyper wrapper vs plain view) - Removed dead pure after redirect calls - Verified: login submits, session set, success view rendered
This commit is contained in:
+5
-2
@@ -5,16 +5,16 @@
|
|||||||
|
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
import Data.ByteString.Char8 qualified as C8
|
|
||||||
import Data.ByteString qualified as BS
|
import Data.ByteString qualified as BS
|
||||||
|
import Data.ByteString.Char8 qualified as C8
|
||||||
import Data.ByteString.Lazy qualified as BL
|
import Data.ByteString.Lazy qualified as BL
|
||||||
|
import Data.List (isSuffixOf)
|
||||||
import Effectful
|
import Effectful
|
||||||
import Network.HTTP.Types qualified as HTTP
|
import Network.HTTP.Types qualified as HTTP
|
||||||
import Network.Wai qualified as Wai
|
import Network.Wai qualified as Wai
|
||||||
import Network.Wai.Handler.Warp qualified as Warp
|
import Network.Wai.Handler.Warp qualified as Warp
|
||||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||||
import System.FilePath (takeDirectory)
|
import System.FilePath (takeDirectory)
|
||||||
import Data.List (isSuffixOf)
|
|
||||||
|
|
||||||
import Sis.Database
|
import Sis.Database
|
||||||
import Sis.Page.Activity
|
import Sis.Page.Activity
|
||||||
@@ -88,3 +88,6 @@ router RDashboard = runPage Sis.Page.Dashboard.page
|
|||||||
router RChores = runPage Sis.Page.Chores.page
|
router RChores = runPage Sis.Page.Chores.page
|
||||||
router RHousehold = runPage Sis.Page.Household.page
|
router RHousehold = runPage Sis.Page.Household.page
|
||||||
router RActivity = runPage Sis.Page.Activity.page
|
router RActivity = runPage Sis.Page.Activity.page
|
||||||
|
router RSeed = do
|
||||||
|
seed
|
||||||
|
redirect (routeUri RDashboard)
|
||||||
|
|||||||
Generated
+56
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.61.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-5
@@ -51,6 +51,10 @@ import System.FilePath (takeDirectory)
|
|||||||
import Text.Read (readMaybe)
|
import Text.Read (readMaybe)
|
||||||
|
|
||||||
import Sis.Types
|
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
|
-- 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
|
next PeriodDaily = addDays 1; next PeriodWeekly = addDays 7; next PeriodMonthly = addGregorianMonthsClip 1
|
||||||
|
|
||||||
hashPasswordIO :: Text -> IO Text
|
hashPasswordIO :: Text -> IO Text
|
||||||
hashPasswordIO password = do
|
hashPasswordIO = hashPassword
|
||||||
-- Simple hashing stub — we'll use crypton via Auth.hs in the real implementation
|
|
||||||
pure password
|
|
||||||
|
|
||||||
generateTokenIO :: IO Text
|
generateTokenIO :: IO Text
|
||||||
generateTokenIO = do
|
generateTokenIO = do
|
||||||
-- Simple token stub
|
bytes <- getEntropy 32
|
||||||
pure "dummy-token"
|
pure $ TE.decodeUtf8 $ B64.encode bytes
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Database open helper
|
-- Database open helper
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ page = do
|
|||||||
case mSession of
|
case mSession of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
redirect (routeUri RLogin)
|
redirect (routeUri RLogin)
|
||||||
pure $ hyper ActivityPage $ el "Redirecting..."
|
|
||||||
Just us -> do
|
Just us -> do
|
||||||
hhs <- getUserHouseholds (UserId (usUserId us))
|
hhs <- getUserHouseholds (UserId (usUserId us))
|
||||||
case hhs of
|
case hhs of
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ page = do
|
|||||||
case mSession of
|
case mSession of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
redirect (routeUri RLogin)
|
redirect (routeUri RLogin)
|
||||||
pure $ hyper ChoresPage $ el "Redirecting..."
|
|
||||||
Just us -> do
|
Just us -> do
|
||||||
hhs <- getUserHouseholds (UserId (usUserId us))
|
hhs <- getUserHouseholds (UserId (usUserId us))
|
||||||
case hhs of
|
case hhs of
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ page = do
|
|||||||
case mSession of
|
case mSession of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
redirect (routeUri RLogin)
|
redirect (routeUri RLogin)
|
||||||
pure $ hyper DashboardPage $ el "Redirecting..."
|
|
||||||
Just us -> do
|
Just us -> do
|
||||||
today <- liftIO (utctDay <$> getCurrentTime)
|
today <- liftIO (utctDay <$> getCurrentTime)
|
||||||
hhs <- getUserHouseholds (UserId (usUserId us))
|
hhs <- getUserHouseholds (UserId (usUserId us))
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ page = do
|
|||||||
case mSession of
|
case mSession of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
redirect (routeUri RLogin)
|
redirect (routeUri RLogin)
|
||||||
pure $ hyper HouseholdPage $ el "Redirecting..."
|
|
||||||
Just us -> do
|
Just us -> do
|
||||||
hhs <- getUserHouseholds (UserId (usUserId us))
|
hhs <- getUserHouseholds (UserId (usUserId us))
|
||||||
case hhs of
|
case hhs of
|
||||||
|
|||||||
+13
-4
@@ -41,10 +41,19 @@ instance (DB :> es, IOE :> es) => HyperView LoginPage es where
|
|||||||
Just u
|
Just u
|
||||||
| verifyPassword (lfPassword formData') (userPasswordHash u) -> do
|
| verifyPassword (lfPassword formData') (userPasswordHash u) -> do
|
||||||
saveSession (UserSession (unUserId (userId u)))
|
saveSession (UserSession (unUserId (userId u)))
|
||||||
redirect (routeUri RDashboard)
|
pure (loginSuccessView)
|
||||||
pure $ hyper LoginPage $ el "Redirecting..."
|
|
||||||
_ -> pure (loginView (Just "Invalid email or password"))
|
_ -> 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 :: Maybe Text -> View LoginPage ()
|
||||||
loginView mError = do
|
loginView mError = do
|
||||||
el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto" $ do
|
el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto" $ do
|
||||||
@@ -71,5 +80,5 @@ page = do
|
|||||||
case mSession of
|
case mSession of
|
||||||
Just _ -> do
|
Just _ -> do
|
||||||
redirect (routeUri RDashboard)
|
redirect (routeUri RDashboard)
|
||||||
pure $ hyper LoginPage $ el "Redirecting..."
|
|
||||||
Nothing -> pure $ hyper LoginPage $ loginView Nothing
|
Nothing -> pure $ hyper LoginPage $ loginView Nothing
|
||||||
|
|||||||
+11
-3
@@ -49,8 +49,16 @@ instance (DB :> es, IOE :> es) => HyperView SignupPage es where
|
|||||||
pwHash <- liftIO (hashPassword (sfPassword form))
|
pwHash <- liftIO (hashPassword (sfPassword form))
|
||||||
uid <- createUser (sfDisplayName form) (sfEmail form) pwHash
|
uid <- createUser (sfDisplayName form) (sfEmail form) pwHash
|
||||||
saveSession (UserSession (unUserId uid))
|
saveSession (UserSession (unUserId uid))
|
||||||
redirect (routeUri RDashboard)
|
pure (signupSuccessView)
|
||||||
pure $ hyper SignupPage $ el "Redirecting..."
|
|
||||||
|
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 :: Maybe Text -> View SignupPage ()
|
||||||
signupView mError = do
|
signupView mError = do
|
||||||
el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto" $ do
|
el @ att "class" nbContainerClass @ att "style" "max-width:480px;margin:4rem auto" $ do
|
||||||
@@ -81,5 +89,5 @@ page = do
|
|||||||
case mSession of
|
case mSession of
|
||||||
Just _ -> do
|
Just _ -> do
|
||||||
redirect (routeUri RDashboard)
|
redirect (routeUri RDashboard)
|
||||||
pure $ hyper SignupPage $ el "Redirecting..."
|
|
||||||
Nothing -> pure $ hyper SignupPage $ signupView Nothing
|
Nothing -> pure $ hyper SignupPage $ signupView Nothing
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ data AppRoute
|
|||||||
| RChores
|
| RChores
|
||||||
| RHousehold
|
| RHousehold
|
||||||
| RActivity
|
| RActivity
|
||||||
|
| RSeed
|
||||||
deriving stock (Eq, Generic, Show)
|
deriving stock (Eq, Generic, Show)
|
||||||
|
|
||||||
instance Route AppRoute where
|
instance Route AppRoute where
|
||||||
|
|||||||
@@ -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); });
|
||||||
@@ -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); });
|
||||||
Reference in New Issue
Block a user