Merge userservice to backend

This commit is contained in:
Otto Ahoniemi
2021-12-30 17:02:44 +02:00
parent 836335b9d5
commit 62ba593d6f
47 changed files with 275 additions and 7633 deletions
+17 -51
View File
@@ -1,4 +1,4 @@
name: "Release oodikone to production"
name: 'Release oodikone to production'
on:
release:
@@ -6,23 +6,23 @@ on:
jobs:
build_and_release_backend:
name: "Build and release backend"
name: 'Build and release backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Login to DockerHub"
- name: 'Login to DockerHub'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Extract release version"
- name: 'Extract release version'
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
# This steps extracts release version from GITHUB_REF env var
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
- name: "Build and push"
- name: 'Build and push'
uses: docker/build-push-action@v2
with:
build-args: |
@@ -38,26 +38,26 @@ jobs:
target: production
build_and_release_frontend:
name: "Build and release frontend"
name: 'Build and release frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Login to DockerHub"
- name: 'Login to DockerHub'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Get current time"
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
- name: "Extract release version"
- name: 'Extract release version'
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
# This steps extracts release version from GITHUB_REF env var
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
- name: "Build and push"
- name: 'Build and push'
uses: docker/build-push-action@v2
with:
build-args: |
@@ -72,11 +72,11 @@ jobs:
push: true
tags: toska/oodikone2-frontend:latest
target: production
- name: "Create a container from built image "
- name: 'Create a container from built image '
run: docker create --name frontend toska/oodikone2-frontend:latest
- name: "Copy assets from the container"
- name: 'Copy assets from the container'
run: docker cp frontend:/usr/src/app/build ./build
- name: "Create Sentry release"
- name: 'Create Sentry release'
uses: getsentry/action-release@v1
env:
SENTRY_URL: ${{ secrets.SENTRY_URL }}
@@ -90,48 +90,15 @@ jobs:
url_prefix: '~/static/js'
version: ${{ steps.extract_version.outputs.VERSION }}
build_and_release_userservice:
name: "Build and release userservice"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Login to DockerHub"
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Extract release version"
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
# This steps extracts release version from GITHUB_REF env var
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
- name: "Build and push"
uses: docker/build-push-action@v2
with:
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
cache-from: |
type=registry,ref=toska/oodikone2-userservice:staging
type=registry,ref=toska/oodikone2-userservice:latest
cache-to: type=inline
context: services/userservice
push: true
tags: toska/oodikone2-userservice:latest
target: production
report_success:
runs-on: ubuntu-latest
name: "Report deployment success to oodikone notifications"
name: 'Report deployment success to oodikone notifications'
needs:
- build_and_release_backend
- build_and_release_frontend
- build_and_release_userservice
if: success()
steps:
- name: "Send notification to slack"
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
@@ -141,14 +108,13 @@ jobs:
report_failure:
runs-on: ubuntu-latest
name: "Report deployment failure to oodikone notifications"
name: 'Report deployment failure to oodikone notifications'
needs:
- build_and_release_backend
- build_and_release_frontend
- build_and_release_userservice
if: failure()
steps:
- name: "Send notification to slack"
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
+3 -75
View File
@@ -45,19 +45,6 @@ jobs:
'eslint --ext .jsx,.js services/frontend' \
'stylelint services/frontend/src/**/*.css'
lint_userservice:
name: 'Lint userservice'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
- name: 'Install dependencies'
run: npm ci && cd services/userservice && npm ci
- name: 'Run frontend linter'
run: npm run eslint services/userservice
# === Build images ===
build_backend:
name: 'Build backend'
@@ -115,32 +102,6 @@ jobs:
with:
path: /tmp/frontend.tar
build_userservice:
name: 'Build userservice'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: 'Build image and export it'
uses: docker/build-push-action@v2
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
cache-from: |
type=registry,ref=toska/oodikone2-userservice:staging
type=registry,ref=toska/oodikone2-userservice:latest
cache-to: type=inline
context: services/userservice
push: false
tags: toska/oodikone2-userservice:staging
target: production
outputs: type=docker,dest=/tmp/userservice.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
with:
path: /tmp/userservice.tar
build_and_push_anon_sis_db:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
runs-on: ubuntu-latest
@@ -201,7 +162,7 @@ jobs:
with:
filters: |
migrations:
- 'services/userservice/src/database/migrations/**'
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
@@ -209,31 +170,19 @@ jobs:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Let userservice run migrations, then push new version of image to toska hub'
- name: 'Let backend run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
docker-compose --file docker-compose.ci.yml up --detach userservice
docker-compose --file docker-compose.ci.yml up --detach backend
sleep 30 # wait for sis-updater-worker to start and run migrations
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
# === Integration tests ===
#
run_userservice_integration_tests:
name: 'Run userservice integration tests'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Run tests included in docker build'
run: docker-compose --file docker-compose.test.yml up --build userservice-test
# === End-to-end tests ===
test_course_statistics:
name: 'Test course statistics'
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -248,7 +197,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -279,7 +227,6 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -294,7 +241,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -325,7 +271,6 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -340,7 +285,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -371,7 +315,6 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -386,7 +329,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -417,7 +359,6 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -432,7 +373,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -463,7 +403,6 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -478,7 +417,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -509,7 +447,6 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -524,7 +461,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -559,7 +495,6 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -574,7 +509,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -602,8 +536,6 @@ jobs:
needs:
- lint_backend
- lint_frontend
- lint_userservice
- run_userservice_integration_tests
- test_course_statistics
- test_custom_population
- test_filters
@@ -622,7 +554,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to DockerHub'
uses: docker/login-action@v1
with:
@@ -649,7 +580,6 @@ jobs:
run: |
docker push toska/oodikone2-backend:staging
docker push toska/oodikone2-frontend:staging
docker push toska/oodikone2-userservice:staging
- name: 'Send deployment success notification to slack'
if: success()
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
@@ -672,8 +602,6 @@ jobs:
needs:
- lint_backend
- lint_frontend
- lint_userservice
- run_userservice_integration_tests
- test_course_statistics
- test_custom_population
- test_filters
+1 -1
View File
@@ -79,7 +79,7 @@ It is recommended to spend some time to become familiar with `docker` and `docke
- `docker-compose ps`: view the containers in the running environment
- `docker-compose logs frontend`: print logs for just frontend
- `docker-compose logs --follow --tail 100 backend`: print last hundred rows of backend logs and begin to follow them in your terminal window
- `docker exec -it userservice sh`: open bash terminal inside userservice container
- `docker exec -it backend sh`: open bash terminal inside backend container
- `docker exec -it sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database.
### User types for development
+1 -1
View File
@@ -44,7 +44,7 @@ curl --request GET --url 'http://localhost:8082/v1/students?token=dev'
- Ensure the newest user-db/kone-db image is pulled from the registry with `docker pull`
- Start oodikone, let it run migrations
- Modify db to wanted state. Tips:
- Easy way to create new users is to modify dev user details in the frontend API config. Userservice creates a new user during login if the given uid is not present in user-db.
- Easy way to create new users is to modify dev user details in the frontend API config. A new user is created during login if the given uid is not present in user-db.
By default, previous user-db/kone-db images are used as a base. If you want to build the image from a clean Postgres image, just comment out toska hub image and uncomment the clean Postgres image.
+2 -16
View File
@@ -9,17 +9,16 @@ services:
- kone-db
- redis
- sis-db
- user-db
environment:
CI: 'true'
DB_URL_USER: postgres://postgres@user-db:5432/user-db
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
FRONT_URL: http://localhost:1337
REDIS: redis
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
TOKEN_SECRET: IWannaBeTheVeryBest
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
USERSERVICE_URL: http://userservice:4567
image: toska/oodikone2-backend:staging
frontend:
@@ -63,25 +62,12 @@ services:
POSTGRES_HOST_AUTH_METHOD: trust
image: registry.toska.cs.helsinki.fi/user-db
userservice:
<<: *defaults
container_name: userservice
depends_on:
- user-db
environment:
CI: 'true'
DB_URL: postgres://postgres@user-db:5432/user-db
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
image: toska/oodikone2-userservice:staging
nginx:
image: nginx:latest
container_name: nginx
depends_on:
- frontend
- backend
- userservice
ports:
- 1337:80
volumes:
+1 -5
View File
@@ -1,7 +1,7 @@
services:
backend:
environment:
DB_URL_USER: postgres://postgres@user-db:5432/user-db-real
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db-real
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db-real
@@ -18,9 +18,5 @@ services:
DB_URL: postgres://postgres@sis-db:5432/sis-db-real
SIS_IMPORTER_DATABASE: sis-importer-db-real
userservice:
environment:
DB_URL: postgres://postgres@user-db:5432/user-db-real
volumes:
redis-data-real:
-10
View File
@@ -1,10 +0,0 @@
services:
userservice-test:
build:
context: services/userservice
target: test
container_name: userservice-test
environment:
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
+2 -24
View File
@@ -26,18 +26,17 @@ services:
- kone-db
- redis
- sis-db
- user-db
env_file:
- .env
environment:
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
DB_URL_USER: postgres://postgres@user-db:5432/user-db
FRONT_URL: http://localhost:3000
REDIS: redis
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
TOKEN_SECRET: IWannaBeTheVeryBest
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
USERSERVICE_URL: http://userservice:4567
ports:
- 8080:8080
profiles:
@@ -165,26 +164,6 @@ services:
volumes:
- user-db-data:/data
userservice:
<<: *defaults
build:
<<: *build_defaults
context: services/userservice
container_name: userservice
depends_on:
- user-db
environment:
DB_URL: postgres://postgres@user-db:5432/user-db
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
ports:
- 4567:4567
profiles:
- oodikone
volumes:
- userservice-nodemod:/usr/src/app/node_modules
- ./services/userservice/:/usr/src/app
volumes:
backend-nodemod:
frontend-nodemod:
@@ -195,4 +174,3 @@ volumes:
sis-updater-scheduler-nodemod:
sis-updater-worker-nodemod:
user-db-data:
userservice-nodemod:
-1
View File
@@ -165,7 +165,6 @@ set_up_oodikone_from_scratch() {
folders_to_set_up=(
"$PROJECT_ROOT"
"$PROJECT_ROOT/services/frontend"
"$PROJECT_ROOT/services/userservice"
"$PROJECT_ROOT/services/backend"
"$PROJECT_ROOT/updater/sis-updater-scheduler"
"$PROJECT_ROOT/updater/sis-updater-worker"
+2 -4
View File
@@ -36,6 +36,7 @@ const MATOMO_URL = process.env.MATOMO_URL
// Other stuff
const DB_URL_KONE = process.env.DB_URL_KONE
const DB_URL_USER = process.env.DB_URL_USER
let DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS, 10)
if (isNaN(DB_MAX_CONNECTIONS)) {
@@ -47,10 +48,8 @@ if (DB_MAX_CRON_CONNECTIONS < 1) {
}
const redis = process.env.REDIS
const TOKEN_SECRET = process.env.TOKEN_SECRET
const SECRET_TOKEN = process.env.SECRET_TOKEN
const DB_SCHEMA_KONE = process.env.DB_SCHEMA_KONE || 'public'
const USERSERVICE_URL = process.env.USERSERVICE_URL
const SIS_UPDATER_URL = process.env.SIS_UPDATER_URL
const formatURL = url => {
@@ -60,14 +59,13 @@ const formatURL = url => {
const addSlashToEnd = url => (url.endsWith('/') ? url : url + '/')
module.exports = {
DB_URL_USER,
DB_URL_KONE,
DB_MAX_CONNECTIONS,
DB_MAX_CRON_CONNECTIONS,
redis,
TOKEN_SECRET,
SECRET_TOKEN,
DB_SCHEMA_KONE,
USERSERVICE_URL: formatURL(USERSERVICE_URL),
requiredGroup,
SIS_UPDATER_URL,
ANALYTICS_INFLUXDB_URL: formatURL(ANALYTICS_INFLUXDB_URL),
+39 -1
View File
@@ -46,6 +46,10 @@ const sequelizeKone = new Sequelize(conf.DB_URL_KONE, {
})
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
const sequelizeUser = new Sequelize(conf.DB_URL_USER, {
logging: false,
})
const initializeDatabaseConnection = async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const waitSeconds = 60
@@ -55,7 +59,7 @@ const initializeDatabaseConnection = async () => {
break
} catch (e) {
if (i === waitSeconds) {
logger.error(`Could not connect to database in ${waitSeconds} seconds`)
logger.error(`Could not connect to kone database in ${waitSeconds} seconds`)
throw e
}
await sleep(1000)
@@ -82,11 +86,45 @@ const initializeDatabaseConnection = async () => {
logger.error({ message: 'Kone Migration error: ', meta: e })
throw e
}
for (let i = 1; i <= waitSeconds; i++) {
try {
await sequelizeUser.authenticate()
break
} catch (e) {
if (i === waitSeconds) {
logger.error(`Could not connect to user database in ${waitSeconds} seconds`)
throw e
}
await sleep(1000)
}
}
try {
const migrator = new Umzug({
storage: 'sequelize',
storageOptions: {
sequelize: sequelizeUser,
tableName: 'migrations',
},
migrations: {
params: [sequelizeUser.getQueryInterface(), Sequelize],
path: `${process.cwd()}/src/database/migrations_user`,
pattern: /\.js$/,
},
})
const migrations = await migrator.up()
logger.info({ message: 'Kone Migrations up to date', meta: migrations })
} catch (e) {
logger.error({ message: 'Kone Migration error: ', meta: e })
throw e
}
}
const dbConnections = new DbConnection()
module.exports = {
dbConnections,
sequelizeKone,
sequelizeUser,
initializeDatabaseConnection,
}
+125
View File
@@ -0,0 +1,125 @@
const Sequelize = require('sequelize')
const { sequelizeUser } = require('../database/connection')
const UserElementDetails = sequelizeUser.define(
'user_elementdetails',
{
userId: {
primaryKey: true,
type: Sequelize.BIGINT,
},
elementDetailCode: {
primaryKey: true,
type: Sequelize.STRING,
},
},
{
tablename: 'user_elementdetails',
}
)
const AccessGroup = sequelizeUser.define('access_group', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
group_code: {
type: Sequelize.STRING,
unique: true,
},
group_info: {
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
const UserFaculties = sequelizeUser.define('user_faculties', {
userId: {
primaryKey: true,
type: Sequelize.BIGINT,
references: {
model: 'users',
key: 'id',
},
},
faculty_code: {
primaryKey: true,
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
const User = sequelizeUser.define(
'users',
{
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
full_name: { type: Sequelize.STRING },
username: {
type: Sequelize.STRING,
unique: true,
},
email: { type: Sequelize.STRING },
language: { type: Sequelize.STRING },
sisu_person_id: { type: Sequelize.STRING },
last_login: { type: Sequelize.DATE },
},
{
tableName: 'users',
timestamps: false,
}
)
const HyGroup = sequelizeUser.define('hy_group', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
code: { type: Sequelize.STRING },
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
User.hasMany(UserElementDetails, { as: 'programme' })
UserElementDetails.belongsTo(User)
User.belongsToMany(AccessGroup, {
through: 'user_accessgroup',
as: 'accessgroup',
})
AccessGroup.belongsToMany(User, { through: 'user_accessgroup' })
User.belongsToMany(HyGroup, { through: 'user_hy_group', as: 'hy_group' })
HyGroup.belongsToMany(User, { through: 'user_hy_group' })
User.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
module.exports = {
User,
UserElementDetails,
AccessGroup,
HyGroup,
UserFaculties,
sequelizeUser,
}
+50 -30
View File
@@ -1,6 +1,4 @@
const axios = require('axios')
const _ = require('lodash')
const { USERSERVICE_URL } = require('../conf-backend')
const UnitService = require('./units')
const elementDetailService = require('./elementdetails')
const { filterStudentnumbersByAccessrights } = require('./students')
@@ -8,8 +6,8 @@ const { userDataCache } = require('./cache')
const { getImporterClient } = require('../util/importerClient')
const logger = require('../util/logger')
const { facultiesAndProgrammesForTrends } = require('../services/organisations')
const client = axios.create({ baseURL: USERSERVICE_URL, headers: { secret: process.env.USERSERVICE_SECRET } })
const User = require('./users/users')
const AccessGroup = require('./users/accessgroups')
// Private helpers
const enrichProgrammesFromFaculties = faculties =>
@@ -50,9 +48,18 @@ const checkStudyGuidanceGroupsAccess = async hyPersonSisuId => {
const byUsernameData = async username => {
// OK, enriched
const url = `/user/${username}/user_data`
const response = await client.get(url)
return enrichWithProgrammes(response.data)
const user = await User.byUsernameMinified(username)
const roles = user.accessgroup.map(({ group_code }) => group_code)
const rights = User.getUserProgrammes(user)
const faculties = user.faculty.map(({ faculty_code }) => faculty_code)
const data = {
email: user.email,
full_name: user.full_name,
roles,
rights,
faculties,
}
return enrichWithProgrammes(data)
}
// Exported helpers
@@ -95,48 +102,61 @@ const getStudentsUserCanAccess = async (studentnumbers, roles, userId) => {
}
// FOR ROUTES 1-to-1 mapping
const modifyAccess = async body => enrichWithProgrammes((await client.post('/modifyaccess', body)).data)
const modifyAccess = async body => {
const { username, accessgroups } = body
await User.modifyRights(username, accessgroups)
const user = await User.byUsername(username)
return enrichWithProgrammes(User.getUserData(user))
}
const getAccessGroups = async () => enrichWithProgrammes((await client.get('/access_groups')).data)
const getAccessGroups = async () => enrichWithProgrammes(await AccessGroup.findAll())
const enableElementDetails = async (uid, codes) =>
enrichWithProgrammes((await client.post('/add_rights', { uid, codes })).data.user)
const enableElementDetails = async (uid, codes) => {
await User.addProgrammes(uid, codes)
const user = await User.byId(uid)
return enrichWithProgrammes(User.getUserData(user))
}
const findAll = async () => enrichWithProgrammes((await client.get('/findall')).data)
const findAll = async () => enrichWithProgrammes((await User.findAll()).map(User.getUserData))
const setFaculties = async (uid, faculties) =>
enrichWithProgrammes((await client.post('/set_faculties', { uid, faculties })).data.user)
const setFaculties = async (uid, faculties) => {
await User.setFaculties(uid, faculties)
const user = await User.byId(uid)
return enrichWithProgrammes(User.getUserData(user))
}
const removeElementDetails = async (uid, codes) =>
enrichWithProgrammes((await client.post('/remove_rights', { uid, codes })).data.user)
const removeElementDetails = async (uid, codes) => {
await User.removeProgrammes(uid, codes)
const user = await User.byId(uid)
return enrichWithProgrammes(User.getUserData(user))
}
const updateUser = async (username, fields) => {
userDataCache.del(username)
return enrichWithProgrammes((await client.put(`/user/${username}`, fields)).data)
const uid = username
const user = await User.byUsername(uid)
if (!user) {
throw new Error(`User ${uid} not found`)
}
await User.updateUser(user, fields)
const returnedUser = await User.byUsername(username)
return enrichWithProgrammes(User.getUserData(returnedUser))
}
const getLoginDataWithoutToken = async (uid, full_name, hyGroups, affiliations, email, hyPersonSisuId) => {
const hasStudyGuidanceGroupAccess = await checkStudyGuidanceGroupsAccess(hyPersonSisuId)
return (
await client.post('/loginwithouttoken', {
const result = await User.loginWithoutToken(
uid,
full_name,
hyGroups,
affiliations,
email,
hyPersonSisuId,
hasStudyGuidanceGroupAccess,
})
).data
hasStudyGuidanceGroupAccess
)
return result
}
const superlogin = async (uid, asUser) =>
(
await client.post('/superlogin', {
uid,
asUser,
})
).data
const superlogin = async (uid, asUser) => await User.superlogin(uid, asUser)
module.exports = {
updateUser,
@@ -1,4 +1,4 @@
const { AccessGroup } = require('../models/index')
const { AccessGroup } = require('../../models/models_user')
const { Op } = require('sequelize')
const byId = id => AccessGroup.findByPk(id)
@@ -1,4 +1,4 @@
const { HyGroup } = require('../models/index')
const { HyGroup } = require('../../models/models_user')
const { Op } = require('sequelize')
const byId = id => HyGroup.findByPk(id)
@@ -1,13 +1,28 @@
const Sequelize = require('sequelize')
const _ = require('lodash')
const moment = require('moment')
const { User, UserElementDetails, AccessGroup, HyGroup, UserFaculties, sequelize } = require('../models')
const {
User,
UserElementDetails,
AccessGroup,
HyGroup,
UserFaculties,
sequelizeUser,
} = require('../../models/models_user')
const AccessService = require('./accessgroups')
const HyGroupService = require('./hygroups')
const { requiredGroup, courseStatisticsGroup, hyOneGroup } = require('../conf')
const Op = Sequelize.Op
const TOKEN_VERSION = 1.1 // When token structure changes, increment in userservice, backend and frontend
// move these to conf
const courseStatisticsGroup = 'grp-oodikone-basic-users'
const hyOneGroup = 'hy-one'
const requiredGroup =
process.env.NODE_ENV === 'staging'
? ['grp-oodikone-staging-users', 'grp-oodikone-basic-staging-users']
: ['grp-oodikone-users', 'grp-oodikone-basic-users']
// Refactor this token version away
const TOKEN_VERSION = 1.1 // When token structure changes, increment in backend and frontend
// help for refactoring away from token
const generateTokenData = async (uid, mockedBy = null) => {
@@ -245,7 +260,7 @@ const removeProgrammes = async (uid, codes) => {
}
const setFaculties = async (uid, faculties) => {
await sequelize.transaction(async transaction => {
await sequelizeUser.transaction(async transaction => {
await UserFaculties.destroy({ where: { userId: uid }, transaction })
for (const faculty of faculties) {
await UserFaculties.create({ userId: uid, faculty_code: faculty }, { transaction })
-3
View File
@@ -1,3 +0,0 @@
.dockerignore
Dockerfile
node_modules/
-5
View File
@@ -1,5 +0,0 @@
{
"rules": {
"no-console": "off"
}
}
-34
View File
@@ -1,34 +0,0 @@
FROM node:14.17-alpine3.13 as base
WORKDIR /usr/src/app
FROM base as development
ENV NODE_ENV=development
EXPOSE 4567
COPY ./package* /usr/src/app/
RUN npm ci
COPY . /usr/src/app
CMD ["npm", "run", "dev"]
FROM base as test
ENV NODE_ENV=test
RUN apk add --no-cache sqlite=3.34.1-r0 && rm -rf /var/cache/apk/*
ENV DB_URL=sqlite::memory:
COPY ./package* /usr/src/app/
RUN npm ci
COPY . /usr/src/app/
CMD ["npm", "run", "test"]
FROM base as production
ENV NODE_ENV=production
ENV NODE_ENV=production
ARG SENTRY_ENVIRONMENT
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE
EXPOSE 4567
COPY --chown=node:node ./package* /usr/src/app/
RUN npm ci --production
COPY --chown=node:node . /usr/src/app/
USER node
CMD [ "npm", "start" ]
-147
View File
@@ -1,147 +0,0 @@
require('express-async-errors')
const express = require('express')
const Sentry = require('@sentry/node')
const User = require('./src/services/users')
const AccessGroup = require('./src/services/accessgroups')
const { initializeDatabaseConnection } = require('./src/database/connection')
const initializeSentry = require('./src/util/sentry')
const errorMiddleware = require('./src/middlewares/errorMiddleware')
initializeDatabaseConnection()
.then(() => {
const app = express()
initializeSentry(app)
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
const port = 4567
const checkSecret = require('./src/middlewares/secret')
app.use(express.json())
app.use(checkSecret)
app.get('/ping', (req, res) => res.json({ message: 'pong ' }))
app.get('/findall', async (req, res) => {
const users = await User.findAll()
res.json(users.map(User.getUserData))
})
app.get('/user/:uid/user_data', async (req, res) => {
const uid = req.params.uid
const user = await User.byUsernameMinified(uid)
const roles = user.accessgroup.map(({ group_code }) => group_code)
const rights = User.getUserProgrammes(user)
const faculties = user.faculty.map(({ faculty_code }) => faculty_code)
res.json({
email: user.email,
full_name: user.full_name,
roles,
rights,
faculties,
})
})
app.post('/loginwithouttoken', async (req, res) => {
const { uid, full_name, hyGroups, email, hyPersonSisuId, hasStudyGuidanceGroupAccess } = req.body
console.log(uid, full_name, 'getting the login data without token!')
const { payload, isNew } = await User.loginWithoutToken(
uid,
full_name,
hyGroups,
email,
hyPersonSisuId,
hasStudyGuidanceGroupAccess
)
res.status(200).json({ payload, isNew })
})
app.post('/superlogin', async (req, res) => {
const { uid, asUser } = req.body
console.log(`${uid} superlogging as ${asUser}`)
const token = await User.superlogin(uid, asUser)
if (token) {
res.status(200).json(token)
}
res.status(400).end()
})
app.put('/user/:uid', async (req, res) => {
const uid = req.params.uid
const user = await User.byUsername(uid)
if (!user) {
return res.status(400).json({ error: 'invalid username given' })
}
await User.updateUser(user, req.body)
const returnedUser = await User.byUsername(uid)
res.json(User.getUserData(returnedUser))
})
app.post('/modifyaccess', async (req, res) => {
const { username, accessgroups } = req.body
try {
await User.modifyRights(username, accessgroups)
const user = await User.byUsername(username)
res.status(200).json(User.getUserData(user))
} catch (e) {
res.status(400).json({ e })
}
})
app.post('/add_rights', async (req, res) => {
const { uid, codes } = req.body
console.log('adding rights to ', uid)
try {
await User.addProgrammes(uid, codes)
const user = await User.byId(uid)
res.status(200).json({ user: User.getUserData(user) })
} catch (e) {
console.log(e)
res.status(400).json({ e })
}
})
app.post('/remove_rights', async (req, res) => {
const { uid, codes } = req.body
console.log('removing rights from ', uid)
try {
await User.removeProgrammes(uid, codes)
const user = await User.byId(uid)
res.status(200).json({ user: User.getUserData(user) })
} catch (e) {
console.log(e)
res.status(400).json({ e })
}
})
app.post('/set_faculties', async (req, res) => {
const { uid, faculties } = req.body
try {
await User.setFaculties(uid, faculties)
const user = await User.byId(uid)
res.status(200).json({ user: User.getUserData(user) })
} catch (e) {
console.log(e)
res.status(400).json({ e })
}
})
app.get('/access_groups', async (req, res) => {
try {
const groups = await AccessGroup.findAll()
res.status(200).json(groups)
} catch (e) {
res.status(400).end()
}
})
app.use(errorMiddleware)
const server = app.listen(port, () => console.log(`Userservice listening on port ${port}!`))
process.on('SIGTERM', () => {
server.close(() => {
console.log('Process terminated')
})
})
})
.catch(e => {
process.exitCode = 1
console.log(e)
})
File diff suppressed because it is too large Load Diff
-27
View File
@@ -1,27 +0,0 @@
{
"name": "userservice",
"private": true,
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"lint": "eslint .",
"lintfix": "prettier --write '**/*.{js,json}' && eslint --fix .",
"test": "jest --verbose --runInBand --silent=false"
},
"dependencies": {
"@sentry/node": "^6.11.0",
"@sentry/tracing": "^6.11.0",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"jsonwebtoken": "^8.5.1",
"moment": "^2.24.0",
"pg": "^8.7.1",
"sequelize": "^6.6.2",
"umzug": "^2.2.0"
},
"devDependencies": {
"jest": "^24.8.0",
"nodemon": "^2.0.7",
"sqlite3": "^5.0.2"
}
}
-27
View File
@@ -1,27 +0,0 @@
const { NODE_ENV, DB_URL, SECRET, SENTRY_RELEASE, SENTRY_ENVIRONMENT, CI } = process.env
// Sentry
const sentryRelease = SENTRY_RELEASE || ''
const sentryEnvironment = SENTRY_ENVIRONMENT || ''
const runningInCI = CI === 'true'
const isStaging = NODE_ENV === 'staging'
const courseStatisticsGroup = 'grp-oodikone-basic-users'
const hyOneGroup = 'hy-one'
const requiredGroup = isStaging
? ['grp-oodikone-staging-users', 'grp-oodikone-basic-staging-users']
: ['grp-oodikone-users', 'grp-oodikone-basic-users']
module.exports = {
DB_URL,
SECRET,
requiredGroup,
courseStatisticsGroup,
sentryRelease,
sentryEnvironment,
runningInCI,
hyOneGroup,
}
@@ -1,51 +0,0 @@
const Sequelize = require('sequelize')
const Umzug = require('umzug')
const { DB_URL } = require('../conf')
const sequelize = new Sequelize(DB_URL, {
logging: false,
})
const initializeDatabaseConnection = async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const waitSeconds = 60
for (let i = 1; i <= waitSeconds; i++) {
try {
await sequelize.authenticate()
break
} catch (e) {
if (i === waitSeconds) {
console.log(`Could not connect to database in ${waitSeconds} seconds`)
throw e
}
console.log('.')
await sleep(1000)
}
}
try {
const migrator = new Umzug({
storage: 'sequelize',
storageOptions: {
sequelize: sequelize,
tableName: 'migrations',
},
logging: console.log,
migrations: {
params: [sequelize.getQueryInterface(), Sequelize],
path: `${process.cwd()}/src/database/migrations`,
pattern: /\.js$/,
},
})
const migrations = await migrator.up()
console.log('Migrations up to date', migrations)
} catch (e) {
console.log('Migration error')
throw e
}
}
const forceSyncDatabase = async () => {
await sequelize.sync({ force: true })
}
module.exports = { sequelize, initializeDatabaseConnection, forceSyncDatabase }
@@ -1,23 +0,0 @@
const Sentry = require('@sentry/node')
const { ApplicationError } = require('../util/customErrors')
const errorHandler = (error, req, res, next) => {
console.error(`${error.message} ${error.name} ${error.stack}`)
console.error(`Body of failing request: ${JSON.stringify(req.body)}`)
// enrich with user details from req body
Sentry.captureException({
...error,
reqBody: req.body,
})
if (res.headersSent) {
return next(error)
}
const normalizedError = error instanceof ApplicationError ? error : new ApplicationError(error.message)
return res.status(normalizedError.status).json(normalizedError)
}
module.exports = errorHandler
@@ -1,5 +0,0 @@
const { SECRET } = require('../conf')
const checkSecret = (req, res, next) => (req.headers.secret == SECRET ? next() : res.status(403).end())
module.exports = checkSecret
-125
View File
@@ -1,125 +0,0 @@
const Sequelize = require('sequelize')
const { sequelize } = require('../database/connection')
const UserElementDetails = sequelize.define(
'user_elementdetails',
{
userId: {
primaryKey: true,
type: Sequelize.BIGINT,
},
elementDetailCode: {
primaryKey: true,
type: Sequelize.STRING,
},
},
{
tablename: 'user_elementdetails',
}
)
const AccessGroup = sequelize.define('access_group', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
group_code: {
type: Sequelize.STRING,
unique: true,
},
group_info: {
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
const UserFaculties = sequelize.define('user_faculties', {
userId: {
primaryKey: true,
type: Sequelize.BIGINT,
references: {
model: 'users',
key: 'id',
},
},
faculty_code: {
primaryKey: true,
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
const User = sequelize.define(
'users',
{
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
full_name: { type: Sequelize.STRING },
username: {
type: Sequelize.STRING,
unique: true,
},
email: { type: Sequelize.STRING },
language: { type: Sequelize.STRING },
sisu_person_id: { type: Sequelize.STRING },
last_login: { type: Sequelize.DATE },
},
{
tableName: 'users',
timestamps: false,
}
)
const HyGroup = sequelize.define('hy_group', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
code: { type: Sequelize.STRING },
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
User.hasMany(UserElementDetails, { as: 'programme' })
UserElementDetails.belongsTo(User)
User.belongsToMany(AccessGroup, {
through: 'user_accessgroup',
as: 'accessgroup',
})
AccessGroup.belongsToMany(User, { through: 'user_accessgroup' })
User.belongsToMany(HyGroup, { through: 'user_hy_group', as: 'hy_group' })
HyGroup.belongsToMany(User, { through: 'user_hy_group' })
User.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
module.exports = {
User,
UserElementDetails,
AccessGroup,
HyGroup,
UserFaculties,
sequelize,
}
@@ -1,25 +0,0 @@
class ApplicationError extends Error {
constructor(message, status, extra) {
super()
Error.captureStackTrace(this, this.constructor)
this.name = this.constructor.name
this.message = message || 'Something went wrong. Please try again.'
this.status = status || 500
this.extra = extra || {}
}
toJSON() {
return {
error: this.message,
}
}
}
module.exports = {
ApplicationError,
}
-18
View File
@@ -1,18 +0,0 @@
const Sentry = require('@sentry/node')
const Tracing = require('@sentry/tracing')
const { sentryRelease, sentryEnvironment, runningInCI } = require('../conf')
const initializeSentry = app => {
if (!sentryRelease || !sentryEnvironment || runningInCI) return
Sentry.init({
dsn: 'https://020b79f0cbb14aad94cc9d69a1ea9d52@sentry.cs.helsinki.fi/2',
environment: sentryEnvironment,
release: sentryRelease,
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
})
}
module.exports = initializeSentry