Compare commits

..

151 Commits
V3 ... master

Author SHA1 Message Date
Arthur VALIN b8af3b267c [db] clear db
continuous-integration/drone/push Build is failing Details
10 months ago
luevard a395c753dc Patch BetDetail when it has 0 participation
continuous-integration/drone/push Build is passing Details
10 months ago
Lucas EVARD 36c186d359 [db] Clear db
continuous-integration/drone/push Build is passing Details
10 months ago
Lucas EVARD 8d886b8592 Test users and start testing bets
continuous-integration/drone/push Build is passing Details
10 months ago
avalin 4b2e9c22a3 Fix getWonNotifications
continuous-integration/drone/push Build is passing Details
10 months ago
Lucas EVARD 8480b42275 See these own bets
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 32ba8e77b1 Change username by id of user
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 047b631392 [db] Change username by id of user
continuous-integration/drone/push Build is failing Details
11 months ago
Lucas EVARD 1fcda17412 Add https for image
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 64f07774ec Merge branch 'master' of https://codefirst.iut.uca.fr/git/AllDev/Api
continuous-integration/drone/push Build is failing Details
11 months ago
Lucas EVARD d3481797f9 Fix code smell
11 months ago
Lucas EVARD 7e4e4e0a5c Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD ef5cc36bae Transférer les fichiers vers 'Documentation/Images'
continuous-integration/drone/push Build is failing Details
11 months ago
Lucas EVARD bc57517aa1 [no_ci] Modification du README
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD c9225557df Fix Error and add route for add new users to a private bet
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD e8922f95b3 Merge branch 'master' of https://codefirst.iut.uca.fr/git/AllDev/Api
11 months ago
Lucas EVARD 0f3e5dec99 [db] Merge pull request 'privateBet' (#18) from privateBet into master
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD d000e900dc Merge branch 'master' into privateBet
continuous-integration/drone/pr Build was killed Details
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD f6e13ebb06 Merge remote-tracking branch 'origin/privateBet'
11 months ago
Lucas EVARD 30334c63cb [no_ci] Allow users to get only authorized bets
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build was killed Details
11 months ago
Lucas EVARD f29f3f438e Merge pull request ' add /bets/users routes' (#17) from getParticipationInformation into master
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD b5dffc403d Merge branch 'master' into getParticipationInformation
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 4cfc3543e4 add /bets/users routes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
11 months ago
luevard 6b657de563 [no_ci] Remove private bet when you can't see it
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 0d9e0971f8 Add Private Bet
continuous-integration/drone/push Build is passing Details
11 months ago
avalin fb87d8e7f9 Fix BetAnswerInfos
continuous-integration/drone/push Build is passing Details
11 months ago
avalin 90b099183c Fix user stats
continuous-integration/drone/push Build is passing Details
11 months ago
avalin 4db74afce2 Add won participation to bet detail
continuous-integration/drone/push Build is passing Details
11 months ago
avalin deca69585f Add total stakes and total participants to bet reponse [db]
continuous-integration/drone/push Build is passing Details
11 months ago
avalin 59ef808123 Fix user search
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 2b13728b9c Add logs for every routes
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 441e213d3e Merge
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 110aab80a7 Merge remote-tracking branch 'origin/profilePicture'
11 months ago
luevard 5229765d6b Add UserDTO information (number of friends, number of bets and the best win)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
11 months ago
luevard 9069432225 Clean Base64 from database
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 73d33cc5f0 Last change URI
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 8af569d8c0 Last change URI
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 8be915ac9c Change URI
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 7aff06a86d Change URI
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 19aa5620a6 Change URI
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD f9ae6c399b Test User image
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD fbd2492517 [db] Reboot db
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 6de6f78fe2 [no_ci] Add SQL table for user image
11 months ago
luevard e291e0064b [no_ci] Add routes for inject,get and remove image of a user. Update pom.xml dependencies.
continuous-integration/drone/push Build is passing Details
11 months ago
Arthur VALIN f3aebdb8c3 fix/winnings (#15)
continuous-integration/drone/push Build is passing Details
11 months ago
avalin 772a615a4a Fix bet status update and createdBy
continuous-integration/drone/push Build is passing Details
11 months ago
avalin c6c0b67dd3 fix bet serialization
continuous-integration/drone/push Build is passing Details
11 months ago
avalin 95184858da Add fuzzy user search with levenshtein
continuous-integration/drone/push Build is passing Details
11 months ago
avalin a15b77342b Fix daily gift [db]
continuous-integration/drone/push Build is passing Details
11 months ago
Arthur VALIN 8d8bccc7a3 fix/friends (#14) [db]
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 3e198b7605 Mise à jour de '.drone.star'
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 0e11d077d2 [no_ci] Test upload image
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD de80aecc63 Merge pull request 'popularBet' (#13) from popularBet into master
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 7312bcab77 Merge
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is passing Details
11 months ago
luevard dc32e767ea Merge branch 'master' into popularBet
11 months ago
Lucas EVARD c64db61e25 revert f82aba9a02
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is failing Details
11 months ago
Lucas DELANIER 9b66d1f01b add filter get bets (#11)
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas DELANIER 17c526b8b3 Mise à jour de 'README.md'
11 months ago
Arthur VALIN e5b262af14 revert 667cdc5472
11 months ago
Arthur VALIN 0b80c4dc90 Mise à jour de '.drone.star'
11 months ago
luevard f82aba9a02 Refactor and add PopularityScore for Mocking Data Source
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 75666ad2a3 Refactor and add PopularityScore for Mocking Data Source
continuous-integration/drone/push Build is failing Details
11 months ago
luevard a3aa69b07d debug Popular bet
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 74ddd915f9 [db] debug Popular bet
continuous-integration/drone/push Build is passing Details
11 months ago
luevard ddacad6885 debug Popular bet
continuous-integration/drone/push Build is passing Details
11 months ago
luevard b03bf7e121 debug Popular bet
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 1c3fb9a7f3 [db] Patch cache problem
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD f2a84adfcc [db] Raise database
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD f68a7e7d64 [db] Raise database
continuous-integration/drone/push Build was killed Details
11 months ago
luevard 9ff2e8d896 Add route and column for get the most popular bet
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas DELANIER 6168d50bfa add filter get bets (#11)
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas DELANIER 7e1f4cab95 Mise à jour de 'README.md'
continuous-integration/drone/push Build is passing Details
11 months ago
Arthur VALIN 8fc6f68dc1 revert 667cdc5472
continuous-integration/drone/push Build is passing Details
11 months ago
Arthur VALIN 667cdc5472 Mise à jour de '.drone.star'
continuous-integration/drone/push Build encountered an error Details
11 months ago
luevard bf9ddc93f9 Connect with email OR username
continuous-integration/drone/push Build is passing Details
11 months ago
luevard c60e3905aa Add friend mock and refactor code
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD dfda4bd689 Mise à jour de 'Sources/src/main/kotlin/allin/Application.kt'
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD e6abc4464c [db] Merge pull request 'addFriend' (#12) from addFriend into master
continuous-integration/drone/push Build was killed Details
11 months ago
luevard cad34d0f2a Patch mock DataSource
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
11 months ago
luevard d200bef2c3 Merge friend route
continuous-integration/drone/push Build is failing Details
11 months ago
luevard d4da0fd3f6 [no_ci] Add friend routes
continuous-integration/drone/push Build is passing Details
11 months ago
avalin af013b6666 Fix entities usages
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 7c49c34167 [no_ci] Add Friend methods and entity
continuous-integration/drone/push Build is passing Details
11 months ago
luevard b9825bb5ca [no_ci] Add friend interface
continuous-integration/drone/push Build is passing Details
11 months ago
avalin cae02af684 Refactor and fixes
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 0621d8c406 Mise à jour de 'Sources/src/main/kotlin/allin/Application.kt'
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 1aa6eb1329 Remove cast UUID to String
continuous-integration/drone/push Build is passing Details
11 months ago
luevard 8f75e78f3c Saving UUID of the user who created a bet
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD dc929370d1 Test no-override database
continuous-integration/drone/push Build is passing Details
11 months ago
Lucas EVARD 65e80d318f Test création base de données[db]
continuous-integration/drone/push Build is passing Details
11 months ago
Emre KARTAL 51d55581e6 Mise à jour de '.drone.star'
continuous-integration/drone/push Build is passing Details
11 months ago
Emre KARTAL c54d1a7623 Mise à jour de '.drone.star'
continuous-integration/drone/push Build encountered an error Details
11 months ago
Lucas EVARD 9d210298f8 Mise à jour de '.drone.star'
continuous-integration/drone/push Build encountered an error Details
11 months ago
Emre KARTAL 977df4b696 Add starlark
11 months ago
Lucas EVARD 2bef653094 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build was killed Details
1 year ago
Lucas EVARD 8ca388de26 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD f946ae909b Add swaggerUrl
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD db50ca3c52 Add Swagger's rootHostPath
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD 10374d62f8 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build was killed Details
1 year ago
Lucas EVARD 5847fbe32d Merge remote-tracking branch 'origin/master'
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD 734c6c6fb1 Add Swagger's comments
1 year ago
Arthur VALIN 30c4cf199f 🐛 Fix status update
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 62aa1bbf9b 🐛 Fix coins on victory (mock)
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 0dd1017e81 🐛 Fix toConfirm
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN ab2c861902 🐛 Fix mock participation
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD 01a016b34f Add Swagger in route /swagger-ui
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD 07e58ece6f Change Kotlin's API (1.4.11 to 1.9.10) and add swagger (not working actually)
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN be3885a040 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN afb40214e5 🐛 Fix pg create table
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 464bdfeee1 Merge pull request 'bet_confirmation' (#10) from bet_confirmation into master
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN cd7e20a4b8 Bet result and history
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD edc5da54d0 Modify SQL query to trigger daily gift at 00:00
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 0aa192efaf fix confirm route args
continuous-integration/drone/push Build is failing Details
1 year ago
Arthur VALIN 3e4f6f77ff try fix pg enums
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN d7bca71cc8 try fix pg enums
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 3b3844fd50 try fix pg enums
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN fc9449de60 try fix pg enums
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 4a7f695d10 Fix confirmation and pg enums
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 9e1fcee33b Start bet confirmation
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas DELANIER e38fd0ff61 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 7ba9d2715d Merge Stub_Db
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN a877762d2d Merge branch 'Stub_Db'
1 year ago
Arthur VALIN f58b906c38 Update bet verify delay
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN b26a3f57bd try fix api pg enum
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 9ebc9aec4d try fix api pg enum
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD d221ac0b88 Merge pull request 'daily_gift' (#7) from daily_gift into master
continuous-integration/drone/push Build is passing Details
1 year ago
luevard 27892f9d83 Merge remote-tracking branch 'origin/daily_gift' into daily_gift
continuous-integration/drone/push Build is passing Details
1 year ago
luevard 3582fdb034 Add DailyGift route
1 year ago
Arthur VALIN e813015ab2 try fix api pg enum
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD cb7c2ffdbb UsersEntity Merge
continuous-integration/drone/push Build is failing Details
1 year ago
luevard 648081f714 Merge branch 'master' into daily_gift
1 year ago
Arthur VALIN d0d8b484ce try fix api pg enum
continuous-integration/drone/push Build is passing Details
1 year ago
luevard cbc26a9581 Add DailyGift route
continuous-integration/drone/push Build is passing Details
1 year ago
Lucas EVARD 7692c6b463 revert 1374f48ed2
continuous-integration/drone/push Build is passing Details
1 year ago
luevard 1374f48ed2 Add DailyGift route
continuous-integration/drone/push Build is failing Details
1 year ago
Arthur VALIN 53798d3bbf fix api pg enum
continuous-integration/drone/push Build is passing Details
1 year ago
luevard 7dda014bc3 Add modify coins
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 408370dc2b fix api
continuous-integration/drone/push Build is passing Details
1 year ago
luevard 3375090815 Add dailygift route
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 52d19d6fd9 fix api
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 8f9310a58d Merge remote-tracking branch 'origin/Stub_Db' into Stub_Db
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN be81c92571 fix api
1 year ago
Arthur VALIN 41730c1759 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 79fc07a6ba Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 07042b5379 Merge remote-tracking branch 'origin/Stub_Db' into Stub_Db
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 65cc0d8fde Bet status, type and update status
1 year ago
Lucas EVARD 59f5c73ca1 Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
luevard dee7856db2 Add Daily gift route and SQL request
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN 1b0c7624fc Mise à jour de '.drone.yml'
continuous-integration/drone/push Build is passing Details
1 year ago
Arthur VALIN addc40f4bf Add abstract data source
continuous-integration/drone/push Build is passing Details
1 year ago
root 2b3a79bd0f Fixed current bets route 🐛
continuous-integration/drone/push Build is passing Details
1 year ago

@ -0,0 +1,147 @@
def main(ctx):
commit_message = ctx.build.message.lower()
if "readme.md" in commit_message or "[no_ci]" in commit_message:
return nullPipeline()
if "[db]" in commit_message:
return [
ci(ctx),
cd(ctx),
db(ctx)
]
else :
return [
ci(ctx),
cd(ctx),
]
def nullPipeline():
return {
"kind": "pipeline",
"name": "Nothing",
"steps": []
}
def ci(ctx):
CI = {
"kind": "pipeline",
"name": "CI",
"steps": [
{
"name": "compilation",
"image": "maven:3-openjdk-11",
"commands": [
"cd Sources",
"mvn clean package",
]
},
{
"name": "code-analysis",
"image": "openjdk:8-jdk",
"commands": [
"export SONAR_SCANNER_VERSION=4.7.0.2747",
"export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux",
"curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip",
"unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/",
"export PATH=$SONAR_SCANNER_HOME/bin:$PATH",
"export SONAR_SCANNER_OPTS=\"-server\"",
"sonar-scanner -D sonar.projectKey=Api-Allin -D sonar.sources=./Sources -D sonar.host.url=https://codefirst.iut.uca.fr/sonar",
],
"settings": {
"sources": "./src/",
},
"environment": {
"SONAR_TOKEN": {"from_secret": "SECRET_TOKEN"},
},
}
]
}
return CI
def cd(ctx):
CD = {
"kind": "pipeline",
"name": "CD",
"volumes": [
{
"name": "images",
"temp": {}
}
],
"steps": [
{
"name": "hadolint",
"image": "hadolint/hadolint:latest-alpine",
"commands": [
"hadolint Sources/Dockerfile"
]
},
{
"name": "docker-image",
"image": "plugins/docker",
"settings": {
"dockerfile": "Sources/Dockerfile",
"context": "Sources",
"registry": "hub.codefirst.iut.uca.fr",
"repo": "hub.codefirst.iut.uca.fr/lucas.evard/api",
"username": {"from_secret": "SECRET_REGISTRY_USERNAME"},
"password": {"from_secret": "SECRET_REGISTRY_PASSWORD"}
}
},
{
"name": "deploy-container",
"image": "hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest",
"environment": {
"CODEFIRST_CLIENTDRONE_ENV_DATA_SOURCE": "postgres",
"CODEFIRST_CLIENTDRONE_ENV_CODEFIRST_CONTAINER": {"from_secret": "CODEFIRST_CONTAINER"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_DB": {"from_secret": "db_database"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_USER": {"from_secret": "db_user"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_PASSWORD": {"from_secret": "db_password"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_HOST": {"from_secret": "db_host"},
"CODEFIRST_CLIENTDRONE_ENV_SALT": {"from_secret": "SALT"},
"ADMINS": "lucasevard,emrekartal,arthurvalin,lucasdelanier",
"IMAGENAME": "hub.codefirst.iut.uca.fr/lucas.evard/api:latest",
"CONTAINERNAME": "api",
"COMMAND": "create",
"OVERWRITE": "true",
},
"depends_on": [
"docker-image"
],
"volumes": [
{
"name": "images",
"path": "/uploads"
}
]
}
]
}
return CD
def db(ctx):
DB = {
"kind": "pipeline",
"name": "DB",
"steps": [
{
"name": "deploy-container-postgres",
"image": "hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest",
"environment": {
"IMAGENAME": "postgres:latest",
"CONTAINERNAME": "postgresapi",
"COMMAND": "create",
"OVERWRITE": "false",
"PRIVATE": "false",
"ADMINS": "lucasevard,emrekartal,arthurvalin,lucasdelanier",
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_ROOT_PASSWORD": {"from_secret": "db_root_password"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_DB": {"from_secret": "db_database"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_USER": {"from_secret": "db_user"},
"CODEFIRST_CLIENTDRONE_ENV_POSTGRES_PASSWORD": {"from_secret": "db_password"}
}
}
]
}
return DB

@ -29,7 +29,6 @@ steps:
- export SONAR_SCANNER_OPTS="-server"
- sonar-scanner -D sonar.projectKey=Api-Allin -D sonar.sources=./Sources -D sonar.host.url=https://codefirst.iut.uca.fr/sonar
---
kind: pipeline
@ -46,24 +45,6 @@ steps:
commands:
- hadolint Sources/Dockerfile
- name: deploy-container-postgres
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest
environment:
IMAGENAME: postgres:latest
CONTAINERNAME: postgresapi
COMMAND: create
OVERWRITE: false
PRIVATE: false
ADMINS: lucasevard,emrekartal,arthurvalin,lucasdelanier
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_ROOT_PASSWORD:
from_secret: db_root_password
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_DB:
from_secret: db_database
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_USER:
from_secret: db_user
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_PASSWORD:
from_secret: db_password
- name: docker-image
image: plugins/docker
settings:
@ -83,6 +64,9 @@ steps:
CONTAINERNAME: api
COMMAND: create
OVERWRITE: true
CODEFIRST_CLIENTDRONE_ENV_DATA_SOURCE: postgres
CODEFIRST_CLIENTDRONE_ENV_CODEFIRST_CONTAINER:
from_secret: CODEFIRST_CONTAINER
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_DB:
from_secret: db_database
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_USER:
@ -96,3 +80,20 @@ steps:
ADMINS: lucasevard,emrekartal,arthurvalin,lucasdelanier
depends_on: [docker-image]
- name: deploy-container-postgres
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest
environment:
IMAGENAME: postgres:latest
CONTAINERNAME: postgresapi
COMMAND: create
OVERWRITE: false
PRIVATE: false
ADMINS: lucasevard,emrekartal,arthurvalin,lucasdelanier
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_ROOT_PASSWORD:
from_secret: db_root_password
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_DB:
from_secret: db_database
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_USER:
from_secret: db_user
CODEFIRST_CLIENTDRONE_ENV_POSTGRES_PASSWORD:
from_secret: db_password

2
.gitignore vendored

@ -36,3 +36,5 @@ out/
.vscode/
!**/src/target/**
**/src/target/**
/src/target

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

@ -44,6 +44,10 @@
- Pour stocker les données des utilisateurs et des paris, nous utilisons une base de données PostgreSQL sur laquelle l'API s'appuie pour récupérer et enregistrer des données. En raison de sa conformité aux dernières normes SQL, garantissant une compatibilité et une évolutivité optimales, ainsi que de son efficacité à gérer un grand nombre de données.
</br>
<img src="Documentation/Images/ktorm.png" width="50" /> **Ktorm**
- Nous avons choisi Ktorm comme ORM pour notre projet en raison de sa compatibilité native avec le framework Ktor. Ktorm permet de mapper notre modèle aux tables de la base de données PostgreSQL de manière fluide et efficace, réduisant les erreurs potentielles liées à la manipulation directe de SQL.
# Outils
Pour la partie API, nous utilisons plusieurs outils et méthodes :
@ -65,6 +69,10 @@ Afin de garantir la sécurité des échanges d'informations entre notre applicat
Ils sont essentiels pour une transmission sécurisée des données entre les différentes couches de l'application.
Contribuent ainsi à maintenir l'intégrité et la cohérence des données tout au long du processus.
### Mock et Base de données
En cas de problème sur le déployement et l'accès à la base de données, des mocks seront utilisables afin de simuler le comportement que pourrait avoir la base de données mais de manière temporaire. Ces mocks permettront de continuer le développement et les tests de l'application sans interruption, garantissant ainsi la productivité. Une fois que l'accès à la base de données sera rétabli, il suffira de désactiver les mocks pour revenir à l'utilisation normale de la base de données PostgreSQL.
# Controllers
Notre API est organisée en utilisant une séparation logique des routes par le biais de controllers.
@ -80,15 +88,14 @@ Le controller BET gère toutes les opérations liées aux paris. Il permet de r
:white_check_mark: Cette séparation permet une gestion plus claire et modulaire des fonctionnalités de l'API, facilitant la maintenance et l'extension de notre système.
# Déploiement
Le déploiement est réalisé sur Code First via les services **Drone** et **Runner** ! :rocket:
Lien de l'API sur codefirst : [API All In](https://codefirst.iut.uca.fr/containers/AllDev-api)
<div align = center>
© AllDev - API
</div>

@ -8,5 +8,6 @@ EXPOSE 8080
RUN mkdir /app
COPY --from=build /home/maven/src/target/*-with-dependencies.jar /app/ktor-docker-sample.jar
ENTRYPOINT ["java", "-jar", "/app/ktor-docker-sample.jar"]

@ -10,10 +10,9 @@
<properties>
<kotlin.version>1.9.10</kotlin.version>
<serialization.version>1.5.0</serialization.version>
<ktor_version>2.3.4</ktor_version>
<ktor_version>2.3.11</ktor_version>
<kotlin.code.style>official</kotlin.code.style>
<kotlin_version>1.9.10</kotlin_version>
<logback_version>1.4.11</logback_version>
<logback_version>1.4.14</logback_version>
<slf4j_version>2.0.9</slf4j_version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
@ -35,18 +34,33 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-netty-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>io.github.smiley4</groupId>
<artifactId>ktor-swagger-ui</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-swagger-jvm</artifactId>
<version>${ktor_version}</version>
</dependency>
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-support-postgresql</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
@ -96,7 +110,7 @@
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin_version}</version>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@ -115,6 +129,11 @@
<artifactId>ktor-server-auth-jvm</artifactId>
<version>2.3.4</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
@ -123,7 +142,24 @@
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-auth-jwt-jvm</artifactId>
<version>2.3.4</version>
<version>2.3.11</version>
</dependency>
<dependency>
<groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-generators</artifactId>
<version>1.0.50</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@ -138,9 +174,9 @@
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin_version}</version>
<version>${kotlin.version}</version>
<configuration>
<jvmTarget>1.8</jvmTarget>
<jvmTarget>11</jvmTarget>
</configuration>
<executions>
<execution>

@ -1,9 +1,15 @@
package allin
import allin.entities.*
import allin.data.AllInDataSource
import allin.data.mock.MockDataSource
import allin.data.postgres.PostgresDataSource
import allin.routing.*
import allin.utils.*
import allin.utils.TokenManager
import allin.utils.TokenManager.Companion.Claims.USERNAME
import allin.utils.kronJob
import com.typesafe.config.ConfigFactory
import io.github.smiley4.ktorswaggerui.SwaggerUI
import io.github.smiley4.ktorswaggerui.data.SwaggerUiSort
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
@ -12,21 +18,34 @@ import io.ktor.server.config.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import org.ktorm.database.Database
import java.time.ZonedDateTime
import kotlin.time.Duration.Companion.minutes
import kotlin.time.ExperimentalTime
val db_database=System.getenv().get("POSTGRES_DB")
val db_user=System.getenv().get("POSTGRES_USER")
val db_password=System.getenv().get("POSTGRES_PASSWORD")
val db_host=System.getenv().get("POSTGRES_HOST")
@ExperimentalTime
val BET_VERIFY_DELAY = 1.minutes
val database = Database.connect("jdbc:postgresql://$db_host/$db_database", user = db_user, password = db_password)
val data_source = System.getenv()["DATA_SOURCE"]
val isCodeFirstContainer = System.getenv()["CODEFIRST_CONTAINER"].orEmpty()
val hostIP = "0.0.0.0"
val hostPort = 8080
private val allInDataSource: AllInDataSource = when (data_source) {
"mock" -> MockDataSource()
"postgres" -> PostgresDataSource()
else -> MockDataSource()
}
val Application.dataSource: AllInDataSource
get() = allInDataSource
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
embeddedServer(Netty, port = hostPort, host = hostIP) {
extracted()
}.start(wait = true)
}
@OptIn(ExperimentalTime::class)
private fun Application.extracted() {
val config = HoconApplicationConfig(ConfigFactory.load())
val tokenManager = TokenManager.getInstance(config)
@ -35,22 +54,45 @@ private fun Application.extracted() {
verifier(tokenManager.verifyJWTToken())
realm = config.property("realm").getString()
validate { jwtCredential ->
if (jwtCredential.payload.getClaim("username").asString().isNotEmpty())
if (jwtCredential.payload.getClaim(USERNAME).asString().isNotEmpty())
JWTPrincipal(jwtCredential.payload)
else null
}
}
}
install(ContentNegotiation) {
json()
install(ContentNegotiation) { json() }
install(SwaggerUI) {
swagger {
swaggerUrl = "swagger"
rootHostPath = isCodeFirstContainer
swaggerUrl = "$isCodeFirstContainer/swagger"
onlineSpecValidator()
displayOperationId = true
showTagFilterInput = true
sort = SwaggerUiSort.HTTP_METHOD
}
info {
title = "Allin API"
version = "latest"
description = "Allin API"
license {
name = "Apache 2.0"
url = "https://www.apache.org/licenses/LICENSE-2.0.html"
}
}
}
basicRouter()
userRouter()
betRouter()
participationRouter()
betDetailRouter()
friendRouter()
kronJob(BET_VERIFY_DELAY) {
dataSource.betDataSource.updateBetStatuses(ZonedDateTime.now())
}
BasicRouting()
UserRouter()
BetRouter()
ParticipationRouter()
BetDetailRouter()
UsersEntity.createUserTable()
BetsEntity.createBetsTable()
ResponsesEntity.createResponseTable()
ParticipationsEntity.createParticipationTable()
}

@ -0,0 +1,8 @@
package allin.data
abstract class AllInDataSource {
abstract val userDataSource: UserDataSource
abstract val betDataSource: BetDataSource
abstract val participationDataSource: ParticipationDataSource
abstract val friendDataSource: FriendDataSource
}

@ -0,0 +1,26 @@
package allin.data
import allin.dto.UserDTO
import allin.model.*
import java.time.ZonedDateTime
interface BetDataSource {
fun getAllBets(filters: List<BetFilter>, userDTO: UserDTO): List<Bet>
fun getBetById(id: String): Bet?
fun getBetDetailById(id: String, userid: String): BetDetail?
fun getBetsNotFinished(): List<Bet>
fun addBet(bet: Bet)
fun removeBet(id: String): Boolean
fun updateBet(data: UpdatedBetData): Boolean
fun updateBetStatuses(date: ZonedDateTime)
fun getToConfirm(user: UserDTO): List<BetDetail>
fun confirmBet(betId: String, result: String)
fun getWonNotifications(userid: String): List<BetResultDetail>
fun getHistory(userid: String): List<BetResultDetail>
fun getCurrent(userid: String): List<BetDetail>
fun getMostPopularBet(): Bet?
fun updatePopularityScore(betId: String)
fun addPrivateBet(bet: Bet)
fun isInvited(betid: String, userId: String): Boolean
fun addUserInPrivatebet(updatedPrivateBet: UpdatedPrivateBet)
}

@ -0,0 +1,20 @@
package allin.data
import allin.dto.UserDTO
import allin.model.FriendStatus
interface FriendDataSource {
fun addFriend(sender: String, receiver: String)
fun getFriendFromUserId(id: String): List<UserDTO>
fun getFriendRequestsFromUserId(id: String): List<UserDTO>
fun deleteFriend(senderId: String, receiverId: String): Boolean
fun isFriend(firstUser: String, secondUser: String): Boolean
fun filterUsersByUsername(fromUserId: String, search: String): List<UserDTO>
fun getFriendStatus(firstUser: String, secondUser: String) =
if (isFriend(firstUser, secondUser)) {
if (isFriend(secondUser, firstUser)) {
FriendStatus.FRIEND
} else FriendStatus.REQUESTED
} else FriendStatus.NOT_FRIEND
}

@ -0,0 +1,9 @@
package allin.data
import allin.model.Participation
interface ParticipationDataSource {
fun addParticipation(participation: Participation)
fun getParticipationFromBetId(betid: String): List<Participation>
fun deleteParticipation(id: String): Boolean
}

@ -0,0 +1,19 @@
package allin.data
import allin.dto.UserDTO
import allin.model.User
interface UserDataSource {
fun getUserByUsername(username: String): Pair<UserDTO?, String?>
fun addUser(user: User)
fun deleteUser(username: String): Boolean
fun addCoins(username: String, amount: Int)
fun removeCoins(username: String, amount: Int)
fun userExists(username: String): Boolean
fun emailExists(email: String): Boolean
fun canHaveDailyGift(username: String): Boolean
fun addImage(userid: String, image: ByteArray)
fun removeImage(userid: String)
fun getImage(userid: String): String?
fun getUserById(id: String): UserDTO?
}

@ -0,0 +1,254 @@
package allin.data.mock
import allin.data.BetDataSource
import allin.dto.UserDTO
import allin.model.*
import allin.model.BetStatus.*
import java.time.ZonedDateTime
import kotlin.math.roundToInt
class MockBetDataSource(private val mockData: MockDataSource.MockData) : BetDataSource {
private val bets get() = mockData.bets
private val results get() = mockData.results
private val users get() = mockData.users
private val participations get() = mockData.participations
private val resultNotifications get() = mockData.resultNotifications
private val betInfos get() = mockData.betInfos
private val answerInfos get() = mockData.answerInfos
override fun getAllBets(filters: List<BetFilter>, userDTO: UserDTO): List<Bet> {
return when {
filters.isEmpty() -> bets
filters.size == 1 -> {
val filter = filters[0]
when (filter) {
BetFilter.PUBLIC -> bets.filter { !it.isPrivate }
BetFilter.INVITATION -> bets.filter { it.isPrivate }
BetFilter.FINISHED -> bets.filter { it.status == FINISHED }
BetFilter.IN_PROGRESS -> bets.filter {
it.status in listOf(IN_PROGRESS, WAITING, CLOSING)
}
}.map { it }
}
else -> {
bets.filter { bet ->
val public = (BetFilter.PUBLIC in filters) && !bet.isPrivate
val invitation = (BetFilter.INVITATION in filters) && bet.isPrivate
val finished =
(BetFilter.FINISHED in filters) and ((bet.status == FINISHED) or (bet.status == CANCELLED))
val inProgress = (BetFilter.IN_PROGRESS in filters) and (bet.status in listOf(
IN_PROGRESS,
WAITING,
CLOSING
))
(public || invitation) && (finished or inProgress)
}.map { it }
}
}
}
override fun getBetById(id: String): Bet? =
bets.find { it.id == id }
override fun getBetDetailById(id: String, username: String): BetDetail? =
bets.find { it.id == id }?.toBetDetail(username)
override fun removeBet(id: String): Boolean {
betInfos.removeIf { it.id == id }
answerInfos.removeIf { it.betId == id }
return bets.removeIf { it.id == id }
}
override fun updateBet(data: UpdatedBetData): Boolean {
return bets.find { it.id == data.id }?.let {
it.isPrivate = data.isPrivate
} != null
}
override fun getBetsNotFinished(): List<Bet> =
bets.filter { it.endBet >= ZonedDateTime.now() }
override fun addBet(bet: Bet) {
bets += bet
betInfos += BetInfo(id = bet.id, totalStakes = 0, totalParticipants = 0)
bet.response.forEach {
answerInfos += BetAnswerInfo(
betId = bet.id,
response = it,
totalStakes = 0,
odds = 1f
)
}
}
override fun updateBetStatuses(date: ZonedDateTime) {
bets.forEachIndexed { idx, bet ->
if (bet.status != CANCELLED && bet.status != FINISHED && date >= bet.endRegistration) {
bets[idx] = when {
date >= bet.endBet && date.plusWeeks(1) >= bet.endBet -> {
cancelBet(bet)
}
date >= bet.endBet -> bet.copy(status = CLOSING)
else -> bet.copy(status = WAITING)
}
}
}
}
private fun cancelBet(bet: Bet): Bet {
participations
.filter { it.betId == bet.id }
.forEach { p ->
users.replaceAll { user ->
if (user.username == p.username) {
user.copy(nbCoins = user.nbCoins + p.stake)
} else user
}
}
return bet.copy(status = CANCELLED)
}
override fun getToConfirm(user: UserDTO): List<BetDetail> =
bets.filter { it.createdBy == user.id && it.status == CLOSING }
.map { it.toBetDetail(user.username) }
override fun confirmBet(betId: String, result: String) {
results.add(
BetResult(
betId = betId,
result = result
)
)
bets.replaceAll {
if (it.id == betId) {
it.copy(status = FINISHED)
} else it
}
val resultAnswerInfo = answerInfos.find { it.betId == betId && it.response == result }
participations.filter { it.betId == betId && it.answer == result }
.forEach { participation ->
val amount = (participation.stake * (resultAnswerInfo?.odds ?: 1f)).roundToInt()
users.replaceAll {
if (it.username == participation.username) {
it.copy(nbCoins = it.nbCoins + amount)
} else it
}
resultNotifications.add(Pair(betId, participation.username))
}
}
override fun getWonNotifications(userid: String): List<BetResultDetail> {
return bets.map { bet ->
val notification = resultNotifications.find { it.first == bet.id } ?: return@map null
val result = results.find { it.betId == bet.id } ?: return@map null
val participation = participations.find { it.userId == userid && it.betId == bet.id }
?: return@map null
if (participation.answer == result.result) {
resultNotifications.remove(notification)
val answerInfo = answerInfos.find { it.betId == bet.id && it.response == participation.answer }
BetResultDetail(
betResult = result,
bet = bet,
participation = participation,
amount = (participation.stake * (answerInfo?.odds ?: 1f)).roundToInt(),
won = true
)
} else null
}.mapNotNull { it }
}
override fun getHistory(username: String): List<BetResultDetail> {
return bets.map { bet ->
val result = results.find { it.betId == bet.id } ?: return@map null
val participation = participations.find { it.username == username && it.betId == bet.id }
?: return@map null
val won = participation.answer == result.result
val answerInfo = answerInfos.find {
it.betId == bet.id && it.response == participation.answer
}
BetResultDetail(
betResult = result,
bet = bet,
participation = participation,
amount = if (won) {
(participation.stake * (answerInfo?.odds ?: 1f)).roundToInt()
} else participation.stake,
won = won
)
}.mapNotNull { it }
}
override fun getCurrent(username: String): List<BetDetail> {
return bets.mapNotNull { bet ->
when (bet.status) {
CANCELLED, FINISHED -> return@mapNotNull null
else -> {
val userParticipation = participations.find { it.username == username && it.betId == bet.id }
if (userParticipation == null) return@mapNotNull null
return@mapNotNull bet.toBetDetail(username)
}
}
}
}
private fun Bet.toBetDetail(username: String): BetDetail {
val participation = participations.find { it.username == username && it.betId == this.id }
val participations = participations.filter { it.betId == this.id }
return BetDetail(
bet = this,
answers = getBetAnswerDetail(
bet = this,
participations = participations,
infos = answerInfos.filter { it.betId == this.id }
),
participations = participations,
userParticipation = participation,
wonParticipation = if (this.status == FINISHED) {
val result = results.find { it.betId == this.id }
result?.let { r ->
participations
.filter { it.answer == r.result }
.maxBy { it.stake }
}
} else null
)
}
override fun getMostPopularBet() =
mockData.bets.filter { !it.isPrivate && it.status == WAITING }.maxBy { it.popularityscore }
override fun updatePopularityScore(betId: String) {
val bet = mockData.bets.firstOrNull { it.id == betId } ?: return
val participations = mockData.participations.filter { it.betId == betId }
val score = participations.size * participations.size + participations.sumOf { it.stake }
bet.popularityscore = score
}
override fun addPrivateBet(bet: Bet) {
addBet(bet)
bet.userInvited?.forEach {
mockData.privatebets.add(InvitationBet(bet.id, it))
}
}
override fun isInvited(betid: String, userId: String) =
mockData.privatebets.filter { (it.betid == betid) and (it.userId == userId) }.isNotEmpty()
override fun addUserInPrivatebet(updatedPrivateBet: UpdatedPrivateBet) {
updatedPrivateBet.usersInvited?.forEach {
mockData.privatebets.add(InvitationBet(updatedPrivateBet.betid, it))
}
}
}

@ -0,0 +1,32 @@
package allin.data.mock
import allin.data.*
import allin.model.*
import java.time.ZonedDateTime
class MockDataSource : AllInDataSource() {
init {
println("APP STARTING ON MOCK DATA SOURCE")
}
class MockData {
val bets by lazy { mutableListOf<Bet>() }
val betInfos by lazy { mutableListOf<BetInfo>() }
val answerInfos by lazy { mutableListOf<BetAnswerInfo>() }
val results by lazy { mutableListOf<BetResult>() }
val resultNotifications by lazy { mutableListOf<Pair<String, String>>() }
val users by lazy { mutableListOf<User>() }
val lastGifts by lazy { mutableMapOf<String, ZonedDateTime>() }
val participations by lazy { mutableListOf<Participation>() }
val friends by lazy { mutableListOf<Friend>() }
val privatebets by lazy { mutableListOf<InvitationBet>() }
}
private val mockData by lazy { MockData() }
override val userDataSource: UserDataSource by lazy { MockUserDataSource(mockData) }
override val betDataSource: BetDataSource by lazy { MockBetDataSource(mockData) }
override val participationDataSource: ParticipationDataSource by lazy { MockParticipationDataSource(mockData) }
override val friendDataSource: FriendDataSource by lazy { MockFriendDataSource(mockData) }
}

@ -0,0 +1,51 @@
package allin.data.mock
import allin.data.FriendDataSource
import allin.dto.UserDTO
import allin.model.Friend
import allin.model.FriendStatus
class MockFriendDataSource(private val mockData: MockDataSource.MockData) : FriendDataSource {
private val friends get() = mockData.friends
private val users get() = mockData.users
override fun addFriend(sender: String, receiver: String) {
mockData.friends.add(Friend(sender, receiver))
}
override fun getFriendFromUserId(id: String) =
friends.map { Friend(sender = it.sender, receiver = it.receiver) }
.filter { it.sender == id }
.mapNotNull {
users.find { usr -> it.receiver == usr.id }
?.toDto(
friendStatus = if (isFriend(it.receiver, id)) {
FriendStatus.FRIEND
} else FriendStatus.REQUESTED
)
}
override fun getFriendRequestsFromUserId(id: String): List<UserDTO> {
return friends
.filter { (it.receiver == id) && !isFriend(id, it.sender) }
.mapNotNull {
users.find { usr -> usr.id == it.sender }
?.toDto(friendStatus = FriendStatus.NOT_FRIEND)
}
}
override fun deleteFriend(senderId: String, receiverId: String) =
friends.removeIf { (it.sender == senderId) && (it.receiver == receiverId) }
override fun isFriend(firstUser: String, secondUser: String) =
friends.any { (it.sender == firstUser) and (it.receiver == secondUser) }
override fun filterUsersByUsername(fromUserId: String, search: String): List<UserDTO> =
users.filter { (it.username.contains(search, ignoreCase = true)) }
.map { user ->
user.toDto(friendStatus = getFriendStatus(fromUserId, user.id))
}
}

@ -0,0 +1,88 @@
package allin.data.mock
import allin.data.ParticipationDataSource
import allin.model.Participation
class MockParticipationDataSource(private val mockData: MockDataSource.MockData) : ParticipationDataSource {
private val participations get() = mockData.participations
private val betInfos get() = mockData.betInfos
private val answerInfos get() = mockData.answerInfos
override fun addParticipation(participation: Participation) {
participations += participation
var betTotalStakes = 0
betInfos.replaceAll {
if (participation.betId == it.id) {
betTotalStakes = it.totalStakes + participation.stake
val betTotalParticipants = it.totalParticipants + 1
it.copy(
totalStakes = betTotalStakes,
totalParticipants = betTotalParticipants
)
} else {
it
}
}
answerInfos.replaceAll {
if (participation.betId == it.betId) {
if (participation.answer == it.response) {
val answerTotalStakes = it.totalStakes + participation.stake
val probability = answerTotalStakes / betTotalStakes.toFloat()
it.copy(
totalStakes = answerTotalStakes,
odds = 1 / probability
)
} else {
val probability = it.totalStakes / betTotalStakes.toFloat()
it.copy(odds = 1 / probability)
}
} else {
it
}
}
}
override fun getParticipationFromBetId(betid: String): List<Participation> =
participations.filter { it.betId == betid }
override fun deleteParticipation(id: String): Boolean {
val participation = participations.find { it.id == id }
val result = participations.remove(participation)
var betTotalStakes = 0
betInfos.replaceAll {
if (participation?.betId == it.id) {
betTotalStakes = it.totalStakes - participation.stake
val betTotalParticipants = it.totalParticipants - 1
it.copy(
totalStakes = betTotalStakes,
totalParticipants = betTotalParticipants
)
} else {
it
}
}
answerInfos.replaceAll {
if (participation?.betId == it.betId) {
if (participation.answer == it.response) {
val answerTotalStakes = it.totalStakes - participation.stake
val probability = answerTotalStakes / betTotalStakes.toFloat()
it.copy(
totalStakes = answerTotalStakes,
odds = 1 / probability
)
} else {
val probability = it.totalStakes / betTotalStakes.toFloat()
it.copy(odds = 1 / probability)
}
} else {
it
}
}
return result
}
}

@ -0,0 +1,115 @@
package allin.data.mock
import allin.data.UserDataSource
import allin.dto.UserDTO
import allin.model.User
import java.time.ZonedDateTime
class MockUserDataSource(private val mockData: MockDataSource.MockData) : UserDataSource {
private val users get() = mockData.users
private val lastGifts get() = mockData.lastGifts
override fun getUserByUsername(username: String): Pair<UserDTO?, String?> =
users.find { (it.username == username) or (it.email == username) }?.let { usr ->
Pair(
UserDTO(
id = usr.id,
username = usr.username,
email = usr.email,
nbCoins = usr.nbCoins,
token = usr.token,
image = null,
nbBets = mockData.participations.count { it.userId == usr.id },
nbFriends = mockData.friends.count { f ->
f.receiver == usr.username &&
mockData.friends.any { it.sender == usr.username && it.receiver == f.sender }
},
bestWin = mockData.participations
.filter {
(it.id == usr.id) &&
(mockData.results.find { r -> r.betId == it.betId })?.result == it.answer
}
.maxBy { it.stake }
.stake,
friendStatus = null,
),
usr.password
)
} ?: Pair(null, null)
override fun addUser(user: User) {
users += user
}
override fun deleteUser(username: String): Boolean =
users.removeIf { (it.username == username) or (it.email == username) }
override fun addCoins(username: String, amount: Int) {
users.find { it.username == username }?.let {
it.nbCoins += amount
}
}
override fun removeCoins(username: String, amount: Int) {
users.find { it.username == username }?.let {
it.nbCoins -= amount
}
}
override fun userExists(username: String) =
users.any { it.username == username }
override fun emailExists(email: String) =
users.any { it.email == email }
override fun canHaveDailyGift(username: String): Boolean {
val value = lastGifts[username]?.let {
it.plusDays(1) <= ZonedDateTime.now()
} ?: true
lastGifts[username] = ZonedDateTime.now()
return value
}
override fun addImage(userid: String, image: ByteArray) {
val user = users.find { it.id == userid }
if (user != null) {
user.image = image.toString()
}
}
override fun removeImage(userid: String) {
val user = users.find { it.id == userid }
if (user != null) {
user.image = null
}
}
override fun getImage(userid: String) =
users.find { it.id == userid }?.image
override fun getUserById(id: String) =
mockData.users.find { it.id == id }?.let { usr ->
UserDTO(
id = usr.id,
username = usr.username,
email = usr.email,
nbCoins = usr.nbCoins,
token = usr.token,
image = null,
nbBets = mockData.participations.count { it.userId == usr.id },
nbFriends = mockData.friends.count { f ->
f.receiver == usr.username &&
mockData.friends.any { it.sender == usr.username && it.receiver == f.sender }
},
bestWin = mockData.participations
.filter {
(it.id == usr.id) &&
(mockData.results.find { r -> r.betId == it.betId })?.result == it.answer
}
.maxBy { it.stake }
.stake,
friendStatus = null,
)
}
}

@ -0,0 +1,296 @@
package allin.data.postgres
import allin.data.BetDataSource
import allin.data.postgres.entities.*
import allin.dto.UserDTO
import allin.model.*
import org.ktorm.database.Database
import org.ktorm.dsl.*
import org.ktorm.entity.*
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
class PostgresBetDataSource(private val database: Database) : BetDataSource {
override fun getAllBets(filters: List<BetFilter>, userDTO: UserDTO): List<Bet> {
return when {
filters.isEmpty() -> database.bets.map { it.toBet(database) }
.filter { (!it.isPrivate) or (isInvited(it.id, userDTO.id)) or (it.createdBy == userDTO.username) }
filters.size == 1 -> {
val filter = filters.first()
when (filter) {
BetFilter.PUBLIC -> database.bets.filter { !it.isPrivate }
BetFilter.INVITATION -> database.bets.filter { it.isPrivate }
BetFilter.FINISHED -> database.bets.filter { it.status eq BetStatus.FINISHED }
BetFilter.IN_PROGRESS -> database.bets.filter {
it.status inList listOf(BetStatus.IN_PROGRESS, BetStatus.WAITING, BetStatus.CLOSING)
}
}.map { it.toBet(database) }
.filter { (!it.isPrivate) or (isInvited(it.id, userDTO.id)) or (it.createdBy == userDTO.id) }
}
else -> {
database.bets.filter { bet ->
val public = (BetFilter.PUBLIC in filters) and !bet.isPrivate
val invitation = (BetFilter.INVITATION in filters) and bet.isPrivate
val finished =
(BetFilter.FINISHED in filters) and ((bet.status eq BetStatus.FINISHED) or (bet.status eq BetStatus.CANCELLED))
val inProgress = (BetFilter.IN_PROGRESS in filters) and (bet.status inList listOf(
BetStatus.IN_PROGRESS,
BetStatus.WAITING,
BetStatus.CLOSING
))
(public or invitation) and (finished or inProgress)
}.map { it.toBet(database) }
.filter { (!it.isPrivate) or (isInvited(it.id, userDTO.id)) or (it.createdBy == userDTO.id) }
}
}
}
override fun getBetById(id: String): Bet? =
database.bets.find { it.id eq id }?.toBet(database)
override fun getBetDetailById(id: String, userid: String): BetDetail? =
database.bets.find { it.id eq id }?.toBetDetail(database, userid)
override fun getBetsNotFinished(): List<Bet> {
val currentTime = ZonedDateTime.now(ZoneId.of("+02:00"))
return database.bets
.filter { it.endBet greaterEq currentTime.toInstant() }
.map { it.toBet(database) }
}
override fun getToConfirm(user: UserDTO): List<BetDetail> {
return database.bets
.filter {
(it.createdBy eq user.id) and (BetsEntity.status eq BetStatus.CLOSING)
}
.map { it.toBetDetail(database, user.id) }
}
override fun confirmBet(betId: String, result: String) {
database.bets.find { it.id eq betId }?.let { bet ->
bet.status = BetStatus.FINISHED
bet.flushChanges()
database.betResults.add(
BetResultEntity {
this.bet = bet
this.result = result
}
)
}
val resultAnswerInfo = database.betAnswerInfos
.find { (it.betId eq betId) and (it.response eq result) }
?.toBetAnswerInfo()
database.participations.filter {
(ParticipationsEntity.betId eq betId) and
(ParticipationsEntity.answer eq result)
}.forEach { participation ->
database.betResultNotifications.add(
BetResultNotificationEntity {
this.betId = betId
this.userid = participation.userid
}
)
val amount = (participation.stake * (resultAnswerInfo?.odds ?: 1f)).roundToInt()
database.update(UsersEntity) { usr ->
set(usr.nbCoins, usr.nbCoins + amount)
where { usr.id eq participation.userid }
}
}
}
override fun getWonNotifications(userid: String): List<BetResultDetail> {
return database.betResultNotifications
.filter { it.userid eq userid }
.flatMap { notif ->
notif.delete()
database.participations
.filter {
(it.userid eq userid) and
(it.betId eq notif.betId)
}
.mapNotNull { participation ->
database.betResults
.find { it.betId eq participation.bet.id }
?.toBetResultDetail(
database,
participation
)
}
}
}
override fun getHistory(userid: String): List<BetResultDetail> {
return database.participations
.filter { it.userid eq userid }
.mapNotNull { participation ->
database.betResults
.find { it.betId eq participation.bet.id }
?.toBetResultDetail(
database,
participation
)
}
}
override fun getCurrent(userid: String): List<BetDetail> {
return database.participations
.filter { it.userid eq userid }
.mapNotNull {
if (it.bet.status !in listOf(BetStatus.FINISHED, BetStatus.CANCELLED)) {
it.bet.toBetDetail(
database = database,
userid = userid
)
} else null
}
}
override fun getMostPopularBet(): Bet? {
return database.bets
.filter { (it.isPrivate eq false) and (it.status eq BetStatus.IN_PROGRESS) }
.sortedByDescending { it.popularityscore }
.firstOrNull()
?.toBet(database)
}
override fun updatePopularityScore(betId: String) {
database.bets.filter { it.id eq betId }.firstOrNull() ?: return
val participations = database.participations.filter { it.betId eq betId }
val score = (participations.count() * participations.count()) + participations.map { it.stake }.sum()
database.update(BetsEntity) {
set(it.popularityscore, score)
where { it.id eq betId }
}
}
override fun addBet(bet: Bet) {
database.bets.add(
BetEntity {
this.id = bet.id
this.endBet = bet.endBet.toInstant()
this.endRegistration = bet.endRegistration.toInstant()
this.zoneId = bet.endBet.zone.id
this.sentenceBet = bet.sentenceBet
this.theme = bet.theme
this.isPrivate = bet.isPrivate
this.createdBy = bet.createdBy
this.status = bet.status
this.type = bet.type
}
)
database.betInfos.add(
BetInfoEntity {
this.id = bet.id
this.totalStakes = 0
this.totalParticipants = 0
}
)
val responses = if (bet.type == BetType.BINARY) {
listOf(YES_VALUE, NO_VALUE)
} else {
bet.response
}
responses.forEach { response ->
database.betAnswerInfos.add(
BetAnswerInfoEntity {
this.betId = bet.id
this.response = response
this.totalStakes = 0
this.odds = 1f
}
)
if (bet.type == BetType.CUSTOM) {
database.responses.add(
ResponseEntity {
this.betId = bet.id
this.response = response
}
)
}
}
}
override fun removeBet(id: String): Boolean {
database.betInfos.removeIf { it.id eq id }
database.betAnswerInfos.removeIf { it.betId eq id }
return database.bets.removeIf { it.id eq id } > 0
}
override fun updateBet(data: UpdatedBetData): Boolean {
return database.update(BetsEntity) {
set(BetsEntity.isPrivate, data.isPrivate)
where { BetsEntity.id eq data.id }
} > 0
}
override fun updateBetStatuses(date: ZonedDateTime) {
database.bets
.filter {
(date.toInstant() greaterEq BetsEntity.endRegistration) and
(BetsEntity.status notEq BetStatus.FINISHED) and
(BetsEntity.status notEq BetStatus.CANCELLED)
}.let {
it.filter { date.toInstant() less BetsEntity.endBet }.forEach { bet ->
bet.status = BetStatus.WAITING
bet.flushChanges()
}
it.filter { date.toInstant() greaterEq BetsEntity.endBet }.forEach { bet ->
if (date.toInstant() >= bet.endBet.plus(7, ChronoUnit.DAYS)) {
database.participations
.filter { it.betId eq bet.id }
.forEach { participation ->
database.users.find { it.id eq participation.userid }?.let { user ->
user.nbCoins += participation.stake
user.flushChanges()
}
}
bet.status = BetStatus.CANCELLED
bet.flushChanges()
} else {
bet.status = BetStatus.CLOSING
bet.flushChanges()
}
}
}
}
override fun addPrivateBet(bet: Bet) {
addBet(bet)
bet.userInvited?.forEach {
database.privatebets.add(PrivateBetEntity {
betId = bet.id
userId = it
})
}
}
override fun isInvited(betid: String, userId: String) =
database.privatebets.filter { (it.betid eq betid) and (it.userId eq userId) }.isNotEmpty()
override fun addUserInPrivatebet(updatedPrivateBet: UpdatedPrivateBet) {
updatedPrivateBet.usersInvited?.forEach {
database.privatebets.add(PrivateBetEntity {
betId = updatedPrivateBet.betid
userId = it
})
}
}
}

@ -0,0 +1,164 @@
package allin.data.postgres
import allin.data.*
import allin.ext.execute
import org.ktorm.database.Database
import org.ktorm.support.postgresql.PostgreSqlDialect
class PostgresDataSource : AllInDataSource() {
private val database: Database
init {
val dbDatabase = System.getenv()["POSTGRES_DB"]
val dbUser = System.getenv()["POSTGRES_USER"]
val dbPassword = System.getenv()["POSTGRES_PASSWORD"]
val dbHost = System.getenv()["POSTGRES_HOST"]
val url = "jdbc:postgresql://$dbHost/$dbDatabase"
println("APP STARTING ON POSTGRESQL DATA SOURCE $url")
database = Database.connect(
url = url,
user = dbUser,
password = dbPassword,
dialect = PostgreSqlDialect()
)
database.execute(
"""
CREATE EXTENSION IF not exists fuzzystrmatch;
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF not exists users (
id VARCHAR(255) PRIMARY KEY,
username VARCHAR(255),
password VARCHAR(255),
coins int,
email VARCHAR(255),
lastgift timestamp
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF not exists bet (
id VARCHAR(255) PRIMARY KEY,
theme VARCHAR(255),
endregistration timestamp,
endbet timestamp,
zoneid varchar(500),
sentencebet varchar(500),
isprivate boolean,
createdby varchar(250),
status varchar(20),
type varchar(20),
popularityscore numeric
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF NOT EXISTS betresult (
betid VARCHAR(255) PRIMARY KEY REFERENCES bet,
result varchar(250)
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF NOT EXISTS betresultnotification (
betid VARCHAR(255),
userid varchar(250),
CONSTRAINT pk_id_username PRIMARY KEY (betid, userid)
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF NOT EXISTS participation (
id VARCHAR(255) PRIMARY KEY,
bet VARCHAR(255),
userid varchar(250),
answer varchar(250),
stake int
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF NOT EXISTS response (
betId VARCHAR(255),
response VARCHAR(250),
CONSTRAINT pk_response_id PRIMARY KEY (betId, response)
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF not exists betInfo (
id VARCHAR(255) PRIMARY KEY,
totalStakes int,
totalParticipants int
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF not exists betAnswerInfo (
betId VARCHAR(255),
response VARCHAR(255),
totalStakes int,
odds float,
CONSTRAINT pk_bet_answer_info_id PRIMARY KEY (betId, response)
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF NOT EXISTS friend(
sender VARCHAR(255),
receiver VARCHAR(255),
CONSTRAINT pk_friend PRIMARY KEY (sender,receiver)
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF NOT EXISTS userimage
(
user_id VARCHAR(255) PRIMARY KEY,
image bytea
)
""".trimIndent()
)
database.execute(
"""
CREATE TABLE IF NOT EXISTS privatebet
(
betid VARCHAR(255),
userid VARCHAR(255),
CONSTRAINT pk_privatebet PRIMARY KEY (betid,userid)
)
""".trimIndent()
)
}
override val userDataSource: UserDataSource by lazy { PostgresUserDataSource(database) }
override val betDataSource: BetDataSource by lazy { PostgresBetDataSource(database) }
override val participationDataSource: ParticipationDataSource by lazy { PostgresParticipationDataSource(database) }
override val friendDataSource: FriendDataSource by lazy { PostgresFriendDataSource(database) }
}

@ -0,0 +1,97 @@
package allin.data.postgres
import allin.data.FriendDataSource
import allin.data.postgres.entities.FriendEntity
import allin.data.postgres.entities.friends
import allin.data.postgres.entities.users
import allin.dto.UserDTO
import allin.ext.length
import allin.ext.levenshteinLessEq
import allin.ext.toLowerCase
import allin.model.FriendStatus
import org.ktorm.database.Database
import org.ktorm.dsl.and
import org.ktorm.dsl.div
import org.ktorm.dsl.eq
import org.ktorm.dsl.notEq
import org.ktorm.entity.*
class PostgresFriendDataSource(private val database: Database) : FriendDataSource {
override fun addFriend(sender: String, receiver: String) {
database.friends.add(
FriendEntity {
this.sender = sender
this.receiver = receiver
}
)
}
override fun getFriendFromUserId(id: String): List<UserDTO> {
return database.friends.map { it.toFriend() }
.filter { it.sender == id }
.mapNotNull { friend ->
val receiverUser = database.users.find { usr ->
usr.id eq friend.receiver
}
receiverUser?.toUserDTO(
database,
friendStatus = if (isFriend(friend.receiver, id)) {
FriendStatus.FRIEND
} else FriendStatus.REQUESTED
)
}
}
override fun getFriendRequestsFromUserId(id: String): List<UserDTO> {
return database.friends
.filter { it.receiver eq id }
.mapNotNull {
if (isFriend(firstUser = id, secondUser = it.sender)) {
null
} else {
database.users.find { usr ->
usr.id eq it.sender
}?.toUserDTO(database, friendStatus = FriendStatus.NOT_FRIEND)
}
}
}
override fun deleteFriend(senderId: String, receiverId: String): Boolean {
val result = database.friends.removeIf { (it.sender eq receiverId) and (it.receiver eq senderId) } +
database.friends.removeIf { (it.sender eq senderId) and (it.receiver eq receiverId) }
return result > 0
}
override fun isFriend(firstUser: String, secondUser: String) =
database.friends.any { (it.sender eq firstUser) and (it.receiver eq secondUser) }
override fun filterUsersByUsername(fromUserId: String, search: String): List<UserDTO> {
return database.users
.filter { (it.id notEq fromUserId) }
.mapColumns {
tupleOf(
it.id,
it.username,
it.username.toLowerCase().levenshteinLessEq(
search.lowercase(),
(it.username.length() / 2)
)
)
}
.filter { (_, username, distance) ->
val maxSize = ((username?.length ?: 0) / 2)
distance?.let { it <= maxSize } ?: false
}
.sortedBy { it.second }
.mapNotNull { (id, _, _) ->
id?.let {
val user = database.users.find { it.id eq id }
user?.toUserDTO(database, friendStatus = getFriendStatus(fromUserId, user.id))
}
}
}
}

@ -0,0 +1,68 @@
package allin.data.postgres
import allin.data.ParticipationDataSource
import allin.data.postgres.entities.*
import allin.model.Participation
import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.dsl.insert
import org.ktorm.entity.*
class PostgresParticipationDataSource(private val database: Database) : ParticipationDataSource {
override fun addParticipation(participation: Participation) {
database.insert(ParticipationsEntity) {
set(it.id, participation.id)
set(it.betId, participation.betId)
set(it.userid, participation.userId)
set(it.answer, participation.answer)
set(it.stake, participation.stake)
}
val betInfo = database.betInfos.find { it.id eq participation.betId } ?: BetInfoEntity {
this.id = participation.betId
this.totalStakes = 0
this.totalParticipants = 0
}
betInfo.totalStakes += participation.stake
betInfo.totalParticipants++
database.betInfos.update(betInfo)
database.betAnswerInfos.filter { it.betId eq participation.betId }.forEach {
if (it.response == participation.answer) {
it.totalStakes += participation.stake
}
val probability = (it.totalStakes / betInfo.totalStakes.toFloat())
it.odds = if (probability == 0f) 1f else 1 / probability
it.flushChanges()
}
}
override fun getParticipationFromBetId(betid: String): List<Participation> =
database.participations.filter { it.betId eq betid }.map { it.toParticipation(database) }
override fun deleteParticipation(id: String): Boolean {
val participation = database.participations.find { it.id eq id } ?: return false
database.betInfos.find { it.id eq participation.bet.id }?.let { betInfo ->
betInfo.totalStakes -= participation.stake
betInfo.totalParticipants--
database.betAnswerInfos.filter { it.betId eq participation.bet.id }.forEach {
if (it.response == participation.answer) {
it.totalStakes -= participation.stake
}
val probability = it.totalStakes / betInfo.totalStakes.toFloat()
it.odds = 1 / probability
it.flushChanges()
}
betInfo.flushChanges()
}
return participation.delete() > 0
}
}

@ -0,0 +1,112 @@
package allin.data.postgres
import allin.data.UserDataSource
import allin.data.postgres.entities.*
import allin.dto.UserDTO
import allin.ext.executeWithResult
import allin.model.User
import org.ktorm.database.Database
import org.ktorm.database.use
import org.ktorm.dsl.*
import org.ktorm.entity.add
import org.ktorm.entity.filter
import org.ktorm.entity.find
import org.ktorm.entity.removeIf
import java.time.Instant.now
class PostgresUserDataSource(private val database: Database) : UserDataSource {
override fun getUserByUsername(username: String): Pair<UserDTO?, String?> =
database.users
.find { (it.username eq username) or (it.email eq username) }
?.let { it.toUserDTO(database) to it.password }
?: (null to null)
override fun getUserById(id: String): UserDTO? =
database.users.find { it.id eq id }?.toUserDTO(database)
override fun addUser(user: User) {
database.users.add(
UserEntity {
this.id = user.id
this.nbCoins = user.nbCoins
this.username = user.username
this.password = user.password
this.email = user.email
this.lastGift = now()
}
)
}
override fun deleteUser(username: String): Boolean =
database.users.removeIf { (it.username eq username) or (it.email eq username) } > 0
override fun addCoins(username: String, amount: Int) {
database.update(UsersEntity) {
set(it.nbCoins, it.nbCoins + amount)
where { it.username eq username }
}
}
override fun removeCoins(username: String, amount: Int) {
database.update(UsersEntity) {
set(it.nbCoins, it.nbCoins - amount)
where { it.username eq username }
}
}
override fun userExists(username: String) =
database.users.filter {
(it.username eq username)
}.totalRecords > 0
override fun emailExists(email: String) =
database.users.filter {
(it.email eq email)
}.totalRecords > 0
override fun canHaveDailyGift(username: String): Boolean {
val request =
"SELECT CASE WHEN DATE(NOW()) > DATE(lastgift) THEN true ELSE false END AS is_lastgift_greater_than_1_day FROM users WHERE username = '$username';"
val resultSet = database.executeWithResult(request)
resultSet?.use {
if (resultSet.next()) {
val isDailyGift = resultSet.getBoolean("is_lastgift_greater_than_1_day")
if (isDailyGift) {
database.update(UsersEntity) {
set(UsersEntity.lastGift, now())
where { it.username eq username }
}
}
return isDailyGift
}
}
return false
}
override fun addImage(userid: String, image: ByteArray) {
database.usersimage.add(UserImageEntity {
id = userid
this.image = image
})
}
override fun removeImage(userid: String) {
database.usersimage.removeIf { it.id eq userid }
}
override fun getImage(userid: String): String? {
val resultSet =
database.executeWithResult(
"""
SELECT encode(image, 'base64') AS image
FROM userimage
WHERE user_id = '${userid}'
""".trimIndent()
) ?: return null
if (resultSet.next()) {
resultSet.getString("image")?.let { return it }
}
return null
}
}

@ -0,0 +1,36 @@
package allin.data.postgres.entities
import allin.model.BetAnswerInfo
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.float
import org.ktorm.schema.int
import org.ktorm.schema.varchar
interface BetAnswerInfoEntity : Entity<BetAnswerInfoEntity> {
companion object : Entity.Factory<BetAnswerInfoEntity>()
var betId: String
var response: String
var totalStakes: Int
var odds: Float
fun toBetAnswerInfo() =
BetAnswerInfo(
betId = betId,
response = response,
totalStakes = totalStakes,
odds = odds
)
}
object BetAnswerInfosEntity : Table<BetAnswerInfoEntity>("betanswerinfo") {
val betId = varchar("betid").primaryKey().bindTo { it.betId }
val response = varchar("response").primaryKey().bindTo { it.response }
val totalStakes = int("totalstakes").bindTo { it.totalStakes }
val odds = float("odds").bindTo { it.odds }
}
val Database.betAnswerInfos get() = this.sequenceOf(BetAnswerInfosEntity)

@ -0,0 +1,98 @@
package allin.data.postgres.entities
import allin.model.*
import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.entity.*
import org.ktorm.schema.*
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
interface BetEntity : Entity<BetEntity> {
companion object : Entity.Factory<BetEntity>()
var id: String
var theme: String
var sentenceBet: String
var endRegistration: Instant
var endBet: Instant
var zoneId: String
var isPrivate: Boolean
var status: BetStatus
var type: BetType
var createdBy: String
var popularityscore: Int
fun toBet(database: Database): Bet {
val betInfo = database.betInfos.find { it.id eq this.id }
return Bet(
id = id,
theme = theme,
sentenceBet = sentenceBet,
status = status,
type = type,
endRegistration = ZonedDateTime.ofInstant(endRegistration, ZoneId.of(zoneId)),
endBet = ZonedDateTime.ofInstant(endBet, ZoneId.of(zoneId)),
isPrivate = isPrivate,
response = if (type == BetType.BINARY) {
listOf(YES_VALUE, NO_VALUE)
} else {
database.responses.filter { it.betId eq id }.map { it.response }
},
createdBy = database.users.first { it.id eq createdBy }.username,
popularityscore = popularityscore,
totalStakes = betInfo?.totalStakes ?: 0,
totalParticipants = betInfo?.totalParticipants ?: 0
)
}
fun toBetDetail(database: Database, userid: String): BetDetail {
val bet = this.toBet(database)
val participations = database.participations.filter { it.betId eq bet.id }
val userParticipation = participations.find { it.userid eq userid }
val participationEntities = participations.map { it.toParticipation(database) }
val answerInfos = database.betAnswerInfos
.filter { it.betId eq bet.id }
.map { it.toBetAnswerInfo() }
val betD = BetDetail(
bet = bet,
answers = getBetAnswerDetail(bet, participationEntities, answerInfos),
participations = participationEntities,
userParticipation = userParticipation?.toParticipation(database),
wonParticipation = if (participationEntities.isEmpty()) {
null
} else if (bet.status == BetStatus.FINISHED) {
database.betResults.find { it.betId eq this.id }?.let { r ->
participationEntities
.filter { it.answer == r.result }
.maxBy { it.stake }
}
} else null
)
return betD
}
}
object BetsEntity : Table<BetEntity>("bet") {
val id = varchar("id").primaryKey().bindTo { it.id }
val theme = varchar("theme").bindTo { it.theme }
val sentenceBet = varchar("sentencebet").bindTo { it.sentenceBet }
val endRegistration = timestamp("endregistration").bindTo { it.endRegistration }
val endBet = timestamp("endbet").bindTo { it.endBet }
val zoneId = varchar("zoneid").bindTo { it.zoneId }
val isPrivate = boolean("isprivate").bindTo { it.isPrivate }
val status = enum<BetStatus>("status").bindTo { it.status }
val type = enum<BetType>("type").bindTo { it.type }
val createdBy = varchar("createdby").bindTo { it.createdBy }
val popularityscore = int("popularityscore").bindTo { it.popularityscore }
}
val Database.bets get() = this.sequenceOf(BetsEntity)

@ -0,0 +1,25 @@
package allin.data.postgres.entities
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.int
import org.ktorm.schema.varchar
interface BetInfoEntity : Entity<BetInfoEntity> {
companion object : Entity.Factory<BetInfoEntity>()
var id: String
var totalStakes: Int
var totalParticipants: Int
}
object BetInfosEntity : Table<BetInfoEntity>("betinfo") {
val id = varchar("id").primaryKey().bindTo { it.id }
val totalStakes = int("totalstakes").bindTo { it.totalStakes }
val totalParticipants = int("totalparticipants").bindTo { it.totalParticipants }
}
val Database.betInfos get() = this.sequenceOf(BetInfosEntity)

@ -0,0 +1,51 @@
package allin.data.postgres.entities
import allin.model.BetResult
import allin.model.BetResultDetail
import org.ktorm.database.Database
import org.ktorm.dsl.and
import org.ktorm.dsl.eq
import org.ktorm.entity.Entity
import org.ktorm.entity.find
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.varchar
import kotlin.math.roundToInt
interface BetResultEntity : Entity<BetResultEntity> {
companion object : Entity.Factory<BetResultEntity>()
var bet: BetEntity
var result: String
fun toBetResult() =
BetResult(
betId = bet.id,
result = result
)
fun toBetResultDetail(
database: Database,
participationEntity: ParticipationEntity
): BetResultDetail {
val answerInfo = database.betAnswerInfos.find {
(it.betId eq bet.id) and (it.response eq participationEntity.answer)
}?.toBetAnswerInfo()
return BetResultDetail(
betResult = this.toBetResult(),
bet = bet.toBet(database),
participation = participationEntity.toParticipation(database),
amount = (participationEntity.stake * (answerInfo?.odds ?: 1f)).roundToInt(),
won = participationEntity.answer == result
)
}
}
object BetResultsEntity : Table<BetResultEntity>("betresult") {
val betId = varchar("betid").primaryKey().references(BetsEntity) { it.bet }
val result = varchar("result").bindTo { it.result }
}
val Database.betResults get() = this.sequenceOf(BetResultsEntity)

@ -0,0 +1,21 @@
package allin.data.postgres.entities
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.varchar
interface BetResultNotificationEntity : Entity<BetResultNotificationEntity> {
companion object : Entity.Factory<BetResultNotificationEntity>()
var betId: String
var userid: String
}
object BetResultNotificationsEntity : Table<BetResultNotificationEntity>("betresultnotification") {
val betId = varchar("betid").primaryKey().bindTo { it.betId }
val userid = varchar("userid").primaryKey().bindTo { it.userid }
}
val Database.betResultNotifications get() = this.sequenceOf(BetResultNotificationsEntity)

@ -0,0 +1,28 @@
package allin.data.postgres.entities
import allin.model.Friend
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.varchar
interface FriendEntity : Entity<FriendEntity> {
companion object : Entity.Factory<FriendEntity>()
var sender: String
var receiver: String
fun toFriend() =
Friend(
sender = sender,
receiver = receiver,
)
}
object FriendsEntity : Table<FriendEntity>("friend") {
val sender = varchar("sender").primaryKey().bindTo { it.sender }
val receiver = varchar("receiver").primaryKey().bindTo { it.receiver }
}
val Database.friends get() = this.sequenceOf(FriendsEntity)

@ -0,0 +1,43 @@
package allin.data.postgres.entities
import allin.model.Participation
import allin.utils.AppConfig
import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.entity.Entity
import org.ktorm.entity.first
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.int
import org.ktorm.schema.varchar
interface ParticipationEntity : Entity<ParticipationEntity> {
companion object : Entity.Factory<ParticipationEntity>()
var id: String
var bet: BetEntity
var userid: String
var answer: String
var stake: Int
fun toParticipation(database: Database) =
Participation(
id = id,
betId = bet.id,
userId = userid,
answer = answer,
stake = stake,
username = database.users.first { it.id eq userid }.username,
imageUser = AppConfig.imageManager.getImage(id, database)
)
}
object ParticipationsEntity : Table<ParticipationEntity>("participation") {
val id = varchar("id").primaryKey().bindTo { it.id }
val betId = varchar("bet").references(BetsEntity) { it.bet }
val userid = varchar("userid").bindTo { it.userid }
val answer = varchar("answer").bindTo { it.answer }
val stake = int("stake").bindTo { it.stake }
}
val Database.participations get() = this.sequenceOf(ParticipationsEntity)

@ -0,0 +1,21 @@
package allin.data.postgres.entities
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.varchar
interface PrivateBetEntity : Entity<PrivateBetEntity> {
companion object : Entity.Factory<PrivateBetEntity>()
var betId: String
var userId: String
}
object PrivateBetsEntity : Table<PrivateBetEntity>("privatebet") {
val betid = varchar("betid").bindTo { it.betId }
val userId = varchar("userid").bindTo { it.userId }
}
val Database.privatebets get() = this.sequenceOf(PrivateBetsEntity)

@ -0,0 +1,21 @@
package allin.data.postgres.entities
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.varchar
interface ResponseEntity : Entity<ResponseEntity> {
companion object : Entity.Factory<ResponseEntity>()
var betId: String
var response: String
}
object ResponsesEntity : Table<ResponseEntity>("response") {
val betId = varchar("betid").primaryKey().bindTo { it.betId }
val response = varchar("response").primaryKey().bindTo { it.response }
}
val Database.responses get() = this.sequenceOf(ResponsesEntity)

@ -0,0 +1,62 @@
package allin.data.postgres.entities
import allin.dto.UserDTO
import allin.model.FriendStatus
import allin.utils.AppConfig
import org.ktorm.database.Database
import org.ktorm.dsl.and
import org.ktorm.dsl.eq
import org.ktorm.entity.*
import org.ktorm.schema.Table
import org.ktorm.schema.int
import org.ktorm.schema.timestamp
import org.ktorm.schema.varchar
import java.time.Instant
interface UserEntity : Entity<UserEntity> {
companion object : Entity.Factory<UserEntity>()
var id: String
var username: String
var email: String
var password: String
var nbCoins: Int
var lastGift: Instant
fun toUserDTO(database: Database, friendStatus: FriendStatus? = null) =
UserDTO(
id = id,
username = username,
email = email,
nbCoins = nbCoins,
token = null,
image = AppConfig.imageManager.getImage(id, database),
nbBets = database.participations.count { it.userid eq this.id },
nbFriends = database.friends
.filter { it.receiver eq this.id }
.mapNotNull { p -> database.friends.any { (it.sender eq this.id) and (it.receiver eq p.sender) } }
.count(),
bestWin = database.participations
.filter { (it.id eq this.id) }
.mapNotNull { p ->
if (database.betResults.any { (it.betId eq p.bet.id) and (it.result eq p.answer) }) {
p.stake
} else null
}
.maxOrNull() ?: 0,
friendStatus = friendStatus
)
}
object UsersEntity : Table<UserEntity>("users") {
val id = varchar("id").primaryKey().bindTo { it.id }
val username = varchar("username").bindTo { it.username }
val password = varchar("password").bindTo { it.password }
val nbCoins = int("coins").bindTo { it.nbCoins }
val email = varchar("email").bindTo { it.email }
val lastGift = timestamp("lastgift").bindTo { it.lastGift }
}
val Database.users get() = this.sequenceOf(UsersEntity)

@ -0,0 +1,29 @@
package allin.data.postgres.entities
import allin.model.UserImage
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.Table
import org.ktorm.schema.bytes
import org.ktorm.schema.varchar
interface UserImageEntity : Entity<UserImageEntity> {
companion object : Entity.Factory<UserImageEntity>()
var id: String
var image: ByteArray
fun toUserImage() =
UserImage(
id = id,
image = image,
)
}
object UsersImageEntity : Table<UserImageEntity>("userimage") {
val id = varchar("user_id").primaryKey().bindTo { it.id }
val image = bytes("image").bindTo { it.image }
}
val Database.usersimage get() = this.sequenceOf(UsersImageEntity)

@ -1,4 +1,18 @@
package allin.dto
import allin.model.FriendStatus
import kotlinx.serialization.Serializable
@Serializable
data class UserDTO(val id: String, val username: String, val email: String, val nbCoins: Int, var token:String?)
data class UserDTO(
val id: String,
val username: String,
val email: String,
val nbCoins: Int,
var token: String?,
val image: String?,
var nbBets: Int,
var nbFriends: Int,
var bestWin: Int,
var friendStatus: FriendStatus?
)

@ -1,65 +0,0 @@
package allin.entities
import allin.database
import allin.entities.ResponsesEntity.getResponse
import allin.model.Bet
import allin.utils.Execute
import org.ktorm.dsl.*
import org.ktorm.entity.Entity
import org.ktorm.schema.*
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID.fromString
interface BetEntity : Entity<BetEntity> {
val theme: String
val sentenceBet: String
val endRegistration: ZonedDateTime
val endBet: ZonedDateTime
val isPrivate: Boolean
val createdBy: String
}
object BetsEntity : Table<BetEntity>("bet") {
val id = uuid("id").primaryKey()
val theme = varchar("theme")
val sentenceBet = varchar("sentencebet")
val endRegistration = timestamp("endregistration")
val endBet = timestamp("endbet")
val isPrivate = boolean("isprivate")
val createdBy = varchar("createdby")
fun getBets(): MutableList<Bet> {
return database.from(BetsEntity).select().map {
row -> Bet(
row[id].toString(),
row[theme].toString(),
row[sentenceBet].toString(),
row[endRegistration]!!.atZone(ZoneId.of("Europe/Paris")),
row[endBet]!!.atZone(ZoneId.of("Europe/Paris")),
row[isPrivate]?: false,
getResponse(fromString(row[id].toString())),
row[createdBy].toString()
)
}.toMutableList()
}
fun createBetsTable(){
val request="CREATE TABLE IF not exists bet ( id uuid PRIMARY KEY, theme VARCHAR(255), endregistration timestamp,endbet timestamp,sentencebet varchar(500),isprivate boolean, createdby varchar(250))"
database.Execute(request)
}
fun addBetEntity(bet : Bet) {
database.insert(BetsEntity) {
set(it.id, fromString(bet.id))
set(it.endBet,bet.endBet.toInstant())
set(it.endRegistration,bet.endRegistration.toInstant())
set(it.sentenceBet,bet.sentenceBet)
set(it.theme, bet.theme)
set(it.isPrivate, bet.isPrivate)
set(it.createdBy, bet.createdBy)
}
ResponsesEntity.addResponse(bet.response,fromString(bet.id))
}
}

@ -1,90 +0,0 @@
package allin.entities
import allin.database
import allin.model.Participation
import allin.utils.Execute
import org.ktorm.dsl.*
import org.ktorm.entity.Entity
import org.ktorm.schema.*
import java.util.*
interface ParticipationEntity : Entity<ParticipationEntity> {
val id: String
val betId: String
val username: String
val answer: String
val stake: Int
}
object ParticipationsEntity : Table<BetEntity>("participation") {
val id = uuid("id").primaryKey()
val betId = uuid("bet")
val username = varchar("username")
val answer = varchar("answer")
val stake = int("stake")
fun createParticipationTable(){
val request="CREATE TABLE IF NOT EXISTS participation (id uuid PRIMARY KEY,bet uuid,username varchar(250),answer varchar(250),stake int);"
database.Execute(request)
}
fun addParticipationEntity(participation : Participation){
database.insert(ParticipationsEntity){
set(it.id, UUID.fromString(participation.id))
set(it.betId,UUID.fromString(participation.betId))
set(it.username,participation.username)
set(it.answer,participation.answer)
set(it.stake,participation.stake)
}
}
fun getParticipationEntityFromBetId(betid: String): MutableList<Participation> {
return database.from(ParticipationsEntity)
.select()
.where { betId eq UUID.fromString(betid) }
.map { row ->
Participation(
row[id].toString(),
row[betId].toString(),
row[username].toString(),
row[answer].toString(),
row[stake] ?: 0,
)
}.toMutableList()
}
fun getParticipationEntityFromUserId(user: String, betid: String): MutableList<Participation> {
return database.from(ParticipationsEntity)
.select()
.where { (betId eq UUID.fromString(betid)) and (username eq user) }
.map { row ->
Participation(
row[id].toString(),
row[betId].toString(),
row[username].toString(),
row[answer].toString(),
row[stake] ?: 0,
)
}.toMutableList()
}
fun getParticipationEntity(): MutableList<Participation> {
return database.from(ParticipationsEntity).select().map {
row -> Participation(
row[id].toString(),
row[betId].toString(),
row[username].toString(),
row[answer].toString(),
row[stake]?:0,
)
}.toMutableList()
}
fun deleteParticipation(participation: Participation): Boolean {
val deletedCount = database.delete(ParticipationsEntity) {
it.id eq UUID.fromString(participation.id)
}
return deletedCount > 0
}
}

@ -1,41 +0,0 @@
package allin.entities
import allin.database
import allin.utils.Execute
import org.ktorm.dsl.*
import org.ktorm.entity.Entity
import org.ktorm.schema.Table
import org.ktorm.schema.uuid
import org.ktorm.schema.varchar
import java.util.*
interface ResponseEntity : Entity<ResponseEntity> {
val betId: UUID
val response: String
}
object ResponsesEntity : Table<ResponseEntity>("response") {
val id = uuid("id").primaryKey()
val response = varchar("response").primaryKey()
fun createResponseTable(){
val request="CREATE TABLE IF NOT EXISTS response (id UUID,response VARCHAR(250),CONSTRAINT pk_response_id PRIMARY KEY (id,response));"
database.Execute(request)
}
fun getResponse(idBet: UUID): MutableList<String> {
return database.from(ResponsesEntity)
.select(response)
.where { id eq idBet }
.map { it[response].toString() }.toMutableList()
}
fun addResponse(responses : MutableList<String>, idBet : UUID ) {
responses.forEach {selected ->
database.insert(ResponsesEntity) {
set(it.id, idBet)
set(it.response,selected)
}
}
}
}

@ -1,84 +0,0 @@
package allin.entities
import allin.database
import allin.dto.UserDTO
import allin.model.User
import allin.utils.Execute
import org.ktorm.dsl.*
import org.ktorm.entity.*
import org.ktorm.schema.*
import java.util.UUID.fromString
interface UserEntity : Entity<UserEntity> {
val username: String
var email: String
var password: String
var nbCoins: Int
}
object UsersEntity : Table<UserEntity>("utilisateur") {
val id = uuid("id").primaryKey()
val username = varchar("username")
val password = varchar("password")
val nbCoins = int("coins")
val email = varchar("email")
fun getUserToUserDTO(): MutableList<UserDTO> {
return database.from(UsersEntity).select().map {
row -> UserDTO(
row[id].toString(),
row[username].toString(),
row[email].toString(),
row[nbCoins]?:0,
null
)
}.toMutableList()
}
fun createUserTable(){
val request="CREATE TABLE IF not exists utilisateur ( id uuid PRIMARY KEY, username VARCHAR(255), password VARCHAR(255),coins double precision,email VARCHAR(255))"
database.Execute(request)
}
fun modifyCoins(user: String, cost : Int){
val request = "UPDATE utilisateur SET coins = coins - $cost WHERE username = '$user';"
database.Execute(request)
}
fun getUserByUsernameAndPassword(login: String): Pair<UserDTO?, String?> {
return database.from(UsersEntity)
.select()
.where { (username eq login) /*and (password eq passwordParam)*/ }
.map { row ->
Pair(
UserDTO(
row[id].toString(),
row[username].toString(),
row[email].toString(),
row[nbCoins]?:0,
null
),
row[password].toString()
)
}
.firstOrNull() ?: Pair(null, null)
}
fun addUserEntity(user : User){
database.insert(UsersEntity){
set(it.id,fromString(user.id))
set(it.nbCoins,user.nbCoins)
set(it.username,user.username)
set(it.password,user.password)
set(it.email,user.email)
}
}
fun deleteUserByUsername(username: String): Boolean {
val deletedCount = database.delete(UsersEntity) {
it.username eq username
}
return deletedCount > 0
}
}

@ -0,0 +1,74 @@
package allin.ext
import org.ktorm.database.Database
import org.ktorm.expression.FunctionExpression
import org.ktorm.schema.ColumnDeclaring
import org.ktorm.schema.IntSqlType
import org.ktorm.schema.VarcharSqlType
import java.sql.ResultSet
fun Database.executeWithResult(request: String): ResultSet? {
try {
if (request.isNotEmpty()) {
return this.useTransaction { transaction ->
val connection = transaction.connection
val resultSet = connection.prepareStatement(request).executeQuery()
resultSet
}
}
} catch (e: Exception) {
println(e.message)
return null
}
return null
}
fun Database.execute(request: String) {
if (request.isNotEmpty())
this.useTransaction {
val connection = it.connection
connection.prepareStatement(request).execute()
connection.commit()
}
}
fun ColumnDeclaring<String>.length(): FunctionExpression<Int> {
return FunctionExpression(
functionName = "LENGTH",
arguments = listOf(this.asExpression()),
sqlType = IntSqlType
)
}
fun ColumnDeclaring<String>.toLowerCase(): FunctionExpression<String> {
return FunctionExpression(
functionName = "LOWER",
arguments = listOf(this.asExpression()),
sqlType = VarcharSqlType
)
}
fun ColumnDeclaring<String>.toUpperCase(): FunctionExpression<String> {
return FunctionExpression(
functionName = "UPPER",
arguments = listOf(this.asExpression()),
sqlType = VarcharSqlType
)
}
fun ColumnDeclaring<String>.levenshteinLessEq(
target: ColumnDeclaring<String>,
max: ColumnDeclaring<Int>
): FunctionExpression<Int> {
return FunctionExpression(
functionName = "levenshtein_less_equal",
arguments = listOf(this.asExpression(), target.asExpression(), max.asExpression()),
sqlType = IntSqlType
)
}
fun ColumnDeclaring<String>.levenshteinLessEq(target: String, max: ColumnDeclaring<Int>): FunctionExpression<Int> =
levenshteinLessEq(
wrapArgument(target),
max
)

@ -1,8 +1,9 @@
package allin.ext
import allin.data.UserDataSource
import allin.dto.UserDTO
import allin.entities.UsersEntity
import allin.model.ApiMessage
import allin.utils.TokenManager.Companion.Claims.USERNAME
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
@ -14,11 +15,12 @@ suspend fun PipelineContext<*, ApplicationCall>.hasToken(content: suspend (princ
call.principal<JWTPrincipal>()?.let { content(it) } ?: call.respond(HttpStatusCode.Unauthorized)
suspend fun PipelineContext<*, ApplicationCall>.verifyUserFromToken(
userDataSource: UserDataSource,
principal: JWTPrincipal,
content: suspend (user: UserDTO, password: String) -> Unit
) {
val username = principal.payload.getClaim("username").asString()
val userPassword = UsersEntity.getUserByUsernameAndPassword(username)
userPassword.first?.let { content(it, userPassword.second ?: "") }
?: call.respond(HttpStatusCode.NotFound, ApiMessage.TokenUserNotFound)
val username = principal.payload.getClaim(USERNAME).asString()
val user = userDataSource.getUserByUsername(username)
user.first?.let { content(it, user.second ?: "") }
?: call.respond(HttpStatusCode.NotFound, ApiMessage.TOKEN_USER_NOT_FOUND)
}

@ -1,14 +1,27 @@
package allin.model
object ApiMessage {
const val Welcome = "Welcome on AllIn's API !"
const val TokenUserNotFound = "User not found with the valid token !"
const val UserNotFound = "User not found."
const val BetNotFound = "Bet not found."
const val BetAlreadyExist = "Bet already exists."
const val IncorrectLoginPassword = "Login and/or password incorrect."
const val UserAlreadyExist = "Mail and/or username already exists."
const val InvalidMail = "Invalid mail."
const val ParticipationNotFound = "Participation not found."
const val NotEnoughCoins = "Not enough coins."
const val WELCOME = "Welcome on AllIn's API !"
const val TOKEN_USER_NOT_FOUND = "User not found with the valid token !"
const val USER_NOT_FOUND = "User not found."
const val BET_NOT_FOUND = "Bet not found."
const val BET_ALREADY_EXIST = "Bet already exists."
const val INCORRECT_LOGIN_PASSWORD = "Login and/or password incorrect."
const val USER_ALREADY_EXISTS = "Username already exists."
const val MAIL_ALREADY_EXISTS = "Mail already exists."
const val INVALID_MAIL = "Invalid mail."
const val PARTICIPATION_NOT_FOUND = "Participation not found."
const val NOT_ENOUGH_COINS = "Not enough coins."
const val NO_GIFT = "Can't get daily gift."
const val USER_CANT_BE_DELETE = "This user can't be delete now !"
const val FRIENDS_ALREADY_EXISTS = "User already exists in your Friends List."
const val FRIENDS_DOESNT_EXISTS = "User doesn't exists in your Friends List."
const val FRIENDS_REQUEST_HIMSELF = "The receiver can't be the sender."
const val FILE_NOT_FOUND = "File not found."
const val USER_DOESNT_HAVE_PERMISSION = "This user can't delete or modify this."
const val JWT_TOKEN_INFO = "JWT token of the logged user"
const val ID_BET_INFO = "Id of the desired bet"
const val BET_NOT_FOUND_INFO = "Bet not found in the selected source"
const val NOT_CREATOR_INFO = "The user is not the creator of the bet"
const val USER_UPDATE_INFO = "New User information"
}

@ -1,21 +1,29 @@
package allin.model
import allin.serializer.UUIDSerializer
import allin.model.BetStatus.IN_PROGRESS
import allin.serializer.ZonedDateTimeSerializer
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
import java.util.*
const val YES_VALUE = "Yes"
const val NO_VALUE = "No"
@Serializable
data class Bet(
val id: String = "",
val theme: String,
val sentenceBet: String,
val status: BetStatus = IN_PROGRESS,
val type: BetType,
@Serializable(ZonedDateTimeSerializer::class) val endRegistration: ZonedDateTime,
@Serializable(ZonedDateTimeSerializer::class) var endBet: ZonedDateTime,
var isPrivate: Boolean,
var response: MutableList<String>,
val createdBy: String = ""
var response: List<String>,
val createdBy: String = "",
var popularityscore: Int = 0,
val totalStakes: Int = 0,
val totalParticipants: Int = 0,
val userInvited: List<String>? = null
)
@Serializable
@ -23,5 +31,17 @@ data class UpdatedBetData(
val id: String,
@Serializable(ZonedDateTimeSerializer::class) val endBet: ZonedDateTime,
val isPrivate: Boolean,
val response: MutableList<String>
val response: List<String>
)
@Serializable
data class InvitationBet(
val betid: String,
val userId: String
)
@Serializable
data class UpdatedPrivateBet(
val betid: String,
val usersInvited: List<String>? = null
)

@ -16,19 +16,25 @@ data class BetDetail(
val bet: Bet, // Le Bet
val answers: List<BetAnswerDetail>?, // Pour chaque réponse possible du bet les détails
val participations: List<Participation>?, // La liste des participations
val userParticipation: Participation? // La participation du User current
val userParticipation: Participation?, // La participation du User current
val wonParticipation: Participation? // La participation gagnante
)
fun getBetAnswerDetail(bet: Bet, participations: List<Participation>): List<BetAnswerDetail> {
fun getBetAnswerDetail(
bet: Bet,
participations: List<Participation>,
infos: List<BetAnswerInfo>
): List<BetAnswerDetail> {
return bet.response.map { response ->
val responseParticipations = participations.filter { it.answer == response }
val answerInfo = infos.find { it.response == response }
BetAnswerDetail(
response = response,
totalStakes = responseParticipations.sumOf { it.stake },
totalStakes = answerInfo?.totalStakes ?: 0,
totalParticipants = responseParticipations.size,
highestStake = responseParticipations.maxOfOrNull { it.stake } ?: 0,
odds = if (participations.isEmpty()) 1f else responseParticipations.size / participations.size.toFloat()
odds = answerInfo?.odds ?: 1f
)
}
}

@ -0,0 +1,15 @@
package allin.model
import kotlinx.serialization.Serializable
enum class BetFilter {
PUBLIC,
INVITATION,
IN_PROGRESS,
FINISHED
}
@Serializable
data class BetFiltersRequest(
val filters: List<BetFilter>
)

@ -0,0 +1,18 @@
package allin.model
import kotlinx.serialization.Serializable
@Serializable
data class BetInfo(
var id: String,
var totalStakes: Int,
var totalParticipants: Int
)
@Serializable
data class BetAnswerInfo(
val betId: String,
val response: String,
val totalStakes: Int,
val odds: Float
)

@ -0,0 +1,9 @@
package allin.model
import kotlinx.serialization.Serializable
@Serializable
data class BetResult(
val betId: String,
val result: String
)

@ -0,0 +1,12 @@
package allin.model
import kotlinx.serialization.Serializable
@Serializable
data class BetResultDetail(
val betResult: BetResult,
val bet: Bet,
val participation: Participation,
val amount: Int,
val won: Boolean
)

@ -0,0 +1,9 @@
package allin.model
enum class BetStatus {
IN_PROGRESS,
WAITING,
CLOSING,
FINISHED,
CANCELLED
}

@ -0,0 +1,7 @@
package allin.model
enum class BetType {
MATCH,
BINARY,
CUSTOM
}

@ -0,0 +1,7 @@
package allin.model
data class Friend(
val sender: String,
val receiver: String
)

@ -0,0 +1,7 @@
package allin.model
enum class FriendStatus {
FRIEND,
REQUESTED,
NOT_FRIEND
}

@ -6,9 +6,11 @@ import kotlinx.serialization.Serializable
data class Participation(
val id: String,
val betId: String,
val username: String,
val userId: String,
val answer: String,
val stake: Int
val stake: Int,
val username: String,
val imageUser: String? = null
)
@Serializable

@ -1,7 +1,11 @@
package allin.model
package allin.model
import allin.dto.UserDTO
import kotlinx.serialization.Serializable
const val DEFAULT_COIN_AMOUNT = 500
const val DAILY_GIFT_MIN = 10
const val DAILY_GIFT_MAX = 150
@Serializable
data class User(
@ -9,10 +13,30 @@ data class User(
val username: String,
val email: String,
var password: String,
var nbCoins: Int = 500,
var token: String? = null
var nbCoins: Int = DEFAULT_COIN_AMOUNT,
var token: String? = null,
var image: String? = null,
var bestWin: Int?,
var nbBets: Int,
var nbFriends: Int,
)
{
fun toDto(friendStatus: FriendStatus? = null) =
UserDTO(
id = id,
username = username,
email = email,
nbCoins = nbCoins,
token = token,
friendStatus = friendStatus,
image = image,
bestWin = bestWin?:0,
nbBets = nbBets,
nbFriends=nbFriends
)
}
@Serializable
data class UserRequest(
val username: String,
@ -25,3 +49,9 @@ data class CheckUser(
val login: String,
val password: String
)
@Serializable
data class UserImage(
val id: String,
val image: ByteArray,
)

@ -1,13 +0,0 @@
package allin.routing
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.BasicRouting(){
routing {
get("/") {
call.respond("Bienvenue sur l'API de AlLin!")
}
}
}

@ -1,44 +0,0 @@
package allin.routing
import allin.entities.BetsEntity.getBets
import allin.entities.ParticipationsEntity.getParticipationEntityFromBetId
import allin.entities.ParticipationsEntity.getParticipationEntityFromUserId
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.BetDetail
import allin.model.getBetAnswerDetail
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.BetDetailRouter() {
routing {
authenticate {
get("/betdetail/get/{id}") {
hasToken { principal ->
verifyUserFromToken(principal) { user, _ ->
val id = call.parameters["id"].toString()
val participations = getParticipationEntityFromBetId(id)
val selectedBet = getBets().find { it.id == id }
if (selectedBet != null) {
call.respond(
HttpStatusCode.Accepted,
BetDetail(
selectedBet,
getBetAnswerDetail(selectedBet, participations),
participations.toList(),
getParticipationEntityFromUserId(user.username, id).lastOrNull()
)
)
} else {
call.respond(HttpStatusCode.NotFound, "Bet not found")
}
}
}
}
}
}
}

@ -1,111 +0,0 @@
package allin.routing
import allin.entities.BetsEntity.addBetEntity
import allin.entities.BetsEntity.getBets
import allin.entities.ParticipationsEntity.getParticipationEntity
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.Bet
import allin.model.UpdatedBetData
import allin.utils.AppConfig
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
val tokenManagerBet = AppConfig.tokenManager
fun Application.BetRouter() {
routing {
route("/bets/add") {
authenticate {
post {
hasToken { principal ->
val bet = call.receive<Bet>()
val id = UUID.randomUUID().toString()
val username = tokenManagerBet.getUsernameFromToken(principal)
val bets = getBets()
bets.find { it.id == id }?.let {
call.respond(HttpStatusCode.Conflict, ApiMessage.BetAlreadyExist)
} ?: run {
val betWithId = Bet(
id,
bet.theme,
bet.sentenceBet,
bet.endRegistration,
bet.endBet,
bet.isPrivate,
bet.response,
username
)
addBetEntity(betWithId)
call.respond(HttpStatusCode.Created, betWithId)
}
}
}
}
}
route("/bets/gets") {
get {
// if(bets.size>0)
val bets= getBets()
call.respond(HttpStatusCode.Accepted, bets.toList())
// else call.respond(HttpStatusCode.NoContent)
}
}
route("/bets/get/{id}") {
get {
val bets= getBets()
val id = call.parameters["id"] ?: ""
bets.find { it.id == id }?.let { bet ->
call.respond(HttpStatusCode.Accepted, bet)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound)
}
}
route("/bets/delete") {
post {
val idbet = call.receive<Map<String, String>>()["id"]
val bets= getBets()
bets.find { it.id == idbet }?.let { findbet ->
bets.remove(findbet)
call.respond(HttpStatusCode.Accepted, findbet)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound)
}
}
route("bets/update") {
post {
val updatedBetData = call.receive<UpdatedBetData>()
val bets= getBets()
bets.find { it.id == updatedBetData.id }?.let { findbet ->
findbet.endBet = updatedBetData.endBet
findbet.isPrivate = updatedBetData.isPrivate
findbet.response = updatedBetData.response
call.respond(HttpStatusCode.Accepted, findbet)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.BetNotFound)
}
}
authenticate {
get("/bets/current") {
val bets= getBets()
hasToken { principal ->
verifyUserFromToken(principal) { user, _ ->
val bets = getParticipationEntity()
.filter { it.username == user.username }
.mapNotNull { itParticipation -> bets.find { it.id == itParticipation.betId } }
call.respond(HttpStatusCode.OK, bets)
}
}
}
}
}
}

@ -1,58 +0,0 @@
package allin.routing
import allin.entities.ParticipationsEntity.addParticipationEntity
import allin.entities.ParticipationsEntity.deleteParticipation
import allin.entities.ParticipationsEntity.getParticipationEntity
import allin.entities.UsersEntity.modifyCoins
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.Participation
import allin.model.ParticipationRequest
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
fun Application.ParticipationRouter() {
routing {
authenticate {
post("/participations/add") {
hasToken { principal ->
val participation = call.receive<ParticipationRequest>()
verifyUserFromToken(principal) { user, _ ->
if (user.nbCoins >= participation.stake) {
addParticipationEntity(
Participation(
id = UUID.randomUUID().toString(),
betId = participation.betId,
username = user.username,
answer = participation.answer,
stake = participation.stake
)
)
modifyCoins(user.username,participation.stake)
call.respond(HttpStatusCode.Created)
} else {
call.respond(HttpStatusCode.Forbidden, ApiMessage.NotEnoughCoins)
}
}
}
}
delete("/participations/delete") {
hasToken { principal ->
val participationId = call.receive<String>()
getParticipationEntity().find { it.id == participationId }?.let { participation ->
verifyUserFromToken(principal) { _, _ ->
deleteParticipation(participation)
call.respond(HttpStatusCode.NoContent)
}
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.ParticipationNotFound)
}
}
}
}
}

@ -1,98 +0,0 @@
package allin.routing
import allin.entities.UsersEntity.addUserEntity
import allin.entities.UsersEntity.deleteUserByUsername
import allin.entities.UsersEntity.getUserByUsernameAndPassword
import allin.entities.UsersEntity.getUserToUserDTO
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.CheckUser
import allin.model.User
import allin.model.UserRequest
import allin.utils.AppConfig
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
val RegexCheckerUser = AppConfig.regexChecker
val CryptManagerUser = AppConfig.cryptManager
val tokenManagerUser = AppConfig.tokenManager
const val DEFAULT_COINS = 500
fun Application.UserRouter() {
routing {
route("/users/register") {
post {
val tempUser = call.receive<UserRequest>()
if (RegexCheckerUser.isEmailInvalid(tempUser.email)) {
call.respond(HttpStatusCode.Forbidden, ApiMessage.InvalidMail)
}
val users = getUserToUserDTO()
users.find { it.username == tempUser.username || it.email == tempUser.email }?.let { _ ->
call.respond(HttpStatusCode.Conflict, ApiMessage.UserAlreadyExist)
} ?: run {
val user = User(
id = UUID.randomUUID().toString(),
username = tempUser.username,
email = tempUser.email,
password = tempUser.password,
nbCoins = DEFAULT_COINS,
token = null
)
CryptManagerUser.passwordCrypt(user)
user.token = tokenManagerUser.generateOrReplaceJWTToken(user)
addUserEntity(user)
call.respond(HttpStatusCode.Created, user)
}
}
}
route("/users/login") {
post {
val checkUser = call.receive<CheckUser>()
val user = getUserByUsernameAndPassword(checkUser.login)
if (CryptManagerUser.passwordDecrypt(user.second ?: "", checkUser.password)) {
user.first?.let { userDtoWithToken ->
userDtoWithToken.token = tokenManagerUser.generateOrReplaceJWTToken(userDtoWithToken)
call.respond(HttpStatusCode.OK, userDtoWithToken)
} ?: call.respond(HttpStatusCode.NotFound, ApiMessage.UserNotFound)
} else {
call.respond(HttpStatusCode.NotFound, ApiMessage.IncorrectLoginPassword)
}
}
}
authenticate {
post("/users/delete") {
hasToken { principal ->
verifyUserFromToken(principal) { _, password ->
val checkUser = call.receive<CheckUser>()
if (CryptManagerUser.passwordDecrypt(password, checkUser.password)) {
if (!deleteUserByUsername(checkUser.login)) {
call.respond(HttpStatusCode.InternalServerError, "This user can't be delete now !")
}
call.respond(HttpStatusCode.Accepted, password)
} else {
call.respond(HttpStatusCode.NotFound, "Login and/or password incorrect.")
}
}
}
}
get("/users/token") {
hasToken { principal ->
verifyUserFromToken(principal) { userDto, _ ->
call.respond(HttpStatusCode.OK, userDto)
}
}
}
}
}
}

@ -0,0 +1,31 @@
package allin.routing
import allin.model.ApiMessage
import allin.utils.AppConfig
import io.github.smiley4.ktorswaggerui.dsl.get
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.basicRouter() {
val logManager = AppConfig.logManager
routing {
get("/", {
description = "Hello World of Allin API"
response {
HttpStatusCode.OK to {
description = "Successful Request"
}
HttpStatusCode.InternalServerError to {
description = "Something unexpected happened"
}
}
}) {
logManager.log("Routing","Get '/'")
call.respond(ApiMessage.WELCOME)
}
}
}

@ -0,0 +1,64 @@
package allin.routing
import allin.dataSource
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.BetDetail
import allin.utils.AppConfig
import io.github.smiley4.ktorswaggerui.dsl.get
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
fun Application.betDetailRouter() {
val userDataSource = this.dataSource.userDataSource
val betDataSource = this.dataSource.betDataSource
val logManager = AppConfig.logManager
routing {
authenticate {
get("/betdetail/get/{id}", {
description = "Retrieves the details of a specific bet"
request {
headerParameter<JWTPrincipal>("JWT token of the logged user")
pathParameter<UUID>("Id of the desired detail bet")
}
response {
HttpStatusCode.Accepted to {
description = "The bet can be returned"
body<BetDetail>()
}
HttpStatusCode.NotFound to {
description = "Bet not found in the selected source"
body(ApiMessage.BET_NOT_FOUND)
}
}
}) {
logManager.log("Routing", "GET /betdetail/get/{id}")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val id = call.parameters["id"].toString()
val result = betDataSource.getBetDetailById(id, user.id)
if (result != null) {
logManager.log("Routing", "ACCEPTED GET /betdetail/get/{id}\t${result}")
call.respond(
HttpStatusCode.Accepted,
result
)
} else {
logManager.log("Routing", "${ApiMessage.BET_NOT_FOUND} GET /betdetail/get/{id}")
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
}
}
}
}
}

@ -0,0 +1,419 @@
package allin.routing
import allin.dataSource
import allin.dto.UserDTO
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.*
import allin.utils.AppConfig
import io.github.smiley4.ktorswaggerui.dsl.get
import io.github.smiley4.ktorswaggerui.dsl.post
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
val tokenManagerBet = AppConfig.tokenManager
fun Application.betRouter() {
val userDataSource = this.dataSource.userDataSource
val betDataSource = this.dataSource.betDataSource
val participationDataSource = this.dataSource.participationDataSource
val logManager = AppConfig.logManager
routing {
authenticate {
post("/bets/add", {
description = "Allows a user to create a new bet"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
body<Bet> {
description = "Bet to add in the selected source"
}
}
response {
HttpStatusCode.Created to {
description = "the bet has been added"
body<Bet> {
description = "Bet with assigned id"
}
}
HttpStatusCode.Conflict to {
description = "Id already exist"
body(ApiMessage.BET_ALREADY_EXIST)
}
}
}) {
logManager.log("Routing", "POST /bets/add")
hasToken { principal ->
val bet = call.receive<Bet>()
val id = UUID.randomUUID().toString()
val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username)
betDataSource.getBetById(id)?.let {
logManager.log("Routing", "${ApiMessage.BET_ALREADY_EXIST} /bets/add")
call.respond(HttpStatusCode.Conflict, ApiMessage.BET_ALREADY_EXIST)
} ?: run {
val betWithId = bet.copy(id = id, createdBy = user.first?.id.toString())
if (bet.isPrivate && bet.userInvited?.isNotEmpty() == true) {
betDataSource.addPrivateBet(betWithId)
} else betDataSource.addBet(betWithId)
logManager.log("Routing", "CREATED /bets/add\t${betWithId}")
call.respond(HttpStatusCode.Created, betWithId)
}
}
}
}
authenticate {
post("/bets/gets", {
description = "Allows you to recover all bets"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
body<List<BetFilter>> {
description = "List of filters"
}
}
response {
HttpStatusCode.Accepted to {
description = "The list of bets is available"
body<List<Bet>> {
description = "List of all bet in the selected source"
}
}
}
}) {
logManager.log("Routing", "POST /bets/gets")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val filtersRequest =
kotlin.runCatching { call.receiveNullable<BetFiltersRequest>() }.getOrNull()
val filters =
filtersRequest?.filters ?: emptyList() // Use provided filters or empty list if null
logManager.log("Routing", "ACCEPTED /bets/gets\t${filters}")
call.respond(HttpStatusCode.Accepted, betDataSource.getAllBets(filters, user))
}
}
}
}
authenticate {
get("/bets/popular", {
description = "Allows you to recover the most popular public bets"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "The most popular public bet is available"
body<Bet> {
description = "The most popular public bet"
}
}
}
}) {
logManager.log("Routing", "GET /bets/popular")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { _, _ ->
val bet = betDataSource.getMostPopularBet()
if (bet != null) {
logManager.log("Routing", "ACCEPTED /bets/popular\t${bet}")
call.respond(HttpStatusCode.Accepted, bet)
}
logManager.log("Routing", "${ApiMessage.BET_NOT_FOUND} /bets/popular")
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
}
}
get("/bets/get/{id}", {
description = "Retrieves a specific bet"
request {
pathParameter<String>(ApiMessage.ID_BET_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "The bet is available"
body<Bet> {
description = "Desired bet"
}
}
HttpStatusCode.NotFound to {
description = ApiMessage.BET_NOT_FOUND_INFO
body(ApiMessage.BET_NOT_FOUND)
}
}
}) {
logManager.log("Routing", "GET /bets/get/{id}")
val id = call.parameters["id"] ?: ""
betDataSource.getBetById(id)?.let { bet ->
logManager.log("Routing", "ACCEPTED /bets/get/{id}\t ${bet}")
call.respond(HttpStatusCode.Accepted, bet)
} ?: logManager.log("Routing", "${ApiMessage.BET_NOT_FOUND} /bets/get/{id}")
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
post("/bets/delete", {
description = "Delete a specific bet"
request {
body<Map<String, String>> {
description = ApiMessage.ID_BET_INFO
}
}
response {
HttpStatusCode.Accepted to {
description = "The bet has been deleted"
}
HttpStatusCode.NotFound to {
description = ApiMessage.BET_NOT_FOUND_INFO
body(ApiMessage.BET_NOT_FOUND)
}
}
}) {
logManager.log("Routing", "POST /bets/delete")
val id = call.receive<Map<String, String>>()["id"] ?: ""
if (betDataSource.removeBet(id)) {
logManager.log("Routing", "ACCEPTED /bets/delete")
call.respond(HttpStatusCode.Accepted)
} else {
logManager.log("Routing", "${ApiMessage.BET_NOT_FOUND} /bets/delete")
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
post("bets/update", {
description = "Update a specific bet"
request {
body<UpdatedBetData> {
description = "Information of the updated bet"
}
}
response {
HttpStatusCode.Accepted to {
description = "The bet has been updated"
}
HttpStatusCode.NotFound to {
description = ApiMessage.BET_NOT_FOUND_INFO
body(ApiMessage.BET_NOT_FOUND)
}
}
}) {
logManager.log("Routing", "POST /bets/update")
val updatedBetData = call.receive<UpdatedBetData>()
if (betDataSource.updateBet(updatedBetData)) {
logManager.log("Routing", "ACCEPTED /bets/delete")
call.respond(HttpStatusCode.Accepted)
} else {
logManager.log("Routing", "${ApiMessage.BET_NOT_FOUND} /bets/delete")
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
}
authenticate {
get("/bets/toConfirm", {
description = "Allows a user to know which bets can be validated"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "The list of bets that can be validated is available"
body<List<BetDetail>> {
description = "list of bets that can be validated"
}
}
}
}) {
logManager.log("Routing", "GET /bets/toConfirm")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val response = betDataSource.getToConfirm(user)
logManager.log("Routing", "ACCEPTED /bets/toConfirm\t${response}")
call.respond(HttpStatusCode.Accepted, response)
}
}
}
}
authenticate {
get("/bets/getWon", {
description = "Allows a user to know their won bets"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "The list of won bets is available"
body<List<BetResultDetail>> {
description = "List of won bets"
}
}
}
}) {
logManager.log("Routing", "GET /bets/getWon")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
logManager.log("Routing", "ACCEPTED /bets/getWon")
call.respond(HttpStatusCode.Accepted, betDataSource.getWonNotifications(user.id))
}
}
}
}
authenticate {
get("/bets/history", {
description = "Allows a user to know own history of bets"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "Bet history is available"
body<List<BetResultDetail>> {
description = "Betting history list"
}
}
}
}) {
logManager.log("Routing", "GET /bets/history")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
logManager.log(
"Routing",
"ACCEPTED /bets/toConfirm\t${betDataSource.getHistory(user.id)}"
)
call.respond(HttpStatusCode.Accepted, betDataSource.getHistory(user.id))
}
}
}
}
authenticate {
get("/bets/current", {
description = "Allows a user to know current bets"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "List of current bets is available"
body<List<BetDetail>> {
description = "List of current bets"
}
}
}
}) {
logManager.log("Routing", "GET /bets/current")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
logManager.log(
"Routing",
"ACCEPTED /bets/toConfirm\t${betDataSource.getCurrent(user.id)}"
)
call.respond(HttpStatusCode.Accepted, betDataSource.getCurrent(user.id))
}
}
}
}
authenticate {
post("/bets/confirm/{id}", {
description = "allows the creator of a bet to confirm the final answer"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
pathParameter<String>(ApiMessage.ID_BET_INFO)
body<String> {
description = "Final answer of the bet"
}
}
response {
HttpStatusCode.OK to {
description = "The final answer has been set"
}
HttpStatusCode.Unauthorized to {
description = ApiMessage.NOT_CREATOR_INFO
}
}
}) {
logManager.log("Routing", "GET /bets/confirm/{id}")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val betId = call.parameters["id"] ?: ""
val result = call.receive<String>()
if (betDataSource.getBetById(betId)?.createdBy == user.username) {
betDataSource.confirmBet(betId, result)
logManager.log("Routing", "ACCEPTED /bets/confirm/{id} $result")
call.respond(HttpStatusCode.OK)
} else {
logManager.log("Routing", "UNAUTHORIZED /bets/confirm/{id}")
call.respond(HttpStatusCode.Unauthorized)
}
}
}
}
post("/bets/users", {
description = "gets all userDTO of a bet"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
pathParameter<String>(ApiMessage.ID_BET_INFO)
body<List<UserDTO>> {
description = "UserDTO of the bet"
}
}
response {
HttpStatusCode.Accepted to {
description = "List of 4 user of the selected bet"
}
}
}) {
logManager.log("Routing", "POST /bets/users")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { _, _ ->
val id = call.receive<Map<String, String>>()["id"] ?: ""
val participations = participationDataSource.getParticipationFromBetId(id)
val users =
participations.map { userDataSource.getUserById(it.id) }.toSet().take(4)
.toList()
call.respond(HttpStatusCode.Accepted, users)
}
}
}
post("/bets/pvbet/update", {
description = "Add new users to a private bet"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
body<UpdatedPrivateBet> {
description = "Bet id and list of new users"
}
}
response {
HttpStatusCode.Accepted to {
description = "Invited users list updated"
}
HttpStatusCode.Unauthorized to {
description = ApiMessage.NOT_CREATOR_INFO
}
}
}) {
logManager.log("Routing", "POST /bets/pvbet/update")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val updateRequest = call.receive<UpdatedPrivateBet>()
val bet = betDataSource.getBetById(updateRequest.betid)
if (user.username != bet?.createdBy) {
call.respond(HttpStatusCode.Unauthorized, ApiMessage.USER_DOESNT_HAVE_PERMISSION)
}
betDataSource.addUserInPrivatebet(updateRequest)
call.respond(HttpStatusCode.Accepted, updateRequest)
}
}
}
}
}
}

