Compare commits

...

50 Commits

Author SHA1 Message Date
Settel
1243dec6b9 doc: fix typo 2024-02-19 08:11:04 +01:00
Settel
a7602eed6e version: bump to 3.4
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-02-19 08:10:09 +01:00
Settel
ae2f0407fa feat: use podman, if available or docker otherwise
doc: update README.md
2024-02-19 08:09:42 +01:00
Settel
c60976a06a version: bump to 3.3
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-18 19:24:54 +01:00
Settel
ad58cbc577 feat: add link to KnowYT website (instead of link to video) 2024-02-18 19:24:42 +01:00
Settel
40cdfe42be version: bump to 3.2 2024-02-18 14:10:17 +01:00
Settel
6f69ca91af chore: fix linter warnings 2024-02-18 14:10:17 +01:00
Settel
113a5fb16e chore: update npm libraries
bugfix: fix warning about ambiguous imports or types
2024-02-18 14:10:17 +01:00
Settel
a0c8e9592f version: bump to 3.1 2024-02-18 14:10:17 +01:00
Settel
512edbc62b feat: issue a warning on admin page when there're no teams yet 2024-02-18 14:10:17 +01:00
Settel
2bcb322488 bugfix: clear cameo cookie on logout, too 2024-02-18 14:10:17 +01:00
Settel
3a19c5b3f7 bugfix: fix typo 2024-02-18 14:10:17 +01:00
Settel
bde47843bf version: bump to 3.0 2024-02-18 14:10:17 +01:00
Settel
33bd7e8bab feat: use JWT for authentication
BREAKING CHANGE: changes format of cookie
2024-02-18 14:10:12 +01:00
Settel
bcc446ed16 feat: use JWT for authentication (WIP)
BREAKING CHANGE: changes format of cookie
2024-02-18 14:09:44 +01:00
Settel
0f0b2ede64 refactor: remove IsCameo flag from JSON 2024-02-18 10:34:13 +01:00
Settel
a06e50b704 refactor: extract checkAuthCode() 2024-02-18 10:29:20 +01:00
Settel
ca609fad94 refactor: move Cameo() to separate file 2024-02-18 10:26:31 +01:00
Settel
ca4dddec2e bugfix: fix linter warnings 2024-02-18 10:24:40 +01:00
Settel
a5257506da version: bump to 2.28
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-02 21:54:13 +01:00
Settel
8375a3b5bd chore: replaced deprecated package build-url with query-string 2024-02-02 21:49:03 +01:00
Settel
bad7e137ad chore: upgrade from ohmyfetch to ofetch after author renamed the package 2024-02-02 21:25:42 +01:00
Settel
a8ba49bda4 bugfix: fix warning about wrong import of type 2024-02-02 21:19:20 +01:00
Settel
163b1bbfc5 chore: update .woodpecker.yml to match new syntax requirements
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-01 12:10:47 +01:00
Settel
e25f803c8e chore: update .woodpecker.yml to match new syntax requirements
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
2024-02-01 12:09:05 +01:00
Settel
7ed8c73112 chore: update .woodpecker.yml to match new syntax requirements
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-01 12:07:21 +01:00
Settel
1d235c1583 chore: update .woodpecker.yml to match new syntax requirements
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-01 12:04:40 +01:00
Settel
96c04835bc chore: update README.md
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-01 11:53:36 +01:00
Settel
0b7db9297c version: bump to 2.27 2024-02-01 11:40:09 +01:00
Settel
3b86b04de2 feat: add link to explaination video 2024-02-01 11:39:49 +01:00
Settel
e6208e9ec2 chore: update Copyright notice to 2024 2024-02-01 11:39:35 +01:00
Settel
0e6fef049f doc: update README.md 2023-06-30 21:24:17 +02:00
Settel
b5191039ee version: bump to 2.26
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2023-06-19 13:13:22 +02:00
Settel
5d2d33ebe3 bugfix: fix typo 2023-06-19 13:13:11 +02:00
Settel
967cf68a7f version: bump to 2.25
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-06-19 10:42:17 +02:00
Settel
24c93623dc feat: show number of quotes played by player 2023-06-19 10:42:00 +02:00
Settel
fee9caff08 version: bump to 2.24
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-06-18 15:28:00 +02:00
Settel
c86dc32335 bugfix: read version from config.public.version instead of config.version 2023-06-18 15:27:46 +02:00
Settel
915c78dd58 version: bump to 2.23
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-06-18 15:08:28 +02:00
Settel
6787f1c3f8 feat: show checkmark for already played quotes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-06-18 15:07:45 +02:00
Settel
4cd1bf6984 version: bump to 2.22
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-06-18 11:39:48 +02:00
Settel
959ba20d98 bugfix: create missing directory 2023-06-18 11:39:48 +02:00
Settel
695d007fd7 chores: use pnpm 8.x.y, update pnpm-lock.yaml
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-06-18 11:36:49 +02:00
Settel
0d255c1ee5 bugfix: install pnpm in CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-06-18 11:31:25 +02:00
Settel
9ba74f5b2f version: bump to 2.21
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-06-18 11:21:17 +02:00
Settel
b359d30d8c feat: use pnpm instead of yarn 2023-06-18 11:20:50 +02:00
Settel
0f805962a4 chores: upgrade npm packages 2023-06-17 20:31:32 +02:00
Settel
7e4c344ec1 feat: publish to docker.io 2023-06-04 14:31:02 +02:00
Settel
7de4f1cad8 version: bump to 2.20
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline failed
2023-06-04 11:40:46 +02:00
Settel
fcfed00d5b feat: check for slirp4netns tool during setup (needed for rootless Podman) 2023-06-04 11:40:31 +02:00
45 changed files with 7144 additions and 4633 deletions

