Compare commits

...

10 commits

11 changed files with 307 additions and 7 deletions

View file

@ -0,0 +1,70 @@
describe('The admin nav link', () => {
beforeEach(() => {
cy.exec('npm run db:seed')
})
afterEach(() => {
cy.exec('npm run db:wipe')
})
describe('when logged in as an admin', () => {
beforeEach(() => {
cy.loginAsAdmin()
})
it('shows the admin link on the home page', () => {
cy.visit('/home')
cy.get('#admin-link')
.should('be.visible')
.and('have.attr', 'href', '/admin')
})
it('shows the admin link on the today page', () => {
cy.visit('/today')
cy.get('#admin-link')
.should('be.visible')
.and('have.attr', 'href', '/admin')
})
it('shows the admin link on the user texts page', () => {
cy.visit('/texts')
cy.get('#admin-link')
.should('be.visible')
.and('have.attr', 'href', '/admin')
})
it('navigates to the admin page when clicked', () => {
cy.visit('/home')
cy.get('#admin-link').click()
cy.url().should('include', '/admin')
cy.get('h1').should('contain', 'Admin')
})
})
describe('when logged in as a regular user', () => {
beforeEach(() => {
cy.loginAsUser()
})
it('does not show the admin link on the home page', () => {
cy.visit('/home')
cy.get('#admin-link').should('not.be.visible')
})
it('does not show the admin link on the today page', () => {
cy.visit('/today')
cy.get('#admin-link').should('not.be.visible')
})
it('does not show the admin link on the user texts page', () => {
cy.visit('/texts')
cy.get('#admin-link').should('not.be.visible')
})
it('does not show the admin link on a user text page', () => {
cy.intercept('GET', '/api/texts/0').as('getText')
cy.visit('/texts/0')
cy.wait('@getText')
cy.get('#admin-link').should('not.be.visible')
})
})
})

View file