@ -0,0 +1,218 @@
package allin.routing
import allin.dataSource
import allin.dto.UserDTO
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.utils.AppConfig
import io.github.smiley4.ktorswaggerui.dsl.get
import io.github.smiley4.ktorswaggerui.dsl.post
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.friendRouter() {
val userDataSource = this.dataSource.userDataSource
val friendDataSource = this.dataSource.friendDataSource
val logManager = AppConfig.logManager
routing {
authenticate {
get("/friends/gets", {
description = "Allows you to recover all friends of a JWT Token"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "The list of friends is available"
body<List<UserDTO>> {
description = "List of friends"
}
}
}
}) {
logManager.log("Routing", "GET /friends/gets")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { _, _ ->
val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username).first
logManager.log(
"Routing",
"ACCEPTED /friends/gets\t${friendDataSource.getFriendFromUserId(user?.id.toString())}"
)
call.respond(HttpStatusCode.Accepted, friendDataSource.getFriendFromUserId(user?.id.toString()))
}
}
}
get("/friends/requests", {
description = "Allows you to recover all friend requests of a JWT Token"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.Accepted to {
description = "The list of friend requests is available"
body<List<UserDTO>> {
description = "List of friend requests"
}
}
}
}) {
logManager.log("Routing", "GET /friends/requests")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { _, _ ->
val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username).first
logManager.log(
"Routing", "ACCEPTED /friends/requests\t${
friendDataSource.getFriendRequestsFromUserId(user?.id.toString())
}"
)
call.respond(
HttpStatusCode.Accepted,
friendDataSource.getFriendRequestsFromUserId(user?.id.toString())
)
}
}
}
post("/friends/add", {
description = "Allows a user to add a friend"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
body<String> {
description = "User to add in the friends list"
}
}
response {
HttpStatusCode.Created to {
description = "the friend has been added"
body<String> {
description = "Friend with assigned id"
}
}
HttpStatusCode.Conflict to {
description = "Friend already exist in the friends list"
body(ApiMessage.FRIENDS_ALREADY_EXISTS)
}
}
}) {
logManager.log("Routing", "POST /friends/add")
hasToken { principal ->
val requestMap = call.receive<Map<String, String>>()
val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(
HttpStatusCode.BadRequest,
"Username is missing"
)
val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username).first
val userFriend = userDataSource.getUserByUsername(usernameFriend).first
if (user == null || userFriend == null) {
logManager.log("Routing", "${ApiMessage.USER_NOT_FOUND} /friends/add")
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND)
} else if (userFriend == user) {
logManager.log("Routing", "${ApiMessage.FRIENDS_REQUEST_HIMSELF} /friends/add")
call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_REQUEST_HIMSELF)
} else {
val friendlist = friendDataSource.getFriendFromUserId(user.id)
if (friendlist.map { it.id }.contains(userFriend.id)) {
logManager.log("Routing", "${ApiMessage.FRIENDS_ALREADY_EXISTS} /friends/add")
call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_ALREADY_EXISTS)
} else {
logManager.log("Routing", "ACCEPTED /friends/add\t${usernameFriend}")
friendDataSource.addFriend(user.id, userFriend.id)
call.respond(HttpStatusCode.Created, usernameFriend)
}
}
}
}
post("/friends/delete", {
description = "Allows a user to delete a friend"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
body<String> {
description = "User to delete in the friends list"
}
}
response {
HttpStatusCode.Created to {
description = "the friend has been delete"
body<String> {
description = "Friend with assigned id"
}
}
HttpStatusCode.Conflict to {
description = "Friend doesn't exist in the friends list"
body(ApiMessage.FRIENDS_DOESNT_EXISTS)
}
}
}) {
logManager.log("Routing", "POST /friends/delete")
hasToken { principal ->
val requestMap = call.receive<Map<String, String>>()
val usernameFriend = requestMap["username"] ?: return@hasToken call.respond(
HttpStatusCode.BadRequest,
"Username is missing"
)
val username = tokenManagerBet.getUsernameFromToken(principal)
val user = userDataSource.getUserByUsername(username).first
val userFriend = userDataSource.getUserByUsername(usernameFriend).first
if (user == null || userFriend == null) {
logManager.log("Routing", "${ApiMessage.USER_NOT_FOUND} /friends/delete")
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_NOT_FOUND)
} else {
if (friendDataSource.deleteFriend(user.id, userFriend.id)) {
logManager.log("Routing", "ACCEPTED /friends/delete\t${usernameFriend}")
call.respond(HttpStatusCode.Created, usernameFriend)
} else {
logManager.log("Routing", "${ApiMessage.FRIENDS_DOESNT_EXISTS} /friends/delete")
call.respond(HttpStatusCode.Conflict, ApiMessage.FRIENDS_DOESNT_EXISTS)
}
}
}
}
get("/friends/search/{search}", {
description = "Search for users based on username"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
pathParameter<String>("Search string")
}
response {
HttpStatusCode.OK to {
body<List<UserDTO>> {
description = "Filtered users."
}
}
}
}) {
logManager.log("Routing", "GET /friends/search/{search}")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { userDto, _ ->
val users = friendDataSource.filterUsersByUsername(
fromUserId = userDto.id,
search = call.parameters["search"] ?: ""
)
logManager.log("Routing", "ACCEPTED /friends/search/{search}\t${users}")
call.respond(HttpStatusCode.OK, users)
}
}
}
}
}
}

