Compare commits

...
Author SHA1 Message Date
Chen, Tai-Lin aebe587ad3 update 2024-04-30 01:40:42 +08:00
Chen, Tai-Lin c96ee83fa3 update quest\permission 2024-04-08 22:39:23 +08:00
Lin, Hung-Shin bf44cce580 Ts project refs (#108)
# TypeScript Project References
Change shared packages to use [project
reference](https://www.typescriptlang.org/docs/handbook/project-references.html).
Project references allow TypeScript to automatically build dependency
packages. By this change, we can speed up and simplify the building
process of `server` and `app`. Vite doesn't support project references
yet, so we need to run `yarn workspace app build:refs` before starting
Vite, or you can simply run `yarn build` to build everything.

# ts-node and SWC

Inspired by @chreamy #106 , I changed to use ts-node as dev-server
runner, and use [swc](https://swc.rs/) for loader to speed up, which
remove the needs of having a separate tsc daemon.
2024-04-06 00:10:28 +08:00
wancat 7c88a60d94 fix: change test to typescript 2024-04-06 00:03:27 +08:00
wancat 1f55ae09d6 fix: build app docker image 2024-04-05 23:59:26 +08:00
wancat e2b5bb3b70 fix: terminal type 2024-04-05 23:04:51 +08:00
wancat ba69685ea2 fix: analytics build image 2024-04-05 22:59:22 +08:00
wancat 84e65c9ef6 feat: enable swc and ts-node in server dev 2024-04-05 22:42:02 +08:00
wancat f466a7a793 feat: use babel to replace ts-jest (improve performance) 2024-04-05 22:42:02 +08:00
wancat d74a549b89 fix: docker 2024-04-05 22:42:02 +08:00
wancat 33a1a110e8 feat: use project references to build dependencies efficiently 2024-04-05 22:42:02 +08:00
Lin, Hung-Shin d4c9d4577c 117 fix oauth (#127)
* change session cookie from 'strict' to 'lax' to fix "forbidden" issue
* add scope settings to Google

some additional improvements:
* update ESLint settings to allow unused parameters before used ones

some related discussions:
* https://github.com/auth0/passport-auth0/issues/70
*
https://stackoverflow.com/questions/59221899/add-samesite-attribute-to-passport-js-session-cookie
2024-04-05 22:24:16 +08:00
wancat 27b6096633 fix: eslint unused error 2024-04-05 22:22:44 +08:00
Annie Huang a8a367f477 delete testing id 2024-04-05 22:19:38 +08:00
wancat 78b92ee8ce fix: session store type 2024-04-05 22:19:38 +08:00
Annie Huang 14fb870838 fix hint display problems 2024-04-05 22:19:38 +08:00
wancat 455fde41ab fix: OAuth flow by setting scope and cookie to 'lax' 2024-04-05 17:56:09 +08:00
dependabot[bot] ff1cdc9a95 build(deps-dev): bump vite from 4.5.2 to 4.5.3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 15:48:55 +08:00
wancat df140c64f8 fix: Update eslint rule for unused variables 2024-04-01 11:53:02 +08:00
Lin, Hung-Shin 4efd670ed6 build(deps): bump follow-redirects from 1.15.5 to 1.15.6 (#114)
Bumps
[follow-redirects](https://github.com/follow-redirects/follow-redirects)
from 1.15.5 to 1.15.6.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/35a517c5861d79dc8bff7db8626013d20b711b06"><code>35a517c</code></a>
Release version 1.15.6 of the npm package.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/c4f847f85176991f95ab9c88af63b1294de8649b"><code>c4f847f</code></a>
Drop Proxy-Authorization across hosts.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/8526b4a1b2ab3a2e4044299377df623a661caa76"><code>8526b4a</code></a>
Use GitHub for disclosure.</li>
<li>See full diff in <a
href="https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.15.5&new-version=1.15.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-03-28 21:45:49 +08:00
Lin, Hung-Shin 4814626a71 build(deps): bump express from 4.18.2 to 4.19.2 (#115)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [express](https://github.com/expressjs/express) from 4.18.2 to
4.19.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/releases">express's
releases</a>.</em></p>
<blockquote>
<h2>4.19.2</h2>
<h2>What's Changed</h2>
<ul>
<li><a
href="https://github.com/expressjs/express/commit/0b746953c4bd8e377123527db11f9cd866e39f94">Improved
fix for open redirect allow list bypass</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.19.1...4.19.2">https://github.com/expressjs/express/compare/4.19.1...4.19.2</a></p>
<h2>4.19.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix ci after location patch by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5552">expressjs/express#5552</a></li>
<li>fixed un-edited version in history.md for 4.19.0 by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5556">expressjs/express#5556</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.19.0...4.19.1">https://github.com/expressjs/express/compare/4.19.0...4.19.1</a></p>
<h2>4.19.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix typo in release date by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5527">expressjs/express#5527</a></li>
<li>docs: nominating <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> to be
project captian by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5511">expressjs/express#5511</a></li>
<li>docs: loosen TC activity rules by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5510">expressjs/express#5510</a></li>
<li>Add note on how to update docs for new release by <a
href="https://github.com/crandmck"><code>@​crandmck</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5541">expressjs/express#5541</a></li>
<li><a
href="https://redirect.github.com/expressjs/express/pull/5551/commits/660ccf5fa33dd0baab069e5c8ddd9ffe7d8bbff1">Prevent
open redirect allow list bypass due to encodeurl</a></li>
<li>Release 4.19.0 by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5551">expressjs/express#5551</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/crandmck"><code>@​crandmck</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5541">expressjs/express#5541</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.18.3...4.19.0">https://github.com/expressjs/express/compare/4.18.3...4.19.0</a></p>
<h2>4.18.3</h2>
<h2>Main Changes</h2>
<ul>
<li>Fix routing requests without method</li>
<li>deps: body-parser@1.20.2
<ul>
<li>Fix strict json error message on Node.js 19+</li>
<li>deps: content-type@~1.0.5</li>
<li>deps: raw-body@2.5.2</li>
</ul>
</li>
</ul>
<h2>Other Changes</h2>
<ul>
<li>Use https: protocol instead of deprecated git: protocol by <a
href="https://github.com/vcsjones"><code>@​vcsjones</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5032">expressjs/express#5032</a></li>
<li>build: Node.js@16.18 and Node.js@18.12 by <a
href="https://github.com/abenhamdine"><code>@​abenhamdine</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5034">expressjs/express#5034</a></li>
<li>ci: update actions/checkout to v3 by <a
href="https://github.com/armujahid"><code>@​armujahid</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5027">expressjs/express#5027</a></li>
<li>test: remove unused function arguments in params by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5124">expressjs/express#5124</a></li>
<li>Remove unused originalIndex from acceptParams by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5119">expressjs/express#5119</a></li>
<li>Fixed typos by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5117">expressjs/express#5117</a></li>
<li>examples: remove unused params by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5113">expressjs/express#5113</a></li>
<li>fix: parameter str is not described in JSDoc by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5130">expressjs/express#5130</a></li>
<li>fix: typos in History.md by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5131">expressjs/express#5131</a></li>
<li>build : add Node.js@19.7 by <a
href="https://github.com/abenhamdine"><code>@​abenhamdine</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5028">expressjs/express#5028</a></li>
<li>test: remove unused function arguments in params by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5137">expressjs/express#5137</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/blob/master/History.md">express's
changelog</a>.</em></p>
<blockquote>
<h1>4.19.2 / 2024-03-25</h1>
<ul>
<li>Improved fix for open redirect allow list bypass</li>
</ul>
<h1>4.19.1 / 2024-03-20</h1>
<ul>
<li>Allow passing non-strings to res.location with new encoding handling
checks</li>
</ul>
<h1>4.19.0 / 2024-03-20</h1>
<ul>
<li>Prevent open redirect allow list bypass due to encodeurl</li>
<li>deps: cookie@0.6.0</li>
</ul>
<h1>4.18.3 / 2024-02-29</h1>
<ul>
<li>Fix routing requests without method</li>
<li>deps: body-parser@1.20.2
<ul>
<li>Fix strict json error message on Node.js 19+</li>
<li>deps: content-type@~1.0.5</li>
<li>deps: raw-body@2.5.2</li>
</ul>
</li>
<li>deps: cookie@0.6.0
<ul>
<li>Add <code>partitioned</code> option</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/expressjs/express/commit/04bc62787be974874bc1467b23606c36bc9779ba"><code>04bc627</code></a>
4.19.2</li>
<li><a
href="https://github.com/expressjs/express/commit/da4d763ff6ba9df6dbd8f1f0b1d05412dda934d5"><code>da4d763</code></a>
Improved fix for open redirect allow list bypass</li>
<li><a
href="https://github.com/expressjs/express/commit/4f0f6cc67d531431c096ea006c2191b92931bbc3"><code>4f0f6cc</code></a>
4.19.1</li>
<li><a
href="https://github.com/expressjs/express/commit/a003cfab034fbadb1c78ae337ee8ab389adda217"><code>a003cfa</code></a>
Allow passing non-strings to res.location with new encoding handling
checks f...</li>
<li><a
href="https://github.com/expressjs/express/commit/a1fa90fcea7d8e844e1c9938ad095d62669c3abd"><code>a1fa90f</code></a>
fixed un-edited version in history.md for 4.19.0</li>
<li><a
href="https://github.com/expressjs/express/commit/11f2b1db227fd42c2508c427032c1ec671b306be"><code>11f2b1d</code></a>
build: fix build due to inconsistent supertest behavior in older
versions</li>
<li><a
href="https://github.com/expressjs/express/commit/084e36506a18774f85206a65d8da04dc1107fc1b"><code>084e365</code></a>
4.19.0</li>
<li><a
href="https://github.com/expressjs/express/commit/0867302ddbde0e9463d0564fea5861feb708c2dd"><code>0867302</code></a>
Prevent open redirect allow list bypass due to encodeurl</li>
<li><a
href="https://github.com/expressjs/express/commit/567c9c665d0de4c344b8e160146050770233783c"><code>567c9c6</code></a>
Add note on how to update docs for new release (<a
href="https://redirect.github.com/expressjs/express/issues/5541">#5541</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/69a4cf2819c4449ec6ea45649691fb43a528d5d1"><code>69a4cf2</code></a>
deps: cookie@0.6.0</li>
<li>Additional commits viewable in <a
href="https://github.com/expressjs/express/compare/4.18.2...4.19.2">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~wesleytodd">wesleytodd</a>, a new releaser
for express since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=express&package-manager=npm_and_yarn&previous-version=4.18.2&new-version=4.19.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-03-28 21:45:13 +08:00
dependabot[bot] 1179faf176 build(deps): bump express from 4.18.2 to 4.19.2
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-28 13:44:47 +00:00
Lin, Hung-Shin a9acf54139 Chris - convert analytics to TS (#106) 2024-03-28 21:43:17 +08:00
wancat b64f49915f fix: restore server changes 2024-03-28 21:39:26 +08:00
wancat ef279ba00e fix: building and production starting scripts 2024-03-28 21:35:07 +08:00
wancat eb106c020a fix: missing express type annotation 2024-03-28 21:28:18 +08:00
Lin, Hung-Shin 432084cc66 Delete package-lock.json 2024-03-28 21:23:30 +08:00
Lin, Hung-Shin 2fb1315c5a Feat typescript frontend (#107)
There is still tons of errors in e2e. Fix it after merging.
2024-03-28 21:22:42 +08:00
wancat 75f5407dde fix: fix overwritten new response model 2024-03-28 21:18:00 +08:00
wancat ebd713200c refactor: update socket types 2024-03-28 21:06:09 +08:00
wancat bb2a1a145f refactor: move interfaces to types.ts 2024-03-28 20:55:40 +08:00
Chen, Tai-Lin 7f33100132 update 2024-03-25 15:48:37 +08:00
Chen, Tai-Lin 59a0865157 reverse change 2024-03-25 14:53:44 +08:00
chreamy c843a03fc8 feat: analytics UI 2024-03-25 00:08:14 -05:00
dependabot[bot] 182cc7d1bb build(deps): bump follow-redirects from 1.15.5 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 23:56:54 +00:00
chreamy daa3f11b2e test build 2024-03-11 22:16:17 -05:00
chreamy a4c121dc94 test build 2024-03-11 22:14:30 -05:00
chreamy 5adaa47c5c test build 2024-03-11 22:10:32 -05:00
chreamy 3345fb3b3f test build 2024-03-11 22:06:43 -05:00
chreamy 0313671ff5 test build 2024-03-11 21:37:38 -05:00
chreamy e31511fdc1 fix: file imports 2024-03-11 21:33:01 -05:00
chreamy 685be8c1d2 fix: file imports 2024-03-11 21:27:15 -05:00
chreamy 0d2e62be24 fix: file imports 2024-03-11 21:18:44 -05:00
chreamy 962db4e8a4 fix: file imports 2024-03-11 21:14:45 -05:00
chreamy 52d8e86043 fix: file imports 2024-03-11 21:05:02 -05:00
Christopher LiuandLin, Hung-Shin 51fb25aa50 Update packages/analytics/package.json
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:51:49 -05:00
Christopher LiuandLin, Hung-Shin d2d8374926 Update packages/analytics/package.json
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:51:40 -05:00
Christopher LiuandLin, Hung-Shin 4570162e7b Update packages/analytics/nodemon.json
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:51:27 -05:00
Christopher LiuandLin, Hung-Shin 3194c03f48 Update packages/analytics/src/interface.ts
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:50:49 -05:00
Chen, Tai-Lin c4fd92e289 fix types issue 2024-03-11 21:04:32 +08:00
chreamy ce538a049c modified: packages/analytics/src/models/sessions.ts
modified:   packages/analytics/src/models/users.ts
2024-03-05 01:51:30 -06:00
chreamy 36932e1eb1 This part is somehow influencing the checks, please review 2024-03-05 01:43:40 -06:00
chreamy ca3d9b4236 Merge branch 'ts-analytics' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-03-05 01:38:58 -06:00
chreamy 76e6cda0cb revert changes 2024-03-05 01:38:38 -06:00
Christopher Liu 3dee3d0373 Merge branch 'main' into ts-analytics 2024-03-05 02:32:48 -05:00
chreamy 218a25daee feat: user sessions history & fix ts <any> 2024-03-05 01:27:53 -06:00
chreamy 94ae2d7df4 Merge branch 'ts-analytics' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-03-05 01:27:10 -06:00
chreamy a71411d5bf Feat: User Session History 2024-03-05 00:26:22 -06:00
Chen, Tai-Lin 807974d727 fix types issue 2024-03-05 00:27:08 +08:00
Chen, Tai-Lin ec63bba0fe fix using any type 2024-03-04 23:38:18 +08:00
Chen, Tai-Lin ddaefca5fb fix using Ref<any> type 2024-03-04 23:38:18 +08:00
Chen, Tai-Lin ba43b4c5e8 fix wrong yarn package location 2024-03-04 23:38:18 +08:00
Chen, Tai-Lin f52017ef9f update .vue 2024-03-04 23:37:46 +08:00
Chen, Tai-Lin af9b443865 update socket.ts 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 4739849549 update views 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 63a57aa2cf update components 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin ead321cd62 update config 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 516e59f855 update utils 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 4ddbb5aede update app 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin d924ccaaa9 update store 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin c324bfcfee update utils 2024-03-04 23:34:06 +08:00
wancat 817cf8c40e hotfix: redirect to map after login 2024-03-01 23:49:58 +08:00
wancat 4e8a56f127 hotfix: analytics build 2024-03-01 23:21:09 +08:00
wancat 6aed5789a5 Merge remote-tracking branch 'origin/main' into feat-landing-page 2024-03-01 23:13:15 +08:00
Lin, Hung-Shin d737347d0b UI dialogue (#110) 2024-03-01 23:09:39 +08:00
wancat 5041ec9dc6 Contact 2024-03-01 22:48:21 +08:00
wancat c2c792f10c video and about 2024-03-01 22:27:00 +08:00
wancat 85b34abc7e responsive content block 2024-03-01 21:59:12 +08:00
wancat 2bfd2cc06d setup main block 2024-03-01 21:49:31 +08:00
Annie Huang 5dbdadbb27 change text block color 2024-03-01 21:28:51 +08:00
wancat 1e2a43db50 items-center the header 2024-03-01 21:24:21 +08:00
Annie Huang ba7409b7a8 - add icons
- change first page to landing-page
- add text background
- modify text arrangement
2024-03-01 20:36:36 +08:00
Annie Huang f9decee80b fix: scroll bug
modify contact layout
2024-03-01 16:17:13 +08:00
Annie Huang bef134871d - edit position
- add group info
- add opinions
- add start button
2024-02-29 23:06:47 +08:00
Annie Huang 46e27c7456 - add contacts
- change contents
- fix broken page
2024-02-29 15:23:07 +08:00
Annie Huang 9146c433b3 add youtube embedded 2024-02-29 15:23:07 +08:00
Annie Huang 00e421861f Landing page
- header
- jump to section
2024-02-29 15:23:07 +08:00
Annie Huang cdd76f7d0c add new page 2024-02-29 15:23:07 +08:00
wancat a389d2752d disable wait pending in e2e tests 2024-02-29 10:54:53 +08:00
wancat 039da53bbe feat: send status to the frontend 2024-02-29 10:54:53 +08:00
wancat 622d64dd45 fix: overflow in hint 2024-02-29 10:54:53 +08:00
wancat 90e661f77b Display dialogue in hints. Adjust Game page layout 2024-02-29 10:54:53 +08:00
wancat 88ee8f2cc3 feat: send response to frontend 2024-02-29 10:54:53 +08:00
wancat bb4a11b20c fix: use swc to optimize ts-node speed 2024-02-29 00:27:06 +08:00
wancat 031d7a8ceb fix: analytics dockerfile 2024-02-29 00:10:11 +08:00
wancat 48dbac7907 fix: import path and dev server using tsc and nodemon 2024-02-29 00:04:09 +08:00
Christopher Liu ea33434bea Update config.ts 2024-02-28 23:38:33 +08:00
chreamy 923de901c4 convert analytics to TS 2024-02-28 23:38:33 +08:00
Lin, Hung-Shin fb69cb4e10 Feat automation test - Transfer to Typescript (#104)
- Update Readme for new build-up commands (still not the latest version)
- Add Cpress optional OAuth activation
- Transfer testing files into Typescript
2024-02-28 23:29:24 +08:00
wancat 818e4e95e4 fix: compile cypress to commonjs to fix docker issue 2024-02-28 23:20:29 +08:00
Annie Huang 2aa1c84e73 delete port setting of db in testing yml 2024-02-27 22:23:10 +08:00
Annie Huang 1b52349f03 delete redundant code 2024-02-27 14:26:37 +08:00
wancat a9bc2cc127 fix: disable sourceMap in cypress tsconfig 2024-02-27 12:54:30 +08:00
wancat 7c08d21142 fix: downgrade yarn to v1 in docker 2024-02-27 12:02:22 +08:00
wancat b059cca7b2 fix jest types by import @jest/globals 2024-02-27 11:45:10 +08:00
Annie Huang 9e108e397b extend from global settings and modify cypress config 2024-02-27 11:41:37 +08:00
chreamy dc972173a4 Merge branch 'ts-analytics' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-02-26 20:43:25 -06:00
chreamy 56d07f14b4 Merge branch 'main' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-02-26 20:43:16 -06:00
Christopher Liu b47bb62831 Update config.ts 2024-02-26 20:33:54 -06:00
chreamy ece0ec7c41 convert analytics to TS 2024-02-26 20:23:03 -06:00
Annie Huang 004d232a84 try transfering config to ts(but import is failing) 2024-02-27 00:12:07 +08:00
Annie Huang 0a730e4eef exclude cypress config and e2e.js 2024-02-26 23:50:51 +08:00
Annie Huang 5091f8f24f revise jest tsconfig 2024-02-26 23:44:53 +08:00
Annie Huang 87060aa3ef try: fix collison excluding and including 2024-02-23 18:19:33 +08:00
Annie Huang 670a5b2e87 try: fix collison by settings 2024-02-23 17:51:31 +08:00
Annie Huang f540a0166d add include jest 2024-02-23 17:21:09 +08:00
Annie Huang b98f4e4c07 exclude jest 2024-02-23 17:17:23 +08:00
Annie Huang 31eb638a6e fix typo 2024-02-23 17:06:59 +08:00
Annie Huang 583605ef3a try: fix collison of Jest 2024-02-23 16:59:55 +08:00
wancat 8c6b86ad17 restore changes 2024-02-23 15:09:46 +08:00
wancat 8670d2b3bb fix: ignore eslint warning 2024-02-23 15:06:12 +08:00
wancat 07ec58e41c fix: enable restart of db 2024-02-23 15:06:01 +08:00
wancat 025553f42a Merge branch 'main' into feat-automation-test 2024-02-23 14:59:00 +08:00
Lin, Hung-Shin bfa839f2f9 Refactor server (#102) 2024-02-23 14:55:55 +08:00
Annie Huang b57e7adb37 update readme 2024-02-23 13:02:23 +08:00
Annie Huang b1a9a6102c Optional oAuth activation in Cypress 2024-02-23 11:34:47 +08:00
wancat c5a7e77f04 fix: testing base url. change cypress base image 2024-02-22 23:15:29 +08:00
Annie Huang ed142ab27b update declare types 2024-02-22 23:03:43 +08:00
wancat f29dcf810f fix: Dockerfile missing workspace name
This commit updates the Dockerfile to build the @linux-odyssey/models and @linux-odyssey/constants workspaces, in addition to the existing workspaces.
2024-02-22 22:26:45 +08:00
wancat 48cfcb2c36 fix dockerfiles by using classic yarn 2024-02-22 22:24:29 +08:00
wancat 14eea84a36 fix: restore yarn version to classic 2024-02-22 22:03:55 +08:00
wancat c0efce69f4 fix: jest. enable coverage 2024-02-22 21:31:29 +08:00
wancat 37b1bead28 change dependency type 2024-02-22 20:51:49 +08:00
wancat 9f1008336d refactor: routes and index.js 2024-02-22 20:44:47 +08:00
wancat 59590639ee refactor: authController, add session types 2024-02-22 20:35:19 +08:00
wancat 885f1c5b27 refactor: leaderboard controller 2024-02-22 20:18:04 +08:00
wancat ac8e9d1cfa refactor quest controller 2024-02-22 20:16:08 +08:00
wancat fd543ed594 finish session controller 2024-02-22 20:10:03 +08:00
wancat c0ec382510 enable source map in dev mode, add delay for nodemon starting 2024-02-22 20:09:47 +08:00
wancat e84ca12647 fix missing types in utils 2024-02-19 23:57:08 +08:00
wancat c912319967 refactor: userController and add req user type 2024-02-19 23:51:21 +08:00
wancat 4c421b4cb3 refactor: game 2024-02-19 23:13:37 +08:00
wancat 2d05149cea refactor socket 2024-02-19 19:24:18 +08:00
wancat ad66191d88 refactor: validators 2024-02-19 19:09:32 +08:00
wancat 5e8df7ae01 refactor: middlewares 2024-02-19 19:08:34 +08:00
Annie Huang 9bac360498 fixed type error 2024-02-19 18:46:50 +08:00
wancat 7c1d83ccd3 refactor: cli 2024-02-19 18:36:50 +08:00
wancat 4ab3096fa5 refactor: fix file condition checks 2024-02-19 18:35:39 +08:00
wancat 64215d3ef1 refactor: container 2024-02-19 18:23:52 +08:00
wancat 075e21fb89 change scripts from "watch" to "dev" 2024-02-19 18:05:01 +08:00
wancat 9dc0b43737 add types to models 2024-02-19 18:05:01 +08:00
wancat 5489de2432 refactor: auth 2024-02-19 18:05:01 +08:00
wancat d6c95cea4a refactor utils 2024-02-19 18:05:00 +08:00
Annie Huang 2cb82fdcdc fix type error 2024-02-15 21:20:32 +08:00
Annie Huang 2b495f7cda update testing library settings and global functions 2024-02-14 16:16:01 +08:00
Lin, Hung-Shin 9509bbc355 Refactor packages (#101) 2024-02-09 23:19:36 +08:00
wancat c3d9130731 Remove logging config into console 2024-02-09 14:33:27 +08:00
Lin, Hung-Shin 4f9b275211 Fix issue 51 (#99)
Please test if this passes security check, modified
server/src/api/routes/index.js
2024-02-09 14:26:28 +08:00
wancat db16413754 feat: set cookie to secure and strict 2024-02-09 14:24:32 +08:00
Annie Huang 5c17f5ea3c typescript initial edit 2024-02-08 17:49:16 +08:00
wancat ca2a40341c Merge branch 'main' into fix-issue-51 2024-02-08 17:48:11 +08:00
wancat c06d6d6deb change e2e to use node module 2024-02-08 17:40:01 +08:00
Annie Huang f797f4d897 update readme -- build up method 2024-02-07 14:24:54 +08:00
wancat 640d6b58c5 fix ts error 2024-02-07 10:41:46 +08:00
wancat 79a5dac7f1 fix mongo no matching document when deleting container 2024-02-07 10:37:38 +08:00
wancat cfda174dc5 refactor constants 2024-02-07 10:36:56 +08:00
wancat ee03b9999e refactor models to typescript 2024-02-07 10:20:19 +08:00
wancat 253bd04588 finish file-graph and solve jest ts 2024-02-07 10:03:53 +08:00
wancat 2a542302bc update eslint to omit blank line between members 2024-02-06 09:31:20 +08:00
wancat e91774fe3b refactor utils 2024-02-06 09:20:01 +08:00
Lin, Hung-Shin 23f9023f8b Feat typescript (#93)
tired. Merge first
2024-02-05 13:15:53 +08:00
wancat 5b0d1f92c4 setup docker prod 2024-02-05 13:13:34 +08:00
wancat ec4e97a7d6 able to run server without in docker 2024-02-05 12:41:07 +08:00
wancat 19cd66475c add building scripts 2024-02-05 12:33:05 +08:00
wancat c353f81b9f fix vue eslint error 2024-02-05 11:57:53 +08:00
wancat 6e4e1cfed1 fix eslint 2024-02-05 11:49:27 +08:00
wancat 78966d351f change to use esm 2024-01-30 09:06:41 +08:00
wancat eb231e4056 Remove project references and add build:watch all 2024-01-29 13:26:41 +08:00
wancat ba3b40792b add dotenv to server 2024-01-29 13:21:33 +08:00
wancat 7216158053 add app 2024-01-29 13:21:07 +08:00
wancat 5f36f732b1 feat: docker dev mode 2024-01-29 12:42:50 +08:00
wancat 30aefb47f9 feat: setup project reference 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 1531846482 fix: eslint 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin a18e0d9b68 fix: setup Jest with TS 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 557af91d4b feat: change to use commonjs output 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 2bdb804f24 WIP: fixing tests 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 8eaec839d3 feat: add post-build-script to gradually migrate 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 3977db87a3 feat: add typescript to file-graph and server 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin e6abd08963 Fix: Issue 96 (#98) 2024-01-28 20:40:56 -06:00
Lin, Hung-Shin 68e922878b Update tests/package.json 2024-01-29 10:32:38 +08:00
Lin, Hung-Shin e9e3bc25af build(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#95)
Bumps
[follow-redirects](https://github.com/follow-redirects/follow-redirects)
from 1.15.2 to 1.15.4.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/65858205e59f1e23c9bf173348a7a7cbb8ac47f5"><code>6585820</code></a>
Release version 1.15.4 of the npm package.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/7a6567e16dfa9ad18a70bfe91784c28653fbf19d"><code>7a6567e</code></a>
Disallow bracketed hostnames.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/05629af696588b90d64e738bc2e809a97a5f92fc"><code>05629af</code></a>
Prefer native URL instead of deprecated url.parse.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/1cba8e85fa73f563a439fe460cf028688e4358df"><code>1cba8e8</code></a>
Prefer native URL instead of legacy url.resolve.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/72bc2a4229bc18dc9fbd57c60579713e6264cb92"><code>72bc2a4</code></a>
Simplify _processResponse error handling.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/3d42aecdca39b144a0a2f27ea134b4cf67dd796a"><code>3d42aec</code></a>
Add bracket tests.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/bcbb096b32686ecad6cd34235358ed6f2217d4f0"><code>bcbb096</code></a>
Do not directly set Error properties.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/192dbe7ce671ecad813c074bffe3b3f5d3680fee"><code>192dbe7</code></a>
Release version 1.15.3 of the npm package.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/bd8c81e4f32d12f28a35d265f88b1716703687c6"><code>bd8c81e</code></a>
Fix resource leak on destroy.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/9c728c314b06f9595dcd7f245d40731e8a27d79f"><code>9c728c3</code></a>
Split linting and testing.</li>
<li>Additional commits viewable in <a
href="https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.15.2&new-version=1.15.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-01-28 20:31:31 -06:00
Lin, Hung-Shin d4bb5d8f73 build(deps-dev): bump vite from 4.4.12 to 4.5.2 (#100)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 4.4.12 to 4.5.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->4.5.2 (2024-01-19)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)
(<a href="https://github.com/vitejs/vite/commit/eeec23b">eeec23b</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15653">#15653</a></li>
</ul>
<h2><!-- raw HTML omitted -->4.5.1 (2023-12-04)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15223">#15223</a>,
proxy html path should be encoded (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15226">#15226</a>)
(<a href="https://github.com/vitejs/vite/commit/41bb354">41bb354</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15223">#15223</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/15226">#15226</a></li>
</ul>
<h2>4.5.0 (2023-10-18)</h2>
<ul>
<li>feat: backport mdx as known js source (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14560">#14560</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14670">#14670</a>)
(<a href="https://github.com/vitejs/vite/commit/45595ef">45595ef</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14560">#14560</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14670">#14670</a></li>
<li>feat: scan .marko files (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14669">#14669</a>)
(<a href="https://github.com/vitejs/vite/commit/ed7bdc5">ed7bdc5</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14669">#14669</a></li>
<li>feat(ssr): backport ssr.resolve.conditions and
ssr.resolve.externalConditions (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14498">#14498</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14668">#14668</a>)
(<a href="https://github.com/vitejs/vite/commit/520139c">520139c</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14498">#14498</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14668">#14668</a></li>
</ul>
<h2><!-- raw HTML omitted -->4.4.11 (2023-10-05)<!-- raw HTML omitted
--></h2>
<ul>
<li>revert: &quot;fix: use string manipulation instead of regex to
inject esbuild helpers (<a
href="https://github.com/vitejs/vite/commit/54e1275">54e1275</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14094">#14094</a></li>
</ul>
<h2><!-- raw HTML omitted -->4.4.10 (2023-10-03)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: add source map to Web Workers (fix <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14216">#14216</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14217">#14217</a>)
(<a href="https://github.com/vitejs/vite/commit/df6f32f">df6f32f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14216">#14216</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14217">#14217</a></li>
<li>fix: handle errors during <code>hasWorkspacePackageJSON</code>
function (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14394">#14394</a>)
(<a href="https://github.com/vitejs/vite/commit/6f6e5de">6f6e5de</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14394">#14394</a></li>
<li>fix: handle sourcemap correctly when multiple line import exists (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14232">#14232</a>)
(<a href="https://github.com/vitejs/vite/commit/218861f">218861f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14232">#14232</a></li>
<li>fix: if host is specified check whether it is valid (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14013">#14013</a>)
(<a href="https://github.com/vitejs/vite/commit/b1b816a">b1b816a</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14013">#14013</a></li>
<li>fix: include <code>vite/types/*</code> in exports field (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14296">#14296</a>)
(<a href="https://github.com/vitejs/vite/commit/40e99a1">40e99a1</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14296">#14296</a></li>
<li>fix: initWasm options should be optional (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14152">#14152</a>)
(<a href="https://github.com/vitejs/vite/commit/119c074">119c074</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14152">#14152</a></li>
<li>fix: restore builtins list (<a
href="https://github.com/vitejs/vite/commit/f8b9adb">f8b9adb</a>)</li>
<li>fix: use string manipulation instead of regex to inject esbuild
helpers (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14094">#14094</a>)
(<a href="https://github.com/vitejs/vite/commit/128ad8f">128ad8f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14094">#14094</a></li>
<li>fix: ws never connects after restarting server if server.hmr.server
is set (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14127">#14127</a>)
(<a href="https://github.com/vitejs/vite/commit/441642e">441642e</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14127">#14127</a></li>
<li>fix(analysis): warnings for dynamic imports that use static template
literals (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14458">#14458</a>)
(<a href="https://github.com/vitejs/vite/commit/0c6d289">0c6d289</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14458">#14458</a></li>
<li>fix(cli): convert special base (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14283">#14283</a>)
(<a href="https://github.com/vitejs/vite/commit/d4bc0fb">d4bc0fb</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14283">#14283</a></li>
<li>fix(css): remove pure css chunk sourcemap (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14290">#14290</a>)
(<a href="https://github.com/vitejs/vite/commit/cd7e033">cd7e033</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14290">#14290</a></li>
<li>fix(css): reset render cache on renderStart (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14326">#14326</a>)
(<a href="https://github.com/vitejs/vite/commit/d334b3d">d334b3d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14326">#14326</a></li>
<li>fix(glob): trigger HMR for glob in a package (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14117">#14117</a>)
(<a href="https://github.com/vitejs/vite/commit/0f582bf">0f582bf</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14117">#14117</a></li>
<li>fix(import-analysis): preserve importedUrls import order (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14465">#14465</a>)
(<a href="https://github.com/vitejs/vite/commit/269aa43">269aa43</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14465">#14465</a></li>
<li>fix(manifest): preserve pure css chunk assets (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14297">#14297</a>)
(<a href="https://github.com/vitejs/vite/commit/3d63ae6">3d63ae6</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14297">#14297</a></li>
<li>fix(optimizer): define crawlDeps after scanProcessing and
optimizationResult are complete (fix <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/1428">#1428</a>
(<a href="https://github.com/vitejs/vite/commit/fcaf749">fcaf749</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14284">#14284</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14285">#14285</a></li>
<li>fix(resolve): support submodules of optional peer deps (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14489">#14489</a>)
(<a href="https://github.com/vitejs/vite/commit/104971d">104971d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14489">#14489</a></li>
<li>fix(vite): precisely check if files are in dirs (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14241">#14241</a>)
(<a href="https://github.com/vitejs/vite/commit/c4758d1">c4758d1</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14241">#14241</a></li>
<li>feat: improve deno and bun support (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14379">#14379</a>)
(<a href="https://github.com/vitejs/vite/commit/8bc1f9d">8bc1f9d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14379">#14379</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/d0360c12476ccc61e9e78c500ed1bd74ed65a2cf"><code>d0360c1</code></a>
release: v4.5.2</li>
<li><a
href="https://github.com/vitejs/vite/commit/eeec23bbc9d476c54a3a6d36e78455867185a7cb"><code>eeec23b</code></a>
fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/c0751156f0c015f6dbd5c7a58afd8cff2fde1a2f"><code>c075115</code></a>
release: v4.5.1</li>
<li><a
href="https://github.com/vitejs/vite/commit/41bb3546a839ed2c822367b3933770c0693a0fb0"><code>41bb354</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15223">#15223</a>,
proxy html path should be encoded (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15226">#15226</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/055d2b86b0543a7c1a2a4d5bc7298af62bc51fa7"><code>055d2b8</code></a>
release: v4.5.0</li>
<li><a
href="https://github.com/vitejs/vite/commit/ed7bdc520679577509466ce808a1794ba8377204"><code>ed7bdc5</code></a>
feat: scan .marko files (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14669">#14669</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/45595ef82f786d6b321ce002f2cd4951659114ac"><code>45595ef</code></a>
feat: backport mdx as known js source (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14560">#14560</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14670">#14670</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/520139cdff88ae3a0bf89692133cce3e453cb29a"><code>520139c</code></a>
feat(ssr): backport ssr.resolve.conditions and
ssr.resolve.externalConditions...</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v4.5.2/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=4.4.12&new-version=4.5.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-01-28 20:31:13 -06:00
chreamy 7bb7ac3f00 csrf token 2024-01-20 00:14:59 -06:00
chreamy 42278ad51b csrf token 2024-01-20 00:11:48 -06:00
dependabot[bot] 107c153e98 build(deps-dev): bump vite from 4.4.12 to 4.5.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.12 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-20 02:24:08 +00:00
chreamy d45fbee647 fix: CSRF issue #51 2024-01-18 03:23:39 -06:00
chreamy 2ac73a06c9 fix: CSRF issue #51 2024-01-18 03:15:38 -06:00
chreamy 91a9397c58 fix: CSRF issue #51 2024-01-18 03:12:25 -06:00
chreamy af088730e8 fix: issue #96 2024-01-17 22:02:39 -06:00
chreamy 4b9c7de031 fix: issue #96 2024-01-17 21:55:52 -06:00
chreamy 712a1ff319 test 2024-01-17 21:36:44 -06:00
Lin, Hung-Shin 21c1a4d11c Fix Build Quests Error (#94)
#92
2024-01-13 20:18:22 +08:00
dependabot[bot] d069233407 build(deps): bump follow-redirects from 1.15.2 to 1.15.4
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-10 21:59:52 +00:00
Lin, Hung-Shin 27c5794700 fix: no response when buildImage 2023-12-20 13:31:08 +08:00
234 changed files with 7018 additions and 3630 deletions

No files matched your search

+3 -2
View File
@@ -1,3 +1,4 @@
*/node_modules **/node_modules
.git .git
*/dist **/dist
**/tsconfig.tsbuildinfo
+4
View File
@@ -0,0 +1,4 @@
TESTING=true
TESTING_USERNAME=alex
TESTING_PASSWORD=Alex1234
SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6
+1 -1
View File
@@ -2,4 +2,4 @@ vite.config.js
.gitignore .gitignore
data data
mongo-init.js mongo-init.js
app/dist dist
+57 -5
View File
@@ -4,22 +4,33 @@ module.exports = {
node: true, node: true,
jest: true, jest: true,
}, },
plugins: ['cypress'], plugins: ['@typescript-eslint', 'cypress', 'vue'],
extends: [ extends: [
'plugin:vue/vue3-essential', 'plugin:vue/vue3-essential',
'airbnb-base', 'airbnb-base',
'plugin:prettier/recommended', // We added this 'plugin:prettier/recommended', // We added this
'plugin:cypress/recommended', 'plugin:cypress/recommended',
], ],
parser: 'vue-eslint-parser',
parserOptions: { parserOptions: {
// parser: 'babel-eslint', parser: '@typescript-eslint/parser',
ecmaVersion: 13,
sourceType: 'module', sourceType: 'module',
}, },
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
typescript: {},
},
vue: {
version: '3.0', // Explicitly specify Vue 3
},
},
rules: { rules: {
'no-console': 'warn', 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'import/extensions': 'off',
camelcase: ['error', { properties: 'never', ignoreDestructuring: true }], camelcase: ['error', { properties: 'never', ignoreDestructuring: true }],
'no-param-reassign': ['error', { props: false }], 'no-param-reassign': ['error', { props: false }],
'no-underscore-dangle': ['error', { allow: ['_id'] }], 'no-underscore-dangle': ['error', { allow: ['_id'] }],
@@ -29,5 +40,46 @@ module.exports = {
'prettier/prettier': ['error', { endOfLine: 'auto', semi: false }], 'prettier/prettier': ['error', { endOfLine: 'auto', semi: false }],
'import/prefer-default-export': 'off', 'import/prefer-default-export': 'off',
'no-restricted-syntax': ['off', 'ForOfStatement'], 'no-restricted-syntax': ['off', 'ForOfStatement'],
'lines-between-class-members': [
'error',
'always',
{ exceptAfterSingleLine: true },
],
'no-use-before-define': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
}, },
],
// Allow missing file extensions
'import/extensions': [
'warn',
'ignorePackages',
{
js: 'off',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
},
overrides: [
{
files: ['**/tests/**/*.js', '**/tests/**/*.ts', '**/*.test.ts'], // Adjust the pattern to match your test files
rules: {
'import/extensions': 'off', // Turn off the rule for test files
'import/no-extraneous-dependencies': 'off',
},
},
],
} }
+3
View File
@@ -27,6 +27,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
- name: Build
run: yarn build
- name: Lint - name: Lint
run: yarn lint run: yarn lint
-17
View File
@@ -12,23 +12,6 @@ jobs:
uses: ./.github/workflows/ci-checks.yml uses: ./.github/workflows/ci-checks.yml
secrets: inherit secrets: inherit
code_review:
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Code Review GPT
uses: mattzcarey/code-review-gpt@v0.1.4-alpha
with:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MODEL: 'gpt-3.5-turbo'
GITHUB_TOKEN: ${{ github.token }}
build: build:
needs: ci-checks
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
secrets: inherit secrets: inherit
+6
View File
@@ -52,6 +52,9 @@ web_modules/
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
# TypeScript build
built
# Optional npm cache directory # Optional npm cache directory
.npm .npm
@@ -132,3 +135,6 @@ dist
.yarn/build-state.yml .yarn/build-state.yml
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
#screenshots
tests/cypress/screenshots
+1 -1
View File
@@ -1,2 +1,2 @@
data/* data/*
app/dist dist
+2 -1
View File
@@ -1,4 +1,5 @@
{ {
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
} }
+12 -5
View File
@@ -17,15 +17,22 @@
## Development ## Development
Run everything (frontend, backend, db, swagger, quests): Run mongoDB first:
docker compose build docker compose -f docker-compose.dev.yml up -d db
docker compose up -d
The db will automatically restarts by Docker.
Build up dependencies:
yarn build:deps
Run dev server (frontend, backend):
yarn dev
If you want to enable social login, you should have OAuth client token in a `.env` file: If you want to enable social login, you should have OAuth client token in a `.env` file:
docker compose --env-file .env up -d
### Build Quest Images ### Build Quest Images
In each quest folder, there is a `game.yml` file, which is the quest definition. And a optional `Dockerfile` to overwrite the base image. If you want to include additional files, put them in a `home` folder. The files in `home` will **not** be copied to the image automatically, you have to copy them in the `Dockerfile`. In each quest folder, there is a `game.yml` file, which is the quest definition. And a optional `Dockerfile` to overwrite the base image. If you want to include additional files, put them in a `home` folder. The files in `home` will **not** be copied to the image automatically, you have to copy them in the `Dockerfile`.
+5 -2
View File
@@ -1,10 +1,12 @@
# Base stage # Base stage
FROM node:20-alpine as base FROM node:20-alpine as base
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock tsconfig.json ./
COPY packages/file-graph/package.json ./packages/file-graph/package.json COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json COPY packages/utils/package.json ./packages/utils/package.json
COPY packages/constants/package.json ./packages/constants/package.json COPY packages/constants/package.json ./packages/constants/package.json
COPY packages/models/package.json ./packages/models/package.json
COPY app/package.json ./app/package.json COPY app/package.json ./app/package.json
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
@@ -18,8 +20,9 @@ FROM base as build
COPY packages/file-graph ./packages/file-graph COPY packages/file-graph ./packages/file-graph
COPY packages/utils ./packages/utils COPY packages/utils ./packages/utils
COPY packages/constants ./packages/constants COPY packages/constants ./packages/constants
COPY packages/models ./packages/models
COPY app ./app COPY app ./app
RUN yarn build RUN yarn workspace app build
# Production stage # Production stage
FROM nginx:stable-alpine as prod FROM nginx:stable-alpine as prod
+1 -1
View File
@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+10 -6
View File
@@ -2,12 +2,10 @@
"name": "app", "name": "app",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "API_TARGET=http://homesrv.dala-banana.ts.net:3000 vite", "dev": "vite",
"dev:local": "vite",
"dev:docker": "API_TARGET=http://backend:3000 vite", "dev:docker": "API_TARGET=http://backend:3000 vite",
"build": "vite build", "build": "vue-tsc -b && vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@@ -19,6 +17,8 @@
"@linux-odyssey/constants": "*", "@linux-odyssey/constants": "*",
"@linux-odyssey/file-graph": "*", "@linux-odyssey/file-graph": "*",
"@linux-odyssey/utils": "*", "@linux-odyssey/utils": "*",
"@linux-odyssey/models": "*",
"@types/dompurify": "^3.0.5",
"axios": "^1.6.0", "axios": "^1.6.0",
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"echarts": "^5.4.3", "echarts": "^5.4.3",
@@ -34,10 +34,14 @@
"xterm-addon-fit": "^0.7.0" "xterm-addon-fit": "^0.7.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.2.3", "@types/validator": "^13.11.9",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/compiler-sfc": "^3.3.13",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"vite": "^4.4.12" "typescript": "^5.3.3",
"vite": "^4.5.3",
"vue-tsc": "^2.0.10"
} }
} }
@@ -1,4 +1,4 @@
export default { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
+9 -61
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router' import { RouterLink, useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
@@ -27,7 +27,7 @@ const password = ref('')
const errorMessage = ref('') const errorMessage = ref('')
const toast = useToast() const toast = useToast()
const availableMethods = ref({}) const availableMethods = ref<{ [key: string]: boolean }>({})
onMounted(async () => { onMounted(async () => {
const res = await api.get('/auth/available-methods') const res = await api.get('/auth/available-methods')
availableMethods.value = res.data availableMethods.value = res.data
@@ -44,7 +44,7 @@ const success = () => {
router.push({ name: 'map' }) router.push({ name: 'map' })
} }
const error = (err) => { const error = (err: string) => {
errorMessage.value = err errorMessage.value = err
} }
@@ -114,10 +114,7 @@ const handleChange = () => {
:icon="['fab', 'google']" :icon="['fab', 'google']"
class="text-text-secondary px-3 text-lg" class="text-text-secondary px-3 text-lg"
/> />
<span class="font-medium"> <span class="font-medium"> 以 Google 繼續 </span>
<!-- Continue with Google -->
以 Google 繼續
</span>
</a> </a>
<a <a
id="GitHubLogin" id="GitHubLogin"
@@ -129,32 +126,17 @@ const handleChange = () => {
:icon="['fab', 'github']" :icon="['fab', 'github']"
class="text-text-secondary px-3 text-lg" class="text-text-secondary px-3 text-lg"
/> />
<span class="font-medium"> <span class="font-medium"> 以 GitHub 繼續 </span>
<!-- Continue with GitHub -->
以 GitHub 繼續
</span>
</a> </a>
<p <p
id="or" id="or"
class="text-text-secondary flex justify-center mt-3" class="text-text-secondary flex justify-center mt-3"
v-if="type === 'login' || type === 'register'" v-if="type === 'login' || type === 'register'"
> >
<!-- or -->
或 或
</p> </p>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<!-- <input
type="text"
id="username"
ref="usernameInput"
class="my-4 bg-bg-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-bg-secondary"
:placeholder="type === 'login' ? 'Email / Username' : 'Username'"
v-model="username"
required
@input="handleChange()"
autocomplete="username"
/> -->
<input <input
type="text" type="text"
id="username" id="username"
@@ -176,17 +158,6 @@ const handleChange = () => {
</label> </label>
</div> </div>
<div class="mb-6" v-if="type === 'register'"> <div class="mb-6" v-if="type === 'register'">
<!-- <input
type="email"
id="email"
ref="emailInput"
class="my-4 bg-bg-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-bg-secondary"
placeholder="Email"
v-model="email"
required
@input="handleChange()"
autocomplete="email"
/> -->
<input <input
type="email" type="email"
id="email" id="email"
@@ -200,15 +171,6 @@ const handleChange = () => {
/> />
</div> </div>
<div class="mb-6" v-if="type === 'login' || type === 'register'"> <div class="mb-6" v-if="type === 'login' || type === 'register'">
<!-- <input
type="password"
id="password"
class="my-4 bg-bg-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-bg-secondary"
placeholder="Password"
v-model="password"
required
@input="handleChange()"
/> -->
<input <input
type="password" type="password"
id="password" id="password"
@@ -231,43 +193,29 @@ const handleChange = () => {
class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full" class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full"
type="submit" type="submit"
> >
<span v-if="type === 'login'"> <span v-if="type === 'login'"> 登入 </span>
<!-- Log In --> <span v-else> 註冊 </span>
登入
</span>
<span v-else>
<!-- Sign Up -->
註冊
</span>
</button> </button>
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3"> <p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
<span id="CheckRegistered" v-if="type === 'register'"> <span id="CheckRegistered" v-if="type === 'register'">
<!-- Already have an account? -->
已經有帳號了嗎? 已經有帳號了嗎?
<RouterLink <RouterLink
id="SwitchtoLogin" id="SwitchtoLogin"
class="text-text-primary font-bold" class="text-text-primary font-bold"
to="/login" to="/login"
> >
<u> <u> 登入 </u>
<!-- Log in -->
登入
</u>
</RouterLink></span </RouterLink></span
> >
<span id="CheckRegistered" v-else-if="type === 'login'"> <span id="CheckRegistered" v-else-if="type === 'login'">
<!-- Don't have an account? -->
還沒有帳號嗎? 還沒有帳號嗎?
<RouterLink <RouterLink
id="SwitchtoRegister" id="SwitchtoRegister"
class="text-text-primary font-bold" class="text-text-primary font-bold"
to="/register" to="/register"
> >
<u> <u> 註冊 </u>
<!-- Sign up -->
註冊
</u>
</RouterLink></span </RouterLink></span
> >
</p> </p>
+2 -8
View File
@@ -1,13 +1,7 @@
<template> <template>
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" /> <font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
<h1 id="CommandBook" class="inline text-text-primary font-xl"> <h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
<!-- Command Cheatsheets -->
指令之書
</h1>
<div id="cheatsheets"> <div id="cheatsheets">
<p class="text-text-secondary"> <p class="text-text-secondary">--指令列表--</p>
<!-- --Command List-- -->
-- --指令列表-- --
</p>
</div> </div>
</template> </template>
+1 -2
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { ref } from 'vue' import { ref } from 'vue'
@@ -47,7 +47,6 @@ setTimeout(() => {
id="QuestCompleted" id="QuestCompleted"
class="text-2xl text-text mb-5 w-full inline-flex justify-center" class="text-2xl text-text mb-5 w-full inline-flex justify-center"
> >
<!-- Quest Completed! -->
關卡完成! 關卡完成!
</h2> </h2>
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent"> <div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
defineProps({
url: {
type: String,
required: true,
},
icons: {
type: Array,
required: true,
},
title: {
type: String,
required: true,
},
desciption: String,
})
</script>
<template>
<div class="flex flex-col">
<a :href="url" target="_blank" class="h-10 w-10" :title="desciption">
<font-awesome-icon
:icon="icons"
:alt="title"
class="text-text-primary h-10 delay-130 hover:-translate-y-1 hover:scale-110"
/>
</a>
</div>
</template>
+40 -53
View File
@@ -1,55 +1,4 @@
<template> <script setup lang="ts">
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">
<!-- Survey -->
問卷
</p>
</a>
</button>
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled"
/>
<p class="text-text-disabled inline ml-1">
<!-- Solution -->
解答
</p>
</button>
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
<font-awesome-icon
:icon="['fas', 'arrow-rotate-left']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">
<!-- Reset -->
重來
</p>
</button>
<button
id="continue"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
<font-awesome-icon
:icon="['far', 'circle-right']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">
<!-- Continue -->
繼續
</p>
</button>
</section>
</template>
<script setup>
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { createSession } from '../store/session' import { createSession } from '../store/session'
@@ -64,9 +13,47 @@ const continuePlay = async () => {
try { try {
router.push({ name: 'map' }) router.push({ name: 'map' })
} catch (err) { } catch (err) {
// useToast().error('Failed to load map')
useToast().error('無法讀取地圖') useToast().error('無法讀取地圖')
console.error(err) console.error(err)
} }
} }
</script> </script>
<template>
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">問卷</p>
</a>
</button>
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled"
/>
<p class="text-text-disabled inline ml-1">解答</p>
</button>
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
<font-awesome-icon
:icon="['fas', 'arrow-rotate-left']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">重來</p>
</button>
<button
id="continue"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
<font-awesome-icon
:icon="['far', 'circle-right']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">繼續</p>
</button>
</section>
</template>
+18 -7
View File
@@ -1,26 +1,34 @@
<script setup> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
const cw = ref(0) const cw = ref(0)
const ch = ref(0) const ch = ref(0)
const fallingCharArr = ref([]) const fallingCharArr = ref<
{ value: string; x: number; y: number; speed: number }[]
>([])
const fontSize = ref(16) const fontSize = ref(16)
const maxColums = ref(0) const maxColums = ref(0)
const charArr = ['0', '1', '', '', ''] const charArr = ['0', '1', '', '', '']
const ctx = ref(null) const ctx = ref<CanvasRenderingContext2D | null>(null)
const randomInt = (min, max) => { const randomInt = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min) + min) return Math.floor(Math.random() * (max - min) + min)
} }
const randomFloat = (min, max) => { const randomFloat = (min: number, max: number) => {
return Math.random() * (max - min) + min return Math.random() * (max - min) + min
} }
const draw = (point) => { const draw = (point: {
value: string
x: number
y: number
speed: number
}) => {
point.value = charArr[randomInt(0, charArr.length)].toUpperCase() point.value = charArr[randomInt(0, charArr.length)].toUpperCase()
if (point.y < 0) point.speed = randomFloat(10, 20) if (point.y < 0) point.speed = randomFloat(10, 20)
if (ctx.value === null) return
ctx.value.fillStyle = '#0F0' ctx.value.fillStyle = '#0F0'
ctx.value.font = `${fontSize.value}px san-serif` ctx.value.font = `${fontSize.value}px san-serif`
ctx.value.fillText(point.value, point.x, point.y) ctx.value.fillText(point.value, point.x, point.y)
@@ -33,6 +41,7 @@ const draw = (point) => {
} }
const update = () => { const update = () => {
if (ctx.value === null) return
ctx.value.fillStyle = 'rgba(0,0,0,0.05)' ctx.value.fillStyle = 'rgba(0,0,0,0.05)'
ctx.value.fillRect(0, 0, cw.value, ch.value) ctx.value.fillRect(0, 0, cw.value, ch.value)
@@ -48,7 +57,7 @@ onMounted(() => {
ch.value = document.documentElement.clientHeight ch.value = document.documentElement.clientHeight
maxColums.value = cw.value / fontSize.value maxColums.value = cw.value / fontSize.value
const canvas = document.querySelector('canvas') const canvas = document.querySelector('canvas') as HTMLCanvasElement
ctx.value = canvas.getContext('2d') ctx.value = canvas.getContext('2d')
canvas.width = cw.value canvas.width = cw.value
@@ -58,6 +67,8 @@ onMounted(() => {
fallingCharArr.value.push({ fallingCharArr.value.push({
x: i * fontSize.value, x: i * fontSize.value,
y: randomFloat(-500, 0), y: randomFloat(-500, 0),
value: '',
speed: 0,
}) })
} }
+1 -1
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
const props = defineProps({ const props = defineProps({
node: { node: {
type: Object, type: Object,
+7 -26
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import { defineProps, onMounted } from 'vue' import { defineProps, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
@@ -21,34 +21,15 @@ const handleLogout = async () => {
reset() reset()
router.push({ name: 'login' }) router.push({ name: 'login' })
} catch (err) { } catch (err) {
// toast.error('Failed to logout')
toast.error('登出失敗') toast.error('登出失敗')
console.error(err) console.error(err)
} }
} }
const handleMap = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// toast.error('Failed to load map')
toast.error('無法讀取地圖')
console.error(err)
}
}
const handleLeaderBoard = async () => {
try {
router.push({ name: 'leaderboard' })
} catch (err) {
// toast.error('Failed to load map')
toast.error('無法讀取排行榜')
console.error(err)
}
}
defineProps({ defineProps({
title: { title: {
type: String, type: String,
required: true, required: false,
}, },
}) })
@@ -56,7 +37,7 @@ onMounted(async () => {
try { try {
await loadUserProfile() await loadUserProfile()
} catch (err) { } catch (err) {
// toast.error('Failed to load user profile') console.error(err)
toast.error('無法讀取使用者資料') toast.error('無法讀取使用者資料')
} }
}) })
@@ -126,18 +107,18 @@ onMounted(async () => {
class="text-text-primary h-full w-full" class="text-text-primary h-full w-full"
/> />
</a> </a>
<button title="LeaderBoard" @click="handleLeaderBoard" class="h-5 w-5"> <RouterLink title="LeaderBoard" to="/leaderboard" class="h-5 w-5">
<font-awesome-icon <font-awesome-icon
:icon="['fas', 'trophy']" :icon="['fas', 'trophy']"
class="text-text-primary h-full w-full" class="text-text-primary h-full w-full"
/> />
</button> </RouterLink>
<button title="Map" @click="handleMap" class="h-5 w-5"> <RouterLink title="Map" to="/map" class="h-5 w-5">
<font-awesome-icon <font-awesome-icon
:icon="['fas', 'map']" :icon="['fas', 'map']"
class="text-text-primary h-full w-full" class="text-text-primary h-full w-full"
/> />
</button> </RouterLink>
<a <a
title="Bug Report" title="Bug Report"
:href="bugReportUrl" :href="bugReportUrl"
+16 -4
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import sessionStore from '../store/session' import sessionStore from '../store/session'
import MarkdownText from './MarkdownText.vue' import MarkdownText from './MarkdownText.vue'
@@ -26,7 +26,9 @@ const right = () => {
</script> </script>
<template> <template>
<div class="w-full flex items-center p-2"> <div class="h-full flex flex-col">
<!-- Hint Header -->
<div class="flex items-center p-2 h-10">
<font-awesome-icon <font-awesome-icon
:icon="['far', 'lightbulb']" :icon="['far', 'lightbulb']"
class="text-yellow-200 p-2 content-center" class="text-yellow-200 p-2 content-center"
@@ -63,16 +65,26 @@ const right = () => {
</button> </button>
</div> </div>
</div> </div>
<div id="hint" class="bg-bg flex flex-wrap p-8"> <div id="hint" class="bg-bg p-8 overflow-y-auto">
<ul> <ul>
<li
v-for="response in sessionStore.session.responses[current]"
:key="response.type"
class="text-text font-xl whitespace-pre-wrap"
>
<div v-for="content in response.content" :key="content">
<MarkdownText :content="content" />
</div>
</li>
<li <li
v-for="hint in sessionStore.session.hints[current]" v-for="hint in sessionStore.session.hints[current]"
:key="hint" :key="hint"
class="text-text font-xl whitespace-pre-wrap" class="text-text-primary font-xl whitespace-pre-wrap"
> >
<MarkdownText :content="hint" /> <MarkdownText :content="hint" />
</li> </li>
</ul> </ul>
<br /> <br />
</div> </div>
</div>
</template> </template>
+1 -1
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import { computed, defineProps } from 'vue' import { computed, defineProps } from 'vue'
import markdown2HTML from '../utils/markdown' import markdown2HTML from '../utils/markdown'
+9 -11
View File
@@ -1,5 +1,5 @@
<!-- eslint-disable no-console --> <!-- eslint-disable no-console -->
<script setup> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
@@ -50,7 +50,7 @@ async function getProgress() {
} }
} }
const genOption = (nodes, edges) => ({ const genOption = (nodes: any, edges: any) => ({
renderer: 'svg', renderer: 'svg',
tooltip: {}, tooltip: {},
animationDurationUpdate: 1500, animationDurationUpdate: 1500,
@@ -94,7 +94,7 @@ const genOption = (nodes, edges) => ({
curveness: 0.08, curveness: 0.08,
}, },
itemStyle: { itemStyle: {
color: (params) => { color: (params: any) => {
const { const {
data: { completed, unlocked }, data: { completed, unlocked },
} = params } = params
@@ -111,7 +111,7 @@ const genOption = (nodes, edges) => ({
], ],
}) })
function getOption(quests, progress) { function getOption(quests: any, progress: any) {
const dag = new DAG(quests) const dag = new DAG(quests)
const nodes = dag.getNodes().map((node) => ({ const nodes = dag.getNodes().map((node) => ({
@@ -120,9 +120,9 @@ function getOption(quests, progress) {
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2, y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
x: marginY * node.layer * 2, x: marginY * node.layer * 2,
completed: progress[node._id]?.completed || false, completed: progress[node._id]?.completed || false,
unlocked: node.requirements.every((req) => progress[req]?.completed), unlocked: node.requirements.every((req: any) => progress[req]?.completed),
})) }))
const lineAppearence = (node) => { const lineAppearence = (node: any) => {
try { try {
const { unlocked } = node const { unlocked } = node
return { return {
@@ -134,7 +134,7 @@ function getOption(quests, progress) {
throw err throw err
} }
} }
const edges = dag.getEdgesArray().map((edge) => ({ const edges = dag.getEdgesArray().map((edge: any) => ({
source: edge[0], source: edge[0],
target: edge[1], target: edge[1],
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])), lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
@@ -145,10 +145,10 @@ function getOption(quests, progress) {
const router = useRouter() const router = useRouter()
const toast = useToast() const toast = useToast()
function initChart(option) { function initChart(option: any) {
chartInstance = init(chartContainer.value) chartInstance = init(chartContainer.value)
chartInstance.setOption(option) chartInstance.setOption(option)
chartInstance.on('click', (params) => { chartInstance.on('click', (params: any) => {
const { const {
data: { id, unlocked }, data: { id, unlocked },
} = params } = params
@@ -156,7 +156,6 @@ function initChart(option) {
if (unlocked) { if (unlocked) {
router.push({ name: 'game', params: { questId: id } }) router.push({ name: 'game', params: { questId: id } })
} else { } else {
// toast.warning('You have not completed the previous quest yet!')
toast.warning('你還沒完成前一個關卡!') toast.warning('你還沒完成前一個關卡!')
} }
}) })
@@ -181,7 +180,6 @@ onMounted(async () => {
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl" class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00" style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
> >
<!-- Get through your linux journey! -->
踏上你的Linux冒險之旅吧! 踏上你的Linux冒險之旅吧!
</h1> </h1>
<div class="flex flex-wrap absolute w-full h-full z-1"> <div class="flex flex-wrap absolute w-full h-full z-1">
+4 -6
View File
@@ -1,10 +1,11 @@
<script setup> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import MarkdownText from './MarkdownText.vue' import MarkdownText from './MarkdownText.vue'
import sessionStore from '../store/session' import sessionStore from '../store/session'
import { Task } from '../types'
const tasks = computed(() => { const tasks = computed(() => {
return sessionStore.session.tasks.map((task) => { return sessionStore.session.tasks.map((task: Task) => {
const prefix = task.completed ? '✓' : '➤' const prefix = task.completed ? '✓' : '➤'
const color = task.completed ? 'text-text-primary' : 'text-text' const color = task.completed ? 'text-text-primary' : 'text-text'
return { return {
@@ -29,10 +30,7 @@ const tasks = computed(() => {
:content="sessionStore.quest.instruction" :content="sessionStore.quest.instruction"
/> />
<br /> <br />
<p id="tasks" class="text-text"> <p id="tasks" class="text-text">任務:</p>
<!-- Tasks: -->
任務:
</p>
<ul v-if="sessionStore.session"> <ul v-if="sessionStore.session">
<li v-for="task in tasks" :key="task.id"> <li v-for="task in tasks" :key="task.id">
<MarkdownText :class="task.class" :content="task.content" /> <MarkdownText :class="task.class" :content="task.content" />
+15 -17
View File
@@ -1,25 +1,23 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useTerminal } from '../store/session'
const terminal = ref<HTMLElement | null>(null)
onMounted(() => {
const term = useTerminal()
if (!terminal.value) return
term.mount(terminal.value)
window.addEventListener('resize', () => term.resizeScreen())
})
</script>
<template> <template>
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" /> <font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
<button id="Terminal" class="text-text h-[5%]"> <button id="Terminal" class="text-text h-[5%]">終端機</button>
<!-- Terminal -->
終端機
</button>
<div <div
id="terminal" id="terminal"
ref="terminal" ref="terminal"
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5" class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
></div> ></div>
</template> </template>
<script setup>
import { onMounted, ref } from 'vue'
import { useTerminal } from '../store/session'
const terminal = ref(null)
onMounted(() => {
const term = useTerminal()
term.mount(terminal.value)
window.addEventListener('resize', () => term.resizeScreen())
})
</script>
+2 -2
View File
@@ -1,6 +1,6 @@
<script setup> <script setup lang="ts">
import FileNode from './FileNode.vue' import FileNode from './FileNode.vue'
import sessionStore from '../store/session.js' import sessionStore from '../store/session'
</script> </script>
<template> <template>
+8 -4
View File
@@ -1,19 +1,21 @@
<script setup> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import TreeChart from './TreeChart.vue' import TreeChart from './TreeChart.vue'
const treeRef = ref(null) const treeRef = ref<HTMLElement | null>(null)
let pos = { top: 0, left: 0, x: 0, y: 0 } let pos = { top: 0, left: 0, x: 0, y: 0 }
function mouseMoveHandler(e) { function mouseMoveHandler(e: MouseEvent) {
const dx = e.clientX - pos.x const dx = e.clientX - pos.x
const dy = e.clientY - pos.y const dy = e.clientY - pos.y
if (treeRef.value === null) return
treeRef.value.scrollTop = pos.top - dy treeRef.value.scrollTop = pos.top - dy
treeRef.value.scrollLeft = pos.left - dx treeRef.value.scrollLeft = pos.left - dx
} }
function mouseUpHandler() { function mouseUpHandler() {
if (treeRef.value === null) return
treeRef.value.style.cursor = 'grab' treeRef.value.style.cursor = 'grab'
treeRef.value.style.removeProperty('user-select') treeRef.value.style.removeProperty('user-select')
@@ -21,7 +23,8 @@ function mouseUpHandler() {
document.removeEventListener('mouseup', mouseUpHandler) document.removeEventListener('mouseup', mouseUpHandler)
} }
function mouseDownHandler(e) { function mouseDownHandler(e: MouseEvent) {
if (treeRef.value === null) return
treeRef.value.style.cursor = 'grabbing' treeRef.value.style.cursor = 'grabbing'
treeRef.value.style.userSelect = 'none' treeRef.value.style.userSelect = 'none'
@@ -37,6 +40,7 @@ function mouseDownHandler(e) {
} }
onMounted(() => { onMounted(() => {
if (treeRef.value === null) return
treeRef.value.style.cursor = 'grab' treeRef.value.style.cursor = 'grab'
treeRef.value.addEventListener('mousedown', mouseDownHandler) treeRef.value.addEventListener('mousedown', mouseDownHandler)
}) })
-7
View File
@@ -1,7 +0,0 @@
export const surveyUrl = '/api/v1/survey'
export const bugReportUrl = '/api/v1/bug-report'
export default {
surveyUrl,
bugReportUrl,
}
+4
View File
@@ -0,0 +1,4 @@
export const surveyUrl = '/api/v1/survey'
export const bugReportUrl = '/api/v1/bug-report'
export const facebookUrl = 'https://www.facebook.com/LinuxOdyssey'
export const githubUrl = 'https://github.com/linux-odyssey/linux-odyssey'
+9
View File
@@ -0,0 +1,9 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H512V512H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_426_5" transform="scale(0.00195312)"/>
</pattern>
<image id="image0_426_5" width="512" height="512" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H512V512H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_424_19" transform="scale(0.00195312)"/>
</pattern>
<image id="image0_424_19" width="512" height="512" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 35 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="474" height="475" viewBox="0 0 474 475" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H474V475H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_426_3" transform="matrix(0.00215972 0 0 0.00215517 -0.00105485 0)"/>
</pattern>
<image id="image0_426_3" width="464" height="464" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 34 KiB

File renamed without changes.
+9 -2
View File
@@ -18,13 +18,18 @@ import {
faMap, faMap,
faTrophy, faTrophy,
faFileInvoice, faFileInvoice,
faEnvelope,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { import {
faLightbulb, faLightbulb,
faCircleQuestion, faCircleQuestion,
faCircleRight, faCircleRight,
} from '@fortawesome/free-regular-svg-icons' } from '@fortawesome/free-regular-svg-icons'
import { faGithub, faGoogle } from '@fortawesome/free-brands-svg-icons' import {
faFacebook,
faGithub,
faGoogle,
} from '@fortawesome/free-brands-svg-icons'
/* add icons to the library */ /* add icons to the library */
library.add( library.add(
@@ -47,5 +52,7 @@ library.add(
faCircleRight, faCircleRight,
faGithub, faGithub,
faGoogle, faGoogle,
faFileInvoice faFileInvoice,
faFacebook,
faEnvelope
) )
View File
File renamed without changes.
+4 -5
View File
@@ -5,15 +5,14 @@ import RegisterPage from './views/RegisterPage.vue'
import ChooseUsernamePage from './views/ChooseUsernamePage.vue' import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import QuestMapPage from './views/QuestMapPage.vue' import QuestMapPage from './views/QuestMapPage.vue'
import LeaderboardPage from './views/LeaderboardPage.vue' import LeaderboardPage from './views/LeaderboardPage.vue'
import LandingPage from './views/LandingPage.vue'
import { isLoggedIn } from './utils/auth' import { isLoggedIn } from './utils/auth'
const routes = [ const routes = [
{ {
path: '/', path: '/',
redirect: { name: 'landing-page',
name: 'register', component: LandingPage,
},
meta: { requiresGuest: true },
}, },
{ {
path: '/login', path: '/login',
@@ -58,7 +57,7 @@ const router = createRouter({
routes, routes,
}) })
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, _from, next) => {
const loggedIn = await isLoggedIn() const loggedIn = await isLoggedIn()
// Check if the route requires authentication // Check if the route requires authentication
if (to.meta.requiresAuth && !loggedIn) { if (to.meta.requiresAuth && !loggedIn) {
@@ -1,10 +1,12 @@
import { reactive } from 'vue' import { reactive } from 'vue'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { FileGraph } from '@linux-odyssey/file-graph' import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
import { IQuest } from '@linux-odyssey/models'
import api from '../utils/api' import api from '../utils/api'
import Socket from '../utils/socket' import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal' import SocketTerminal from '../utils/terminal'
import { LoadQuestError, LoadSessionError } from '../utils/errors' import { LoadQuestError, LoadSessionError } from '../utils/errors'
import { Session, StageResponse } from '../types'
const toast = useToast() const toast = useToast()
@@ -15,15 +17,23 @@ function newSession() {
discovered: false, discovered: false,
}) })
return { return {
_id: '',
status: 'inactive', status: 'inactive',
graph, graph,
pwd: '', pwd: '',
hints: [], hints: [],
tasks: [], tasks: [],
responses: [],
} }
} }
const store = reactive({ interface Store {
session: Session
questId: string
quest: IQuest | null
}
const store = reactive<Store>({
session: newSession(), session: newSession(),
questId: '', questId: '',
quest: null, quest: null,
@@ -32,23 +42,28 @@ const store = reactive({
const socket = new Socket() const socket = new Socket()
const term = new SocketTerminal(40, 80) const term = new SocketTerminal(40, 80)
function setQuest(questId) { function setQuest(questId: string) {
return api.get(`/quests/${questId}`).then((res) => { return api.get(`/quests/${questId}`).then((res) => {
store.questId = res.data._id store.questId = res.data._id
store.quest = res.data store.quest = res.data
}) })
} }
async function setSession(session) { async function setSession(session: Session) {
try { try {
console.log('Setting session...', session)
store.session = session store.session = session
store.session.graph = new FileGraph(session.graph) store.session.graph = new FileGraph(session.graph)
term.reset() term.reset()
await socket.connect(session) await socket.connect(session)
term.focus() term.focus()
} catch (err) { } catch (err: any) {
console.error(err) console.error(err)
if (err instanceof Error) {
toast.error(err.message) toast.error(err.message)
} else {
toast.error('Failed to connect to the session. Please try again later.')
}
throw err throw err
} }
} }
@@ -65,7 +80,7 @@ export async function createSession() {
} }
} }
async function getActiveSession(questId) { async function getActiveSession(questId: string) {
try { try {
const res = await api.post('/sessions/active', { questId }) const res = await api.post('/sessions/active', { questId })
await setSession(res.data) await setSession(res.data)
@@ -76,7 +91,7 @@ async function getActiveSession(questId) {
} }
} }
function updateGraph(event) { function updateGraph(event: { discover: FileObject[]; pwd: string }) {
if (event.discover) { if (event.discover) {
store.session.graph.discover(event.discover) store.session.graph.discover(event.discover)
} }
@@ -85,16 +100,12 @@ function updateGraph(event) {
} }
} }
function updateHints(hints) { function newResponse(response: StageResponse) {
store.session.hints.push(hints) console.log('newResponse', response, 'session', store.session)
} store.session.responses.push(response.responses)
store.session.hints.push(response.hints)
function updateStatus(status) { store.session.tasks = response.tasks
store.session.status = status store.session.status = response.status
}
function setTasks(tasks) {
store.session.tasks = tasks
} }
export function useTerminal() { export function useTerminal() {
@@ -108,7 +119,7 @@ export function reset() {
store.quest = null store.quest = null
} }
export async function init(questId) { export async function init(questId: string) {
if (!questId) throw new Error('No quest ID provided') if (!questId) throw new Error('No quest ID provided')
reset() reset()
try { try {
@@ -126,23 +137,17 @@ export async function init(questId) {
} }
function setup() { function setup() {
socket.on('terminal', (data) => { socket.on('terminal', (data: string) => {
term.write(data) term.write(data)
}) })
term.onData((data) => { term.onData((data: string) => {
socket.emit('terminal', data) socket.emit('terminal', data)
}) })
socket.on('graph', (event) => { socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
updateGraph(event) updateGraph(event)
}) })
socket.on('hints', (event) => { socket.on('response', (response: StageResponse) => {
updateHints(event) newResponse(response)
})
socket.on('tasks', (tasks) => {
setTasks(tasks)
})
socket.on('status', (event) => {
updateStatus(event)
}) })
} }
@@ -15,7 +15,7 @@ export async function loadUserProfile() {
store.email = email store.email = email
store.progress = progress store.progress = progress
return store return store
} catch (err) { } catch (err: any) {
console.error(err) console.error(err)
throw new Error(`Failed to load user profile: ${err.message}`) throw new Error(`Failed to load user profile: ${err.message}`)
} }
+26
View File
@@ -0,0 +1,26 @@
import { FileGraph } from '@linux-odyssey/file-graph'
// eslint-disable-next-line import/no-relative-packages
import { IResponse } from '@linux-odyssey/models'
export interface Task {
id: number
name: string
completed: boolean
}
export interface StageResponse {
status: string
hints: string[]
tasks: Task[]
responses: IResponse[]
}
export interface Session {
_id: string
status: string
graph: FileGraph
pwd: string
hints: string[][]
tasks: Task[]
responses: IResponse[][]
}
@@ -7,5 +7,4 @@ const api = axios.create({
}, },
withCredentials: true, withCredentials: true,
}) })
export default api export default api
@@ -12,7 +12,12 @@ export async function isLoggedIn() {
return loggedIn return loggedIn
} }
function handleRequestError(err) { function handleRequestError(err: {
response: {
data?: any
status?: number
}
}) {
if (!err.response) throw err // Not an axios error if (!err.response) throw err // Not an axios error
const { status } = err.response const { status } = err.response
if (status === 429) { if (status === 429) {
@@ -41,14 +46,14 @@ function handleRequestError(err) {
throw err throw err
} }
export const login = (username, password) => { export const login = (username: string, password: string) => {
return api return api
.post('/auth/login', { username, password }) .post('/auth/login', { username, password })
.then(() => true) .then(() => true)
.catch(handleRequestError) .catch(handleRequestError)
} }
export const register = (username, email, password) => { export const register = (username: string, email: string, password: string) => {
return api return api
.post('/auth/register', { .post('/auth/register', {
username, username,
@@ -59,7 +64,7 @@ export const register = (username, email, password) => {
.catch(handleRequestError) .catch(handleRequestError)
} }
export const checkUsername = async (username) => { export const checkUsername = async (username: string) => {
return api return api
.get('/auth/check-username', { .get('/auth/check-username', {
params: { username }, params: { username },
@@ -68,7 +73,7 @@ export const checkUsername = async (username) => {
.catch(handleRequestError) .catch(handleRequestError)
} }
export const chooseUsername = async (username) => { export const chooseUsername = async (username: string) => {
return api return api
.post('/auth/register-from-session', { username }) .post('/auth/register-from-session', { username })
.then(() => true) .then(() => true)
@@ -1,27 +1,28 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
export class TooManyRequestsError extends Error { export class TooManyRequestsError extends Error {
constructor(message) { constructor(message: string) {
super(message) super(message)
this.name = 'TooManyRequestsError' this.name = 'TooManyRequestsError'
} }
} }
export class ValidationError extends Error { export class ValidationError extends Error {
constructor(message) { constructor(message: string) {
super(message) super(message)
this.name = 'ValidationError' this.name = 'ValidationError'
} }
} }
export class UnauthorizedError extends Error { export class UnauthorizedError extends Error {
constructor(message) { constructor(message: string) {
super(message) super(message)
this.name = 'UnauthorizedError' this.name = 'UnauthorizedError'
} }
} }
export class LoadQuestError extends Error { export class LoadQuestError extends Error {
constructor(message, questId) { questId: string
constructor(message: string, questId: string) {
super(message) super(message)
this.name = 'LoadQuestError' this.name = 'LoadQuestError'
this.questId = questId this.questId = questId
@@ -29,7 +30,8 @@ export class LoadQuestError extends Error {
} }
export class LoadSessionError extends Error { export class LoadSessionError extends Error {
constructor(message, questId) { questId: string
constructor(message: string, questId: string) {
super(message) super(message)
this.name = 'LoadSessionError' this.name = 'LoadSessionError'
this.questId = questId this.questId = questId
@@ -1,7 +1,7 @@
import { marked } from 'marked' import { marked } from 'marked'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
export default function markdown2HTML(markdown) { export default function markdown2HTML(markdown: string) {
const html = marked.parse(markdown) const html = marked.parse(markdown)
return DOMPurify.sanitize(html) return DOMPurify.sanitize(html)
} }
@@ -1,13 +1,18 @@
import io from 'socket.io-client' import { io, Socket } from 'socket.io-client'
import { Session } from '../types'
type SocketCallback = (data: any) => void
class SocketWrapper { class SocketWrapper {
socket: Socket | null
listeners: { event: string; callback: SocketCallback }[]
constructor() { constructor() {
this.socket = null this.socket = null
this.listeners = [] this.listeners = []
} }
connect(session) { connect(session: Session) {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.disconnect() this.disconnect()
this.socket = io('', { this.socket = io('', {
@@ -28,7 +33,7 @@ class SocketWrapper {
}) })
} }
on(event, callback) { on(event: string, callback: SocketCallback) {
this.listeners.push({ event, callback }) this.listeners.push({ event, callback })
if (this.socket) { if (this.socket) {
this.socket.on(event, callback) this.socket.on(event, callback)
@@ -36,12 +41,14 @@ class SocketWrapper {
} }
bindListeners() { bindListeners() {
this.listeners.forEach(({ event, callback }) => { this.listeners.forEach(
this.socket.on(event, callback) ({ event, callback }: { event: string; callback: SocketCallback }) => {
}) if (this.socket !== null) this.socket.on(event, callback)
}
)
} }
emit(event, data) { emit(event: string, data: any) {
if (!this.socket) { if (!this.socket) {
console.warn('Socket is not connected!') console.warn('Socket is not connected!')
return return
@@ -3,9 +3,11 @@ import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css' import 'xterm/css/xterm.css'
class SocketTerminal { class SocketTerminal {
constructor(rows, cols) { private term: Terminal
private fitAddon: FitAddon
constructor(rows: number, cols: number) {
this.term = new Terminal({ this.term = new Terminal({
rendererType: 'canvas', // 渲染类型
rows, // 行数 rows, // 行数
cols, // 不指定行数,自动回车后光标从下一行开始 cols, // 不指定行数,自动回车后光标从下一行开始
convertEol: true, // 启用时,光标将设置为下一行的开头 convertEol: true, // 启用时,光标将设置为下一行的开头
@@ -13,9 +15,11 @@ class SocketTerminal {
disableStdin: false, // 是否应禁用输入 disableStdin: false, // 是否应禁用输入
// cursorStyle: "underline", //光标样式 // cursorStyle: "underline", //光标样式
cursorBlink: true, // 光标闪烁 cursorBlink: true, // 光标闪烁
theme: {
foreground: '#ECECEC', // 字体 foreground: '#ECECEC', // 字体
background: '#000000', // 背景色 background: '#000000', // 背景色
cursor: 'help', // 设置光标 cursor: 'help', // 设置光标
},
fontSize: 18, fontSize: 18,
fontWeight: 'normal', fontWeight: 'normal',
}) })
@@ -24,29 +28,29 @@ class SocketTerminal {
this.fitAddon.fit() this.fitAddon.fit()
} }
write(message) { write(message: string): void {
this.term.write(message) this.term.write(message)
} }
onData(callback) { onData(callback: any): void {
this.term.onData(callback) this.term.onData(callback)
} }
focus() { focus(): void {
this.term.focus() this.term.focus()
} }
resizeScreen() { resizeScreen(): void {
this.fitAddon.fit() this.fitAddon.fit()
} }
mount(reference) { mount(reference: HTMLElement): void {
this.term.open(reference) this.term.open(reference)
this.term.focus() this.term.focus()
this.resizeScreen() this.resizeScreen()
} }
reset() { reset(): void {
this.term.reset() this.term.reset()
} }
} }
+20 -8
View File
@@ -1,14 +1,20 @@
<script setup> <script setup lang="ts">
import { isValidUsername } from '@linux-odyssey/utils' import { isValidUsername } from '@linux-odyssey/utils'
import AuthForm from '../components/AuthForm.vue' import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue' import Background from '../components/DynamicBackground.vue'
import { TooManyRequestsError, ValidationError } from '../utils/errors' import { TooManyRequestsError, ValidationError } from '../utils/errors'
import { checkUsername, chooseUsername } from '../utils/auth' import { checkUsername, chooseUsername } from '../utils/auth'
async function check({ username, error }) { async function check({
username,
error,
}: {
username: string
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
if (username) { if (username) {
if (!isValidUsername(username)) { if (!isValidUsername(username)) {
// error('Invalid username.')
error('無效的帳號名稱') error('無效的帳號名稱')
return return
} }
@@ -16,7 +22,6 @@ async function check({ username, error }) {
await checkUsername(username) await checkUsername(username)
} catch (err) { } catch (err) {
if (err instanceof TooManyRequestsError) { if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。') error('太多請求,兩分鐘後再試一次。')
return return
} }
@@ -24,19 +29,26 @@ async function check({ username, error }) {
error(err.message) error(err.message)
return return
} }
// error('Something went wrong. Please try again later.')
error('出了點問題,請再試一次。') error('出了點問題,請再試一次。')
} }
} }
} }
async function handleSubmit({ username, success, error }) { async function handleSubmit({
username,
success,
error,
}: {
username: string
success: () => void
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
try { try {
await chooseUsername(username) await chooseUsername(username)
success() success()
} catch (err) { } catch (err: any) {
if (err instanceof TooManyRequestsError) { if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。') error('太多請求,兩分鐘後再試一次。')
return return
} }
+9 -13
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import sessionStore, { init } from '../store/session' import sessionStore, { init } from '../store/session'
@@ -28,16 +28,12 @@ const toast = useToast()
onMounted(async () => { onMounted(async () => {
try { try {
await init(props.questId) await init(props.questId)
} catch (err) { } catch (err: any) {
if (err instanceof LoadQuestError) { if (err instanceof LoadQuestError) {
// toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`) toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
return return
} }
if (err instanceof LoadSessionError) { if (err instanceof LoadSessionError) {
// toast.error(
// `Failed to create session for quest: ${err.questId}. Please re-login and try again.`
// )
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`) toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
return return
} }
@@ -61,7 +57,7 @@ onMounted(async () => {
<!-- main --> <!-- main -->
<div id="main" class="h-[94vh] w-full flex p-3 space-x-3"> <div id="main" class="h-[94vh] w-full flex p-3 space-x-3">
<!-- Topic and Command List --> <!-- Topic and Command List -->
<div class="bg-bg-secondary h-full w-1/3 rounded-lg"> <div class="bg-bg-secondary h-full w-1/4 rounded-lg">
<section id="quest" class="h-full p-3 overflow-auto"> <section id="quest" class="h-full p-3 overflow-auto">
<QuestPart /> <QuestPart />
</section> </section>
@@ -71,17 +67,17 @@ onMounted(async () => {
</section> --> </section> -->
</div> </div>
<!-- Terminal and Hint --> <!-- Terminal and Hint -->
<div class="bg-bg h-full w-1/3 rounded-lg"> <div class="bg-bg h-full w-1/2 rounded-lg">
<section id="terminal" class="h-3/5 overflow-hidden"> <section id="hint" class="h-3/5">
<TerminalPart /> <HintPart />
</section> </section>
<hr class="border-border border" /> <hr class="border-border border" />
<section id="hint" class="h-2/5"> <section id="terminal" class="h-2/5 overflow-hidden">
<HintPart /> <TerminalPart />
</section> </section>
</div> </div>
<!-- Visualization --> <!-- Visualization -->
<div class="h-full w-1/3"> <div class="h-full w-1/4">
<div class="bg-border rounded-xl h-[90%] p-2"> <div class="bg-border rounded-xl h-[90%] p-2">
<div class="bg-bg rounded-lg h-full p-2"> <div class="bg-bg rounded-lg h-full p-2">
<section id="visualization" class="bg-bg-primary h-full rounded-md"> <section id="visualization" class="bg-bg-primary h-full rounded-md">
+299
View File
@@ -0,0 +1,299 @@
<script setup lang="ts">
import ContactButton from '../components/ContactButton.vue'
import DynamicBackground from '../components/DynamicBackground.vue'
import { bugReportUrl, facebookUrl, githubUrl } from '../config'
const scrollToSection = (sectionId: string) => {
const section = document.getElementById(sectionId)
if (section) {
const container = document.querySelector('.overflow-y-auto') // Replace '.overflow-y-auto' with the appropriate class or selector for your container
if (container) {
container.scrollTo({
top: section.offsetTop,
behavior: 'smooth', // Add smooth scrolling behavior if desired
})
}
}
}
</script>
<template>
<div class="w-screen h-screen bg-scroll bg-bg">
<DynamicBackground class="w-full h-full" />
<div class="h-screen w-screen absolute top-0 left-0">
<!-- game header -->
<div
class="w-full h-[6vh] px-5 flex flex-row items-center justify-between bg-bg-secondary"
>
<div class="flex flex-row items-center gap-3">
<img src="../img/icon_totem.svg" class="h-8 justify-self-start" />
<button
id="GameTitle"
class="text-text-primary inline-block font-bold whitespace-nowrap pt-1"
style="font-size: 2vh"
>
Linux Odyssey
</button>
</div>
<div class="flex flex-row items-center gap-3">
<button
id="Intro"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('intro')"
style="font-size: 2vh"
>
Intro
</button>
<button
id="Feature"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('feature')"
style="font-size: 2vh"
>
Feature</button
><button
id="Info"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('about')"
style="font-size: 2vh"
>
About
</button>
<button
id="Contact"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('contact')"
style="font-size: 2vh"
>
Contact Us
</button>
<RouterLink
title="Sign In"
id="SignIn"
to="/register"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
style="font-size: 2vh"
>
Sign In
</RouterLink>
<a
title="Bug Report"
:href="bugReportUrl"
target="_blank"
class="h-5 w-5"
>
<font-awesome-icon
:icon="['fas', 'bug']"
class="text-text-primary h-full w-full"
/>
</a>
</div>
</div>
<!-- main content -->
<main class="w-full h-[94vh] overflow-y-auto">
<div class="w-full flex flex-col items-center">
<div
class="flex flex-col md:flex-row mt-10 justify-around gap-5 items-center my-10"
>
<h1 class="text-text-primary text-7xl font-extrabold font-mono">
Linux
</h1>
<img src="../img/icon_totem.svg" class="h-[30vh]" />
<h1 class="text-text-primary text-7xl font-extrabold font-mono">
Odyssey
</h1>
</div>
<!-- intro -->
<div class="w-full md:w-2/3 px-5">
<section
id="intro"
class="text-text grid grid-cols-1 gap-3 justify-items-center"
>
<h1
class="text-text inline-block bg-bg-primary p-5 rounded-xl opacity-90"
style="font-size: 2.5vh"
>
程式設計不僅僅是寫程式碼,還涉及到如何有效地與你的電腦互動。
程式教育卻往往忽略背後「開發環境」的重要性,
使得許多學生在過程中充滿挫折。
「開發環境」是寫程式、執行程式過程所需要的一切工具。
而這些工具需要在終端機輸入指令來使用。
大部分的程式要用來運行的環境,並非 Windows 或 Mac OS,而是 Linux
作業系統。 Linux Odyssey 即是為此設計的線上 Linux 互動教學網站。
</h1>
</section>
<section
id="feature"
class="text-text p-15 grid grid-rows-3 gap-10 items-center pt-10"
>
<div
class="bg-bg-primary p-5 rounded-xl opacity-90 grid grid-cols-3 grid-rows-2 justify-items-center items-center"
>
<img
src="../img/icon_shuttle.svg"
class="h-[18vh] row-span-2"
/>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2"
style="font-size: 3.5vh"
>
初學者也能立即上手
</h1>
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
我們明白對於初學者來說,設定和使用開發工具可能是一大障礙。
學習者無需安裝任何軟體,僅需通過瀏覽器即可連上一個真實的 Linux
環境。
</h1>
</div>
<div
class="bg-bg-primary p-5 rounded-xl opacity-90 grid grid-cols-3 grid-rows-2 justify-items-center items-center"
>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2"
style="font-size: 3.5vh"
>
遊戲般的學習體驗
</h1>
<img src="../img/icon_sword.svg" class="h-[18vh] row-span-2" />
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
Linux Odyssey 融合了遊戲的樂趣與學習的實用性。
透過精心設計的遊戲關卡,學習者可以一邊接受挑戰、破解謎題,
一邊掌握 Linux 指令的運用。
</h1>
</div>
<div
class="bg-bg-primary p-5 rounded-xl opacity-90 grid grid-cols-3 grid-rows-2 justify-items-center items-center"
>
<img
src="../img/icon_settings.svg"
class="h-[18vh] row-span-2"
/>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2 pt-8"
style="font-size: 3.5vh"
>
真實的 Linux 操作之旅
</h1>
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
Linux Odyssey 提供的不僅是理論知識,更是真實的操作體驗。
每一個指令都是在真實的 Linux 環境中執行,
讓學習者能夠真正深入軟體開發的世界。
</h1>
</div>
</section>
<section class="w-full py-10">
<iframe
src="https://www.youtube.com/embed/XzvfmUf1QP8?si=QxnKJMNjsm27fGGF"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
class="w-full aspect-video"
></iframe>
</section>
<section
id="start"
class="text-text bg-bg-secondary border-8 border-text-tertiary rounded-xl transition ease-in-out hover:scale-110 duration-300"
>
<RouterLink title="StartGame" id="StartGame" to="/register">
<div class="text-text text-xl p-2 w-full text-center">
準備好鍵盤,跟我們一起開啟你的 Linux 冒險吧!
</div>
</RouterLink>
</section>
<section
id="about"
class="text-text flex flex-col items-center mt-10 gap-10"
>
<div class="w-full flex flex-col items-center gap-2">
<h1 class="text-text-primary text-3xl font-extrabold font-mono">
團隊介紹
</h1>
<p class="text-lg">國立臺北科技大學 智慧自動化工程科 第二屆</p>
<div class="flex flex-row gap-5 justify-center">
<h1 class="font-bold">Wancat</h1>
<h1 class="font-bold">Neil</h1>
<h1 class="font-bold">A.H.</h1>
<h1 class="font-bold">Ray Gur</h1>
</div>
</div>
<div class="w-full flex flex-col items-center gap-2">
<h1 class="text-text-primary text-3xl font-extrabold font-mono">
得獎肯定
</h1>
<h1 class="text-xl text-center">
大專校院資訊應用服務創新競賽<br />
鈦坦敏捷開發特別獎 第二名
</h1>
</div>
<!-- <h1
class="text-text-primary p-5 font-extrabold font-mono col-span-3 gap-3"
style="font-size: 3.5vh"
>
玩家感想
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"很好玩很像益智遊戲"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"我從來不知道自由軟體的存在,謝謝你們。"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"還蠻好玩的,讓程式變好玩"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"以前不喜歡電腦課現在覺得喜歡一點點"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"程式真的對新手很友善,讓我學到了很多東西"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"感覺做得很好很簡明易懂關卡設計很用心"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"以前我覺得寫程式很難不過現在覺得很有趣"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
</h1> -->
</section>
</div>
<section
id="contact"
class="text-text mt-10 flex flex-col items-center"
>
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
聯絡我們
</h1>
<div class="flex flex-row items-center justify-around mt-5 gap-5">
<ContactButton
:url="facebookUrl"
:icons="['fab', 'facebook']"
title="Facebook"
desciption="Linux Odyssey"
/>
<ContactButton
:url="githubUrl"
:icons="['fab', 'github']"
title="GitHub"
desciption="linux-odyssey/linux-odyssey"
/>
<ContactButton
url="mailto:team@linuxodyssey.xyz"
:icons="['fas', 'envelope']"
title="Email"
desciption="team@linuxodyssey.xyz"
/>
</div>
</section>
</div>
</main>
</div>
</div>
</template>
+9 -3
View File
@@ -1,18 +1,24 @@
<script setup> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import DynamicBackground from '../components/DynamicBackground.vue' import DynamicBackground from '../components/DynamicBackground.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue' import GameHeaderPart from '../components/GameHeaderPart.vue'
import api from '../utils/api' import api from '../utils/api'
const leaderboard = ref([]) const leaderboard = ref<
{
username: string
score: number
completedQuests: string[]
}[]
>([])
const toast = useToast() const toast = useToast()
async function getLeaderboard() { async function getLeaderboard() {
try { try {
const res = await api.get('/leaderboard') const res = await api.get('/leaderboard')
leaderboard.value = res.data leaderboard.value = res.data
} catch (err) { } catch (err: any) {
console.error(err) console.error(err)
toast.error('讀取排行榜失敗') toast.error('讀取排行榜失敗')
} }
+13 -6
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import AuthForm from '../components/AuthForm.vue' import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue' import Background from '../components/DynamicBackground.vue'
@@ -9,28 +9,35 @@ import {
} from '../utils/errors' } from '../utils/errors'
import { login } from '../utils/auth' import { login } from '../utils/auth'
const handleLogin = async ({ username, password, success, error }) => { const handleLogin = async ({
username,
password,
success,
error,
}: {
username: string
password: string
success: () => void
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) => {
try { try {
const isSuccess = await login(username, password) const isSuccess = await login(username, password)
if (isSuccess) success() if (isSuccess) success()
} catch (err) { } catch (err) {
if (err instanceof TooManyRequestsError) { if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。') error('太多請求,兩分鐘後再試一次。')
return return
} }
if (err instanceof UnauthorizedError) { if (err instanceof UnauthorizedError) {
// error('Wrong username or password.')
error('錯誤的帳號名稱或密碼。') error('錯誤的帳號名稱或密碼。')
return return
} }
if (err instanceof ValidationError) { if (err instanceof ValidationError) {
// error('Invalid username or password.')
error('無效的帳號名稱或密碼。') error('無效的帳號名稱或密碼。')
return return
} }
console.error(err) console.error(err)
// error('Something went wrong.')
error('出了點問題。') error('出了點問題。')
} }
} }
+1 -8
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import QuestMap from '../components/QuestMap.vue' import QuestMap from '../components/QuestMap.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue' import GameHeaderPart from '../components/GameHeaderPart.vue'
</script> </script>
@@ -6,13 +6,6 @@ import GameHeaderPart from '../components/GameHeaderPart.vue'
<template> <template>
<div class="h-[6vh] w-full"> <div class="h-[6vh] w-full">
<GameHeaderPart title="關卡地圖" /> <GameHeaderPart title="關卡地圖" />
<!-- <p
class="text-text-primary absolute z-1 font-bold inline-block tracking-widest"
style="left: 1.2%; top: 1.5%; height: 100%; font-size: 2vh"
>
Quest Map
Unix編年史
</p> -->
</div> </div>
<div class="flex flex-col justify-center items-center w-full h-[94vh]"> <div class="flex flex-col justify-center items-center w-full h-[94vh]">
<QuestMap /> <QuestMap />
+27 -13
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import validator from 'validator' import validator from 'validator'
import { isValidUsername } from '@linux-odyssey/utils' import { isValidUsername } from '@linux-odyssey/utils'
import { passwordPolicy } from '@linux-odyssey/constants' import { passwordPolicy } from '@linux-odyssey/constants'
@@ -12,17 +12,28 @@ import {
ValidationError, ValidationError,
} from '../utils/errors' } from '../utils/errors'
function handleRegister({ username, email, password, success, error }) { function handleRegister({
username,
email,
password,
success,
error,
}: {
username: string
email: string
password: string
success: () => void
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
register(username, email, password) register(username, email, password)
.then(success) .then(success)
.catch((err) => { .catch((err) => {
if (err instanceof TooManyRequestsError) { if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。') error('太多請求,兩分鐘後再試一次。')
return return
} }
if (err instanceof UnauthorizedError) { if (err instanceof UnauthorizedError) {
// error('Invalid username or password.')
error('無效的帳號名稱或密碼。') error('無效的帳號名稱或密碼。')
return return
} }
@@ -31,26 +42,31 @@ function handleRegister({ username, email, password, success, error }) {
return return
} }
console.error(err) console.error(err)
// error('Something went wrong.')
error('出了點問題。') error('出了點問題。')
}) })
} }
async function check({ username, email, password, error }) { async function check({
username,
email,
password,
error,
}: {
username: string
email: string
password: string
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
if (username && !isValidUsername(username)) { if (username && !isValidUsername(username)) {
// error('Invalid username.')
error('無效的帳號名稱') error('無效的帳號名稱')
return return
} }
if (email && !validator.isEmail(email)) { if (email && !validator.isEmail(email)) {
// error('Invalid email.')
error('無效的電子郵件') error('無效的電子郵件')
return return
} }
if (password && !validator.isStrongPassword(password, passwordPolicy)) { if (password && !validator.isStrongPassword(password, passwordPolicy)) {
// error(
// 'Your password must be 8+ characters with at least one number, one upper and one lower case letter.'
// )
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。') error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
return return
} }
@@ -62,12 +78,10 @@ async function check({ username, email, password, error }) {
return return
} }
if (err instanceof TooManyRequestsError) { if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。') error('太多請求,兩分鐘後再試一次。')
return return
} }
console.error(err) console.error(err)
// error('Something went wrong.')
error('出了點問題。') error('出了點問題。')
} }
} }
File renamed without changes.
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.vue"],
"references": [
{ "path": "../packages/utils" },
{ "path": "../packages/constants" },
{ "path": "../packages/file-graph" },
{ "path": "../packages/models" }
]
}
File renamed without changes.
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
}
@@ -30,6 +30,7 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./server/src:/app/server/src - ./server/src:/app/server/src
- ./server/dist:/app/server/dist
- ./server/swagger.yaml:/app/server/swagger.yaml - ./server/swagger.yaml:/app/server/swagger.yaml
- ./packages:/app/packages - ./packages:/app/packages
- ./quests:/app/quests - ./quests:/app/quests
@@ -73,6 +74,7 @@ services:
db: db:
image: mongo image: mongo
restart: always
networks: networks:
- internal - internal
ports: ports:
+2
View File
@@ -36,6 +36,8 @@ services:
- DOCKER_NETWORK=linux-odyssey-players - DOCKER_NETWORK=linux-odyssey-players
- SECRET_KEY=${SECRET_KEY} - SECRET_KEY=${SECRET_KEY}
- TRUSTED_PROXIES=${TRUSTED_PROXIES} - TRUSTED_PROXIES=${TRUSTED_PROXIES}
- BACKEND_URL=http://backend:3000
- BASE_URL=${BASE_URL}
analytics: analytics:
image: linuxodyssey/analytics image: linuxodyssey/analytics
+8 -5
View File
@@ -12,6 +12,7 @@ services:
- BASE_URL=http://app-test - BASE_URL=http://app-test
- TESTING_USERNAME=alex - TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234 - TESTING_PASSWORD=Alex1234
# - DEBUG=cypress:*
depends_on: depends_on:
- app-test - app-test
@@ -23,8 +24,9 @@ services:
target: prod target: prod
networks: networks:
internal: internal:
ports: # Only open the port for debugging
- 5173:80 # ports:
# - 5173:80
depends_on: depends_on:
- backend - backend
@@ -43,7 +45,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./logs:/app/logs - ./logs:/app/logs
environment: environment:
- BASE_URL=http://localhost:5173 - BASE_URL=http://app-test
- MONGO_URL=mongodb://user:user-password@db-test:27017/odyssey-test - MONGO_URL=mongodb://user:user-password@db-test:27017/odyssey-test
- DOCKER_NETWORK=linux-odyssey-players - DOCKER_NETWORK=linux-odyssey-players
- SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 # Only for testing - SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 # Only for testing
@@ -57,6 +59,7 @@ services:
- TESTING=true - TESTING=true
- TESTING_USERNAME=alex - TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234 - TESTING_PASSWORD=Alex1234
- BACKEND_URL=http://backend:3000
db-test: db-test:
image: mongo image: mongo
@@ -70,8 +73,8 @@ services:
- MONGO_USER=user - MONGO_USER=user
- MONGO_PASS=user-password - MONGO_PASS=user-password
- MONGO_DB=odyssey-test - MONGO_DB=odyssey-test
ports: # ports:
- 27017:27017 # - 27017:27017
base: base:
image: linuxodyssey/quest-base image: linuxodyssey/quest-base
-5
View File
@@ -1,5 +0,0 @@
BASE_URL=https://example.com
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITNUB_CLIENT_SECRET=
+201
View File
@@ -0,0 +1,201 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type { Config } from 'jest'
const config: Config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls, instances, contexts and results before every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8',
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1', // Redirect .js imports to .ts files
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
}
export default config
+24 -6
View File
@@ -13,13 +13,13 @@
], ],
"scripts": { "scripts": {
"app": "yarn workspace app dev", "app": "yarn workspace app dev",
"app:local": "yarn workspace app dev:local", "build": "yarn workspace server build && yarn workspace app build && yarn workspace analytics build",
"build": "yarn workspace app build", "dev": "concurrently -n \"App,Server\" -c \"bgBlue.bold,bgGreen.bold\" \"yarn workspace app dev\" \"yarn workspace server dev\"",
"server": "yarn workspace server dev", "server": "yarn workspace server dev",
"cli": "yarn workspace client start", "cli": "yarn workspace client start",
"lint": "eslint --ext .js,.vue .", "lint": "eslint .",
"format": "prettier --ignore-path .prettierignore --write app server", "format": "prettier --ignore-path .prettierignore --write app server",
"test": "yarn node --experimental-vm-modules $(yarn bin jest)", "test": "yarn node --experimental-vm-modules --no-warnings $(yarn bin jest)",
"test:e2e": "yarn workspace tests test", "test:e2e": "yarn workspace tests test",
"test:open": "yarn workspace tests test:open", "test:open": "yarn workspace tests test:open",
"analytics": "yarn workspace analytics dev", "analytics": "yarn workspace analytics dev",
@@ -28,14 +28,32 @@
"build:quests": "yarn workspace server build:quests" "build:quests": "yarn workspace server build:quests"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@babel/preset-typescript": "^7.23.3",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vue/eslint-config-prettier": "^7.1.0", "@vue/eslint-config-prettier": "^7.1.0",
"babel-jest": "^29.7.0",
"concurrently": "^8.2.2",
"eslint": "^8.44.0", "eslint": "^8.44.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.15.1", "eslint-plugin-vue": "^9.15.1",
"prettier": "^3.0.0" "jest": "^29.7.0",
"prettier": "^3.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"typescript-eslint": "^7.4.0"
} }
} }
+2
View File
@@ -0,0 +1,2 @@
ADMIN_USERNAME=alex
ADMIN_PASSWORD=Alex1234
+3 -9
View File
@@ -1,10 +1,10 @@
FROM node:20-alpine as build FROM node:20-alpine as build
WORKDIR /app WORKDIR /app
COPY ./package.json yarn.lock ./ COPY ./package.json yarn.lock tsconfig.json ./
COPY packages/analytics/package.json ./packages/analytics/package.json COPY packages/analytics/package.json ./packages/analytics/package.json
COPY packages/models/package.json ./packages/models/package.json COPY packages/models/package.json ./packages/models/package.json
RUN yarn install --frozen-lockfile --production RUN yarn install --frozen-lockfile
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=3001 ENV PORT=3001
EXPOSE 3001 EXPOSE 3001
@@ -15,11 +15,5 @@ COPY packages/analytics ./packages/analytics
COPY packages/models ./packages/models COPY packages/models ./packages/models
ENV NODE_ENV=production ENV NODE_ENV=production
WORKDIR /app/packages/analytics WORKDIR /app/packages/analytics
RUN yarn build
CMD [ "yarn", "start" ] CMD [ "yarn", "start" ]
# Development stage
FROM build as dev
RUN yarn install --frozen-lockfile
WORKDIR /app/packages/analytics
ENV NODE_ENV=development
CMD [ "yarn", "dev" ]
+4 -1
View File
@@ -1,3 +1,6 @@
{ {
"ext": "js,handlebars" "watch": ["src", "views"],
"ext": "ts,handlebars,js",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --loader ts-node/esm src/index.ts"
} }
+14 -4
View File
@@ -6,17 +6,27 @@
"type": "module", "type": "module",
"license": "GPL", "license": "GPL",
"scripts": { "scripts": {
"start": "node src/index.js", "start": "node dist/index.js",
"dev": "nodemon src/index.js" "build": "tsc -b -f",
"watch": "tsc -b --watch",
"dev": "nodemon"
}, },
"dependencies": { "dependencies": {
"@linux-odyssey/models": "*", "@linux-odyssey/models": "*",
"express": "^4.18.2", "dotenv": "^16.4.5",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"express-handlebars": "^7.1.2", "express-handlebars": "^7.1.2",
"mongoose": "^8.0.3" "mongoose": "^8.0.3"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.1" "@swc/core": "^1.4.2",
"@swc/helpers": "^0.5.6",
"@types/express": "^4.17.21",
"@types/node": "^20.11.20",
"nodemon": "^3.1.0",
"regenerator-runtime": "^0.14.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
} }
} }
+115 -1
View File
@@ -9,7 +9,7 @@ th {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
width: 100%; width: 100%;
border: 2px solid black; border: 2px solid #dcdcdd;
} }
th, th,
@@ -17,3 +17,117 @@ td {
width: 1px; width: 1px;
white-space: nowrap; white-space: nowrap;
} }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #000000;
color: #333;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
.title h1 {
color: #00ff00;
text-align: center;
font-size: 50px;
margin-top: 50px;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
Liberation Mono,
Courier New,
monospace;
}
.title img {
width: 135px;
}
.title {
display: flex;
flex-direction: row;
justify-content: center;
gap: 30px;
margin-bottom: 30px;
}
.heading {
color: #ffffff;
}
nav {
display: flex;
flex-direction: row;
justify-content: center;
gap: 30px;
}
.nav a {
color: white;
text-decoration: none;
font-weight: 600;
font-size: 20px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
color: #dcdcdd;
background-color: #24262e;
}
.data-table td {
padding: 10px;
text-align: left;
font-weight: 400;
}
.data-table a {
color: #00dd00;
font-weight: 500;
}
.data-table th {
color: #dcdcdd;
padding: 10px;
border: 1px solid #dcdcdd;
text-align: left;
}
.container {
background-color: #24262e;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.sort {
display: flex;
margin-bottom: 25px;
gap: 15px;
}
.sort a {
background-color: #24262e;
height: 50px;
padding: 0 5 0 5;
border-radius: 10px;
text-decoration: none !important;
}
.sort .btn h3 {
margin: 15 3 0 3;
font-weight: 600;
color: #dcdcdd;
font-size: 15px;
}
.pagination {
width: 100%;
justify-content: center;
margin-top: 20px;
}
+9
View File
@@ -0,0 +1,9 @@
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
</pattern>
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAxYAAAOLCAYAAADNRhU6AAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQEGAAMAAAABAAIAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAAEIAAAAAQAAAQgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAxagAwAEAAAAAQAAA4sAAAAA8/vYWgAAAAlwSFlzAAAomgAAKJoBFzohsgAAA4JpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6SXB0YzR4bXBFeHQ9Imh0dHA6Ly9pcHRjLm9yZy9zdGQvSXB0YzR4bXBFeHQvMjAwOC0wMi0yOS8iPgogICAgICAgICA8ZGM6dGl0bGU+CiAgICAgICAgICAgIDxyZGY6QWx0PgogICAgICAgICAgICAgICA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPmF0dGF0Y2htZW50LlpjcjBOQTwvcmRmOmxpPgogICAgICAgICAgICA8L3JkZjpBbHQ+CiAgICAgICAgIDwvZGM6dGl0bGU+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjI2NDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MjY0PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPjI8L3RpZmY6UGhvdG9tZXRyaWNJbnRlcnByZXRhdGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPElwdGM0eG1wRXh0OkFydHdvcmtUaXRsZT5hdHRhdGNobWVudC5aY3IwTkE8L0lwdGM0eG1wRXh0OkFydHdvcmtUaXRsZT4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cm9mQRgAAEAASURBVHgB7L0HuCxHdS66Ju+c94k6Eorn6ASFIwlJSEhCgBAgiQwmw8MG7OdwMWC/z8bm2s/c+2ziNTa+tq8NlgPXcLHBmGSQSQIEAhQJCgiQBNIJO6fJ/f6/umumZ/bEvSfPqr17uru6wqq/OqxVa60qEQ2KgCKgCCgCikAVBCanj/xqlSR6WRFQBBQBRUARUAQUAUVAEVAEFIHyCAyNnuPs2H3U2bnnon8vn0qvKAKKgCKgCPQ7AuF+B0DbrwgoAoqAIlAegfEdh88MZh0kyMr6+sazy6fUK4qAIqAIKAL9jkCw3wHQ9isCioAioAiURmB29tBZg9HIgwF8KVZWls02OnEOpQwNioAioAgoAorAJgRUsNgEiUYoAoqAIqAIEIFgJPJAfCNuwNhYXRFxHFlZXpbZXeepcKG3iCKgCCgCisAmBEKbYjRCEVAEFAFFoO8RmN11xInH45AlHFlcmBcJBLDhPxiQtbVVmd155k/WV4/d1fdAKQCKgCKgCCgCOQTwmdCgCCgCioAioAjkERif2u9EoxHZ2FiXVDIhqXRKsplMLkEAQkYgGJJs6ph+Q3Ko6IEioAgoAoqAfhT0HlAEFAFFQBHIIbBj9wVLgaAztgqfinh83QgUTiaN67B+MloLa0EL4SIUESf1uH5HcujpgSKgCCgC/Y2A/UL0NwraekVAEVAEFAEZnz50bTabHqN2Yh3aikw6DVOorCdQePIDz7lB0HBScRkaO139LfTeUQQUAUVAETAIqGChN4IioAgoAoqAQSAUklt4sLS0JA6EC/pTmC2ETwWnhuJmA3wvwrGYrC8vyuDY6cs2WveKgCKgCCgC/YuAOm/3b99ryxUBRUARyCGwe99RJwQBYmV1WRLxDUwJBQ0F/SqMhsIzgzKaCpslII6nxIDPRWxq+tSl9bUTt9mrulcEFAFFQBHoPwTUNrb/+lxbrAgoAopAAQK7TrkQbhRpWV1bgV/FBrQVKQgW0E4Yh+3ylk6BcFgcOHZTkxGJDUpq4+f6TSlAVk8UAUVAEegvBPQj0F/9ra1VBBQBRaAAgZHxM50ghAjO/MRZoCTrc9Rmyiz9KUqHQCQEwQLpKXsEQhKNDUly41H9rpSGS2MVAUVAEeh5BHwGsz3fVm2gIqAIKAKKgA+B6R3nvmpgICZDQwNiFsCjhEBNBWd/otlTBaGCa1rQD0Mw7WwoGpUQtBdJCCbj02eXV3H46tZDRUARUAQUgd5DQEeWeq9PtUWKgCKgCNSEwL7TjmIRvA05cfwY0vvkAfpXUKioOvTk/4QEIJNgbYtMVkZHR2Vl6Sf+izXRo4kUAUVAEVAEuhuBqp+N7m6eUq8IKAKKgCJQCoFJLIKXTqVkfu4kNBRMQcHC2yppKooLy2XDLFGcPUqy8NVYlrGJM44XJ9VzRUARUAQUgd5GQAWL3u5fbZ0ioAgoApsQGBs/xxkeHpbl5SWsVZHwppTdyiSBkCoC+S2b5RS1EE/gd4GyZycmDlyzqXKNUAQUAUVAEehZBFRV3bNdqw1TBBQBRWAzAtMzB53JySl59GcPSyIBZ20IBpzZKRIbkFQyWZih4tATVRVFgZoO+mcwZLEGRngQZevK3C4g+qsIKAKKQO8jUPGz0fvN1xYqAoqAItA/COzee8HvRuFoffz4cYlvxLGqNoSKLASEYFhSKczuZBfBM/taxp0oXPg2+mYwG7cQ1rnIJGVsUp25++cO05YqAopAvyOggkW/3wHafkVAEegbBKBD+KMktBLLWFkbUkWD2u0TLPxCBnwtoK6Q5YWTMjGzv1GVNYhmLUYRUAQUAUWgGQjUMiTVjHq1TEVAEVAEFIEWIkATKGooFhYWMXtTwPhWuLM+leP5EU//ibKB1yA8GGGiVCJ+XuhwgX0wIuPjk7I0d79+c0pBpXGKgCKgCPQIAlvx1uuRpmszFAFFQBHoDwQoVLClq6ur+M1CqIDZE9eryGkYeLUo1CQCsFgmLLV50Wba2qBQUzIxeepkfOPkZ4tq0lNFQBFQBBSBHkGgpk9Hj7RVm6EIKAKKQN8hMDN7CCtrh+BXcUxCmA42w0XtGIw2AscBah22EippM1gerps6+Jmh5iIokcigTI1Pjxw7dvfaVmrUPIqAIqAIKAKdjYD6WHR2/yh1ioAioAhsGYHx8XP+OhaLyeLiAhj7jGQwHaxh9g3DT8GgmnBQqepSWoriOOa39TiYdSouiyuLVJtoUAQUAUVAEehBBFRj0YOdqk1SBBQBRYAI7Np9nnPy5ElJp5ISCoddwcLH6Bum33wFtiNglMMaZea0Ia7GIoDZpxzMPhUbGpXE+sP6/SkHncYrAoqAItClCKjGoks7TslWBBQBRaASAjSB2ohvQKhIIFlWso51tM5rECrlb+w1ChmY2pa0wBwrkdyQsSmdhraxGGtpioAioAi0HwEVLNrfB0qBIqAIKAINRWB2xyFw8o6srqy45cLHwjGCRVE1OZOooviGnfqVEiApzE+ONw3t0rxMzh74UcOq0oIUAUVAEVAE2o6AzgrV9i5QAhQBRUARaBwCk1MHnCxmYppfmIdbRco3+5Otw9NY+Hl+e6nRe1NHUUUUZoxAIxKPJyfHJ/Z9I7FxUgWMRmOv5SkCioAi0AYEwm2oU6tUBBQBRUARaAICI6NnfSMUCskKNBWBAFa+5krYQodtf2UULNoVbN0ULkBDOiXLK0ufw1EBhe2iTutVBBQBRUAR2B4Cagq1Pfw0tyKgCCgCHYPA6OjIZQvQVCTinHiJTDzMjnLmTjy3jH2LSK5YHS6CNiedlMjA3oopW0StVqMIKAKKgCKwTQR0lGibAGp2RUARUAQ6AYFdu8935ufnJJmAg7QZMqJQYR2220FhjbKCSRaSgYFhia8/ot+kdnSV1qkIKAKKQIMQUI1Fg4DUYhQBRUARaBcCU9PnOuvr6xAq4p5QQW7dbu2iijJCpY10gUYmwRob8fi6jIyebsQMXtGgCCgCioAi0H0IqPN29/WZUqwIKAKKQA4BTivLGZ8WYQIlxqfCChQe055L2akHkCwCGOMCual0RkZHd48nE/P0u9CgCCgCioAi0GUIcKxIgyKgCCgCikAXIjA9c/DxSCS8010EjyZQZvgfLfGEC2/2pc5smhWASDMFC+yzjON4186YyPeSnUm3UqUIKAKKgCJQDgE1hSqHjMYrAoqAItDBCIyMXDjrOLJzYWFR0uk0ePNufJ1TqMBGeYIB623QoTscmYeUpEERUAQUAUWg2xDoxi9Rt2Gs9CoCioAi0HAEhoazx9fW1iRBZ23y510XPKGCdBv6HUyRCwkDx+l0QkLRXVbc6LqWKcGKgCKgCPQrAipY9GvPa7sVAUWgaxGY3XG+M3dyzhUqTCvIg3czH06Bwt0CQeyDWcmkEjIw/IRublTX3l9KuCKgCCLine truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 630 KiB

-18
View File
@@ -1,18 +0,0 @@
function getOrFailed(key) {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`)
}
return process.env[key]
}
const config = {
host: process.env.HOST || 'localhost',
port: process.env.PORT || 3001,
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
username: getOrFailed('ADMIN_USERNAME'),
password: getOrFailed('ADMIN_PASSWORD'),
}
config.baseUrl = process.env.BASE_URL || `http://${config.host}:${config.port}`
export default config
+34
View File
@@ -0,0 +1,34 @@
import dotenv from 'dotenv'
if (process.env.NODE_ENV !== 'production') {
dotenv.config({ path: '.env.dev' })
}
function getOrFailed(key: string): string {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`)
}
return process.env[key] as string
}
interface Config {
host: string
port: number
db: string
username: string
password: string
baseUrl: string
}
const config: Config = {
host: process.env.HOST || 'localhost',
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3001,
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
username: getOrFailed('ADMIN_USERNAME'),
password: getOrFailed('ADMIN_PASSWORD'),
baseUrl:
process.env.BASE_URL ||
`http://${process.env.HOST || 'localhost'}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 3001}`,
}
export default config
File renamed without changes.
@@ -1,10 +1,12 @@
/* eslint-disable object-shorthand */ import type { Request, Response } from 'express'
import Pagination from '../models/pagination.js' import Pagination from '../models/pagination.js'
import { errorCommands } from '../models/commands.js' import { errorCommands } from '../models/commands.js'
// eslint-disable-next-line import/prefer-default-export export async function commandListController(
export async function commandListController(req, res) { req: Request,
const { nextKey, order } = req.query res: Response
): Promise<void> {
const { nextKey, order } = req.query as { nextKey?: string; order: string }
const itemsPerPage = 100 const itemsPerPage = 100
try { try {
@@ -1,8 +1,8 @@
import { Request, Response } from 'express'
import { userCount } from '../models/users.js' import { userCount } from '../models/users.js'
import { questList } from '../models/quests.js' import { questList } from '../models/quests.js'
// eslint-disable-next-line import/prefer-default-export export async function homeController(req: Request, res: Response) {
export async function homeController(req, res) {
const quests = await questList() const quests = await questList()
const [totalQuests, totalCompleted] = quests.reduce( const [totalQuests, totalCompleted] = quests.reduce(
([total, completed], quest) => [ ([total, completed], quest) => [
@@ -17,8 +17,7 @@ export async function homeController(req, res) {
userCount: await userCount(), userCount: await userCount(),
totalQuests, totalQuests,
totalCompleted, totalCompleted,
completedRate: Math.round((totalCompleted / totalQuests) * 10000) / 100,
} }
payload.completedRate =
Math.round((payload.totalCompleted / payload.totalQuests) * 10000) / 100
res.render('home', payload) res.render('home', payload)
} }
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'
import { questList } from '../models/quests.js' import { questList } from '../models/quests.js'
// eslint-disable-next-line import/prefer-default-export export async function questListController(req: Request, res: Response) {
export async function questListController(req, res) {
const quests = await questList() const quests = await questList()
quests.forEach((quest) => { quests.forEach((quest) => {
quest.failed = quest.started - quest.completed quest.failed = quest.started - quest.completed
@@ -1,8 +1,9 @@
import { Request, Response } from 'express'
import Pagination from '../models/pagination.js' import Pagination from '../models/pagination.js'
import { sessionList, sessionDetail } from '../models/sessions.js' import { sessionList, sessionDetail } from '../models/sessions.js'
export async function sessionListController(req, res) { export async function sessionListController(req: Request, res: Response) {
const { nextKey, order } = req.query const { nextKey, order } = req.query as { nextKey: string; order: string }
const itemsPerPage = 50 const itemsPerPage = 50
try { try {
@@ -21,7 +22,7 @@ export async function sessionListController(req, res) {
} }
} }
export async function sessionDetailController(req, res) { export async function sessionDetailController(req: Request, res: Response) {
const { id } = req.params const { id } = req.params
const session = await sessionDetail(id) const session = await sessionDetail(id)
res.render('sessionDetail', { session }) res.render('sessionDetail', { session })
@@ -1,22 +0,0 @@
import Pagination from '../models/pagination.js'
import { userList } from '../models/users.js'
export async function userListController(req, res) {
const { nextKey, order } = req.query
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const users = await userList(pagination)
const newNextKey = users[users.length - 1]?._id
res.render('users', {
users,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching user data')
}
}
@@ -0,0 +1,36 @@
import { Request, Response } from 'express'
import mongoose from 'mongoose'
import Pagination from '../models/pagination.js'
import { userList, userDetail, idToUser } from '../models/users.js'
export async function userListController(req: Request, res: Response) {
const { nextKey, order } = req.query as { nextKey: string; order: string }
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const users = await userList(pagination)
const newNextKey = users[users.length - 1]?._id
res.render('users', {
users,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching user data')
}
}
export async function userDetailController(req: Request, res: Response) {
const id = new mongoose.Types.ObjectId(req.params.id)
const sessions = await userDetail(id)
try {
const user = await idToUser(id)
res.render('usersDetail', { sessions, user })
} catch (error) {
console.error(error)
res.status(500).send('Error fetching user data')
}
}
File renamed without changes.
+61
View File
@@ -0,0 +1,61 @@
import { Types } from 'mongoose'
export interface User {
username: string
}
export interface SessionDocument {
_id: Types.ObjectId
user: User
quest: string
status: string
createdAt: Date
finishedAt: Date
}
export interface CommandObject {
command: string
pwd: string
stage?: string | null
output: string | undefined
error: string | undefined
createdAt: string
}
export interface SessionObject {
_id: Types.ObjectId
user: string
quest: string
status: string
createdAt: string
finishedAt: string
lastActivityAt: string
usedTime: string
commandCount: number
commands: CommandObject[]
}
export interface SessionDetail {
_id: Types.ObjectId
user: string
quest: string
status: string
createdAt: string
finishedAt: string
usedTime: string
commands: CommandObject[]
}
export interface IUser extends Document {
hashedPassword?: string
socialLogins?: Record<
string,
{
id: string
provider: string
displayName?: string | null | undefined
}
>
}
export interface UserSessionDetail {
_id: Types.ObjectId
quest: string
status: string
createdAt: string
usedTime: string
}
@@ -1,7 +1,8 @@
import { Command } from '@linux-odyssey/models' import { Command } from '@linux-odyssey/models'
import Pagination from './pagination.js'
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export async function errorCommands(pagination) { export async function errorCommands(pagination: Pagination) {
const commands = await Command.aggregate([ const commands = await Command.aggregate([
pagination.match('_id'), pagination.match('_id'),
{ {
@@ -1,74 +0,0 @@
/* eslint-disable lines-between-class-members */
/* eslint-disable class-methods-use-this */
// eslint-disable-next-line max-classes-per-file
import mongoose from 'mongoose'
export const DESC = 'desc'
export const ASC = 'asc'
class Asc {
getOperator() {
return '$gt'
}
getOrder() {
return 1
}
getName() {
return ASC
}
}
class Desc {
getOperator() {
return '$lt'
}
getOrder() {
return -1
}
getName() {
return DESC
}
}
export default class Pagination {
nextKey = null
itemsPerPage = 50
order = null
constructor(order, itemsPerPage = 50, nextKey = null) {
this.itemsPerPage = itemsPerPage
if (!order || order === DESC) {
this.order = new Desc()
} else if (order === ASC) {
this.order = new Asc()
} else {
throw new Error('order must be asc or desc')
}
if (nextKey) {
this.nextKey = new mongoose.Types.ObjectId(nextKey)
}
}
match(field) {
const stage = this.nextKey
? { [field]: { [this.order.getOperator()]: this.nextKey } }
: {}
return { $match: stage }
}
sort(field) {
return { $sort: { [field]: this.order.getOrder() } }
}
limit() {
return { $limit: this.itemsPerPage }
}
getOrder() {
return this.order.getName()
}
}
@@ -0,0 +1,88 @@
/* eslint-disable class-methods-use-this */
// eslint-disable-next-line max-classes-per-file
import mongoose, { Types } from 'mongoose'
export const DESC: string = 'desc'
export const ASC: string = 'asc'
interface Order {
getOperator(): string
getOrder(): number
getName(): string
}
class Asc implements Order {
getOperator(): string {
return '$gt'
}
getOrder(): number {
return 1
}
getName(): string {
return ASC
}
}
class Desc implements Order {
getOperator(): string {
return '$lt'
}
getOrder(): 1 | -1 {
return -1
}
getName(): string {
return DESC
}
}
export default class Pagination {
nextKey: Types.ObjectId | null = null
itemsPerPage: number = 50
order: Order
constructor(
order: string,
itemsPerPage: number = 50,
nextKey: string | null = null
) {
this.itemsPerPage = itemsPerPage
if (!order || order === DESC) {
this.order = new Desc()
} else if (order === ASC) {
this.order = new Asc()
} else {
throw new Error('order must be asc or desc')
}
if (nextKey) {
this.nextKey = new mongoose.Types.ObjectId(nextKey)
}
}
match(field: string) {
const stage: { [key: string]: { [key: string]: Types.ObjectId } } = this
.nextKey
? {
[field]: {
[this.order ? this.order.getOperator() : '$gt']: this.nextKey,
},
}
: {}
return { $match: stage }
}
sort(field: string): { $sort: Record<string, 1 | -1> } {
return { $sort: { [field]: this.order.getOrder() as 1 | -1 } }
}
limit() {
return { $limit: this.itemsPerPage }
}
getOrder() {
return this.order.getName()
}
}
-83
View File
@@ -1,83 +0,0 @@
import { Command, Session } from '@linux-odyssey/models'
function formatTime(time) {
const seconds = Math.floor(time / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
}
export async function sessionList(pagination) {
const sessions = await Session.aggregate([
pagination.match('_id'),
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'user',
},
},
{ $unwind: '$user' },
{
$lookup: {
from: 'commands',
localField: '_id',
foreignField: 'session',
as: 'commands',
},
},
pagination.sort('_id'),
pagination.limit(),
])
return sessions.map(
({
_id,
user,
quest,
status,
createdAt,
finishedAt,
commands,
lastActivityAt,
}) => {
return {
_id,
user: user.username,
quest,
status,
createdAt: createdAt?.toLocaleString(),
finishedAt: finishedAt?.toLocaleString(),
lastActivityAt: lastActivityAt?.toLocaleString(),
usedTime: formatTime(lastActivityAt - createdAt),
commandCount: commands?.length,
}
}
)
}
export async function sessionDetail(id) {
const session = await Session.findById(id).populate('user').exec()
const commands = (await Command.find({ session: id })).map(
({ command, pwd, output, error, createdAt, stage }) => ({
command: command?.slice(0, 20),
pwd,
stage,
output: output?.slice(0, 50),
error: error?.slice(0, 50),
createdAt: createdAt?.toLocaleString(),
})
)
const { _id, user, quest, status, createdAt, finishedAt } = session
return {
_id,
user: user.username,
quest,
status,
createdAt: createdAt?.toLocaleString(),
finishedAt: finishedAt?.toLocaleString(),
usedTime: finishedAt ? formatTime(finishedAt - createdAt) : '',
commands,
}
}
+99
View File
@@ -0,0 +1,99 @@
import { Command, Session } from '@linux-odyssey/models'
import Pagination from './pagination.js'
import {
CommandObject,
SessionDetail,
SessionDocument,
SessionObject,
} from '../interface.js'
function formatTime(time: number) {
const seconds = Math.floor(time / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
}
export async function sessionList(
pagination: Pagination
): Promise<SessionObject[]> {
const sessions = await Session.aggregate([
pagination.match('_id'),
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'user',
},
},
{ $unwind: '$user' },
{
$lookup: {
from: 'commands',
localField: '_id',
foreignField: 'session',
as: 'commands',
},
},
pagination.sort('_id'),
pagination.limit(),
])
return sessions.map((session) => {
const {
_id,
user,
quest,
status,
createdAt,
finishedAt,
commands,
lastActivityAt,
} = session
return {
_id,
user: user.username, // Assuming `user` is always populated.
quest,
status,
createdAt: createdAt?.toLocaleString(),
finishedAt: finishedAt?.toLocaleString(),
lastActivityAt: lastActivityAt?.toLocaleString(),
usedTime:
lastActivityAt && createdAt
? formatTime(lastActivityAt.getTime() - createdAt.getTime())
: 'N/A', // Ensure both dates are present
commandCount: commands?.length || 0,
commands,
}
})
}
export async function sessionDetail(id: string): Promise<SessionDetail> {
const session = (await Session.findById(id)
.populate('user')
.exec()) as unknown as SessionDocument
const commands: CommandObject[] = (await Command.find({ session: id })).map(
({ command, pwd, output, error, createdAt, stage }): CommandObject => ({
command: command?.slice(0, 20),
pwd,
stage,
output: output?.slice(0, 50),
error: error?.slice(0, 50),
createdAt: createdAt?.toLocaleString(),
})
)
const { _id, user, quest, status, createdAt, finishedAt } = session
return {
_id,
user: user.username,
quest,
status,
createdAt: createdAt?.toLocaleString(),
finishedAt: finishedAt?.toLocaleString(),
usedTime: finishedAt
? formatTime(finishedAt.getTime() - createdAt.getTime())
: '',
commands,
}
}
-49
View File
@@ -1,49 +0,0 @@
import { Session, User } from '@linux-odyssey/models'
function loginMethods(user) {
const methods = []
if (user.hashedPassword) methods.push('password')
if (user.socialLogins) methods.push(...Object.keys(user.socialLogins))
return methods
}
export function userCount() {
return User.find().count()
}
export async function userList(pagination) {
const users = await Session.aggregate([
pagination.match('user'),
{
$group: {
_id: '$user',
count: { $sum: 1 },
lastActivityAt: { $max: '$lastActivityAt' },
},
},
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'userData',
},
},
{ $unwind: '$userData' },
pagination.sort('userData._id'),
pagination.limit(),
])
return users.map(({ userData, count, lastActivityAt }) => {
const { _id, username, email, createdAt } = userData
return {
_id,
username,
email,
createdAt: createdAt?.toLocaleString(),
method: loginMethods(userData),
sessionCount: count,
lastActivityAt: lastActivityAt?.toLocaleString(),
}
})
}
+88
View File
@@ -0,0 +1,88 @@
import { Session, User } from '@linux-odyssey/models'
import { Types } from 'mongoose'
import Pagination from './pagination.js'
import { IUser, UserSessionDetail } from '../interface.js'
function loginMethods(user: IUser): string[] {
const methods: string[] = []
if (user.hashedPassword) methods.push('password')
if (user.socialLogins && typeof user.socialLogins === 'object') {
methods.push(...Object.keys(user.socialLogins))
}
return methods
}
function formatTime(time: number) {
const seconds = Math.floor(time / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
}
export function userCount() {
return User.countDocuments({})
}
export async function userList(pagination: Pagination) {
const users = await Session.aggregate([
pagination.match('user'),
{
$group: {
_id: '$user',
count: { $sum: 1 },
lastActivityAt: { $max: '$lastActivityAt' },
},
},
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'userData',
},
},
{ $unwind: '$userData' },
pagination.sort('userData._id'),
pagination.limit(),
])
return users.map(({ userData, count, lastActivityAt }) => {
const { _id, username, email, createdAt } = userData
return {
_id,
username,
email,
createdAt: createdAt?.toLocaleString(),
method: loginMethods(userData),
sessionCount: count,
lastActivityAt: lastActivityAt?.toLocaleString(),
}
})
}
export async function userDetail(
id: Types.ObjectId
): Promise<UserSessionDetail[]> {
const sessions = await Session.find({
user: id,
})
return sessions.map((session): UserSessionDetail => {
const { _id, quest, status, createdAt, lastActivityAt } = session
return {
_id,
quest,
status,
createdAt: createdAt?.toLocaleString(),
usedTime:
lastActivityAt && createdAt
? formatTime(lastActivityAt.getTime() - createdAt.getTime())
: 'N/A', // Ensure both dates are present
}
})
}
export async function idToUser(id: Types.ObjectId): Promise<String> {
const user = await User.findOne({
_id: id,
})
return user?.username || 'User not found'
}
@@ -1,7 +1,10 @@
import { Router } from 'express' import { Router } from 'express'
import { homeController } from './controllers/index.js' import { homeController } from './controllers/index.js'
import { userListController } from './controllers/users.js' import {
userListController,
userDetailController,
} from './controllers/users.js'
import { questListController } from './controllers/quests.js' import { questListController } from './controllers/quests.js'
import { commandListController } from './controllers/commands.js' import { commandListController } from './controllers/commands.js'
import { import {
@@ -13,6 +16,7 @@ const router = Router()
router.get('/', homeController) router.get('/', homeController)
router.get('/users', userListController) router.get('/users', userListController)
router.get('/users/:id', userDetailController)
router.get('/quests', questListController) router.get('/quests', questListController)
router.get('/commands', commandListController) router.get('/commands', commandListController)
router.get('/sessions', sessionListController) router.get('/sessions', sessionListController)
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "./dist"
},
"include": ["src"],
"references": [{ "path": "../models" }],
"ts-node": {
"swc": true
}
}
+14 -8
View File
@@ -1,11 +1,15 @@
<h1>Commands</h1> <h1 class='heading'>Commands</h1>
<div> <div>
<h2>Error commands</h2> <h2 class='heading'>Error commands</h2>
<div class='pagination'> <div class='sort'>
<a href='/commands?order=asc'>Ascending</a> <a href='/commands?order=asc'><div class='btn'>
<a href='/commands?order=desc'>Descending</a> <h3>Ascending</h3>
</div></a>
<a href='/commands?order=desc'><div class='btn'>
<h3>Descending</h3>
</div></a>
</div> </div>
<table> <table class='data-table'>
<tr> <tr>
<th>Command</th> <th>Command</th>
<th>User</th> <th>User</th>
@@ -25,7 +29,9 @@
</tr> </tr>
{{/each}} {{/each}}
</table> </table>
<div class='pagination'> <div class='pagination sort'>
<a href='/commands?nextKey={{nextKey}}&order={{order}}'>Next</a> <a href='/commands?nextKey={{nextKey}}&order={{order}}'><div class='btn'>
<h3>Next</h3>
</div></a>
</div> </div>
</div> </div>
+8 -3
View File
@@ -1,7 +1,11 @@
<body>
<div class='title'>
<img src='icon_totem.svg' class='styled-image' />
<h1>Welcome to {{title}}</h1> <h1>Welcome to {{title}}</h1>
<div> </div>
<h2>Overview</h2> <div class='container'>
<table> <h2 class='heading'>Overview</h2>
<table class='data-table'>
<tr> <tr>
<th>Registered Users</th> <th>Registered Users</th>
<td>{{userCount}}</td> <td>{{userCount}}</td>
@@ -20,3 +24,4 @@
</tr> </tr>
</table> </table>
</div> </div>
</body>
@@ -4,14 +4,12 @@
<link rel='stylesheet' href='/css/style.css' /> <link rel='stylesheet' href='/css/style.css' />
</head> </head>
<body> <body>
<nav> <nav class='nav'>
<ul> <div><a href='/'>Home</a></div>
<li><a href='/'>Home</a></li> <div><a href='/users'>Users</a></div>
<li><a href='/users'>Users</a></li> <div><a href='/quests'>Quests</a></div>
<li><a href='/quests'>Quests</a></li> <div><a href='/commands'>Commands</a></div>
<li><a href='/commands'>Commands</a></li> <div><a href='/sessions'>Sessions</a></div>
<li><a href='/sessions'>Sessions</a></li>
</ul>
</nav> </nav>
{{{body}}} {{{body}}}
</body> </body>
+2 -2
View File
@@ -1,6 +1,6 @@
<h1>Quests</h1> <h1 class='heading'>Quests</h1>
<div> <div>
<table> <table class='data-table'>
<tr> <tr>
<th>Id</th> <th>Id</th>
<th>Started</th> <th>Started</th>
@@ -1,7 +1,7 @@
<h1>{{title}}</h1> {{#with session}}<h1 class='heading'>Session {{_id}}</h1>
<div> <div>
{{#with session}}
<table> <table class='data-table'>
<tr> <tr>
<th>Quest</th> <th>Quest</th>
<td>{{quest}}</td> <td>{{quest}}</td>
@@ -32,9 +32,9 @@
</tr> </tr>
</table> </table>
<h2>Commands</h2> <h2 class='heading'>Commands</h2>
<table> <table class='data-table'>
<tr> <tr>
<th>Stage</th> <th>Stage</th>
<th>PWD</th> <th>PWD</th>
@@ -54,5 +54,5 @@
</tr> </tr>
{{/each}} {{/each}}
</table> </table>
{{/with}}
</div> </div>{{/with}}
+13 -7
View File
@@ -1,10 +1,14 @@
<h1>Sessions</h1> <h1 class='heading'>Sessions</h1>
<div class='pagination'> <div class='sort'>
<a href='/sessions?order=asc'>Ascending</a> <a href='/sessions?order=asc'><div class='btn'>
<a href='/sessions?order=desc'>Descending</a> <h3>Ascending</h3>
</div></a>
<a href='/sessions?order=desc'><div class='btn'>
<h3>Descending</h3>
</div></a>
</div> </div>
<div> <div>
<table> <table class='data-table'>
<tr> <tr>
<th>User</th> <th>User</th>
<th>Quest</th> <th>Quest</th>
@@ -29,6 +33,8 @@
{{/each}} {{/each}}
</table> </table>
</div> </div>
<div class='pagination'> <div class='pagination sort'>
<a href='/sessions?nextKey={{nextKey}}&order={{order}}'>Next</a> <a href='/sessions?nextKey={{nextKey}}&order={{order}}'><div class='btn'>
<h3>Next</h3>
</div></a>
</div> </div>
+14 -8
View File
@@ -1,10 +1,14 @@
<h1>Users List</h1> <h1 class='heading'>Users List</h1>
<div class='pagination'> <div class='sort'>
<a href='/users?order=asc'>Ascending</a> <a href='/users?order=asc'><div class='btn'>
<a href='/users?order=desc'>Descending</a> <h3>Ascending</h3>
</div></a>
<a href='/users?order=desc'><div class='btn'>
<h3>Descending</h3>
</div></a>
</div> </div>
<div> <div>
<table> <table class='data-table'>
<tr> <tr>
<th>Username</th> <th>Username</th>
<th>Last Activity</th> <th>Last Activity</th>
@@ -15,7 +19,7 @@
</tr> </tr>
{{#each users}} {{#each users}}
<tr> <tr>
<td href='/users/{{_id}}'>{{username}}</td> <td><a href='/users/{{_id}}'>{{username}}</a></td>
<td>{{lastActivityAt}}</td> <td>{{lastActivityAt}}</td>
<td>{{sessionCount}}</td> <td>{{sessionCount}}</td>
<td>{{email}}</td> <td>{{email}}</td>
@@ -25,6 +29,8 @@
{{/each}} {{/each}}
</table> </table>
</div> </div>
<div class='pagination'> <div class='pagination sort'>
<a href='/users?nextKey={{nextKey}}&order={{order}}'>Next</a> <a href='/users?nextKey={{nextKey}}&order={{order}}'><div class='btn'>
<h3>Next</h3>
</div></a>
</div> </div>
@@ -0,0 +1,21 @@
<h1 class='heading'>{{user}}'s Sessions History</h1>
<div>
<table class='data-table'>
<tr>
<th>Quest</th>
<th>Status</th>
<th>Created At</th>
<th>Used Time</th>
<th>Session</th>
</tr>
{{#each sessions}}
<tr>
<td>{{quest}}</td>
<td>{{status}}</td>
<td>{{createdAt}}</td>
<td>{{usedTime}}</td>
<td><a href='/sessions/{{_id}}'>Detail</a></td>
</tr>
{{/each}}
</table>
</div>
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff. Show more