From be33e153359ebdaa066a92f7f4999c66cb40eaee Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 11 Jun 2024 12:09:35 -0400 Subject: [PATCH] bring back more current version of frontend --- backend/authorize.js | 41 ++++- backend/clearContextFacts.js | 46 +++++ backend/deleteFact.js | 93 +++++++--- backend/facts.js | 48 ----- backend/resumeGame.js | 25 ++- backend/startGame.js | 85 ++++++--- .../api/{facts.js => clear-context-facts.js} | 6 +- src/_methods/runTests.js | 2 +- src/_methods/setLevel.js | 6 - src/add-role-fact/add-role-fact.js | 2 +- src/app-component/app-component.html | 72 ++++++-- src/app-component/app-component.js | 103 ++++------- src/index.js | 12 +- src/leaderboard/leaderboard.html | 8 +- src/level/level.html | 116 ++++++------ src/level/level.js | 172 ++++++++++-------- src/splash-screen/splash-screen.html | 152 +++++++++++----- src/splash-screen/splash-screen.js | 15 +- 18 files changed, 622 insertions(+), 382 deletions(-) create mode 100644 backend/clearContextFacts.js delete mode 100644 backend/facts.js rename pages/api/{facts.js => clear-context-facts.js} (53%) diff --git a/backend/authorize.js b/backend/authorize.js index ec9829f..fd7c057 100644 --- a/backend/authorize.js +++ b/backend/authorize.js @@ -1,6 +1,10 @@ 'use strict'; const Archetype = require('archetype'); +const Log = require('../db/log'); +const Player = require('../db/player'); +const connect = require('../db/connect'); +const { inspect } = require('util'); const oso = require('../oso'); const AuthorizeParams = new Archetype({ @@ -28,11 +32,36 @@ const AuthorizeParams = new Archetype({ module.exports = async function authorize(params) { params = new AuthorizeParams(params); - const authorized = await oso.authorize( - { type: 'User', id: `${params.sessionId}_${params.userId}` }, - params.action, - { type: params.resourceType, id: params.resourceId } - ); + const { sessionId } = params; - return { authorized }; + await connect(); + + await Log.info(`authorize ${inspect(params)}`, { + ...params, + function: 'authorize' + }); + + try { + const player = await Player.findOne({ sessionId }).orFail(); + + console.log('Authorize', params, player.contextFacts); + + const authorized = await oso.authorize( + { type: 'User', id: params.userId }, + params.action, + { type: params.resourceType, id: params.resourceId }, + player.contextFacts + ); + return { authorized }; + } catch (err) { + await Log.error(`authorize: ${err.message}`, { + ...params, + function: 'authorize', + message: err.message, + stack: err.stack, + err: inspect(err) + }); + + throw err; + } }; \ No newline at end of file diff --git a/backend/clearContextFacts.js b/backend/clearContextFacts.js new file mode 100644 index 0000000..97a9126 --- /dev/null +++ b/backend/clearContextFacts.js @@ -0,0 +1,46 @@ +'use strict'; + +const Archetype = require('archetype'); +const Log = require('../db/log'); +const Player = require('../db/player'); +const connect = require('../db/connect'); +const extrovert = require('extrovert'); +const { inspect } = require('util'); + +const ClearContextFactsParams = new Archetype({ + sessionId: { + $type: 'string', + $required: true + } +}).compile('ClearContextFactsParams'); + +module.exports = extrovert.toNetlifyFunction(async params => { + params = new ClearContextFactsParams(params); + + await connect(); + + await Log.info(`clearContextFacts ${inspect(params)}`, { + ...params, + function: 'clearContextFacts' + }); + + try { + const { sessionId } = params; + const player = await Player.findOne({ sessionId }).orFail(); + + player.contextFacts = []; + + await player.save(); + return { ok: true }; + } catch (err) { + await Log.error(`clearContextFacts: ${err.message}`, { + ...params, + function: 'clearContextFacts', + message: err.message, + stack: err.stack, + err: inspect(err) + }); + + throw err; + } +}, null, 'clearContextFacts'); \ No newline at end of file diff --git a/backend/deleteFact.js b/backend/deleteFact.js index db90cbb..35da87f 100644 --- a/backend/deleteFact.js +++ b/backend/deleteFact.js @@ -1,8 +1,11 @@ 'use strict'; const Archetype = require('archetype'); +const Log = require('../db/log'); +const Player = require('../db/player'); const assert = require('assert'); -const oso = require('../oso'); +const connect = require('../db/connect'); +const { inspect } = require('util'); const DeleteFactParams = new Archetype({ sessionId: { @@ -24,38 +27,86 @@ const DeleteFactParams = new Archetype({ }, resourceType: { $type: 'string', - $required: true + $required: (v, type, doc) => assert.ok(v != null || doc.role !== 'superadmin') }, resourceId: { $type: 'string', - $required: true + $required: (v, type, doc) => assert.ok(v != null || doc.role !== 'superadmin') }, attribute: { $type: 'string', $validate: (v, type, doc) => assert.ok(v != null || doc.factType !== 'attribute') }, attributeValue: { - $type: 'boolean', + $type: 'string', $validate: (v, type, doc) => assert.ok(v != null || doc.factType !== 'attribute') + }, + actorType: { + $type: 'string' } }).compile('DeleteFactParams'); -module.exports = async function deleteFact(params) { +module.exports = deleteFact; + +async function deleteFact(params) { params = new DeleteFactParams(params); - if (params.factType === 'role') { - const resourceId = params.resourceType === 'Repository' ? `${params.sessionId}_${params.resourceId}` : params.resourceId; - await oso.delete( - 'has_role', - { type: 'User', id: `${params.sessionId}_${params.userId}` }, - params.role, - { type: params.resourceType, id: resourceId } - ); - } else { - await oso.delete( - params.attribute, - { type: 'Repository', id: `${params.sessionId}_${params.resourceId}` }, - { type: 'Boolean', id: !!params.attributeValue + '' } - ); + + await connect(); + + await Log.info(`deleteFact ${inspect(params)}`, { + ...params, + function: 'deleteFact' + }); + + try { + const { sessionId } = params; + const player = await Player.findOne({ sessionId }).orFail(); + + if (params.factType === 'role') { + if (params.role === 'superadmin') { + player.contextFacts = player.contextFacts.filter(fact => { + return fact[0] !== 'has_role' || + fact[1].type !== 'User' || + fact[1].id !== params.userId || + fact[2] !== params.role; + }); + } else { + player.contextFacts = player.contextFacts.filter(fact => { + return fact[0] !== 'has_role' || + fact[1].type !== (params.actorType ?? 'User') || + fact[1].id !== params.userId || + fact[2] !== params.role || + fact[3]?.type !== params.resourceType || + fact[3]?.id !== params.resourceId; + }); + } + + } else if (params.attribute === 'has_default_role') { + player.contextFacts = player.contextFacts.filter(fact => { + return fact[0] !== 'has_default_role' || + fact[1].type !== params.resourceType || + fact[1].id !== params.resourceId || + fact[2] !== params.attributeValue; + }); + } else { + player.contextFacts = player.contextFacts.filter(fact => { + return fact[0] !== params.attribute || + fact[1].type !== params.resourceType || + fact[1].id !== params.resourceId || + fact[2].id !== params.attributeValue; + }); + } + await player.save(); + return { ok: true, player }; + } catch (err) { + await Log.error(`deleteFact: ${err.message}`, { + ...params, + function: 'deleteFact', + message: err.message, + stack: err.stack, + err: inspect(err) + }); + + throw err; } - return { ok: true }; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/backend/facts.js b/backend/facts.js deleted file mode 100644 index 09312c9..0000000 --- a/backend/facts.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const Archetype = require('archetype'); -const oso = require('../oso'); - -const FactsParams = new Archetype({ - sessionId: { - $type: 'string', - $required: true - }, - userId: { - $type: ['string'], - $required: true - } -}).compile('FactsParams'); - -module.exports = async function facts(params) { - params = new FactsParams(params); - const facts = []; - for (const userId of params.userId) { - const factsForUser = await oso.get( - 'has_role', - { type: 'User', id: `${params.sessionId}_${userId}` }, - null, - null - ); - facts.push(...factsForUser); - } - for (const repo of ['osohq/sample-apps', 'osohq/nodejs-client', 'osohq/configs']) { - let factsForRepo = await oso.get( - 'is_protected', - { type: 'Repository', id: `${params.sessionId}_${repo}` }, - null, - null - ); - facts.push(...factsForRepo); - - factsForRepo = await oso.get( - 'is_public', - { type: 'Repository', id: `${params.sessionId}_${repo}` }, - null, - null - ); - facts.push(...factsForRepo); - } - - return { facts }; -}; \ No newline at end of file diff --git a/backend/resumeGame.js b/backend/resumeGame.js index d54325c..97b6bd1 100644 --- a/backend/resumeGame.js +++ b/backend/resumeGame.js @@ -1,8 +1,10 @@ 'use strict'; const Archetype = require('archetype'); +const Log = require('../db/log'); const Player = require('../db/player'); const connect = require('../db/connect'); +const { inspect } = require('util'); const ResumeGameParams = new Archetype({ sessionId: { @@ -16,9 +18,26 @@ module.exports = async function resumeGame(params) { await connect(); - const player = await Player.findOne({ - sessionId + await Log.info(`resumeGame ${inspect(params)}`, { + ...params, + function: 'resumeGame' }); + + try { + const player = await Player.findOne({ + sessionId + }); - return { player }; + return { player }; + } catch (err) { + await Log.error(`resumeGame: ${err.message}`, { + ...params, + function: 'resumeGame', + message: err.message, + stack: err.stack, + err: inspect(err) + }); + + throw err; + } }; diff --git a/backend/startGame.js b/backend/startGame.js index 98caced..5eb6239 100644 --- a/backend/startGame.js +++ b/backend/startGame.js @@ -1,8 +1,11 @@ 'use strict'; const Archetype = require('archetype'); +const Log = require('../db/log'); const Player = require('../db/player'); const connect = require('../db/connect'); +const extrovert = require('extrovert'); +const { inspect } = require('util'); const oso = require('../oso'); const StartGameParams = new Archetype({ @@ -17,40 +20,64 @@ const StartGameParams = new Archetype({ email: { $type: 'string', $required: true + }, + password: { + $type: 'string' } }).compile('StartGameParams'); -module.exports = async function handler(params) { - const { sessionId, name, email } = new StartGameParams(params); +module.exports = async function startGame(params) { + const { sessionId, name, email, password } = new StartGameParams(params); + + if (process.env.OSO_GOLF_PASSWORD && process.env.OSO_GOLF_PASSWORD !== password) { + throw new Error('Incorrect password'); + } await connect(); - const player = await Player.create({ - sessionId, - name, - email + await Log.info(`startGame ${inspect(params)}`, { + ...params, + function: 'startGame' }); + + try { + const player = await Player.create({ + sessionId, + name, + email + }); + + await oso.tell( + 'has_relation', + { type: 'Repository', id: `${params.sessionId}_osohq/configs` }, + 'organization', + { type: 'Organization', id: 'osohq' } + ); + + await oso.tell( + 'has_relation', + { type: 'Repository', id: `${params.sessionId}_osohq/sample-apps` }, + 'organization', + { type: 'Organization', id: 'osohq' } + ); + + await oso.tell( + 'has_relation', + { type: 'Repository', id: `${params.sessionId}_osohq/nodejs-client` }, + 'organization', + { type: 'Organization', id: 'osohq' } + ); + + return { player }; + } catch (err) { + await Log.error(`startGame: ${err.message}`, { + ...params, + function: 'startGame', + message: err.message, + stack: err.stack, + err: inspect(err) + }); - await oso.tell( - 'has_relation', - { type: 'Repository', id: `${sessionId}_osohq/configs` }, - 'organization', - { type: 'Organization', id: 'osohq' } - ); - - await oso.tell( - 'has_relation', - { type: 'Repository', id: `${sessionId}_osohq/sample-apps` }, - 'organization', - { type: 'Organization', id: 'osohq' } - ); - - await oso.tell( - 'has_relation', - { type: 'Repository', id: `${sessionId}_osohq/nodejs-client` }, - 'organization', - { type: 'Organization', id: 'osohq' } - ); - - return { player }; -}; + throw err; + } +}; \ No newline at end of file diff --git a/pages/api/facts.js b/pages/api/clear-context-facts.js similarity index 53% rename from pages/api/facts.js rename to pages/api/clear-context-facts.js index dfe2f1a..e0f374a 100644 --- a/pages/api/facts.js +++ b/pages/api/clear-context-facts.js @@ -1,11 +1,11 @@ 'use strict'; -import facts from '../../backend/facts'; +import clearContextFacts from '../../backend/clear-context-facts'; export default async function handler(req, res) { try { - const result = await facts(req.body); - res.status(200).json(result); + const result = await clearContextFacts(req.body); + return res.status(200).json(result); } catch (error) { console.error(error.stack); res.status(500).json({ message: error.message }); diff --git a/src/_methods/runTests.js b/src/_methods/runTests.js index 4b9ae98..5e11ba6 100644 --- a/src/_methods/runTests.js +++ b/src/_methods/runTests.js @@ -12,7 +12,7 @@ async function runTests(state = window.state) { let passed = true; const results = []; await Promise.all(state.constraints.map(async(constraint, index) => { - const authorized = await axios.get('/.netlify/functions/authorize', { + const authorized = await axios.get('/api/authorize', { params: { sessionId: state.sessionId, userId: constraint.userId, diff --git a/src/_methods/setLevel.js b/src/_methods/setLevel.js index aa33d6e..d06121e 100644 --- a/src/_methods/setLevel.js +++ b/src/_methods/setLevel.js @@ -22,10 +22,4 @@ module.exports = async function setLevel(level, retainContextFacts, state = wind state.results = []; state.showNextLevelButton = false; state.facts = []; - - if (!retainContextFacts) { - await axios.post('/.netlify/functions/clearContextFacts', { - sessionId: state.sessionId - }); - } }; \ No newline at end of file diff --git a/src/add-role-fact/add-role-fact.js b/src/add-role-fact/add-role-fact.js index b9a579e..35caa87 100644 --- a/src/add-role-fact/add-role-fact.js +++ b/src/add-role-fact/add-role-fact.js @@ -72,7 +72,7 @@ module.exports = app => app.component('add-role-fact', { } const factType = 'role'; - await axios.put('/.netlify/functions/tell', { + await axios.put('/api/tell', { sessionId: this.state.sessionId, factType, actorType: this.actorType, diff --git a/src/app-component/app-component.html b/src/app-component/app-component.html index 6d7c3b6..df81f49 100644 --- a/src/app-component/app-component.html +++ b/src/app-component/app-component.html @@ -1,42 +1,84 @@
-
-
- -
-
- Oso Golf -
-
-
- Read the Oso policy below, and then add Oso facts to satisfy the given constraints as fast as you can! -
+
+
+ +
+
+ Oso Golf +
+
+
+ Read the Oso policy below, and then add Oso facts to satisfy the given constraints as fast as you can! +
+
Hole: {{state.level}} / {{levels.length}}
Par Total: {{par}}
Time Elapsed: {{elapsedTime}}
- + + +
+ Are you sure you want to restart? You will lose all progress you've made. +
+
+ + Restart + + + +
+
- + + +
+ +
-
+
+
+
+ +
+
+ Oso Golf +
+
+
You completed the game!
+ + +
Congratulations! Check out where you placed on the leaderboard.
+
+ +
Start Over
+ +
\ No newline at end of file diff --git a/src/app-component/app-component.js b/src/app-component/app-component.js index 60c56af..0256b0f 100644 --- a/src/app-component/app-component.js +++ b/src/app-component/app-component.js @@ -1,14 +1,18 @@ 'use strict'; const axios = require('axios'); +const bson = require('bson'); const levels = require('../../levels'); +const runTests = require('../_methods/runTests'); +const setLevel = require('../_methods/setLevel'); const template = require('./app-component.html'); module.exports = app => app.component('app-component', { inject: ['state'], data: () => ({ currentTime: new Date(), - status: 'loading' + status: 'loading', + showRestartConfirmModal: false }), template, computed: { @@ -46,73 +50,41 @@ module.exports = app => app.component('app-component', { }, methods: { async test() { - this.state.results = []; - this.state.showNextLevelButton = null; - let passed = true; - for (const constraint of this.state.constraints) { - const resourceId = constraint.resourceType === 'Repository' ? - `${this.state.sessionId}_${constraint.resourceId}` : - constraint.resourceId; - const authorized = await axios.get('/api/authorize', { - params: { - sessionId: this.state.sessionId, - userId: constraint.userId, - action: constraint.action, - resourceType: constraint.resourceType, - resourceId - } - }).then(res => res.data.authorized); - const pass = authorized === !constraint.shouldFail; - this.state.results.push({ ...constraint, pass }); - if (!pass) { - passed = false; - } - } - this.state.showNextLevelButton = passed; - }, - async verifySolutionForLevel() { - const { player } = await axios.post('/api/verify-solution-for-level', { - sessionId: this.state.sessionId, - level: this.state.level - }).then(res => res.data); - this.state.level = player.levelsCompleted + 1; - this.state.par = player.par; - this.state.results = []; - this.state.showNextLevelButton = false; - const facts = [...this.state.facts]; - this.state.facts = []; - - await Promise.all(facts.map(fact => this.deleteFact(fact))); - - if (this.state.level < levels.length + 1) { - this.state.constraints = levels[this.state.level - 1].constraints; - await this.loadFacts(); - await this.test(); - } + await runTests(this.state); }, restart() { window.localStorage.setItem('_gitclubGameSession', ''); window.location.reload(); }, - async loadFacts() { - const facts = await axios.put('/api/facts', { - sessionId: this.state.sessionId, - userId: [...new Set(this.state.constraints.map(c => c.userId))] - }).then(res => res.data.facts); - - this.state.facts = facts.map(fact => { - return fact[0] === 'has_role' ? { - factType: 'role', - userId: fact[1].id.replace(this.state.sessionId, '').replace(/^_/, ''), - role: fact[2], - resourceType: fact[3]?.type, - resourceId: fact[3]?.id?.replace(this.state.sessionId, '')?.replace(/^_/, '') - } : { + loadFacts(player) { + this.state.facts = player.contextFacts.map(fact => { + if (fact[0] === 'has_role') { + return { + _id: new bson.ObjectId(), + factType: 'role', + actorType: fact[1].type, + userId: fact[1].id, + role: fact[2], + resourceType: fact[3]?.type, + resourceId: fact[3]?.id + }; + } else if (fact[0] === 'has_group') { + return { + _id: new bson.ObjectId(), + factType: 'attribute', + attribute: fact[0], + resourceType: fact[1].type, + resourceId: fact[1].id, + attributeValue: fact[2].id + }; + } + return { + _id: new bson.ObjectId(), factType: 'attribute', attribute: fact[0], resourceType: fact[1].type, - resourceId: fact[1].id.replace(this.state.sessionId, '').replace(/^_/, ''), - attributeValue: fact[2].id === 'true' + resourceId: fact[1].id, + attributeValue: typeof fact[2] === 'string' ? fact[2] : fact[2].id === 'true' }; }); } @@ -130,14 +102,13 @@ module.exports = app => app.component('app-component', { if (player == null) { return; } - this.state.level = player.levelsCompleted + 1; - if (this.state.level < levels.length + 1) { - this.state.constraints = levels[this.state.level - 1].constraints; - await this.loadFacts(); - await this.test(); - } + await setLevel(player.levelsCompleted + 1, true, this.state); this.state.par = player.par; this.state.startTime = new Date(player.startTime); + this.state.name = player.name; + this.state.player = player; + this.loadFacts(player); this.status = 'loaded'; + await this.test(); } }); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 84f0758..9245856 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,9 @@ const components = require('./components'); const levels = require('../levels'); const vanillatoasts = require('vanillatoasts'); +window.setLevel = require('./_methods/setLevel'); +window.runTests = require('./_methods/runTests'); + const app = Vue.createApp({ template: `
@@ -19,18 +22,20 @@ const app = Vue.createApp({ setup() { const state = Vue.reactive({ organizations: ['osohq', 'acme'], - repositories: ['osohq/sample-apps', 'osohq/configs', 'osohq/nodejs-client'], + repositories: ['osohq/sample-apps', 'osohq/configs', 'osohq/nodejs-client', 'acme/website'], constraints: levels[0].constraints, results: [], facts: [], sessionId, level: 0, + currentLevel: null, par: 0, startTime: null, errors: {}, showNextLevelButton: false, name: '', - email: '' + email: '', + player: null }); Vue.provide('state', state); @@ -40,8 +45,9 @@ const app = Vue.createApp({ return state; }, async errorCaptured(err) { + const title = err?.response?.data?.message ?? err.message; vanillatoasts.create({ - title: err.message, + title, icon: '/images/failure.jpg', timeout: 5000, positionClass: 'bottomRight' diff --git a/src/leaderboard/leaderboard.html b/src/leaderboard/leaderboard.html index 6fe1cbd..0914a30 100644 --- a/src/leaderboard/leaderboard.html +++ b/src/leaderboard/leaderboard.html @@ -17,14 +17,16 @@ + - + + @@ -38,4 +40,6 @@ - \ No newline at end of file + + + diff --git a/src/level/level.html b/src/level/level.html index 2332243..2050510 100644 --- a/src/level/level.html +++ b/src/level/level.html @@ -1,9 +1,9 @@
-
+
Oso Policy
-
+
@@ -13,14 +13,7 @@ {{level?.description}}
-
Constraints
-
- - {{constraint.userId}} {{constraint.shouldFail ? 'cannot' : 'can'}} {{constraint.action}} {{constraint.resourceType}} {{constraint.resourceId}} -
-
- {{testsInProgress ? 'Tests running on Oso Cloud...' : 'Tests ran successfully on Oso Cloud'}} -
+
@@ -30,71 +23,82 @@
-
+
{{displayRoleFact(fact)}} -   Delete Fact +    + + Delete +
- Repository {{fact.resourceId}} has attribute {{fact.attribute}} set to {{fact.attributeValue}} -   Delete Fact + {{displayAttributeFact(fact)}} +    + + Delete +
No strokes yet! Add some below.
+
+ + + +
+ Are you sure you want to delete all the strokes you've taken so far? +
+
+ + Delete All + + + +
+
+
-
+
-
- Add Role -
- User - - has role - - - on resource - - - - - Add Role - +
Add Attribute
- Repository + @@ -111,7 +115,7 @@
+ +
+ +
diff --git a/src/level/level.js b/src/level/level.js index f0392db..a48c223 100644 --- a/src/level/level.js +++ b/src/level/level.js @@ -1,7 +1,9 @@ 'use strict'; const axios = require('axios'); -const levels = require('../../levels'); +const bson = require('bson'); +const runTests = require('../_methods/runTests'); +const setLevel = require('../_methods/setLevel'); const template = require('./level.html'); const vanillatoasts = require('vanillatoasts'); @@ -52,10 +54,11 @@ has_permission(actor: Actor, "delete", repo: Repository) if module.exports = app => app.component('level', { inject: ['state'], - props: ['onTest', 'onLoadFacts'], + props: ['status'], data: () => ({ userId: null, attributeFact: { + resourceType: null, resourceId: null, attribute: null, attributeValue: null @@ -64,14 +67,28 @@ module.exports = app => app.component('level', { resourceType: null, resourceId: null, role: null - } + }, + deleteInProgress: false, + showDeleteAllModal: false }), template, computed: { polarCode() { - return levels[this.state.level - 1]?.polarCode - ? levels[this.state.level - 1].polarCode + const code = this.state.currentLevel?.polarCode + ? this.state.currentLevel.polarCode : defaultPolarCode; + + return Prism.highlight(code, Prism.languages.ruby); + }, + allResources() { + let ret = ['Organization', 'Repository', 'User']; + if (this.state.currentLevel?.repositories?.length === 0) { + ret = ret.filter(type => type !== 'Organization'); + } + if (!this.state.currentLevel?.groups) { + ret = ret.filter(type => type !== 'User'); + } + return ret; }, allUsers() { return [...new Set(this.state.constraints.map(c => c.userId))]; @@ -87,30 +104,53 @@ module.exports = app => app.component('level', { 'reader', 'admin', 'maintainer', 'editor', 'member', 'superadmin' ]; }, - allResources() { - return ['Organization', 'Repository']; - }, allAttributes() { - return ['is_public', 'is_protected']; + if (this.attributeFact.resourceType === 'Organization') { + return ['has_default_role']; + } + if (this.attributeFact.resourceType === 'Repository') { + return ['is_public', 'is_protected']; + } + if (this.attributeFact.resourceType === 'User') { + return ['has_group']; + } + + return []; + }, + allAttributeValues() { + if (this.attributeFact.resourceType === 'Organization') { + return ['reader', 'admin', 'maintainer', 'editor']; + } + if (this.attributeFact.resourceType === 'Repository') { + return ['true', 'false']; + } + if (this.attributeFact.resourceType === 'User') { + return this.state.currentLevel?.groups ?? []; + } + + return []; }, resourceIds() { - if (this.roleFact.resourceType === 'Organization') { + if (this.attributeFact.resourceType === 'Organization') { return this.state.organizations; } - if (this.roleFact.resourceType === 'Repository') { + if (this.attributeFact.resourceType === 'Repository') { return this.state.repositories; } + if (this.attributeFact.resourceType === 'User') { + return [...new Set(this.state.constraints.map(c => c.userId))]; + } return []; }, level() { - return levels[this.state.level - 1]; + return this.state.currentLevel; }, testsInProgress() { return this.state.constraints.length > 0 && this.state.constraints.length !== this.state.results.length; }, parForLevel() { - const parForLevel = levels[this.state.level - 1].par; + const parForLevel = this.state.currentLevel?.par; const par = this.state.facts.length - parForLevel; return par < 0 ? par : `+${par}`; @@ -127,44 +167,9 @@ module.exports = app => app.component('level', { } }, methods: { - async addRoleFact() { - const { roleFact } = this; - if (!this.userId || !roleFact.role || ((!roleFact.resourceType || !roleFact.resourceId) && !this.isGlobalRole)) { - vanillatoasts.create({ - title: 'Missing a required field', - icon: '/images/failure.jpg', - timeout: 5000, - positionClass: 'bottomRight' - }); - return; - } - - const factType = 'role'; - await axios.put('/api/tell', { - sessionId: this.state.sessionId, - factType, - userId: this.userId, - role: this.roleFact.role, - resourceType: this.roleFact.resourceType, - resourceId: this.roleFact.resourceId - }).then(res => res.data); - this.state.facts.push({ - factType, - userId: this.userId, - ...this.roleFact - }); - this.roleFact = { - resourceType: null, - resourceId: null, - role: null - }; - this.userId = null; - - await this.onTest(); - }, async addAttributeFact() { const { attributeFact } = this; - if (!attributeFact.resourceId || !attributeFact.attribute || attributeFact.attributeValue == null) { + if (!attributeFact.resourceType || !attributeFact.resourceId || !attributeFact.attribute || attributeFact.attributeValue == null) { vanillatoasts.create({ title: 'Missing a required field', icon: '/images/failure.jpg', @@ -174,7 +179,7 @@ module.exports = app => app.component('level', { return; } - const resourceType = 'Repository'; + const resourceType = attributeFact.resourceType; const factType = 'attribute'; await axios.put('/api/tell', { sessionId: this.state.sessionId, @@ -184,6 +189,7 @@ module.exports = app => app.component('level', { ...this.attributeFact }).then(res => res.data); this.state.facts.push({ + _id: new bson.ObjectId(), factType, userId: this.userId, resourceType, @@ -197,21 +203,49 @@ module.exports = app => app.component('level', { this.userId = null; - await this.onTest(); + await runTests(this.state); }, displayRoleFact(fact) { if (fact.role === 'superadmin') { - return `User ${fact.userId} has role ${fact.role}`; + return `${fact.actorType || 'User'} ${fact.userId} has role ${fact.role}`; + } + return `${fact.actorType || 'User'} ${fact.userId} has role ${fact.role} on ${fact.resourceType} ${fact.resourceId}`; + }, + displayAttributeFact(fact) { + if (fact.attribute === 'has_group') { + return `User ${fact.resourceId} belongs to Group ${fact.attributeValue}`; } - return `User ${fact.userId} has role ${fact.role} on ${fact.resourceType} ${fact.resourceId}`; + const resourceType = fact.resourceType ?? 'Repository'; + return `${resourceType} ${fact.resourceId} has attribute ${fact.attribute} set to ${fact.attributeValue}`; }, async deleteFact(fact) { - await axios.put('/api/delete-fact', { - sessionId: this.state.sessionId, - ...fact - }).then(res => res.data); - this.state.facts = this.state.facts.filter(f => fact !== f); - await this.onTest(); + this.deleteInProgress = true; + try { + const params = { ...fact }; + delete params._id; + await axios.put('/api/delete-fact', { + sessionId: this.state.sessionId, + ...params + }).then(res => res.data); + this.state.facts = this.state.facts.filter(f => fact !== f); + + await runTests(this.state); + } finally { + this.deleteInProgress = false; + } + }, + async deleteAllFacts() { + this.deleteInProgress = true; + try { + await axios.put('/api/clear-context-facts', { + sessionId: this.state.sessionId + }).then(res => res.data); + this.state.facts = []; + + await runTests(this.state); + } finally { + this.deleteInProgress = false; + } }, displayImageForTestResult(index) { if (!this.state.results[index]) { @@ -224,23 +258,11 @@ module.exports = app => app.component('level', { sessionId: this.state.sessionId, level: this.state.level }).then(res => res.data); - this.state.level = player.levelsCompleted + 1; - this.state.par = player.par; - this.state.results = []; - this.state.showNextLevelButton = false; - const facts = [...this.state.facts]; - this.state.facts = []; - - await Promise.all(facts.map(fact => this.deleteFact(fact))); - if (this.state.level < levels.length + 1) { - this.state.constraints = levels[this.state.level - 1].constraints; - await this.onLoadFacts(); - await this.onTest(); - } + await setLevel(player.levelsCompleted + 1, false, this.state); + this.state.par = player.par; + this.state.player = player; + await runTests(this.state); } - }, - mounted() { - Prism.highlightElement(this.$refs.codeSnippet); } }); \ No newline at end of file diff --git a/src/splash-screen/splash-screen.html b/src/splash-screen/splash-screen.html index 0dde9cf..eb2ed4e 100644 --- a/src/splash-screen/splash-screen.html +++ b/src/splash-screen/splash-screen.html @@ -1,52 +1,110 @@ -
-
- -
- +
+
+
+
-

- {{state.errors.name}} -

-
-
- -
- +
+ Oso Golf
-

- {{errors.email}} -

- - Start Game - -
- - View Leaderboard - +
+
+ +
+
+

+ Welcome to the Oso Golf Tournament. ⛳️ + This is a 1-day event where we challenge the greatest minds to complete 9 holes of Oso Golf. + Oso Golf is a logic game, similar to “Regex Golf”, that is designed to teach you authorization principles by completing permissions with as few objects as possible. +

+

+ 🏆 Each person who completes the challenge will receive a limited-edition sticker pack. + We will award winners based on two criteria 1) Time to solve 2) Par (the number of facts you add to a particular challenge). +

+

+ Your first play-though will qualify you for prizes. Your subsequent play-throughs do not count for prizes. +

+ +
+ +
+ +
+

+ {{state.errors.name}} +

+
+
+ +
+ +
+

+ {{errors.email}} +

+
+
+ +
+ +
+

+ {{errors.password}} +

+
+ + Start Game + +
+ + View Leaderboard + +
+ +
+

+ Join our Community Slack and find us in the #oso-golf channel! +

+ +

The tournament begins on November 15, 2023 at 10AM ET and ends November 15 at 5PM ET.

+ + By signing up for the tournament via this form, you consent for Oso to communicate with you regarding your participation in The Oso Golf tournament. +
+
\ No newline at end of file diff --git a/src/splash-screen/splash-screen.js b/src/splash-screen/splash-screen.js index 405eaf5..efee64f 100644 --- a/src/splash-screen/splash-screen.js +++ b/src/splash-screen/splash-screen.js @@ -1,13 +1,19 @@ 'use strict'; const axios = require('axios'); +const levels = require('../../levels'); const template = require('./splash-screen.html'); module.exports = app => app.component('splash-screen', { inject: ['state'], - data: () => ({ email: '', name: '', errors: {} }), + data: () => ({ email: '', name: '', password: '', errors: {} }), props: ['onStartGame'], template, + computed: { + hasPassword() { + return HAS_PASSWORD; + } + }, methods: { async startGame() { if (this.state.level !== 0) { @@ -20,6 +26,9 @@ module.exports = app => app.component('splash-screen', { if (!this.name) { this.errors.name = 'Name is required'; } + if (!this.password && this.hasPassword) { + this.errors.password = 'Password is required'; + } if (Object.keys(this.errors).length > 0) { return; } @@ -27,10 +36,12 @@ module.exports = app => app.component('splash-screen', { const { player } = await axios.post('/api/start-game', { sessionId: this.state.sessionId, name: this.name, - email: this.email + email: this.email, + password: this.password }).then(res => res.data); this.state.level = 1; + this.state.currentLevel = levels[0]; this.state.currentTime = new Date(); this.state.startTime = new Date(player.startTime); await this.onStartGame();
Rank NameLevels CompletedHoles Completed Par Time
#{{index + 1}} {{player.name}} {{player.levelsCompleted}} {{par(player)}}