@ -0,0 +1,115 @@
package allin.routing
import allin.dataSource
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.ApiMessage
import allin.model.Participation
import allin.model.ParticipationRequest
import allin.utils.AppConfig
import io.github.smiley4.ktorswaggerui.dsl.delete
import io.github.smiley4.ktorswaggerui.dsl.post
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
fun Application.participationRouter() {
val userDataSource = this.dataSource.userDataSource
val participationDataSource = this.dataSource.participationDataSource
val betDataSource = this.dataSource.betDataSource
val logManager = AppConfig.logManager
routing {
authenticate {
post("/participations/add", {
description = "Allows a user to add a stake to a bet"
request {
headerParameter<JWTPrincipal>("JWT token of the logged user")
body<ParticipationRequest> {
description = "Participation in a bet"
}
}
response {
HttpStatusCode.Created to {
description = "The stake has been bet"
}
HttpStatusCode.Forbidden to {
description = "User does not have enough coins"
body(ApiMessage.NOT_ENOUGH_COINS)
}
}
}) {
logManager.log("Routing", "POST /participations/add")
hasToken { principal ->
val participation = call.receive<ParticipationRequest>()
verifyUserFromToken(userDataSource, principal) { user, _ ->
if (betDataSource.getBetById(participation.betId) == null) {
logManager.log("Routing", "${ApiMessage.BET_NOT_FOUND} /participations/add")
call.respond(HttpStatusCode.NotFound, ApiMessage.BET_NOT_FOUND)
}
if (user.nbCoins >= participation.stake) {
participationDataSource.addParticipation(
Participation(
id = UUID.randomUUID().toString(),
betId = participation.betId,
userId = user.id,
answer = participation.answer,
stake = participation.stake,
username = user.username
)
)
userDataSource.removeCoins(username = user.username, amount = participation.stake)
betDataSource.updatePopularityScore(participation.betId)
logManager.log("Routing", "CREATED /participations/add")
call.respond(HttpStatusCode.Created)
} else {
logManager.log("Routing", "${ApiMessage.NOT_ENOUGH_COINS} /participations/add")
call.respond(HttpStatusCode.Forbidden, ApiMessage.NOT_ENOUGH_COINS)
}
}
}
}
delete("/participations/delete", {
description = "Allows to delete a participation to a bet"
request {
headerParameter<JWTPrincipal>("JWT token of the logged user")
body<String> {
description = "Id of the participation"
}
}
response {
HttpStatusCode.NotFound to {
description = "Participation was not found"
body(ApiMessage.PARTICIPATION_NOT_FOUND)
}
HttpStatusCode.NoContent to {
description = "The operation was successful"
}
}
}) {
logManager.log("Routing", "DELETE /participations/delete")
hasToken {
val participationId = call.receive<String>()
if (participationDataSource.deleteParticipation(participationId)) {
logManager.log("Routing", "ACCEPTED /participations/delete")
call.respond(HttpStatusCode.NoContent)
} else {
logManager.log("Routing", "${ApiMessage.PARTICIPATION_NOT_FOUND} /participations/delete")
call.respond(HttpStatusCode.NotFound, ApiMessage.PARTICIPATION_NOT_FOUND)
}
}
}
}
}
}