@ -69,6 +69,60 @@ describe('Authentication flows', () => {
cy.url().should('include', '/login')
})
it('logout button on today page works', () => {
cy.loginAsUser()
cy.visit('/today')
cy.get('#logout').click()
cy.url().should('include', '/login')
cy.visit('/today')
cy.url().should('include', '/login')
})
it('logout button on user texts list page works', () => {
cy.loginAsUser()
cy.visit('/texts')
cy.get('#logout').click()
cy.url().should('include', '/login')
cy.visit('/texts')
cy.url().should('include', '/login')
})
it('logout button on user specific text page works', () => {
cy.loginAsUser()
cy.visit('/texts/0')
cy.get('#logout').click()
cy.url().should('include', '/login')
cy.visit('/texts/0')
cy.url().should('include', '/login')
})
it('logout button on admin page works', () => {
cy.loginAsAdmin()
cy.visit('/admin')
cy.get('#logout').click()
cy.url().should('include', '/login')
cy.visit('/admin')
cy.url().should('include', '/login')
})
it('logout button on admin texts list page works', () => {
cy.loginAsAdmin()
cy.visit('/admin/texts')
cy.get('#logout').click()
cy.url().should('include', '/login')
cy.visit('/admin/texts')
cy.url().should('include', '/login')
})
it('logout button on admin specific text page works', () => {
cy.loginAsAdmin()
cy.visit('/admin/texts/0')
cy.get('#logout').click()
cy.url().should('include', '/login')
cy.visit('/admin/texts/0')
cy.url().should('include', '/login')
})
it('non-admin user hitting /admin gets 403', () => {
cy.loginAsUser()
cy.request({

127
data/seedMore.php Normal file
View file

@ -0,0 +1,127 @@
<?php
$texts = [
[
'id' => 0,
'name' => 'Tanach',
'userId' => 1,
],
];
$nodes = [
[
'id' => 0,
'title' => 'Tanach',
'textId' => 0,
'parentNodeId' => null,
],
[
'id' => 1,
'title' => 'Torah',
'textId' => 0,
'parentNodeId' => 0,
],
[
'id' => 2,
'title' => 'Neviim',
'textId' => 0,
'parentNodeId' => 0,
],
[
'id' => 3,
'title' => 'Kesuvim',
'textId' => 0,
'parentNodeId' => 0,
],
[
'id' => 4,
'title' => 'Bereishis',
'textId' => 0,
'parentNodeId' => 1,
],
[
'id' => 5,
'title' => 'Shmos',
'textId' => 0,
'parentNodeId' => 1,
],
[
'id' => 6,
'title' => 'Vayikra',
'textId' => 0,
'parentNodeId' => 1,
],
[
'id' => 7,
'title' => 'Bamidbar',
'textId' => 0,
'parentNodeId' => 1,
],
[
'id' => 8,
'title' => 'Devarim',
'textId' => 0,
'parentNodeId' => 1,
],
[
'id' => 9,
'title' => 'Bereishis',
'textId' => 0,
'parentNodeId' => 4,
],
[
'id' => 10,
'title' => 'Noach',
'textId' => 0,
'parentNodeId' => 4,
],
[
'id' => 11,
'title' => 'Lech Lecha',
'textId' => 0,
'parentNodeId' => 4,
],
];
// Default credentials:
// admin@example.com / admin1234 (admin)
// user@example.com / password1 (regular user)
// user2@example.com / password2 (second regular user, no texts seeded)
$users = [
[
'id' => 0,
'email' => 'admin@example.com',
'passwordHash' => password_hash('admin1234', PASSWORD_DEFAULT),
'isAdmin' => true,
],
[
'id' => 1,
'email' => 'user@example.com',
'passwordHash' => password_hash('password1', PASSWORD_DEFAULT),
'isAdmin' => false,
],
[
'id' => 2,
'email' => 'user2@example.com',
'passwordHash' => password_hash('password2', PASSWORD_DEFAULT),
'isAdmin' => false,
],
];
$plans = [];
$scheduledNodes = [];
$sessions = [];
$fileDataMap = [
'texts.json' => $texts,
'nodes.json' => $nodes,
'users.json' => $users,
'plans.json' => $plans,
'scheduledNodes.json' => $scheduledNodes,
'sessions.json' => $sessions,
];
foreach ($fileDataMap as $file => $data) {
$path = __DIR__ . "/$file";
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT));
}

18
public/js/nav.js Normal file
View file

@ -0,0 +1,18 @@
document.addEventListener('DOMContentLoaded', async () => {
const adminLink = document.getElementById('admin-link');
if (adminLink === null) {
return;
}
const response = await fetch('/api/auth/me', {
credentials: 'same-origin',
});
if (!response.ok) {
return;
}
const body = await response.json();
if (body.user && body.user.isAdmin === true) {
adminLink.hidden = false;
}
});

View file

@ -32,8 +32,15 @@ document.addEventListener('DOMContentLoaded', () => {
function fetchAndRenderNodes(textId) {
return fetch('/api/nodes/' + textId, { credentials: 'same-origin' })
.then(res => res.json())
.then(function (response) {
if (!response.ok) {
return null;
}
return response.json();
})
.then(nodes => {
if (!Array.isArray(nodes)) return;
const existing = document.querySelector('#text-detail > ul');
if (existing) existing.remove();

View file

@ -17,6 +17,8 @@
<a class="btn btn-secondary" href="/today">
Today's schedule
</a>
<a id="admin-link" class="btn btn-secondary"
href="/admin" hidden>Admin</a>
<button id="logout" class="btn btn-danger">Logout</button>
</div>
</div>
@ -43,6 +45,7 @@
</div>
</div>
<script src="/js/auth.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/home.js"></script>
</body>
</html>

View file

@ -9,14 +9,18 @@
<body>
<header class="site-header">
<div class="site-header-inner">
<a class="btn btn-secondary" href="/admin/texts" id="back">
Back to Texts
</a>
<div class="cluster">
<a class="btn btn-secondary" href="/admin/texts" id="back">
Back to Texts
</a>
<button id="logout" class="btn btn-danger">Logout</button>
</div>
</div>
</header>
<main class="container container-wide stack">
<div id="text-detail" class="node-tree stack"></div>
</main>
<script src="/js/auth.js"></script>
<script src="/js/text.js"></script>
</body>
</html>

View file

@ -14,6 +14,7 @@
<a class="btn btn-secondary" href="/admin" id="back">
Back to Admin
</a>
<button id="logout" class="btn btn-danger">Logout</button>
</div>
</div>
</header>
@ -31,6 +32,7 @@
</div>
</form>
</main>
<script src="/js/auth.js"></script>
<script src="/js/texts.js"></script>
</body>
</html>

View file

@ -12,6 +12,9 @@
<h1>Today</h1>
<div class="cluster">
<a class="btn btn-secondary" href="/home">Home</a>
<a id="admin-link" class="btn btn-secondary"
href="/admin" hidden>Admin</a>
<button id="logout" class="btn btn-danger">Logout</button>
</div>
</div>
</header>
@ -22,6 +25,7 @@
</p>
</main>
<script src="/js/auth.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/today.js"></script>
</body>
</html>

View file

@ -9,14 +9,22 @@
<body>
<header class="site-header">
<div class="site-header-inner">
<a class="btn btn-secondary" href="/texts" id="back">
Back to My Texts
</a>
<h1>Text</h1>
<div class="cluster">
<a class="btn btn-secondary" href="/texts" id="back">
My texts
</a>
<a id="admin-link" class="btn btn-secondary"
href="/admin" hidden>Admin</a>
<button id="logout" class="btn btn-danger">Logout</button>
</div>
</div>
</header>
<main class="container container-wide stack">
<div id="text-detail" class="node-tree stack"></div>
</main>
<script src="/js/auth.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/text.js"></script>
</body>
</html>

View file

@ -14,6 +14,8 @@
<a class="btn btn-secondary" href="/home" id="back">
Back to Home
</a>
<a id="admin-link" class="btn btn-secondary"
href="/admin" hidden>Admin</a>
<button id="logout" class="btn btn-danger">Logout</button>
</div>
</div>
@ -33,6 +35,7 @@
</form>
</main>
<script src="/js/auth.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/userTexts.js"></script>
</body>
</html>