View File

@ -1,18 +1,23 @@
pipeline:
when:
branch: prod
steps:
frontend:
image: node:16-alpine
image: node:18-alpine
commands:
- ( cd client && yarn install )
- ( cd client/ && yarn generate )
- yarn global add pnpm
- ( cd client && pnpm run setup )
- ( cd client/ && pnpm run generate )
- mkdir -p client/.output/public/
backend:
image: golang:1.18-alpine
image: golang:1.20-alpine
commands:
- apk add go-bindata
- apk add make
- make -C server setup
- make -C server build
- mkdir -p build/files/data
- cp server/knowyt build/files/
build-publish:
@ -36,5 +41,3 @@ pipeline:
from_secret: matrix.bw-messenger.de.username
password:
from_secret: matrix.bw-messenger.de.password
branches: prod

View File

@ -1,7 +1,8 @@
TMUX_SESSION=knowyt
VERSION=$(shell grep version client/package.json | cut -d\" -f4)
CONTAINER=$(shell which podman || which docker)
.PHONY: info run-all run-server run-client run-tmux build podman clean
.PHONY: info setup run-all run-server run-client run-tmux build clean
info:
@echo available targets:
@ -9,7 +10,16 @@ info:
setup:
@echo "I checking for tools"
@for binary in go go-bindata tmux node yarn npx podman pexec inotifyloop inotifywait newuidmap; do \
@echo -n " container: " ; \
if ! [ -x "$(CONTAINER)" ]; then \
echo "neither podman nor docker found" ;\
exit 1 ;\
else \
echo "$(CONTAINER)" ;\
fi
@for binary in go go-bindata tmux node pnpm npx \
pexec inotifyloop inotifywait \
newuidmap slirp4netns; do \
echo -n " $$binary: " ; \
if ! which "$$binary"; then \
echo "not found" ;\
@ -17,7 +27,7 @@ setup:
fi ;\
done
@echo "I installing client dependencies"
( cd client && yarn install )
( cd client && pnpm run setup )
@echo "I create client output directory"
mkdir -p client/.output/public/
@echo "I installing server dependencies"
@ -39,33 +49,36 @@ run-tmux:
tmux attach-session -t "$(TMUX_SESSION)"
run-client:
(cd client/ && yarn dev)
(cd client/ && pnpm dev)
run-server:
$(MAKE) -C server run-loop
build:
echo $(VERSION)
(cd client/ && yarn generate)
(cd client/ && pnpm run generate)
$(MAKE) -C server build
$(MAKE) podman-build
$(MAKE) podman-save
$(MAKE) container-build
podman-build:
container-build:
mkdir -p build/files/data
cp server/knowyt build/files/
podman build --tag knowyt:$(VERSION) .
$(CONTAINER) build --tag knowyt:$(VERSION) .
podman-save:
container-save:
rm -f build/knowyt-$(VERSION).tar
podman save knowyt:$(VERSION) -o build/knowyt-$(VERSION).tar
$(CONTAINER) save knowyt:$(VERSION) -o build/knowyt-$(VERSION).tar
ls -lh build/knowyt-$(VERSION).tar
podman-run:
podman run --rm -it -p 32039:32039 -v $$(pwd)/server/data/:/data --name knowyt knowyt:$(VERSION)
container-run:
$(CONTAINER) run --rm -it -p 8080:32039 -v $$(pwd)/server/data/:/data --name knowyt knowyt:$(VERSION)
podman-stop:
podman stop knowyt
container-stop:
$(CONTAINER) stop knowyt
container-publish:
$(CONTAINER) push knowyt:$(VERSION) docker.io/settel/knowyt:$(VERSION)
$(CONTAINER) push knowyt:$(VERSION) docker.io/settel/knowyt:latest
clean:
rm -rf client/.output/

View File

@ -1,22 +1,45 @@
# Know Your Teammates
Know Your Teammates is a free team building game for 3-20 players that can be played in a browser.
It has two phases: during collection phase, the players are asked to enter 3-5 facts about themselves, eg. hobbies, books, movies they enjoy, places they have visited or other fun facts. In play phase, one fact is presented to all players and they have to guess who wrote it. The real fun is in the discussion following it :-)
[Watch video (3:30mins)](https://www.sirlab.de/knowyt/know-your-teammates.mp4)
See website https://www.sirlab.de/linux/games/knowyt/
## Installation
install dependencies and check for missing tools
```
make setup
```
## run (development mode)
start client and server in a split terminal window
```
make run-tmux
```
This will start frontend (port 3000) and backend (port 32039) as two separate services. Hot reload is active, changes will take effect immediately.
## build podman/docker image
Point your browser at [http://localhost:3000/](http://localhost:3000/) to start playing.
## build and run podman/docker image
```
make setup # only needed on first run
make build
make container-run
```
@ -27,4 +50,6 @@ AGPLv3 (GNU Affero General Public License)
## Author
© 2021-2022 Achim Settelmeier <knowyt@m1.sirlab.de>
© 2021-2024 Achim Settelmeier <knowyt@m1.sirlab.de>
https://www.sirlab.de/

View File

@ -1,25 +1,35 @@
{
"name": "knowyt",
"version": "2.19",
"version": "3.4",
"private": true,
"scripts": {
"lint": "tsc-strict",
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview"
"setup": "pnpm install"
},
"devDependencies": {
"nuxt": "^3.0.0",
"sass": "^1.56.1",
"sass-loader": "^13.2.0",
"typescript-strict-plugin": "^2.1.0"
"@types/node": "^18.19.17",
"http-proxy": "^1.18.1",
"nuxt": "^3.10.2",
"sass": "^1.71.0",
"sass-loader": "^13.3.3",
"typescript-strict-plugin": "^2.3.0",
"webpack": "^5.90.2"
},
"dependencies": {
"@pinia/nuxt": "^0.4.4",
"build-url": "^6.0.1",
"nuxt-icons": "^3.0.0",
"typescript": "^4.9.3",
"vue-contenteditable": "^4.1.0"
"@pinia/nuxt": "^0.4.11",
"@vue/reactivity": "^3.4.19",
"@vue/runtime-core": "^3.4.19",
"@vue/runtime-dom": "^3.4.19",
"@vue/shared": "^3.4.19",
"nuxt-icons": "^3.2.1",
"ofetch": "^1.3.3",
"query-string": "^8.2.0",
"typescript": "^5.3.3",
"vue": "^3.4.19",
"vue-contenteditable": "^4.1.0",
"vue-router": "^4.2.5"
}
}

6715
client/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="100"
viewBox="0 0 128 100"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="checkmark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="2.3786088"
inkscape:cx="10.720552"
inkscape:cy="21.441105"
inkscape:window-width="1920"
inkscape:window-height="1181"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"
showborder="true" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#4bc417;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 93.253283,6.1273107 C 100.05266,14.282038 108.74264,17.439899 117.91216,22.197816 104.79242,27.143529 52.350943,76.132207 44.577742,96.156471 42.587301,88.868507 16.14146,63.021362 10.575889,61.270758 c 6.682863,-3.009693 20.41554,-9.619427 23.978573,-15.275893 0.758717,5.579065 9.653081,21.558694 9.653081,21.558694 8.596139,-4.086698 48.882074,-51.182042 49.04574,-61.4262483 z"
id="path790"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -27,6 +27,7 @@ const createNewQuote = () => {
newQuote.value = {
id: ':new:' + Date.now(),
quote: '',
isPlayed: false,
}
}

View File

@ -1,13 +1,13 @@
<template>
<div class="copyright-notice__version" @click="openInfoModal">
v{{ config.version }}, © 2021-2023, Settel
v{{ config.public.version }}, © 2021-2024, Settel
</div>
<InfoModal v-if="showInfoModal" @close="closeInfoModal" />
</template>
<script setup lang="ts">
import { useRuntimeConfig } from '#app'
import { ref } from 'vue'
import { ref, computed } from 'vue'
const config = useRuntimeConfig()

View File

@ -11,9 +11,10 @@
<script setup lang="ts">
import { useRouter } from '#app'
import { ref, Ref } from 'vue'
import { ref } from 'vue'
import useAuth from '@/composables/useAuth';
import useI18n from '@/composables/useI18n';
import type { Ref } from 'vue'
const { $t } = useI18n({
'login': { en: 'log in', de: 'login'},

View File

@ -13,7 +13,7 @@
</template>
<script setup lang="ts">
import { Player } from '@/composables/engine.d';
import type { Player } from '@/composables/engine.d';
defineProps<{
player: Player,

View File

@ -1,7 +1,10 @@
<template>
<div :class="cssClasses">
<div v-if="quote.isPlayed" class="quote-card__marker__is-played">
<nuxt-icon name="checkmark" filled />
</div>
<div v-if="editable && !isEditMode" class="quote-card__action-buttons">
<QuoteCardActionButton @click="editQuote" icon="edit" />
<QuoteCardActionButton v-if="!quote.isPlayed" @click="editQuote" icon="edit" />
<QuoteCardActionButton @click="deleteQuote" icon="trash" />
</div>
<div class="quote-card__text-container">
@ -180,5 +183,18 @@ const keydown = async (ev: KeyboardEvent) => {
top: -24px;
display: flex;
}
&__marker__is-played {
position: absolute;
left: -28px;
top: -40px;
width: 128px;
height: 100px;
.nuxt-icon svg {
width: unset;
height: unset;
}
}
}
</style>

View File

@ -1,32 +1,37 @@
<template>
<AdminInfoTile title="Games">
<table class="gameinfos-tile__table">
<tr class="gameinfos-tile__row">
<th class="gameinfos-tile__table-head">{{ $t('team-name')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('lang')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('state')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('num-players')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('gamemasters')}}</th>
</tr>
<tr
:class="{ 'gameinfos-tile__row': true, 'gameinfos-tile__row__disabled': game.state == 'disabled' }"
v-for="game in games"
:key="game.id"
@click="selectGame(game)"
>
<div>
<AdminInfoTile v-if="gamesCount > 0" title="Games">
<table class="gameinfos-tile__table">
<tr class="gameinfos-tile__row">
<th class="gameinfos-tile__table-head">{{ $t('team-name')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('lang')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('state')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('num-players')}}</th>
<th class="gameinfos-tile__table-head">{{ $t('gamemasters')}}</th>
</tr>
<tr
:class="{ 'gameinfos-tile__row': true, 'gameinfos-tile__row__disabled': game.state == 'disabled' }"
v-for="game in games"
:key="game.id"
@click="selectGame(game)"
>
<td class="gameinfos-tile__cell">{{ game.name }}</td>
<td class="gameinfos-tile__cell">{{ game.lang }}</td>
<td class="gameinfos-tile__cell">{{ game.state }}</td>
<td class="gameinfos-tile__cell">{{ game.players.length }}</td>
<td class="gameinfos-tile__cell">{{ getGamemastersFromGame(game).join(', ') }}</td>
</tr>
</table>
</AdminInfoTile>
</tr>
</table>
</AdminInfoTile>
<AdminInfoTile v-if="gamesCount == 0" :title="$t('no-games-1')">
<p>{{ $t('no-games-2') }}</p>
</AdminInfoTile>
</div>
</template>
<script setup lang="ts">
import { navigateTo } from "#app"
import { ref } from 'vue'
import { ref, computed } from 'vue'
import useI18n from '@/composables/useI18n'
import useEngine from '@/composables/useEngine'
import type { GameInfo } from '@/composables/engine.d'
@ -37,11 +42,16 @@ const { $t } = useI18n({
state: { en: 'State', de: 'Status' },
'num-players': { en: '# Players', de: '# Spieler' },
gamemasters: { en: 'Gamemaster(s)', de: 'Gamemaster(s)' },
'no-games-1': { en: 'The list of teams is empty', de: 'Die Liste der Teams ist noch leer'},
'no-games-2': { en: 'Log out and click the "create team" button on the start page.', de: 'Logge dich aus und klicke auf der Startseite auf "Team erstellen".'},
})
const { fetchGameInfos, cameo } = useEngine()
const games = ref(await fetchGameInfos())
const gamesCount = computed(() => {
return Object.keys(games.value).length
})
const getGamemastersFromGame = (game: GameInfo) => game.players.filter((player) => player.role === 'gamemaster').map((player) => player.name)
const selectGame = async (game: GameInfo): Promise<void> => {

View File

@ -30,7 +30,7 @@ const emit = defineEmits(['icon-top-click', 'icon-bottom-click'])
&__container {
position: relative;
min-width: 300px;
margin: 40px;
margin: 16px;
padding: 16px 30px;
background-color: $admin-tile-background-color;
border: $admin-tile-border;

View File

@ -23,7 +23,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { PlayerEdit } from '@/composables/engine.d'
import type { PlayerEdit } from '@/composables/engine.d'
import type { Button } from '@/components/admin/PlayerModal'
const props = defineProps<{

View File

@ -3,10 +3,10 @@
<AdminInfoTile title="Players" icon-top="reload" @icon-top-click="reload" icon-bottom="add" @icon-bottom-click="addPlayer">
<table class="players-tile__table">
<tr>
<th class="players-tile__table-head">{{ $t('name') }}:</th>
<th class="players-tile__table-head">{{ $t('num-quotes') }}</th>
<th class="players-tile__table-head">{{ $t('score') }}</th>
<th class="players-tile__table-head">{{ $t('last-logged-in') }}</th>
<th class="players-tile__table-head players-tile__cell">{{ $t('name') }}:</th>
<th class="players-tile__table-head players-tile__cell">{{ $t('num-quotes') }}</th>
<th class="players-tile__table-head players-tile__cell">{{ $t('score') }}</th>
<th class="players-tile__table-head players-tile__cell">{{ $t('last-logged-in') }}</th>
</tr>
<tr v-for="player in players" class="players-tile__row" :key="player.id" @click="editPlayer(player)">
<td class="players-tile__cell">
@ -15,7 +15,7 @@
<nuxt-icon name="crown" filled />
</div>
</td>
<td class="players-tile__cell">{{ player.numQuotes }}</td>
<td class="players-tile__cell">{{ player.numQuotesPlayed }} / {{ player.numQuotes }}</td>
<td class="players-tile__cell">{{ player.score }}</td>
<td class="players-tile__cell">{{ !player.isIdle ? 'online' : player.lastLoggedIn === 0 ? '-' : datetime(player.lastLoggedIn) }}</td>
</tr>
@ -42,7 +42,7 @@ const emit = defineEmits(['update'])
const { $t } = useI18n({
name: { en: 'Name', de: 'Name' },
'num-quotes': { en: '# quotes', de: '# Quotes' },
'num-quotes': { en: '# quotes played', de: '# Aussagen gespielt' },
'score': { en: 'Score', de: 'Score' },
'last-logged-in': { en: 'last logged in', de: 'zuletzt eingeloggt' },
})
@ -135,7 +135,7 @@ const playerDialogSubmit = async (action: ButtonAction): Promise<void> => {
}
&__cell {
padding-right: 8px;
padding-right: 16px;
}
&__is-gamemaster {

View File

@ -18,12 +18,14 @@ export type PlayerInfo = PlayerEdit & {
lastLoggedIn: number
isPlaying: boolean
numQuotes: number
numQuotesPlayed: number
role: Role
}
export type Quote = {
id: string
quote: string
isPlayed: boolean
}
export type Quotes = Array<Quote>

View File

@ -1,14 +1,13 @@
import buildUrl from 'build-url'
import queryString from 'query-string'
export type QueryParams = {
[name: string]: string
}
export async function callApi(path: string, queryParams?: QueryParams) {
const url = buildUrl('/', {
path,
queryParams,
})
const url = path + (
queryParams ? '?' + queryString.stringify(queryParams) : ''
)
return await $fetch(url)
}

View File

@ -3,9 +3,9 @@ import { useUserinfoStore } from "@/stores/UserinfoStore"
import { useGameinfoStore } from "@/stores/GameinfoStore"
import { useRoundStore } from "@/stores/RoundStore"
import { usePlayersStore } from "@/stores/PlayersStore"
import { EngineContext } from '@/composables/useEngine'
import useAlert from '@/composables/useAlert'
import useI18n from '@/composables/useI18n'
import type { EngineContext } from '@/composables/useEngine'
type EngineResponse = {
version: string,

View File

@ -1,5 +1,5 @@
import { EngineContext } from '@/composables/useEngine'
import { useUserinfoStore } from "@/stores/UserinfoStore"
import type { EngineContext } from '@/composables/useEngine'
import type { GameInfo, GameInfos, PlayerEdit, Lang, CreateGameStatus } from '@/composables/engine.d'
export async function fetchGameInfo(this: EngineContext): Promise<GameInfo> {

View File

@ -1,5 +1,5 @@
import { EngineContext } from '@/composables/useEngine'
import { useUserinfoStore } from "@/stores/UserinfoStore"
import type { EngineContext } from '@/composables/useEngine'
export async function collectQuotes(this: EngineContext): Promise<void> {
const userInfoStore = useUserinfoStore()

View File

@ -1,5 +1,5 @@
import { useUserinfoStore } from "@/stores/UserinfoStore"
import { EngineContext } from '@/composables/useEngine'
import type { EngineContext } from '@/composables/useEngine'
export async function saveSelection(this: EngineContext, selection: string): Promise<void> {
const userInfoStore = useUserinfoStore()

View File

@ -1,7 +1,8 @@
import { Ref, ref } from 'vue'
import { ref } from 'vue'
import type { Quotes } from '@/composables/engine.d'
import { useUserinfoStore } from "@/stores/UserinfoStore"
import { EngineContext } from '@/composables/useEngine'
import type { Ref } from 'vue'
import type { EngineContext } from '@/composables/useEngine'
type QuotesResponse = {
quotes: Quotes

View File

@ -1,5 +1,4 @@
import { useUserinfoStore } from "@/stores/UserinfoStore"
import { EngineContext } from '@/composables/useEngine'
import type { EngineContext } from '@/composables/useEngine'
export async function setupApp(this: EngineContext, authcode: string): Promise<void> {
await this.callApi('/api/setupApp', {

View File

@ -1,6 +1,6 @@
import { useUserinfoStore } from "@/stores/UserinfoStore"
import { EngineContext } from '@/composables/useEngine'
import useAlert from '@/composables/useAlert'
import type { EngineContext } from '@/composables/useEngine'
export function start(this: EngineContext): void {
if (this.isActive && !this.shouldStop) {

View File

@ -1,4 +1,5 @@
import { Ref, ref } from 'vue'
import { ref } from 'vue'
import type { Ref } from 'vue'
export type AlertMessages = Array<string>

View File

@ -1,11 +1,12 @@
import { useUserinfoStore, Userinfo } from '@/stores/UserinfoStore'
import { useUserinfoStore } from '@/stores/UserinfoStore'
import { useEngineStore } from '@/stores/EngineStore'
import { useGameinfoStore } from '@/stores/GameinfoStore'
import { usePlayersStore } from '@/stores/PlayersStore'
import { useRoundStore } from '@/stores/RoundStore'
import useI18n from './useI18n'
import { $fetch } from 'ohmyfetch'
import { $fetch } from 'ofetch'
import type { Userinfo } from '@/stores/UserinfoStore'
export type AllowRole = '' | 'player' | 'gamemaster' | 'admin' | 'setup'
export type AllowRoles = Array<AllowRole>

View File

@ -1,5 +1,5 @@
import { Ref, ref } from 'vue'
import { callApi, QueryParams } from '@/composables/engine/callApi'
import { ref } from 'vue'
import { callApi } from '@/composables/engine/callApi'
import { start, stop } from '@/composables/engine/startStop'
import { setupApp } from '@/composables/engine/setupApp'
import { fetchUpdate } from '@/composables/engine/fetchUpdate'
@ -7,6 +7,8 @@ import { loadQuotes, getQuotesRef, deleteQuote, saveQuote } from '@/composables/
import { fetchGameInfo, fetchGameInfos, setGameLang, setGameName, savePlayer, removePlayer, createGame, cameo, logoutCameo } from '@/composables/engine/gameManagement'
import { collectQuotes, startGame, continueGame, resetGame, finishGame, disableGame, removeGame } from '@/composables/engine/gameState'
import { saveSelection } from '@/composables/engine/play'
import type { Ref } from 'vue'
import type { QueryParams } from '@/composables/engine/callApi'
import type { Quotes, GameInfo, GameInfos, PlayerEdit, Lang, CreateGameStatus } from '@/composables/engine.d'
export interface EngineContext {

View File

@ -24,6 +24,7 @@ await useAuth().authenticateAndLoadUserInfo(['admin'])
&__tiles {
display: flex;
margin: 24px;
}
}
</style>

View File

@ -42,6 +42,7 @@ updateGameinfo()
&__tiles {
display: flex;
margin: 24px;
}
&__tiles-spacer {

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="page-index__page">
<TitleBox />
<div class="page-index__action-box">
<div class="page-index__space" />
@ -12,6 +12,17 @@
</div>
<div class="page-index__space" />
</div>
<div class="page-index__about">
<a
href="https://www.sirlab.de/linux/games/knowyt/"
target="_blank" rel="noopener"
>
<Button :border="false">
<div class="page-index__about-text">{{ $t('about') }}</div>
</Button>
</a>
</div>
<CopyrightNotice />
<CreateTeamDialog v-if="showCreateTeamDialog" @close="closeCreateTeamDialog" />
</div>
@ -25,6 +36,7 @@ import CopyrightNotice from '../components/CopyrightNotice.vue';
const { $t } = useI18n({
'create-team': { en: 'Create Team ...', de: 'Team erstellen ...' },
'about': { en: 'about the game', de: 'Über das Spiel' },
})
await useAuth().authenticateAndLoadUserInfo([''])
@ -45,6 +57,11 @@ body {
}
.page-index {
&__page {
display: flex;
flex-direction: column;
}
&__action-box {
display: flex;
width: 100%;
@ -84,5 +101,10 @@ body {
&__space {
flex-grow: 1;
}
&__about {
margin: 48px 0;
align-self: center;
}
}
</style>

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { Players } from '@/composables/engine.d';
import type { Players } from '@/composables/engine.d';
export const usePlayersStore = defineStore('PlayersStore', {
state: () => {

File diff suppressed because it is too large Load Diff

View File

@ -56,7 +56,7 @@ func (app *Application) removeGame(gm *game.Game) error {
log.Error("failed to remove quotes\n")
return nil
}
log.Info("game %s: removing state file", gm.GetId())
log.Info("game %s: removing state file\n", gm.GetId())
stateFilename := path.Join(gameBaseDir, "state.json")
if err := os.Remove(stateFilename); err != nil {
if !errors.Is(err, os.ErrNotExist) {

View File

@ -5,7 +5,15 @@ func (gm *Game) GetGameInfo() *GameInfoJson {
for i := range gameInfo.Players {
quotes := gm.getQuotesInfoByUserId(gameInfo.Players[i].Id)
numPlayed := 0
for j := range quotes {
if quotes[j].IsPlayed {
numPlayed++
}
}
gameInfo.Players[i].NumberOfQuotes = len(quotes)
gameInfo.Players[i].NumberOfQuotesPlayed = numPlayed
}
return gameInfo
}

View File

@ -25,9 +25,10 @@ func (gm *Game) getQuotesInfoByUserId(usrId string) []Quote {
for _, quote := range gm.quotes {
if quote.GetSourceId() == usrId {
quotes = append(quotes, Quote{
Id: quote.GetId(),
Quote: quote.GetQuote(),
Created: quote.GetCreated(),
Id: quote.GetId(),
Quote: quote.GetQuote(),
Created: quote.GetCreated(),
IsPlayed: quote.IsPlayed(),
})
}
}

View File

@ -79,9 +79,10 @@ type GameJson struct {
}
type Quote struct {
Id string `json:"id"`
Quote string `json:"quote"`
Created int64 `json:"created"`
Id string `json:"id"`
Quote string `json:"quote"`
Created int64 `json:"created"`
IsPlayed bool `json:"isPlayed"`
}
type QuotesInfo struct {
@ -89,16 +90,17 @@ type QuotesInfo struct {
}
type PlayerInfoJson struct {
Id string `json:"id"`
Name string `json:"name"`
Created int64 `json:"created"`
LastLoggedIn int64 `json:"lastLoggedIn"`
Score int `json:"score"`
IsPlaying bool `json:"isPlaying"`
IsIdle bool `json:"isIdle"`
NumberOfQuotes int `json:"numQuotes"`
AuthCode string `json:"authcode,omitempty"`
Role string `json:"role"`
Id string `json:"id"`
Name string `json:"name"`
Created int64 `json:"created"`
LastLoggedIn int64 `json:"lastLoggedIn"`
Score int `json:"score"`
IsPlaying bool `json:"isPlaying"`
IsIdle bool `json:"isIdle"`
NumberOfQuotes int `json:"numQuotes"`
NumberOfQuotesPlayed int `json:"numQuotesPlayed"`
AuthCode string `json:"authcode,omitempty"`
Role string `json:"role"`
}
type GameInfoJson struct {

View File

@ -3,6 +3,7 @@ module sirlab.de/go/knowyt
go 1.18
require (
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0
github.com/imkira/go-observer v1.0.3
)

View File

@ -1,3 +1,5 @@
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imkira/go-observer v1.0.3 h1:l45TYAEeAB4L2xF6PR2gRLn2NE5tYhudh33MLmC7B80=

View File

@ -0,0 +1,41 @@
package handler
import (
"fmt"
"net/http"
"sirlab.de/go/knowyt/user"
)
func (authMux *AuthMux) Cameo(usr *user.User, w http.ResponseWriter, r *http.Request) {
if usr.IsAdmin() {
cookie := authMux.createCookie()
cookie.Name = cookie.Name + "-cameo"
usrCameo, err := authMux.checkAuthCode(r)
if err != nil {
http.SetCookie(w, cookie)
authMux.accessDenied(w, r)
return
}
cookie.Value = usrCameo.GetId()
cookie.MaxAge = 0
http.SetCookie(w, cookie)
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "ok")
return
}
// non-admin: remove cameo cookie
usrCameo := usr.GetCameo()
if usrCameo != nil && usrCameo.IsAdmin() {
cookie := authMux.createCookie()
cookie.Name = cookie.Name + "-cameo"
http.SetCookie(w, cookie)
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "ok")
return
}
authMux.accessDenied(w, r)
}

View File

@ -0,0 +1,25 @@
package handler
import (
"fmt"
"net/http"
"sirlab.de/go/knowyt/user"
)
func (authMux *AuthMux) checkAuthCode(r *http.Request) (*user.User, error) {
r.ParseForm()
form := r.Form
code := form.Get("code")
if len(code) != 6 {
return nil, fmt.Errorf("invalid code \"%s\"", code)
}
usr, err := authMux.app.GetUserByAuthcode(code)
if err != nil {
return nil, fmt.Errorf("invalid code: \"%s\"", code)
}
return usr, nil
}

View File

@ -5,7 +5,6 @@ import (
"net/http"
"sirlab.de/go/knowyt/log"
"sirlab.de/go/knowyt/user"
)
func (authMux *AuthMux) createCookie() *http.Cookie {
@ -20,12 +19,17 @@ func (authMux *AuthMux) createCookie() *http.Cookie {
func (authMux *AuthMux) Logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, authMux.createCookie())
cameoCookie := authMux.createCookie()
cameoCookie.Name = cameoCookie.Name + "-cameo"
http.SetCookie(w, cameoCookie)
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "ok")
}
func (authMux *AuthMux) Login(w http.ResponseWriter, r *http.Request) {
usr, err := authMux.checkCode(r)
usr, err := authMux.checkAuthCode(r)
if err != nil {
log.ErrorLog(err)
http.SetCookie(w, authMux.createCookie())
@ -34,71 +38,31 @@ func (authMux *AuthMux) Login(w http.ResponseWriter, r *http.Request) {
}
if !usr.IsAdmin() {
// check, if game is enabled
gm, err := authMux.app.GetGameById(usr.GetGameId())
if err != nil || !gm.IsActive() {
log.ErrorLog(fmt.Errorf("game %s disabled for user %s", gm.GetId(), usr.GetName()))
log.ErrorLog(fmt.Errorf("game %s disabled for user %s (%s)", gm.GetId(), usr.GetName(), usr.GetId()))
http.SetCookie(w, authMux.createCookie())
authMux.accessDenied(w, r)
return
}
}
log.Info("%s logged into game %s\n", usr.GetName(), usr.GetGameId())
log.Info("%s (%s) logged into game %s\n", usr.GetName(), usr.GetId(), usr.GetGameId())
tokenString, err := authMux.createToken(usr.GetId())
if err != nil {
log.ErrorLog(fmt.Errorf("failed to create JWT for user id %s (%s)", usr.GetName(), usr.GetId()))
log.ErrorLog(err)
http.SetCookie(w, authMux.createCookie())
authMux.accessDenied(w, r)
return
}
cookie := authMux.createCookie()
cookie.Value = usr.GetId() + ":" + usr.GetAuthCode()
cookie.Value = tokenString
cookie.MaxAge = 0
http.SetCookie(w, cookie)
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "ok")
}
func (authMux *AuthMux) checkCode(r *http.Request) (*user.User, error) {
r.ParseForm()
form := r.Form
code := form.Get("code")
if len(code) != 6 {
return nil, fmt.Errorf("invalid code \"%s\"", code)
}
usr, err := authMux.app.GetUserByAuthcode(code)
if err != nil {
return nil, fmt.Errorf("invalid code: \"%s\"", code)
}
return usr, nil
}
func (authMux *AuthMux) Cameo(usr *user.User, w http.ResponseWriter, r *http.Request) {
if usr.IsAdmin() {
cookie := authMux.createCookie()
cookie.Name = cookie.Name + "-cameo"
usrCameo, err := authMux.checkCode(r)
if err != nil {
http.SetCookie(w, cookie)
authMux.accessDenied(w, r)
return
}
cookie.Value = usrCameo.GetId()
cookie.MaxAge = 0
http.SetCookie(w, cookie)
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "ok")
return
}
// non-admin: remove cameo cookie
usrCameo := usr.GetCameo()
if usrCameo != nil && usrCameo.IsAdmin() {
cookie := authMux.createCookie()
cookie.Name = cookie.Name + "-cameo"
http.SetCookie(w, cookie)
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "ok")
return
}
authMux.accessDenied(w, r)
}

View File

@ -3,7 +3,6 @@ package handler
import (
"fmt"
"net/http"
"strings"
"sirlab.de/go/knowyt/user"
)
@ -30,22 +29,11 @@ func (authMux *AuthMux) accessDenied(w http.ResponseWriter, r *http.Request) {
}
func (authMux *AuthMux) getUserFromSession(r *http.Request) (*user.User, error) {
authCookie, err := r.Cookie("knowyt-auth")
usr, err := authMux.validateSessionAndGetUser(r)
if err != nil {
return nil, fmt.Errorf("invalid cookie")
}
vals := strings.SplitN(authCookie.Value, ":", 2)
usr, usrErr := authMux.app.GetUserById(vals[0])
if usrErr != nil {
return nil, fmt.Errorf("invalid cookie")
}
if usr.GetAuthCode() != vals[1] {
return nil, fmt.Errorf("invalid cookie")
}
if usr.IsAdmin() {
if cookieCameo, err := r.Cookie("knowyt-auth-cameo"); err == nil {
if usrCameo, err := authMux.app.GetUserById(cookieCameo.Value); err == nil {

View File

@ -0,0 +1,64 @@
package handler
import (
"crypto/rand"
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt"
"sirlab.de/go/knowyt/user"
)
var secretKey []byte = nil
func (authMux *AuthMux) createToken(uid string) (string, error) {
if secretKey == nil {
secretKey = make([]byte, 32)
if _, err := rand.Read(secretKey); err != nil {
return "", err
}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512,
jwt.MapClaims{
"uid": uid,
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
return token.SignedString(secretKey)
}
func (authMux *AuthMux) validateSessionAndGetUser(r *http.Request) (*user.User, error) {
tokenString, err := r.Cookie("knowyt-auth")
if err != nil {
return nil, err
}
token, err := jwt.Parse(tokenString.Value, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("invalid JWT")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid JWT")
}
userId := claims["uid"].(string)
if len(userId) == 0 {
return nil, fmt.Errorf("invalid JWT")
}
usr, err := authMux.app.GetUserById(userId)
if err != nil {
return nil, err
}
return usr, nil
}

View File

@ -37,5 +37,5 @@ type UserinfoJson struct {
Name string `json:"name"`
Role string `json:"role"`
GameId string `json:"game"`
IsCameo bool `json:"isCameo",omitempty`
IsCameo bool `json:"-"`
}

View File

@ -20,7 +20,7 @@ func NewUserFromFile(fileName string) (*User, error) {
// var usr User
var userJson UserJson
if err := json.Unmarshal(jsonBytes, &userJson); err != nil {
return nil, fmt.Errorf("%s: %v\n", fileName, err)
return nil, fmt.Errorf("%s: %v", fileName, err)
} else {
_, fileNameShort := path.Split(fileName)
id := strings.TrimSuffix(fileNameShort, ".json")