@ -0,0 +1,289 @@
package allin.routing
import allin.dataSource
import allin.dto.UserDTO
import allin.ext.hasToken
import allin.ext.verifyUserFromToken
import allin.model.*
import allin.utils.AppConfig
import io.github.smiley4.ktorswaggerui.dsl.get
import io.github.smiley4.ktorswaggerui.dsl.post
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.File
import java.util.*
val RegexCheckerUser = AppConfig.regexChecker
val CryptManagerUser = AppConfig.cryptManager
val tokenManagerUser = AppConfig.tokenManager
val imageManagerUser = AppConfig.imageManager
val urlManager = AppConfig.urlManager
const val DEFAULT_COINS = 500
fun Application.userRouter() {
val userDataSource = this.dataSource.userDataSource
val logManager = AppConfig.logManager
routing {
post("/users/register", {
description = "Allows a user to register"
request {
body<UserRequest> {
description = ApiMessage.USER_UPDATE_INFO
}
}
response {
HttpStatusCode.Created to {
description = "User created"
body<User> {
description = "The new user"
}
}
HttpStatusCode.Conflict to {
description = "Email or username already taken"
body(ApiMessage.USER_ALREADY_EXISTS)
}
HttpStatusCode.Forbidden to {
description = "Email invalid"
body(ApiMessage.INVALID_MAIL)
}
}
}) {
logManager.log("Routing", "POST /users/register")
val tempUser = call.receive<UserRequest>()
if (RegexCheckerUser.isEmailInvalid(tempUser.email)) {
logManager.log("Routing", "${ApiMessage.INVALID_MAIL} /users/register")
call.respond(HttpStatusCode.Forbidden, ApiMessage.INVALID_MAIL)
} else if (userDataSource.userExists(tempUser.username)) {
logManager.log("Routing", "${ApiMessage.USER_ALREADY_EXISTS} /users/register")
call.respond(HttpStatusCode.Conflict, ApiMessage.USER_ALREADY_EXISTS)
} else if (userDataSource.emailExists(tempUser.email)) {
logManager.log("Routing", "${ApiMessage.MAIL_ALREADY_EXISTS} /users/register")
call.respond(HttpStatusCode.Conflict, ApiMessage.MAIL_ALREADY_EXISTS)
} else {
val user = User(
id = UUID.randomUUID().toString(),
username = tempUser.username,
email = tempUser.email,
password = tempUser.password,
nbCoins = DEFAULT_COINS,
token = null,
bestWin = 0,
nbFriends = 0,
nbBets = 0,
)
CryptManagerUser.passwordCrypt(user)
user.token = tokenManagerUser.generateOrReplaceJWTToken(user)
userDataSource.addUser(user)
logManager.log("Routing", "ACCEPTED /users/register\t${user}")
call.respond(HttpStatusCode.Created, user)
}
}
post("/users/login", {
description = "Allows a user to login"
request {
body<CheckUser> {
description = ApiMessage.USER_UPDATE_INFO
}
}
response {
HttpStatusCode.OK to {
description = "User logged in"
body<UserDTO>()
}
HttpStatusCode.NotFound to {
description = "Invalid credentials"
body(ApiMessage.USER_NOT_FOUND)
}
}
}) {
logManager.log("Routing", "POST /users/login")
val checkUser = call.receive<CheckUser>()
val user = userDataSource.getUserByUsername(checkUser.login)
if (CryptManagerUser.passwordDecrypt(user.second ?: "", checkUser.password)) {
user.first?.let { userDtoWithToken ->
userDtoWithToken.token = tokenManagerUser.generateOrReplaceJWTToken(userDtoWithToken)
logManager.log("Routing", "ACCEPTED /users/login\t${userDtoWithToken}")
call.respond(HttpStatusCode.OK, userDtoWithToken)
} ?: logManager.log("Routing", "${ApiMessage.USER_NOT_FOUND} /users/login")
call.respond(HttpStatusCode.NotFound, ApiMessage.USER_NOT_FOUND)
} else {
logManager.log("Routing", "${ApiMessage.INCORRECT_LOGIN_PASSWORD} /users/login")
call.respond(HttpStatusCode.NotFound, ApiMessage.INCORRECT_LOGIN_PASSWORD)
}
}
get("/users/images/{fileName}") {
logManager.log("Routing", "GET /users/images/{fileName}")
val fileName = call.parameters["fileName"]
val urlfile = "images/$fileName"
val file = File("$urlfile.png")
if (file.exists()) {
call.respondFile(file)
} else {
val imageBytes = userDataSource.getImage(fileName.toString())
if (imageBytes != null) {
imageManagerUser.saveImage(urlfile, imageBytes)
logManager.log("Routing", "ACCEPTED /users/images/{fileName}")
call.respondFile(file)
} else {
logManager.log("Routing", "${ApiMessage.FILE_NOT_FOUND} /users/images/{fileName}")
call.respond(HttpStatusCode.NotFound, ApiMessage.FILE_NOT_FOUND)
}
}
}
authenticate {
post("/users/delete", {
description = "Allow you to delete your account"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
body<CheckUser> {
description = ApiMessage.USER_UPDATE_INFO
}
}
response {
HttpStatusCode.InternalServerError to {
description = "User can't be delete"
body(ApiMessage.USER_CANT_BE_DELETE)
}
HttpStatusCode.Accepted to {
body<String> {
description = "Password of the user"
}
}
HttpStatusCode.NotFound to {
description = "User not found"
body(ApiMessage.INCORRECT_LOGIN_PASSWORD)
}
}
}) {
logManager.log("Routing", "POST /users/delete")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { _, password ->
val checkUser = call.receive<CheckUser>()
if (CryptManagerUser.passwordDecrypt(password, checkUser.password)) {
if (!userDataSource.deleteUser(checkUser.login)) {
logManager.log("Routing", "${ApiMessage.USER_CANT_BE_DELETE} /users/delete")
call.respond(HttpStatusCode.InternalServerError, ApiMessage.USER_CANT_BE_DELETE)
}
logManager.log("Routing", "ACCEPTED /users/delete")
call.respond(HttpStatusCode.Accepted, password)
} else {
logManager.log("Routing", "${ApiMessage.INCORRECT_LOGIN_PASSWORD} /users/delete")
call.respond(HttpStatusCode.NotFound, ApiMessage.INCORRECT_LOGIN_PASSWORD)
}
}
}
}
get("/users/token", {
description = "Allows you to retrieve the user linked to a JWT token"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.OK to {
body<UserDTO> {
description = "Limited user information"
}
}
}
}) {
logManager.log("Routing", "GET /users/token")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { userDto, _ ->
logManager.log("Routing", "ACCEPTED /users/token\t${userDto}")
call.respond(HttpStatusCode.OK, userDto)
}
}
}
get("/users/gift", {
description = "Allows you to collect your daily gift"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
}
response {
HttpStatusCode.OK to {
description = "Daily gift allowed !"
body<Int> {
description = "Number of coins offered"
}
}
HttpStatusCode.MethodNotAllowed to {
description = "You can't have you daily gift now"
body(ApiMessage.NO_GIFT)
}
}
}) {
logManager.log("Routing", "GET /users/gift")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { userDto, _ ->
if (userDataSource.canHaveDailyGift(userDto.username)) {
val dailyGift = (DAILY_GIFT_MIN..DAILY_GIFT_MAX).random()
userDataSource.addCoins(userDto.username, dailyGift)
logManager.log("Routing", "ACCEPTED /users/gift\t${dailyGift}")
call.respond(HttpStatusCode.OK, dailyGift)
logManager.log("Routing", "${ApiMessage.NO_GIFT} /users/gift")
} else call.respond(HttpStatusCode.MethodNotAllowed, ApiMessage.NO_GIFT)
}
}
}
post("/users/images", {
description = "Allow you to add a profil image"
request {
headerParameter<JWTPrincipal>(ApiMessage.JWT_TOKEN_INFO)
body<CheckUser> {
description = ApiMessage.USER_UPDATE_INFO
}
}
response {
HttpStatusCode.Accepted to {
description = "Image added"
}
HttpStatusCode.NotFound to {
description = "User not found"
body(ApiMessage.INCORRECT_LOGIN_PASSWORD)
}
}
}) {
logManager.log("Routing", "POST /users/images")
hasToken { principal ->
verifyUserFromToken(userDataSource, principal) { user, _ ->
val base64Image = call.receiveText()
val urlfile = "images/${user.id}"
val imageByteArray = imageManagerUser.saveImage(urlfile, base64Image)
if (imageByteArray != null && imageByteArray.isNotEmpty()) {
userDataSource.removeImage(user.id)
userDataSource.addImage(user.id, imageByteArray)
logManager.log("Routing", "ACCEPTED /users/images")
call.respond(HttpStatusCode.OK, "${urlManager.getURL()}users/${urlfile}")
}
logManager.log("Routing", "${ApiMessage.FILE_NOT_FOUND} /users/images")
call.respond(HttpStatusCode.Conflict, ApiMessage.FILE_NOT_FOUND)
}
}
}
}
}
}

