diff --git a/.eslintrc.js b/.eslintrc.js
index b9b5429..1d3cb3d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -27,5 +27,6 @@ module.exports = {
'no-var': 'error',
'no-plusplus': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto', semi: false }],
+ 'import/prefer-default-export': 'off',
},
}
diff --git a/README.md b/README.md
index c0cc3f0..7f649e2 100644
--- a/README.md
+++ b/README.md
@@ -32,10 +32,14 @@ Run Cypress locally, using the current development containers:
Run Cypress in container, create brand-new containers along with it:
- docker compose -f docker-compose.testing.yml run cypress
+ ./docker-scripts.sh test
+
+ # or with building
+
+ ./docker-scripts.sh test --build
# tearing off the containers
- docker compose -f docker-compose.testing.yml down
+ ./docker-scripts.sh down
### Deployment
diff --git a/app/Dockerfile b/app/Dockerfile
index eafe05a..df72388 100644
--- a/app/Dockerfile
+++ b/app/Dockerfile
@@ -5,6 +5,7 @@ COPY package.json yarn.lock ./
COPY app/package.json ./app/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
+COPY packages/constants/package.json ./packages/constants/package.json
RUN yarn install --frozen-lockfile
# Dev stage
@@ -16,6 +17,7 @@ CMD [ "yarn", "dev:docker", "--host", "0.0.0.0" ]
FROM base as build
COPY packages/file-graph ./packages/file-graph
COPY packages/utils ./packages/utils
+COPY packages/constants ./packages/constants
COPY app ./app
RUN yarn build
diff --git a/app/index.html b/app/index.html
index 0ff4559..8388c4b 100644
--- a/app/index.html
+++ b/app/index.html
@@ -5,12 +5,6 @@
Vite + Vue
-
diff --git a/app/package.json b/app/package.json
index 9746862..7152e58 100644
--- a/app/package.json
+++ b/app/package.json
@@ -11,15 +11,17 @@
"preview": "vite preview"
},
"dependencies": {
- "@linux-odyssey/file-graph": "*",
- "@linux-odyssey/utils": "*",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
+ "@linux-odyssey/file-graph": "*",
+ "@linux-odyssey/utils": "*",
+ "@linux-odyssey/constants": "*",
"axios": "^1.4.0",
"socket.io-client": "^4.7.1",
+ "validator": "^13.11.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"xterm": "^5.2.1",
diff --git a/app/src/components/AuthForm.vue b/app/src/components/AuthForm.vue
index d8c2088..7438907 100644
--- a/app/src/components/AuthForm.vue
+++ b/app/src/components/AuthForm.vue
@@ -128,6 +128,7 @@ const handleChange = () => {
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
:placeholder="type === 'login' ? 'Email / Username' : 'Username'"
v-model="username"
+ required
@input="handleChange()"
autocomplete="username"
/>
@@ -147,6 +148,7 @@ const handleChange = () => {
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
placeholder="Email"
v-model="email"
+ required
@input="handleChange()"
autocomplete="email"
/>
@@ -158,6 +160,7 @@ const handleChange = () => {
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
placeholder="Password"
v-model="password"
+ required
@input="handleChange()"
/>
diff --git a/app/src/components/ControlPalette.vue b/app/src/components/ControlPalette.vue
index bc14d7d..d8ad208 100644
--- a/app/src/components/ControlPalette.vue
+++ b/app/src/components/ControlPalette.vue
@@ -30,13 +30,9 @@
diff --git a/app/src/components/TerminalPart.vue b/app/src/components/TerminalPart.vue
index 3453db1..1bf86d8 100644
--- a/app/src/components/TerminalPart.vue
+++ b/app/src/components/TerminalPart.vue
@@ -10,12 +10,12 @@
diff --git a/app/src/utils/auth.js b/app/src/utils/auth.js
index 8e6a478..4094066 100644
--- a/app/src/utils/auth.js
+++ b/app/src/utils/auth.js
@@ -1,31 +1,83 @@
import api from './api'
+import {
+ TooManyRequestsError,
+ UnauthorizedError,
+ ValidationError,
+} from './errors'
+
export async function isLoggedIn() {
const res = await api.get('/auth/check-session')
const { loggedIn } = res.data
return loggedIn
}
-export const login = async (username, password) => {
- try {
- await api.post('/auth/login', { username, password })
- return true
- } catch (err) {
- if (err.response?.status === 401) {
- return false
- }
- throw err
+function handleRequestError(err) {
+ if (!err.response) throw err // Not an axios error
+ const { status } = err.response
+ if (status === 429) {
+ throw new TooManyRequestsError(err.response.data.message)
}
+ if (status === 401) {
+ throw new UnauthorizedError(err.response.data.message)
+ }
+ if (status === 400) {
+ const { errors } = err.response.data
+ if (Array.isArray(errors) && errors.length > 0) {
+ const { path, msg } = errors[0]
+ switch (path) {
+ case 'username':
+ throw new ValidationError('Username is already taken')
+ case 'email':
+ throw new ValidationError('Email is already taken')
+ case 'password':
+ throw new ValidationError('Password is too weak')
+ default:
+ throw new ValidationError(msg)
+ }
+ }
+ throw new ValidationError(err.response.data.message)
+ }
+ throw err
+}
+
+export const login = (username, password) => {
+ return api
+ .post('/auth/login', { username, password })
+ .then(() => true)
+ .catch(handleRequestError)
}
export const register = (username, email, password) => {
- return api.post('/auth/register', {
- username,
- password,
- email,
- })
+ return api
+ .post('/auth/register', {
+ username,
+ password,
+ email,
+ })
+ .then(() => true)
+ .catch(handleRequestError)
+}
+
+export const checkUsername = async (username) => {
+ return api
+ .get('/auth/check-username', {
+ params: { username },
+ })
+ .then(() => true)
+ .catch(handleRequestError)
+}
+
+export const chooseUsername = async (username) => {
+ return api
+ .post('/auth/register-from-session', { username })
+ .then(() => true)
+ .catch(handleRequestError)
}
export const logout = () => {
- return api.post('/auth/logout')
+ return api
+ .post('/auth/logout')
+ .then(() => true)
+ .catch(handleRequestError)
}
diff --git a/app/src/utils/errors.js b/app/src/utils/errors.js
new file mode 100644
index 0000000..2fe0cbb
--- /dev/null
+++ b/app/src/utils/errors.js
@@ -0,0 +1,27 @@
+/* eslint-disable max-classes-per-file */
+export class TooManyRequestsError extends Error {
+ constructor(message) {
+ super(message)
+ this.name = 'TooManyRequestsError'
+ }
+}
+
+export class ValidationError extends Error {
+ constructor(message) {
+ super(message)
+ this.name = 'ValidationError'
+ }
+}
+
+export class UnauthorizedError extends Error {
+ constructor(message) {
+ super(message)
+ this.name = 'UnauthorizedError'
+ }
+}
+
+export default {
+ TooManyRequestsError,
+ ValidationError,
+ UnauthorizedError,
+}
diff --git a/app/src/utils/session.js b/app/src/utils/session.js
index e659bff..bbf0f3b 100644
--- a/app/src/utils/session.js
+++ b/app/src/utils/session.js
@@ -1,6 +1,8 @@
import { ref } from 'vue'
import { FileGraph } from '@linux-odyssey/file-graph'
import api from './api'
+import Socket from './socket'
+import SocketTerminal from './terminal'
class SessionManager {
constructor(questId) {
@@ -16,17 +18,44 @@ class SessionManager {
)
this.pwd = ref('')
this.status = ref('inactive')
+ this.socket = new Socket()
+ this.term = new SocketTerminal(40, 80)
+ }
+
+ init() {
+ this.socket.on('terminal', (data) => {
+ this.term.write(data)
+ })
+ this.term.onData((data) => {
+ this.socket.emit('terminal', data)
+ })
+ this.socket.on('graph', (event) => {
+ this.handleGraphUpdate(event)
+ })
+ this.socket.on('hints', (event) => {
+ this.handleHintUpdate(event)
+ })
+ this.socket.on('tasks', (tasks) => {
+ this.setTasks(tasks)
+ })
+ this.socket.on('status', (event) => {
+ this.handleStatusUpdate(event)
+ })
+ return this.getActiveSession()
}
getSession() {
return this.session.value
}
- setSession(session) {
+ async setSession(session) {
this.session.value = session
this.graph.value = new FileGraph(session.graph)
this.hints.value = session.hints
this.status.value = session.status
+ this.term.reset()
+ await this.socket.connect(session)
+ this.term.focus()
}
handleGraphUpdate(event) {
@@ -58,33 +87,18 @@ class SessionManager {
this.setSession(data)
}
- async getSessionList() {
- const res = await api.get('/sessions', {
- params: { quest_id: this.questId },
+ async getActiveSession() {
+ const res = await api.post('/sessions/active', {
+ quest_id: this.questId,
})
- const { data } = res
- return data
- }
-
- async lastSession() {
- const sessions = await this.getSessionList()
- if (sessions.length === 0) {
- return
+ const session = res.data
+ console.log(session)
+ try {
+ await this.setSession(session)
+ } catch (err) {
+ console.log(err)
+ this.createSession().catch(console.error)
}
- const { _id: sessionId } = sessions[sessions.length - 1]
- const { data: session } = await api.get(`/sessions/${sessionId}`)
- console.log(
- `Last Session ID: ${session._id}, Quest: ${session.quest}, Created At: ${session.createdAt}`
- )
- this.setSession(session)
- }
-
- async lastOrCreate() {
- await this.lastSession()
- if (!this.session.value) {
- await this.createSession()
- }
- console.log(this.session.value)
}
}
diff --git a/app/src/utils/socket.js b/app/src/utils/socket.js
index 69050ca..54fbb49 100644
--- a/app/src/utils/socket.js
+++ b/app/src/utils/socket.js
@@ -7,22 +7,28 @@ class SocketWrapper {
}
connect(session) {
- if (this.socket) {
- this.socket.off()
- this.socket.disconnect()
- }
+ return new Promise((resolve, reject) => {
+ if (this.socket) {
+ this.socket.off()
+ this.socket.disconnect()
+ }
- this.socket = io('', {
- query: {
- session_id: session._id,
- },
+ this.socket = io('', {
+ query: {
+ session_id: session._id,
+ },
+ })
+
+ this.socket.on('connect', () => {
+ console.log('Socket connected to session', session._id)
+ })
+
+ this.socket.once('terminal', () => {
+ resolve()
+ })
+ this.socket.once('error', reject)
+ this.bindListeners()
})
-
- this.socket.on('connect', () => {
- console.log('Socket connected to session', session._id)
- })
-
- this.bindListeners()
}
on(event, callback) {
@@ -32,6 +38,12 @@ class SocketWrapper {
}
}
+ once(event, callback) {
+ if (this.socket) {
+ this.socket.once(event, callback)
+ }
+ }
+
bindListeners() {
this.listeners.forEach(({ event, callback }) => {
this.socket.on(event, callback)
@@ -47,4 +59,4 @@ class SocketWrapper {
}
}
-export default new SocketWrapper()
+export default SocketWrapper
diff --git a/app/src/utils/terminal.js b/app/src/utils/terminal.js
index be6c381..528dd8d 100644
--- a/app/src/utils/terminal.js
+++ b/app/src/utils/terminal.js
@@ -1,6 +1,5 @@
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
-import socket from './socket'
import 'xterm/css/xterm.css'
class SocketTerminal {
@@ -24,13 +23,18 @@ class SocketTerminal {
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
this.fitAddon.fit()
- this.socket = socket
- this.socket.on('terminal', (message) => {
- this.term.write(message)
- })
- this.term.onData((key) => {
- this.socket.emit('terminal', key)
- })
+ }
+
+ write(message) {
+ this.term.write(message)
+ }
+
+ onData(callback) {
+ this.term.onData(callback)
+ }
+
+ focus() {
+ this.term.focus()
}
resizeScreen() {
@@ -43,9 +47,9 @@ class SocketTerminal {
this.resizeScreen()
}
- clear() {
- this.term.clear()
+ reset() {
+ this.term.reset()
}
}
-export default new SocketTerminal(40, 100)
+export default SocketTerminal
diff --git a/app/src/views/ChooseUsernamePage.vue b/app/src/views/ChooseUsernamePage.vue
index decc2b4..082aae9 100644
--- a/app/src/views/ChooseUsernamePage.vue
+++ b/app/src/views/ChooseUsernamePage.vue
@@ -2,44 +2,46 @@
import { isValidUsername } from '@linux-odyssey/utils'
import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue'
+import { TooManyRequestsError, ValidationError } from '../utils/errors'
+import { checkUsername, chooseUsername } from '../utils/auth'
-import api from '../utils/api'
-
-async function checkUsername({ username, error }) {
- if (username && !isValidUsername(username)) {
- error('Invalid username.')
- return
- }
- if (username.length > 3) {
- try {
- const { data } = await api.get('/auth/check-username', {
- params: { username },
- })
-
- const { available } = data
- if (!available) {
- error('Username is already taken.')
- }
- } catch {
+async function check({ username, error }) {
+ if (username) {
+ if (!isValidUsername(username)) {
error('Invalid username.')
+ return
+ }
+ try {
+ await checkUsername(username)
+ } catch (err) {
+ if (err instanceof TooManyRequestsError) {
+ error('Too many requests. Try again in 2 minutes.')
+ return
+ }
+ if (err instanceof ValidationError) {
+ error(err.message)
+ return
+ }
+ error('Something went wrong. Please try again later.')
}
}
}
-function handleSubmit({ username, success, error }) {
- api
- .post('/auth/register-from-session', {
- username,
- })
- .then(success)
- .catch((err) => {
- if (err.response?.status === 409) {
- error('Username is already taken.')
- } else {
- console.error(err.message)
- error('Something went wrong.')
- }
- })
+async function handleSubmit({ username, success, error }) {
+ try {
+ await chooseUsername(username)
+ success()
+ } catch (err) {
+ if (err instanceof TooManyRequestsError) {
+ error('Too many requests. Try again in 2 minutes.')
+ return
+ }
+ if (err instanceof ValidationError) {
+ error(err.message)
+ return
+ }
+ error(err.message)
+ }
}
@@ -51,7 +53,7 @@ function handleSubmit({ username, success, error }) {
diff --git a/app/src/views/GamePage.vue b/app/src/views/GamePage.vue
index 6312731..075472b 100644
--- a/app/src/views/GamePage.vue
+++ b/app/src/views/GamePage.vue
@@ -1,6 +1,5 @@
diff --git a/app/src/views/LoginPage.vue b/app/src/views/LoginPage.vue
index e9e515a..0c12075 100644
--- a/app/src/views/LoginPage.vue
+++ b/app/src/views/LoginPage.vue
@@ -2,15 +2,32 @@
import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue'
+import {
+ TooManyRequestsError,
+ UnauthorizedError,
+ ValidationError,
+} from '../utils/errors'
import { login } from '../utils/auth'
const handleLogin = async ({ username, password, success, error }) => {
try {
const isSuccess = await login(username, password)
if (isSuccess) success()
- else error('Wrong username or password.')
} catch (err) {
+ if (err instanceof TooManyRequestsError) {
+ error('Too many requests. Try again in 2 minutes.')
+ return
+ }
+ if (err instanceof UnauthorizedError) {
+ error('Wrong username or password.')
+ return
+ }
+ if (err instanceof ValidationError) {
+ error('Invalid username or password.')
+ return
+ }
console.error(err)
+ error('Something went wrong.')
}
}
diff --git a/app/src/views/RegisterPage.vue b/app/src/views/RegisterPage.vue
index f12b16d..5e691a7 100644
--- a/app/src/views/RegisterPage.vue
+++ b/app/src/views/RegisterPage.vue
@@ -1,55 +1,62 @@