generated from legonzaur/webapp-back
Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
SESSION_SECRET=dev
|
||||||
|
|
||||||
|
CLIENT_ENDPOINT=http://localhost:3000
|
||||||
|
API_ENDPOINT=http://localhost:8765
|
||||||
|
API_PREFIX=/api
|
||||||
|
DISCORD_CLIENTID=
|
||||||
|
DISCORD_CLIENTSECRET=
|
||||||
|
|
||||||
|
PG_ENDPOINT=postgres://127.0.0.1:concept
|
||||||
|
PG_PASSWORD=postgres
|
||||||
|
PG_USER=test
|
||||||
|
|
||||||
|
DEV_MODE=true
|
||||||
+401
@@ -0,0 +1,401 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff:
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
|
||||||
|
# Sensitive or high-churn files:
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.xml
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
|
||||||
|
# Gradle:
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-debug/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin:
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
## File-based project format:
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
## Plugin-specific files:
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
### VisualStudio template
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Visual Studio 2015 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# CodeRush
|
||||||
|
.cr/
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
### macOS template
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
=======
|
||||||
|
# Local
|
||||||
|
.env
|
||||||
|
dist
|
||||||
|
.webpack
|
||||||
|
.serverless/**/*.zip
|
||||||
|
mongodb-keyfile
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "events"]
|
||||||
|
path = events
|
||||||
|
url = git@git.legonzaur.fr:concept/events.git
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"orta.vscode-jest",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"vue.volar"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch via NPM",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": ["run", "start:dev", "-b", "swc"],
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**",
|
||||||
|
"${workspaceFolder}/node_modules/**/*.js",
|
||||||
|
"${workspaceFolder}/lib/**/*.js"
|
||||||
|
],
|
||||||
|
"type": "node",
|
||||||
|
"console": "internalConsole",
|
||||||
|
"outputCapture": "std",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
|
"javascript.preferences.quoteStyle": "single",
|
||||||
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
"eslint.format.enable": true,
|
||||||
|
"eslint.useFlatConfig": true,
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"files.exclude": {
|
||||||
|
"**/dist": true,
|
||||||
|
"**/node_modules": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "echo",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker compose up -d",
|
||||||
|
"runOptions": {"runOn": "folderOpen"},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:lts-alpine AS build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npx nest build
|
||||||
|
|
||||||
|
# étape de production
|
||||||
|
FROM node:lts-alpine AS production-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-stage /app/dist/ /app
|
||||||
|
COPY --from=build-stage /app/node_modules/ /app/node_modules
|
||||||
|
EXPOSE 8765
|
||||||
|
CMD ["node", "main"]
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
volumes:
|
||||||
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=test
|
||||||
|
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
environment:
|
||||||
|
- PGADMIN_DEFAULT_EMAIL=postgres@postgres.com
|
||||||
|
- PGADMIN_DEFAULT_PASSWORD=postgres
|
||||||
|
- PGADMIN_DISABLE_POSTFIX=true
|
||||||
|
volumes:
|
||||||
|
- pgadmin_data:/var/lib/pgadmin
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
command: "redis-server --save 60 1"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
||||||
|
pgadmin_data:
|
||||||
|
redis_data:
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ['**/.eslintrc.js'],
|
||||||
|
},
|
||||||
|
...compat.extends(
|
||||||
|
'plugin:@typescript-eslint/recommended-type-checked',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': typescriptEslintEslintPlugin,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 5,
|
||||||
|
sourceType: 'module',
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'require-await': 'off',
|
||||||
|
'@typescript-eslint/require-await': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true,
|
||||||
|
"plugins": ["@nestjs/swagger"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+12661
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"name": "nest-typescript-starter",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Nest TypeScript starter repository",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"migration:generate": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:generate -d ./src/db/typeorm.config.ts ./src/db/migrations/migration",
|
||||||
|
"migration:create": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:create -d ./src/db/typeorm.config.ts ./src/db/migrations/migration",
|
||||||
|
"migration:run": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run -d ./src/db/typeorm.config.ts",
|
||||||
|
"migration:revert": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:revert -d ./src/db/typeorm.config.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.1.0",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.1.0",
|
||||||
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
|
"@nestjs/mapped-types": "^2.1.0",
|
||||||
|
"@nestjs/platform-express": "^11.1.0",
|
||||||
|
"@nestjs/swagger": "^11.2.0",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"@types/ranjs": "^1.22.7",
|
||||||
|
"connect-redis": "^8.0.3",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
|
"pg": "^8.15.6",
|
||||||
|
"random": "^5.4.0",
|
||||||
|
"redis": "^5.0.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.2",
|
||||||
|
"typeorm": "^0.3.22"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@eslint/js": "^9.26.0",
|
||||||
|
"@nestjs/cli": "^11.0.7",
|
||||||
|
"@nestjs/schematics": "^11.0.5",
|
||||||
|
"@nestjs/testing": "^11.1.0",
|
||||||
|
"@swc/cli": "^0.7.3",
|
||||||
|
"@swc/core": "^1.11.24",
|
||||||
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/express-session": "^1.18.1",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/node": "^22.15.12",
|
||||||
|
"@types/supertest": "^6.0.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
||||||
|
"@typescript-eslint/parser": "^8.32.0",
|
||||||
|
"@typescript-eslint/typescript-estree": "^8.32.0",
|
||||||
|
"eslint": "^9.26.0",
|
||||||
|
"eslint-config-prettier": "^10.1.2",
|
||||||
|
"eslint-plugin-prettier": "^5.4.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"supertest": "^7.1.0",
|
||||||
|
"ts-jest": "^29.3.2",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-eslint": "^8.32.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "./",
|
||||||
|
"modulePaths": [
|
||||||
|
"<rootDir>"
|
||||||
|
],
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { GamesModule } from './games/games.module';
|
||||||
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { User } from './users/entities/user.entity';
|
||||||
|
import { Game } from './games/entities/game.entity';
|
||||||
|
import { Topic } from './word/entities/topic.entity';
|
||||||
|
import { Word } from './word/entities/word.entity';
|
||||||
|
import { Player } from './games/entities/player.entity';
|
||||||
|
import { ConceptInTurn } from './games/entities/concept_in_turn.entity';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { WordModule } from './word/words.module';
|
||||||
|
import { Message } from './games/entities/message.entity';
|
||||||
|
import { LobbyModule } from './lobby/lobby.module';
|
||||||
|
|
||||||
|
const configService = new ConfigService();
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
envFilePath: '.env',
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'postgres',
|
||||||
|
host: configService.get('PG_HOST'),
|
||||||
|
// url: process.env.PG_ENDPOINT,
|
||||||
|
password: configService.get('PG_PASSWORD'),
|
||||||
|
username: configService.get('PG_USER'),
|
||||||
|
database: 'concept',
|
||||||
|
synchronize: configService.get('DEV_MODE') == 'true',
|
||||||
|
entities: [User, Player, Game, Message, ConceptInTurn, Topic, Word],
|
||||||
|
}),
|
||||||
|
LobbyModule,
|
||||||
|
GamesModule,
|
||||||
|
UsersModule,
|
||||||
|
WordModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
envFilePath: '.env',
|
||||||
|
isGlobal: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const configService = new ConfigService();
|
||||||
|
|
||||||
|
const AppDataSource = new DataSource({
|
||||||
|
type: 'postgres',
|
||||||
|
host: configService.get('PG_HOST'),
|
||||||
|
// url: process.env.PG_ENDPOINT,
|
||||||
|
password: configService.get('PG_PASSWORD'),
|
||||||
|
username: configService.get('PG_USER'),
|
||||||
|
database: 'concept',
|
||||||
|
applicationName: 'concept',
|
||||||
|
synchronize: false,
|
||||||
|
entities: ['**/*.entity.ts'],
|
||||||
|
migrations: ['src/db/migrations/*-migration.ts'],
|
||||||
|
migrationsRun: false,
|
||||||
|
logging: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AppDataSource;
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Sse,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '../../users/guards/auth.guard';
|
||||||
|
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Game } from '../entities/game.entity';
|
||||||
|
import { GamePipe, GameStartedPipe } from '../pipe/game.pipe';
|
||||||
|
|
||||||
|
import { Observable, fromEvent, map } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { GameService } from '../services/games.service';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
// import { WordResponseDTO } from 'events/dto/wordresponse.dto';
|
||||||
|
import { WordRecieveDTO } from 'src/word/dto/wordrecieve.dto';
|
||||||
|
import { LobbyService } from 'src/lobby/lobby.service';
|
||||||
|
import { WordResponseDTO } from 'events/dto/wordresponse.dto';
|
||||||
|
import { GetUser } from 'src/users/user.pipe';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
|
||||||
|
@ApiTags('game')
|
||||||
|
@Controller('games/:id')
|
||||||
|
export class GamesController {
|
||||||
|
constructor(
|
||||||
|
private readonly lobbyService: LobbyService,
|
||||||
|
private readonly gameService: GameService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('/start')
|
||||||
|
@ApiOperation({ summary: 'Start game' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Game successfully started.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'Bad Request',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 403,
|
||||||
|
description: 'You can only start games you own',
|
||||||
|
})
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async start(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Param('id', GamePipe) game: Game,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (game.players.length < 2 && !this.configService.get('DEV_MODE')) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Not enough teams to start the game',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (game.ownerId != user.id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only start games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.gameService.start(game);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('surrender')
|
||||||
|
@ApiOperation({ summary: 'Leave a game that has already started' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
surrender(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Param('id', GameStartedPipe) game: Game,
|
||||||
|
) {
|
||||||
|
return this.lobbyService.leaveGame(game, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('kick/:playerId')
|
||||||
|
@ApiOperation({ summary: 'Kick a player' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
kick(
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GamePipe) game: Game,
|
||||||
|
@Param('playerId') playerId: string,
|
||||||
|
) {
|
||||||
|
if (user.id != game.ownerId) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You do do not own this game',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.lobbyService.kickPlayer(game, user, playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('currentWord')
|
||||||
|
@ApiOperation({ summary: 'Read Current Word' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
getCurrentWord(
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GameStartedPipe) game: Game,
|
||||||
|
) {
|
||||||
|
if (user.id != game.currentPlayer.userId) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new WordResponseDTO(game.currentWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/message')
|
||||||
|
@ApiOperation({ summary: 'try to guess a word' })
|
||||||
|
@ApiBody({ type: WordRecieveDTO })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async postMessage(
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GameStartedPipe) game: Game,
|
||||||
|
@Body('value') value: string,
|
||||||
|
) {
|
||||||
|
const player = game.players.find((p) => p.userId == user.id);
|
||||||
|
if (!player) {
|
||||||
|
throw new HttpException('You are not in this game', HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
if (player.hasGuessed) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot send messages if you have already guessed the word',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.gameService.postMessage(game, player, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sse('/subscribe')
|
||||||
|
subscribe(@Param('id') id: string): Observable<{ data: string }> {
|
||||||
|
return fromEvent(this.eventEmitter, 'sse.game.' + id).pipe(
|
||||||
|
map((payload: any) => {
|
||||||
|
return {
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class MessageRecieveDTO {
|
||||||
|
constructor(partial: Partial<MessageRecieveDTO>) {
|
||||||
|
this.value = partial.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
OneToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Player } from './player.entity';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
import { Word } from 'src/word/entities/word.entity';
|
||||||
|
import { Message } from './message.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Game {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
seed: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: 'ownerId' })
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
owner: Promise<User>;
|
||||||
|
|
||||||
|
@Column({ name: 'ownerId' })
|
||||||
|
ownerId: string;
|
||||||
|
|
||||||
|
@OneToMany(() => Player, (player) => player.game, {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
@JoinTable()
|
||||||
|
players: Player[];
|
||||||
|
|
||||||
|
@OneToOne(() => Player, { nullable: true, eager: true })
|
||||||
|
@JoinColumn()
|
||||||
|
currentPlayer: Player;
|
||||||
|
|
||||||
|
@ManyToOne(() => Word, { nullable: true, eager: true })
|
||||||
|
currentWord: Word;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
started: boolean;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
turn: number = 0;
|
||||||
|
|
||||||
|
@OneToMany(() => Message, (msg) => msg.game, {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
_messages: Message[];
|
||||||
|
|
||||||
|
get messages() {
|
||||||
|
if (!this._messages) return [];
|
||||||
|
return [...this._messages].sort((a, b) => a.id - b.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Player } from './player.entity';
|
||||||
|
|
||||||
|
import { Game } from './game.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Message {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: 'authorId' })
|
||||||
|
@ManyToOne(() => Player, { onDelete: 'CASCADE', eager: true, nullable: true })
|
||||||
|
author?: Player;
|
||||||
|
|
||||||
|
get authorId() {
|
||||||
|
return this.author?.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ManyToOne(() => Game, { onDelete: 'CASCADE' })
|
||||||
|
game: Promise<Game>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { User } from '../../users/entities/user.entity';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Game } from './game.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Player {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@JoinColumn({ name: 'userId' })
|
||||||
|
@ManyToOne(() => User, { onDelete: 'CASCADE' })
|
||||||
|
user: Promise<User>;
|
||||||
|
|
||||||
|
@Column({ name: 'userId' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Game, { onDelete: 'CASCADE' })
|
||||||
|
game: Game;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
score: number = 0;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
hasGuessed: boolean = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
|
import { Game } from './entities/game.entity';
|
||||||
|
import { UsersModule } from 'src/users/users.module';
|
||||||
|
import { GamesController } from './controllers/games.controller';
|
||||||
|
import { GameService } from './services/games.service';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Player } from './entities/player.entity';
|
||||||
|
import { WordModule } from 'src/word/words.module';
|
||||||
|
import { Message } from './entities/message.entity';
|
||||||
|
import { LobbyModule } from 'src/lobby/lobby.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
UsersModule,
|
||||||
|
WordModule,
|
||||||
|
forwardRef(() => LobbyModule),
|
||||||
|
TypeOrmModule.forFeature([Game]),
|
||||||
|
TypeOrmModule.forFeature([Player]),
|
||||||
|
TypeOrmModule.forFeature([Message]),
|
||||||
|
],
|
||||||
|
controllers: [GamesController],
|
||||||
|
providers: [GameService],
|
||||||
|
})
|
||||||
|
export class GamesModule {}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
PipeTransform,
|
||||||
|
Injectable,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Game } from '../entities/game.entity';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GamePipe implements PipeTransform {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Game) private gameRepository: Repository<Game>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async transform(value: number) {
|
||||||
|
const game = await this.gameRepository.findOneBy({ id: value });
|
||||||
|
if (!game) {
|
||||||
|
throw new HttpException('Game not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameStartedPipe implements PipeTransform {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Game) private gameRepository: Repository<Game>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async transform(value: number) {
|
||||||
|
const game = await this.gameRepository.findOneBy({ id: value });
|
||||||
|
if (!game) {
|
||||||
|
throw new HttpException('Game not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (!game.started) {
|
||||||
|
throw new HttpException(
|
||||||
|
'The game must be started',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Game } from '../entities/game.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Player } from '../entities/player.entity';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GameEndTurnEvent,
|
||||||
|
GameMessageDeleteEvent,
|
||||||
|
GameMessageEvent,
|
||||||
|
GameStartEvent,
|
||||||
|
} from 'events/game.events';
|
||||||
|
import random from 'random';
|
||||||
|
import { Message } from '../entities/message.entity';
|
||||||
|
import { WordService } from 'src/word/word.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Game) private gameRepository: Repository<Game>,
|
||||||
|
@InjectRepository(Message) private messageResporitory: Repository<Message>,
|
||||||
|
@InjectRepository(Player) private playerRepository: Repository<Player>,
|
||||||
|
private readonly wordService: WordService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async start(game: Game) {
|
||||||
|
const rng = random.clone(game.seed);
|
||||||
|
game.started = true;
|
||||||
|
game.currentPlayer = rng.choice(game.players);
|
||||||
|
const words = await this.getShuffledWordList(game);
|
||||||
|
game.currentWord = words[0];
|
||||||
|
|
||||||
|
await this.gameRepository.save(game);
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
'sse.game.' + game.id,
|
||||||
|
new GameStartEvent(game.currentPlayer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async postMessage(game: Game, author: Player, value: string) {
|
||||||
|
const message = this.messageResporitory.create();
|
||||||
|
message.game = Promise.resolve(game);
|
||||||
|
|
||||||
|
if (
|
||||||
|
value.toLowerCase() == game.currentWord.value.toLowerCase() &&
|
||||||
|
author.id != game.currentPlayer.id
|
||||||
|
) {
|
||||||
|
message.value = author.userId;
|
||||||
|
author.hasGuessed = true;
|
||||||
|
author.score++;
|
||||||
|
game.currentPlayer.score++;
|
||||||
|
await this.playerRepository.save([author, game.currentPlayer]);
|
||||||
|
} else {
|
||||||
|
message.author = author;
|
||||||
|
message.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.messageResporitory.save(message);
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
'sse.game.' + game.id,
|
||||||
|
new GameMessageEvent(message),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (game.messages.length > 3) {
|
||||||
|
await this.deleteMessage(game, game.messages[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
game.players
|
||||||
|
.filter((p) => p.id != game.currentPlayer.id)
|
||||||
|
.every((p) => p.hasGuessed)
|
||||||
|
) {
|
||||||
|
await this.endTurn(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteMessage(game: Game, message: Message) {
|
||||||
|
const oldMessageId = message.id;
|
||||||
|
await this.messageResporitory.remove(message);
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
'sse.game.' + game.id,
|
||||||
|
new GameMessageDeleteEvent(oldMessageId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async endTurn(game: Game) {
|
||||||
|
const smartPlayers = game.players.filter((p) => p.hasGuessed);
|
||||||
|
smartPlayers.forEach((p) => (p.hasGuessed = false));
|
||||||
|
|
||||||
|
const wordList = await this.getShuffledWordList(game);
|
||||||
|
game.turn++;
|
||||||
|
game.currentWord = wordList[game.turn % wordList.length];
|
||||||
|
const playerIndex = game.players.findIndex(
|
||||||
|
(p) => p.userId == game.currentPlayer.userId,
|
||||||
|
);
|
||||||
|
game.currentPlayer = game.players[(playerIndex + 1) % game.players.length];
|
||||||
|
|
||||||
|
// await this.currentConceptReporitory.remove(game.currentConcept)
|
||||||
|
await this.gameRepository.save(game);
|
||||||
|
await this.playerRepository.save(smartPlayers);
|
||||||
|
|
||||||
|
await this.messageResporitory.delete({ game });
|
||||||
|
|
||||||
|
this.eventEmitter.emit(
|
||||||
|
'sse.game.' + game.id,
|
||||||
|
new GameEndTurnEvent(game.currentPlayer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuffles the word list using a per-game seeded rng
|
||||||
|
*/
|
||||||
|
async getShuffledWordList(game: Game) {
|
||||||
|
const rng = random.clone(game.seed);
|
||||||
|
const words = await this.wordService.listActive();
|
||||||
|
return words
|
||||||
|
.map((value) => ({ value, sort: rng.float() }))
|
||||||
|
.sort((a, b) => a.sort - b.sort)
|
||||||
|
.map(({ value }) => value);
|
||||||
|
}
|
||||||
|
|
||||||
|
end(_game: Game, _winner: Player) {
|
||||||
|
throw Error('Not Implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { LobbyController } from './lobby.controller';
|
||||||
|
import { LobbyService } from './lobby.service';
|
||||||
|
import { Game } from '../games/entities/game.entity';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Player } from '../games/entities/player.entity';
|
||||||
|
import { GameService } from '../games/services/games.service';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
describe('LobbyController', () => {
|
||||||
|
let controller: LobbyController;
|
||||||
|
let _service: LobbyService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [LobbyController],
|
||||||
|
providers: [LobbyService, GameService],
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'sqlite',
|
||||||
|
database: ':memory:',
|
||||||
|
entities: [Game, Player, User],
|
||||||
|
synchronize: true,
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forFeature([Game, Player, User]),
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<LobbyController>(LobbyController);
|
||||||
|
_service = module.get<LobbyService>(LobbyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
UseGuards,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Sse,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { LobbyService } from './lobby.service';
|
||||||
|
import { AuthGuard } from '../users/guards/auth.guard';
|
||||||
|
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { GamePipe } from '../games/pipe/game.pipe';
|
||||||
|
import { Game } from '../games/entities/game.entity';
|
||||||
|
import { Observable, fromEvent, map } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { GameResponseDTO } from '../../events/dto/gameresponse.dto';
|
||||||
|
import { GetUser } from 'src/users/user.pipe';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
|
||||||
|
@ApiTags('lobby')
|
||||||
|
@Controller('games')
|
||||||
|
export class LobbyController {
|
||||||
|
constructor(
|
||||||
|
private readonly lobbyService: LobbyService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create game' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Game successfully created.',
|
||||||
|
// type: ReadGameDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'You can only create one game at a time',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 403, description: 'Forbidden.' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async create(@GetUser() user: User) {
|
||||||
|
const games = await this.lobbyService.findByOwner(user);
|
||||||
|
if (games) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only create one game at a time',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const createdGame = await this.lobbyService.create(user);
|
||||||
|
return new GameResponseDTO(createdGame);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List all games' })
|
||||||
|
async findAll(): Promise<GameResponseDTO[]> {
|
||||||
|
const games = await this.lobbyService.findAll();
|
||||||
|
return games.map((g) => new GameResponseDTO(g));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sse('/subscribe')
|
||||||
|
subscribe(): Observable<{ data: string }> {
|
||||||
|
return fromEvent(this.eventEmitter, 'sse.lobby').pipe(
|
||||||
|
map((payload) => {
|
||||||
|
return {
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get game information' })
|
||||||
|
findOne(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GamePipe) game: Game,
|
||||||
|
): GameResponseDTO {
|
||||||
|
return new GameResponseDTO(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Delete game' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async remove(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GamePipe) game: Game,
|
||||||
|
@GetUser() user: User,
|
||||||
|
) {
|
||||||
|
if (game.ownerId != user.id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only delete games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.lobbyService.remove(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/join')
|
||||||
|
@ApiOperation({ summary: 'Join game' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async join(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GamePipe) game: Game,
|
||||||
|
@GetUser() user: User,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.players.some((p) => p.userId == user.id)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You are already in this game',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await this.lobbyService.joinGame(game, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/leave')
|
||||||
|
@ApiOperation({ summary: "Leave a game that hasn't already started" })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
leave(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GamePipe) game: Game,
|
||||||
|
@GetUser() user: User,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return this.lobbyService.leaveGame(game, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
|
import { UsersModule } from 'src/users/users.module';
|
||||||
|
import { LobbyController } from './lobby.controller';
|
||||||
|
import { LobbyService } from './lobby.service';
|
||||||
|
import { GamesModule } from 'src/games/games.module';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Game } from 'src/games/entities/game.entity';
|
||||||
|
import { Player } from 'src/games/entities/player.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
UsersModule,
|
||||||
|
forwardRef(() => GamesModule),
|
||||||
|
TypeOrmModule.forFeature([Game]),
|
||||||
|
TypeOrmModule.forFeature([Player]),
|
||||||
|
],
|
||||||
|
controllers: [LobbyController],
|
||||||
|
providers: [LobbyService],
|
||||||
|
exports: [LobbyService],
|
||||||
|
})
|
||||||
|
export class LobbyModule {}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { LobbyService } from './lobby.service';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
import { Game } from '../games/entities/game.entity';
|
||||||
|
import { Player } from '../games/entities/player.entity';
|
||||||
|
import { GameService } from '../games/services/games.service';
|
||||||
|
|
||||||
|
describe('LobbyService', () => {
|
||||||
|
let service: LobbyService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [LobbyService, GameService],
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'sqlite',
|
||||||
|
database: ':memory:',
|
||||||
|
entities: [Game, Player, User],
|
||||||
|
synchronize: true,
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forFeature([Game, Player, User]),
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<LobbyService>(LobbyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import {
|
||||||
|
LobbyCreateEvent,
|
||||||
|
LobbyDeleteEvent,
|
||||||
|
LobbyJoinEvent,
|
||||||
|
} from 'events/lobby.events';
|
||||||
|
import { GameResponseDTO } from 'events/dto/gameresponse.dto';
|
||||||
|
import { GameDeleteEvent, GameJoinEvent } from 'events/game.events';
|
||||||
|
import { Player } from 'src/games/entities/player.entity';
|
||||||
|
import { Game } from 'src/games/entities/game.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LobbyService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Game) private gameRepository: Repository<Game>,
|
||||||
|
@InjectRepository(Player) private playerRepository: Repository<Player>,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(owner: User) {
|
||||||
|
const createdGame = this.gameRepository.create();
|
||||||
|
createdGame.owner = Promise.resolve(owner);
|
||||||
|
createdGame.seed = randomUUID();
|
||||||
|
|
||||||
|
await this.gameRepository.save(createdGame);
|
||||||
|
|
||||||
|
const g = new GameResponseDTO(createdGame);
|
||||||
|
this.eventEmitter.emit('sse.lobby', new LobbyCreateEvent(g));
|
||||||
|
|
||||||
|
await this.joinGame(createdGame, owner);
|
||||||
|
return createdGame;
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinGame(game: Game, user: User) {
|
||||||
|
const player = this.playerRepository.create();
|
||||||
|
player.user = Promise.resolve(user);
|
||||||
|
player.game = game;
|
||||||
|
await this.playerRepository.save(player);
|
||||||
|
this.eventEmitter.emit('sse.lobby', new LobbyJoinEvent(player, game));
|
||||||
|
this.eventEmitter.emit('sse.game.' + game.id, new GameJoinEvent(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
findByOwner(user: User) {
|
||||||
|
return this.gameRepository.findOneBy({ owner: user });
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveGame(_game: Game, _user: User) {
|
||||||
|
throw Error('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
kickPlayer(_game: Game, _user: User, _kickedPlayerId: string) {
|
||||||
|
throw Error('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Game[]> {
|
||||||
|
return this.gameRepository.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(game: Game) {
|
||||||
|
this.eventEmitter.emit('sse.lobby', new LobbyDeleteEvent(game.id));
|
||||||
|
this.eventEmitter.emit('sse.game.' + game.id, new GameDeleteEvent());
|
||||||
|
await this.gameRepository.remove(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import * as session from 'express-session';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import { RedisStore } from 'connect-redis';
|
||||||
|
import { createClient } from 'redis';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const redisClient = createClient();
|
||||||
|
redisClient.connect().catch(console.error);
|
||||||
|
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.enableCors({ origin: 'http://localhost:3000', credentials: true });
|
||||||
|
app.setGlobalPrefix(process.env.API_PREFIX);
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Concept Game')
|
||||||
|
.setDescription('Concept')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api', app, document);
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session.default({
|
||||||
|
secret: process.env.SESSION_SECRET,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
name: 'concept.session',
|
||||||
|
store: new RedisStore({ client: redisClient, prefix: 'concept:' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen(8765);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap();
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Entity, Column, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class User {
|
||||||
|
@PrimaryColumn({})
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
avatar: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { SessionData } from 'express-session';
|
||||||
|
import { UsersService } from '../users.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(private userService: UsersService) {}
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const http = context.switchToHttp();
|
||||||
|
const request = http.getRequest<Request>();
|
||||||
|
if (!(request.session as SessionData).user) {
|
||||||
|
throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
const user = await this.userService.findUser(request.session.user.id);
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Session expired. Please log in',
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.user = user;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
createParamDecorator,
|
||||||
|
ExecutionContext,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
export const GetUser = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest<Request>();
|
||||||
|
if (!request.user) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You are not authentified',
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return request.user;
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
Session,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Redirect,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { UserResponseDTO } from '../../events/dto/userresponse.dto';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SessionData } from 'express-session';
|
||||||
|
import { AuthGuard } from './guards/auth.guard';
|
||||||
|
|
||||||
|
const configService = new ConfigService();
|
||||||
|
|
||||||
|
@ApiTags('auth')
|
||||||
|
@Controller()
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
@Get('/login')
|
||||||
|
@Redirect()
|
||||||
|
async login(@Query('code') code: string, @Session() session: SessionData) {
|
||||||
|
await this.usersService.createDiscordToken({ code }, session);
|
||||||
|
await this.usersService.discordLogin(session);
|
||||||
|
return { url: configService.get<string>('CLIENT_ENDPOINT') };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/logout')
|
||||||
|
@Redirect()
|
||||||
|
async logout(@Session() session: SessionData) {
|
||||||
|
await this.usersService.discordLogout(session);
|
||||||
|
return { url: configService.get<string>('CLIENT_ENDPOINT') };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/whoami')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async remove(@Session() session: SessionData): Promise<UserResponseDTO> {
|
||||||
|
if (!session.tokenData || !session.user) {
|
||||||
|
throw new HttpException(
|
||||||
|
"Session doesn't contains auth information",
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (new Date(session.tokenData.expires) > new Date()) {
|
||||||
|
return this.usersService.findUser(session.user.id);
|
||||||
|
}
|
||||||
|
await this.usersService.discordLogin(session);
|
||||||
|
return new UserResponseDTO(session.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/users/:id')
|
||||||
|
async get(@Param('id') id: string) {
|
||||||
|
const user = await this.usersService.findUser(id);
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return new UserResponseDTO(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([User])],
|
||||||
|
controllers: [UsersController],
|
||||||
|
providers: [UsersService],
|
||||||
|
exports: [UsersService],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { UserResponseDTO } from '../../events/dto/userresponse.dto';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SessionData } from 'express-session';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User) private usersRepository: Repository<User>,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createDiscordToken(
|
||||||
|
{
|
||||||
|
code,
|
||||||
|
refresh_token,
|
||||||
|
}: {
|
||||||
|
code?: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
},
|
||||||
|
session: SessionData,
|
||||||
|
): Promise<void> {
|
||||||
|
let body;
|
||||||
|
if (code) {
|
||||||
|
body = new URLSearchParams({
|
||||||
|
// client_id: this.configService.get('DISCORD_CLIENTID'),
|
||||||
|
// client_secret: this.configService.get('DISCORD_CLIENTSECRET'),
|
||||||
|
code,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri:
|
||||||
|
this.configService.get('API_ENDPOINT') +
|
||||||
|
this.configService.get('API_PREFIX') +
|
||||||
|
'/login',
|
||||||
|
});
|
||||||
|
} else if (refresh_token) {
|
||||||
|
body = new URLSearchParams({
|
||||||
|
// client_id: this.configService.get('DISCORD_CLIENTID'),
|
||||||
|
// client_secret: this.configService.get('DISCORD_CLIENTSECRET'),
|
||||||
|
refresh_token,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'Invalid internal call',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const req = await fetch('https://discord.com/api/v10/oauth2/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Authorization: `Basic ${Buffer.from(this.configService.get('DISCORD_CLIENTID') + ':' + this.configService.get('DISCORD_CLIENTSECRET')).toString('base64')}`,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (!req.ok) {
|
||||||
|
const data = await req.json();
|
||||||
|
if (data.error == 'invalid_grant') {
|
||||||
|
throw new HttpException(
|
||||||
|
'Invalid code in request',
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(data);
|
||||||
|
throw new HttpException(
|
||||||
|
"Couldn't fetch Discord api",
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = await req.json();
|
||||||
|
if (!data.token_type || !data.access_token) {
|
||||||
|
console.error(data);
|
||||||
|
throw new HttpException(
|
||||||
|
'Invalid Discord API response',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const expires = new Date(Date.now() + data.expires_in);
|
||||||
|
session.tokenData = {
|
||||||
|
access_token: data.access_token,
|
||||||
|
token_type: data.token_type,
|
||||||
|
refresh_token: data.refresh_token,
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeDiscordToken(refresh_token: string) {
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
client_id: this.configService.get('DISCORD_CLIENTID'),
|
||||||
|
client_secret: this.configService.get('DISCORD_CLIENTSECRET'),
|
||||||
|
token: refresh_token,
|
||||||
|
token_type_hint: 'refresh_token',
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetch('https://discord.com/api/oauth2/token/revoke', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async discordLogin(
|
||||||
|
session: SessionData,
|
||||||
|
recursive: boolean = false,
|
||||||
|
): Promise<User> {
|
||||||
|
const req2 = await fetch('https://discord.com/api/users/@me', {
|
||||||
|
headers: {
|
||||||
|
authorization:
|
||||||
|
session.tokenData.token_type + ' ' + session.tokenData.access_token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!req2.ok) {
|
||||||
|
const error = await req2.json();
|
||||||
|
if (error.error == 'invalid_grant' && !recursive) {
|
||||||
|
await this.createDiscordToken(
|
||||||
|
{
|
||||||
|
refresh_token: session.tokenData.refresh_token,
|
||||||
|
},
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
return this.discordLogin(session);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
throw new HttpException(
|
||||||
|
"Couldn't fetch Discord api",
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data2 = await req2.json();
|
||||||
|
const user = await this.getOrCreateUser(
|
||||||
|
data2.id,
|
||||||
|
new UserResponseDTO(data2),
|
||||||
|
);
|
||||||
|
session.user = user;
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrCreateUser(id: string, data?: UserResponseDTO) {
|
||||||
|
let user = await this.usersRepository.findOneBy({ id });
|
||||||
|
if (!user) {
|
||||||
|
user = this.usersRepository.create({ ...data, id });
|
||||||
|
} else {
|
||||||
|
Object.assign(user, data);
|
||||||
|
}
|
||||||
|
await this.usersRepository.save(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
async findUser(id: string) {
|
||||||
|
return await this.usersRepository.findOneBy({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async discordLogout(session: SessionData) {
|
||||||
|
await this.revokeDiscordToken(session.tokenData.access_token);
|
||||||
|
delete session.user;
|
||||||
|
delete session.tokenData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class WordRecieveDTO {
|
||||||
|
constructor(partial: Partial<WordRecieveDTO>) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { Word } from './word.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Topic {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ManyToMany((_type) => Word, (word) => word.topics)
|
||||||
|
words: Promise<Word[]>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Topic } from './topic.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Word {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: 'ownerId' })
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
owner: Promise<User>;
|
||||||
|
|
||||||
|
@Column({ name: 'ownerId' })
|
||||||
|
ownerId: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
@ManyToMany(() => Topic, (topic) => topic.words)
|
||||||
|
@JoinTable()
|
||||||
|
topics: Topic[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
PipeTransform,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { WordService } from '../word.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WordPipe implements PipeTransform {
|
||||||
|
constructor(private readonly wordService: WordService) {}
|
||||||
|
|
||||||
|
async transform(id: number) {
|
||||||
|
const word = await this.wordService.getById(id);
|
||||||
|
if (!word) {
|
||||||
|
throw new HttpException('Word not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Sse,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AuthGuard } from 'src/users/guards/auth.guard';
|
||||||
|
import { WordRecieveDTO } from './dto/wordrecieve.dto';
|
||||||
|
import { WordPipe } from './pipe/word.pipe';
|
||||||
|
import { Word } from './entities/word.entity';
|
||||||
|
import { GetUser } from 'src/users/user.pipe';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
import { fromEvent, map, Observable } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { WordService } from './word.service';
|
||||||
|
import { WordResponseDTO } from 'events/dto/wordresponse.dto';
|
||||||
|
|
||||||
|
@ApiTags('words')
|
||||||
|
@Controller('words')
|
||||||
|
export class WordController {
|
||||||
|
constructor(
|
||||||
|
private readonly wordService: WordService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Sse('/subscribe')
|
||||||
|
subscribe(): Observable<{ data: string }> {
|
||||||
|
return fromEvent(this.eventEmitter, this.wordService.sse_prefix).pipe(
|
||||||
|
map((payload) => {
|
||||||
|
return {
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Delete a word' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async delete(@Param('id') id: number) {
|
||||||
|
const word = await this.wordService.getById(id);
|
||||||
|
if (!word) {
|
||||||
|
throw new HttpException('Word not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
await this.wordService.delete(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/')
|
||||||
|
@ApiOperation({ summary: 'Submit a word' })
|
||||||
|
@ApiBody({ type: WordRecieveDTO })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async create(@Body('value') value: string, @GetUser() user: User) {
|
||||||
|
const word = await this.wordService.create(value, user);
|
||||||
|
return new WordResponseDTO(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
@ApiOperation({ summary: 'List enabled words' })
|
||||||
|
async read() {
|
||||||
|
return (await this.wordService.listActive()).map(
|
||||||
|
(w) => new WordResponseDTO(w),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/all')
|
||||||
|
@ApiOperation({ summary: 'List all words' })
|
||||||
|
async readAll() {
|
||||||
|
return (await this.wordService.list()).map((w) => new WordResponseDTO(w));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id/enable')
|
||||||
|
@ApiOperation({ summary: 'Enable a word' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async enable(
|
||||||
|
@Param('id', ParseIntPipe) _id: string,
|
||||||
|
@Param('id', WordPipe) word: Word,
|
||||||
|
) {
|
||||||
|
await this.wordService.enable(word);
|
||||||
|
return new WordResponseDTO(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id/disable')
|
||||||
|
@ApiOperation({ summary: 'Disable a word' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async disable(
|
||||||
|
@Param('id', ParseIntPipe) _id: string,
|
||||||
|
@Param('id', WordPipe) word: Word,
|
||||||
|
) {
|
||||||
|
await this.wordService.disable(word);
|
||||||
|
return new WordResponseDTO(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Word } from './entities/word.entity';
|
||||||
|
import {
|
||||||
|
WordDeleteEvent,
|
||||||
|
WordCreateEvent,
|
||||||
|
WordEnableEvent,
|
||||||
|
WordDisableEvent,
|
||||||
|
} from 'events/word.events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WordService {
|
||||||
|
sse_prefix = 'sse.word';
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Word) private wordRepository: Repository<Word>,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getById(id: number) {
|
||||||
|
return await this.wordRepository.findOneBy({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(word: Word) {
|
||||||
|
const wordId = word.id;
|
||||||
|
await this.wordRepository.remove(word);
|
||||||
|
this.eventEmitter.emit(this.sse_prefix, new WordDeleteEvent(wordId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(value: string, owner: User) {
|
||||||
|
const word = this.wordRepository.create();
|
||||||
|
word.owner = Promise.resolve(owner);
|
||||||
|
word.value = value;
|
||||||
|
try {
|
||||||
|
await this.wordRepository.save(word);
|
||||||
|
this.eventEmitter.emit(this.sse_prefix, new WordCreateEvent(word));
|
||||||
|
return word;
|
||||||
|
} catch {
|
||||||
|
throw new HttpException('Word already exists', HttpStatus.CONFLICT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listActive() {
|
||||||
|
const words = await this.wordRepository.findBy({ enabled: true });
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
const words = await this.wordRepository.find();
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable(word: Word) {
|
||||||
|
word.enabled = true;
|
||||||
|
await this.wordRepository.save(word);
|
||||||
|
this.eventEmitter.emit(this.sse_prefix, new WordEnableEvent(word.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async disable(word: Word) {
|
||||||
|
word.enabled = false;
|
||||||
|
await this.wordRepository.save(word);
|
||||||
|
this.eventEmitter.emit(this.sse_prefix, new WordDisableEvent(word.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Word } from './entities/word.entity';
|
||||||
|
import { WordController } from './word.controller';
|
||||||
|
import { UsersModule } from 'src/users/users.module';
|
||||||
|
import { WordService } from './word.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [UsersModule, TypeOrmModule.forFeature([Word])],
|
||||||
|
controllers: [WordController],
|
||||||
|
providers: [WordService],
|
||||||
|
exports: [WordService],
|
||||||
|
})
|
||||||
|
export class WordModule {}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// import * as request from 'supertest';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleFixture = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('/ (GET)', () => {
|
||||||
|
// return request(app.getHttpServer())
|
||||||
|
// .get('/')
|
||||||
|
// .expect(200)
|
||||||
|
// .expect('Hello World!');
|
||||||
|
// });
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": "\\.e2e-spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { type User } from 'src/users/entities/user.entity';
|
||||||
|
declare module 'express-session' {
|
||||||
|
interface SessionData {
|
||||||
|
user?: User;
|
||||||
|
tokenData: {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
refresh_token: string;
|
||||||
|
expires: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'express' {
|
||||||
|
interface Request extends Request {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user