@ -1,5 +1,6 @@
package allin.serializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.PrimitiveKind
@ -7,13 +8,11 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.sql.Timestamp
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalSerializationApi::class)
@Serializer(ZonedDateTime::class)
object ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
private val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z")

@ -1,11 +1,14 @@
package allin.utils
import com.typesafe.config.ConfigFactory
import io.ktor.server.config.HoconApplicationConfig
import io.ktor.server.config.*
object AppConfig {
val config: HoconApplicationConfig = HoconApplicationConfig(ConfigFactory.load())
private val config: HoconApplicationConfig = HoconApplicationConfig(ConfigFactory.load())
val tokenManager = TokenManager.getInstance(config)
val regexChecker= RegexChecker()
val regexChecker = RegexChecker()
val cryptManager = CryptManager()
val imageManager = ImageManager()
val urlManager = URLManager()
val logManager = LogManager()
}

@ -4,9 +4,12 @@ import allin.model.User
import org.mindrot.jbcrypt.BCrypt
class CryptManager {
val salt=addDollarsSecrets(System.getenv().get("SALT").toString())
private val salt = addDollarsSecrets(
System.getenv()["SALT"] ?: throw RuntimeException("Salt is null")
)
// Cette fonction permet de remettre les $ que drone supprime dans les secrets drone
fun addDollarsSecrets(chaine: String): String {
private fun addDollarsSecrets(chaine: String): String {
val stringBuilder = StringBuilder(chaine)
stringBuilder.insert(0, '$')
stringBuilder.insert(3, '$')
@ -14,17 +17,20 @@ class CryptManager {
return stringBuilder.toString()
}
fun passwordCrypt(password : String): String {
return BCrypt.hashpw(password,salt)
fun passwordCrypt(password: String): String {
return BCrypt.hashpw(password, salt)
}
fun passwordCrypt(user: User){
user.password=BCrypt.hashpw(user.password,salt)
fun passwordCrypt(user: User) {
user.password = BCrypt.hashpw(user.password, salt)
}
fun passwordDecrypt(password: String, passwordClear: String): Boolean{
return BCrypt.hashpw(passwordClear,salt)==password
fun passwordDecrypt(password: String, passwordClear: String): Boolean {
return BCrypt.hashpw(passwordClear, salt) == password
}
fun CheckPassword(hashed: String, clear: String): Boolean{
return BCrypt.checkpw(hashed,clear)
fun checkPassword(hashed: String, clear: String): Boolean {
return BCrypt.checkpw(hashed, clear)
}
}

@ -1,13 +0,0 @@
package allin.utils
import allin.database
import org.ktorm.database.Database
fun Database.Execute(request: String){
if(!request.isNullOrEmpty())
database.useTransaction {
val connection = it.connection
connection.prepareStatement(request).execute()
connection.commit()
}
}

@ -0,0 +1,40 @@
package allin.utils
import allin.data.postgres.entities.usersimage
import allin.routing.imageManagerUser
import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.entity.find
import java.io.File
import java.util.*
class ImageManager {
fun saveImage(urlfile: String, base64Image: String): ByteArray? {
val cleanedBase64Image = cleanBase64(base64Image)
val imageBytes = Base64.getDecoder().decode(cleanedBase64Image)
val file = File("${urlfile}.png")
file.parentFile.mkdirs()
file.writeBytes(imageBytes)
return imageBytes
}
fun saveImage(urlfile: String, base64Image: ByteArray) {
val file = File("${urlfile}.png")
file.parentFile.mkdirs()
file.writeBytes(base64Image)
}
fun getImage(userId: String, database: Database): String? {
val imageByte = database.usersimage.find { it.id eq userId }?.image ?: return null
val urlfile = "images/$userId"
if (!imageManagerUser.imageAvailable(urlfile)) {
imageManagerUser.saveImage(urlfile, imageByte)
}
return "${AppConfig.urlManager.getURL()}users/${urlfile}"
}
fun imageAvailable(urlfile: String) = File(urlfile).exists()
fun cleanBase64(base64Image: String) = base64Image.replace("\n", "").replace("\r", "")
}

@ -0,0 +1,12 @@
package allin.utils
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class LogManager {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
fun log(type: String,message: String) {
println("[${LocalDateTime.now().format(formatter)}] [${type}] $message")
}
}

@ -1,13 +1,10 @@
package allin.utils
class RegexChecker {
private val emailRegex="^[A-Za-z0-9+_.-]+@(.+)$"
private val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$"
fun isEmailInvalid(email: String): Boolean {
val emailRegex = Regex(emailRegex)
return !emailRegex.matches(email)
}
}

@ -2,26 +2,26 @@ package allin.utils
import allin.dto.UserDTO
import allin.model.User
import allin.utils.TokenManager.Companion.Claims.USERNAME
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.DecodedJWT
import io.ktor.server.auth.jwt.*
import io.ktor.server.config.*
import java.util.*
class TokenManager private constructor(val config: HoconApplicationConfig) {
class TokenManager private constructor(config: HoconApplicationConfig) {
val audience = config.property("audience").getString()
val secret = config.property("secret").getString()
val issuer = config.property("issuer").getString()
fun generateJWTToken(user: User): String {
private val audience = config.property("audience").getString()
private val secret = config.property("secret").getString()
private val issuer = config.property("issuer").getString()
private fun generateJWTToken(user: User): String {
val expirationDate = System.currentTimeMillis() + 604800000 // une semaine en miliseconde
return JWT.create()
.withAudience(audience)
.withIssuer(issuer)
.withClaim("username", user.username)
.withClaim(USERNAME, user.username)
.withExpiresAt(Date(expirationDate))
.sign(Algorithm.HMAC256(secret))
}
@ -52,30 +52,34 @@ class TokenManager private constructor(val config: HoconApplicationConfig) {
}
}
fun generateJWTToken(user: UserDTO): String {
private fun generateJWTToken(user: UserDTO): String {
val expirationDate = System.currentTimeMillis() + 604800000 // une semaine en miliseconde
return JWT.create()
.withAudience(audience)
.withIssuer(issuer)
.withClaim("username", user.username)
.withClaim(USERNAME, user.username)
.withExpiresAt(Date(expirationDate))
.sign(Algorithm.HMAC256(secret))
}
fun isTokenExpired(token: String): Boolean {
private fun isTokenExpired(token: String): Boolean {
val expirationTime = JWT.decode(token).expiresAt.time
return System.currentTimeMillis() > expirationTime
}
fun getUserToken(user: User): String? = user.token
fun getUserToken(user: UserDTO): String? = user.token
private fun getUserToken(user: User): String? = user.token
private fun getUserToken(user: UserDTO): String? = user.token
fun getUsernameFromToken(principal: JWTPrincipal): String {
return principal.payload.getClaim("username").asString()
return principal.payload.getClaim(USERNAME).asString()
}
companion object {
object Claims {
const val USERNAME = "username"
}
private var instance: TokenManager? = null
fun getInstance(config: HoconApplicationConfig): TokenManager {
return instance ?: synchronized(this) {

@ -0,0 +1,13 @@
package allin.utils
import allin.hostIP
import allin.hostPort
import allin.isCodeFirstContainer
class URLManager {
fun getURL(): String {
return if (isCodeFirstContainer.isEmpty()) {
"http://$hostIP:$hostPort/"
} else "https://codefirst.iut.uca.fr${isCodeFirstContainer}"
}
}

@ -0,0 +1,15 @@
package allin.utils
import kotlinx.coroutines.*
import kotlin.time.Duration
@OptIn(DelicateCoroutinesApi::class)
fun kronJob(duration: Duration, action: () -> Unit) =
GlobalScope.launch {
withContext(Dispatchers.IO) {
while (true) {
runCatching { action() }
delay(duration.inWholeMilliseconds)
}
}
}

@ -1,4 +1,4 @@
secret="secret"
issuer="http://0.0.0.0:8080/"
audience="http://0.0.0.0:8080/"
realm="Access to main page"
realm="allin"

@ -1,4 +1,6 @@
package allin
class ApplicationTest {
}

@ -0,0 +1,129 @@
package allin.data.postgres
import allin.data.postgres.entities.betAnswerInfos
import allin.data.postgres.entities.betInfos
import allin.data.postgres.entities.betResults
import allin.model.*
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.entity.removeIf
import org.ktorm.support.postgresql.PostgreSqlDialect
import java.time.ZoneId
import java.time.ZonedDateTime
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class PostgresBetDataSourceTest {
private lateinit var database: Database
private lateinit var dataSource: PostgresBetDataSource
lateinit var user: User
@BeforeAll
fun setUp() {
val dbDatabase = System.getenv()["POSTGRES_DB"]
val dbUser = System.getenv()["POSTGRES_USER"]
val dbPassword = System.getenv()["POSTGRES_PASSWORD"]
val dbHost = System.getenv()["POSTGRES_HOST"]
val url = "jdbc:postgresql://$dbHost/$dbDatabase"
database = Database.connect(
url = url,
user = dbUser,
password = dbPassword,
dialect = PostgreSqlDialect()
)
user = User(
id = "123",
username = "JohnDoe",
email = "johndoe@example.com",
password = "securePassword123",
nbCoins = 1000,
token = null,
image = null,
bestWin = 500,
nbBets = 50,
nbFriends = 10
)
dataSource = PostgresBetDataSource(database)
PostgresUserDataSource(database).addUser(user)
}
@Test
@Order(1)
fun testAddBet() {
val bet = Bet(
id = "bbba08f7-744f-4d23-9706-b31bdf24f614",
theme = "Sports",
sentenceBet = "Will team A win?",
type = BetType.BINARY,
endRegistration = ZonedDateTime.now(ZoneId.of("+02:00")).plusDays(1),
endBet = ZonedDateTime.now(ZoneId.of("+02:00")).plusDays(2),
isPrivate = false,
response = listOf(YES_VALUE, NO_VALUE),
createdBy = user.id
)
dataSource.addBet(bet)
val retrievedBet = dataSource.getBetById("bbba08f7-744f-4d23-9706-b31bdf24f614")
assertNotNull(retrievedBet)
assertEquals("bbba08f7-744f-4d23-9706-b31bdf24f614", retrievedBet?.id)
}
@Test
@Order(2)
fun testGetAllBets() {
val userDTO = user.toDto()
val bets = dataSource.getAllBets(emptyList(), userDTO)
assertTrue(bets.isNotEmpty())
}
@Test
@Order(3)
fun testUpdateBet() {
val updatedData = UpdatedBetData(
id = "bbba08f7-744f-4d23-9706-b31bdf24f614",
endBet = ZonedDateTime.now(ZoneId.of("+02:00")).plusDays(3),
isPrivate = true,
response = listOf(YES_VALUE, NO_VALUE)
)
val result = dataSource.updateBet(updatedData)
assertTrue(result)
val retrievedBet = dataSource.getBetById("bbba08f7-744f-4d23-9706-b31bdf24f614")
assertNotNull(retrievedBet)
assertTrue(retrievedBet?.isPrivate ?: false)
}
@Test
@Order(4)
fun testConfirmBet() {
dataSource.confirmBet("bbba08f7-744f-4d23-9706-b31bdf24f614", YES_VALUE)
val retrievedBet = dataSource.getBetById("bbba08f7-744f-4d23-9706-b31bdf24f614")
assertNotNull(retrievedBet)
assertEquals(BetStatus.FINISHED, retrievedBet?.status)
}
@Test
@Order(5)
fun testGetBetDetailById() {
val betDetail = dataSource.getBetDetailById("bbba08f7-744f-4d23-9706-b31bdf24f614", user.id)
assertNotNull(betDetail)
assertEquals("bbba08f7-744f-4d23-9706-b31bdf24f614", betDetail?.bet?.id)
}
@AfterAll
fun tearDown() {
database.betResults.removeIf { it.betId eq "bbba08f7-744f-4d23-9706-b31bdf24f614" }
database.betInfos.removeIf { it.id eq "bbba08f7-744f-4d23-9706-b31bdf24f614" }
database.betAnswerInfos.removeIf { it.betId eq "bbba08f7-744f-4d23-9706-b31bdf24f614" }
dataSource.removeBet("bbba08f7-744f-4d23-9706-b31bdf24f614")
PostgresUserDataSource(database).deleteUser(user.username)
}
}

@ -0,0 +1,229 @@
import allin.data.postgres.PostgresUserDataSource
import allin.data.postgres.entities.UsersEntity
import allin.data.postgres.entities.users
import allin.ext.executeWithResult
import allin.model.User
import junit.framework.TestCase.*
import org.junit.jupiter.api.*
import org.ktorm.database.Database
import org.ktorm.dsl.eq
import org.ktorm.dsl.update
import org.ktorm.entity.find
import org.ktorm.support.postgresql.PostgreSqlDialect
import java.time.Instant
import java.util.*
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class PostgresUserDataSourceTest {
private lateinit var userDataSource: PostgresUserDataSource
private lateinit var database: Database
@BeforeAll
fun setUp() {
val dbDatabase = System.getenv()["POSTGRES_DB"]
val dbUser = System.getenv()["POSTGRES_USER"]
val dbPassword = System.getenv()["POSTGRES_PASSWORD"]
val dbHost = System.getenv()["POSTGRES_HOST"]
val url = "jdbc:postgresql://$dbHost/$dbDatabase"
database = Database.connect(
url = url,
user = dbUser,
password = dbPassword,
dialect = PostgreSqlDialect()
)
userDataSource = PostgresUserDataSource(database)
}
@AfterAll
fun delUser() {
userDataSource.deleteUser("JaneDoe")
}
@Test
@Order(1)
fun addUser() {
val user = User(
id = "123",
username = "JohnDoe",
email = "johndoe@example.com",
password = "securePassword123",
nbCoins = 1000,
token = null,
image = null,
bestWin = 500,
nbBets = 50,
nbFriends = 10
)
userDataSource.addUser(user)
val addedUser = database.users.find { it.id eq "123" }
assertNotNull(addedUser)
assertEquals("JohnDoe", addedUser?.username)
}
@Test
@Order(2)
fun getUserByUsername() {
val result = userDataSource.getUserByUsername("JohnDoe")
assertNotNull(result.first)
assertEquals("JohnDoe", result.first?.username)
assertEquals("securePassword123", result.second)
val resultS = userDataSource.getUserByUsername("nonexistent")
assertNull(resultS.first)
assertNull(resultS.second)
}
@Test
@Order(3)
fun getUserById() {
val result = userDataSource.getUserById("123")
assertNotNull(result)
assertEquals("JohnDoe", result?.username)
}
@Test
@Order(4)
fun deleteUser() {
val result = userDataSource.deleteUser("JohnDoe")
assertTrue(result)
val deletedUser = database.users.find { it.id eq "123" }
assertNull(deletedUser)
val resultS = userDataSource.deleteUser("nonexistent")
assertFalse(resultS)
}
@Test
@Order(5)
fun addCoins() {
userDataSource.addUser(
User(
id = "11111",
username = "JaneDoe",
email = "janedoe@example.com",
password = "securePassword456",
nbCoins = 1000,
token = null,
image = null,
bestWin = 500,
nbBets = 50,
nbFriends = 10
)
)
userDataSource.addCoins("JaneDoe", 500)
val updatedUser = database.users.find { it.id eq "11111" }
assertNotNull(updatedUser)
assertEquals(1500, updatedUser?.nbCoins)
}
@Test
@Order(6)
fun removeCoins() {
userDataSource.removeCoins("JaneDoe", 300)
val updatedUser = database.users.find { it.id eq "11111" }
assertNotNull(updatedUser)
assertEquals(1200, updatedUser?.nbCoins)
}
@Test
@Order(7)
fun userExists() {
val result = userDataSource.userExists("JaneDoe")
assertTrue(result)
val resultS = userDataSource.userExists("nonexistent")
assertFalse(resultS)
}
@Test
@Order(8)
fun emailExists() {
val result = userDataSource.emailExists("janedoe@example.com")
assertTrue(result)
val resultS = userDataSource.emailExists("nonexistent@example.com")
assertFalse(resultS)
}
@Test
@Order(9)
fun canHaveDailyGift() {
database.update(UsersEntity) {
set(it.lastGift, Instant.now().minusSeconds(86400 * 2)) // 2 days ago
where { it.username eq "JaneDoe" }
}
val result = userDataSource.canHaveDailyGift("JaneDoe")
assertTrue(result)
val resultS = userDataSource.canHaveDailyGift("JaneDoe")
assertFalse(resultS)
}
@Test
@Order(10)
fun addImage() {
val imageBytes = "sampleImage".toByteArray()
userDataSource.addImage("11111", imageBytes)
val resultSet = database.executeWithResult(
"""
SELECT encode(image, 'base64') AS image
FROM userimage
WHERE user_id = '11111'
""".trimIndent()
)
assertNotNull(resultSet)
if (resultSet != null && resultSet.next()) {
val image = resultSet.getString("image")
assertEquals(Base64.getEncoder().encodeToString(imageBytes), image)
}
}
@Test
@Order(11)
fun getImage() {
val result = userDataSource.getImage("11111")
assertNotNull(result)
}
@Test
@Order(12)
fun removeImage() {
userDataSource.removeImage("11111")
val resultSet = database.executeWithResult(
"""
SELECT encode(image, 'base64') AS image
FROM userimage
WHERE user_id = '11111'
""".trimIndent()
)
assertNotNull(resultSet)
if (resultSet != null && resultSet.next()) {
val image = resultSet.getString("image")
assertNull(image)
}
}
}
Loading…
Cancel
Save