diff --git a/db/migrations/20240822145150_update_queens.js b/db/migrations/20240822145150_update_queens.js new file mode 100644 index 0000000..fdf18c7 --- /dev/null +++ b/db/migrations/20240822145150_update_queens.js @@ -0,0 +1,48 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const up = function (knex) { + return knex + .transaction() + .then(async (trx) => { + const queens = await knex('queens') + .whereNotNull('hive_id') + .where('deleted', false) + .orderBy([ + { column: 'hive_id' }, + { column: 'move_date', order: 'desc' }, + ]); + let lastMoveDate = undefined; + let hiveId = undefined; + for (const queen of queens) { + if (hiveId !== queen.hive_id) { + hiveId = queen.hive_id; + lastMoveDate = undefined; + } + if (!lastMoveDate && queen.modus) { + lastMoveDate = queen.move_date; + } + if (lastMoveDate && queen.modus && queen.move_date < lastMoveDate) { + await trx('queens') + .update({ + modus: false, + modus_date: lastMoveDate, + }) + .where('id', queen.id); + } + if (lastMoveDate && queen.move_date < lastMoveDate) { + lastMoveDate = queen.move_date; + } + } + }) + .catch(function (error) { + console.error(error); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const down = function (knex) {}; diff --git a/db/migrations/20240823150104_alter_queens.js b/db/migrations/20240823150104_alter_queens.js new file mode 100644 index 0000000..cf8deac --- /dev/null +++ b/db/migrations/20240823150104_alter_queens.js @@ -0,0 +1,19 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const up = function (knex) { + return knex.schema.alterTable('queens', (t) => { + t.datetime('move_date').alter(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const down = function (knex) { + return knex.schema.alterTable('queens', (t) => { + t.date('move_date').alter(); + }); +}; diff --git a/db/migrations/20240914104345_alter_movedates.js b/db/migrations/20240914104345_alter_movedates.js new file mode 100644 index 0000000..e7899ee --- /dev/null +++ b/db/migrations/20240914104345_alter_movedates.js @@ -0,0 +1,19 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const up = function (knex) { + return knex.schema.alterTable('movedates', (t) => { + t.index(['hive_id', 'date'], 'movedates_hive_id_date_idx'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const down = function (knex) { + return knex.schema.alterTable('queens', (t) => { + t.dropIndex('movedates_hive_id_date_idx'); + }); +}; diff --git a/db/migrations/20240914104345_view_movedates_previous_apiary.js b/db/migrations/20240914104345_view_movedates_previous_apiary.js new file mode 100644 index 0000000..a53e339 --- /dev/null +++ b/db/migrations/20240914104345_view_movedates_previous_apiary.js @@ -0,0 +1,20 @@ +import { fileURLToPath } from 'url'; +import { readFileSync } from 'fs'; +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const up = function (knex) { + let filename = fileURLToPath(import.meta.url); + filename = filename.replace('.js', '.sql'); + const sql = readFileSync(filename, 'utf8'); + return knex.raw(sql); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export const down = function (knex) { + //return knex.raw('drop view movedates_previous_apiary'); +}; diff --git a/db/migrations/20240914104345_view_movedates_previous_apiary.sql b/db/migrations/20240914104345_view_movedates_previous_apiary.sql new file mode 100644 index 0000000..9a49ba0 --- /dev/null +++ b/db/migrations/20240914104345_view_movedates_previous_apiary.sql @@ -0,0 +1,20 @@ +CREATE VIEW movedates_previous_apiary AS +SELECT + current_move.id AS current_move_id, + current_move.date AS current_move_date, + current_move.hive_id, + previous_apiary.id AS previous_apiary_id, + previous_apiary.name AS previous_apiary_name +FROM ( + SELECT + id, + date, + hive_id, + LAG(apiary_id) OVER (PARTITION BY hive_id ORDER BY date) AS previous_apiary_id + FROM + movedates +) AS current_move +LEFT JOIN + apiaries AS previous_apiary +ON + current_move.previous_apiary_id = previous_apiary.id; \ No newline at end of file diff --git a/docker-compose.api-us.yml b/docker-compose.api-us.yml new file mode 100644 index 0000000..b132d0e --- /dev/null +++ b/docker-compose.api-us.yml @@ -0,0 +1,33 @@ +version: '3.8' +services: + btree-server: + container_name: btree-server + image: hannesoberreiter/btree_server:latest + restart: always + user: 'node' # connected volume logs must have write access as node user chown -R 1000:1000 logs + #entrypoint: [ "/bin/sh", "-c", "tail -f /dev/null" ] + environment: + ENVIRONMENT: production + IS_CHILD: false + volumes: + - ./env:/home/node/app/env + - ./logs:/home/node/app/logs + ports: + - '1339:8101' + logging: + driver: 'local' + networks: + - default + - web + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.app.rule=Host(`us.api.btree.at`)' + - 'traefik.http.routers.app.middlewares=wwwredirect' + +networks: + default: + external: true + name: database_btree-db-network + web: + external: true + name: container_web diff --git a/src/api/controllers/auth.controller.ts b/src/api/controllers/auth.controller.ts index d6ae1f5..95fddd6 100644 --- a/src/api/controllers/auth.controller.ts +++ b/src/api/controllers/auth.controller.ts @@ -15,6 +15,7 @@ import { discourseSecret, env, frontend, + serverLocation, } from '../../config/environment.config.js'; import { autoFill } from '../utils/autofill.util.js'; import { Company } from '../models/company.model.js'; @@ -247,13 +248,17 @@ export default class AuthController { result.name + '&email=' + result.email + - '&oauth=google', + '&oauth=google' + + '&server=' + + serverLocation, ), ); } } catch (e) { req.log.error({ message: 'Error in google callback', error: e }); - return reply.redirect(frontend + '/visitor/login?error=oauth'); + return reply.redirect( + frontend + '/visitor/login?error=oauth&server=' + serverLocation, + ); } const userAgent = buildUserAgent(req); @@ -282,7 +287,7 @@ export default class AuthController { req.log.error(e); throw httpErrors[500]('Failed to create session'); } - reply.redirect(frontend + '/visitor/login'); + reply.redirect(frontend + '/visitor/login&server=' + serverLocation); return reply; } } diff --git a/src/api/controllers/company.controller.ts b/src/api/controllers/company.controller.ts index 84f3251..7852885 100644 --- a/src/api/controllers/company.controller.ts +++ b/src/api/controllers/company.controller.ts @@ -328,7 +328,6 @@ async function specialTypes() { ]; properties.map((property) => { - console.log(property); for (const key of Object.keys(property)) { const item = property[key]; if (item.type === 'boolean') { diff --git a/src/api/controllers/external.controller.ts b/src/api/controllers/external.controller.ts index e08d536..3c6480e 100644 --- a/src/api/controllers/external.controller.ts +++ b/src/api/controllers/external.controller.ts @@ -16,6 +16,11 @@ import { import { createInvoice } from '../utils/foxyoffice.util.js'; import { SOURCE } from '../../config/constants.config.js'; import { MailService } from '../../services/mail.service.js'; +import { + isServerLocationValid, + serverLocation, +} from '../../config/environment.config.js'; +import { Logger } from '../../services/logger.service.js'; export default class ExternalController { static async ical(req: FastifyRequest, reply: FastifyReply) { @@ -92,11 +97,15 @@ export default class ExternalController { if (event.type === 'checkout.session.completed') { let user_id: number; let years = 1; + let server: string = 'eu'; try { const reference = JSON.parse(object.client_reference_id); user_id = reference.user_id; years = reference.quantity ?? 1; + server = isServerLocationValid(reference.server) + ? reference.server + : 'eu'; } catch (e) { const mailer = MailService.getInstance(); mailer.sendRawMail( @@ -108,6 +117,18 @@ export default class ExternalController { throw httpErrors.InternalServerError(); } + if (serverLocation !== server) { + Logger.getInstance().log( + 'info', + 'Stripe Webhook - ignored wrong server', + { + server: server, + current: serverLocation, + }, + ); + return {}; + } + let amount = 0; try { amount = parseFloat(object.amount_total as any) / 100; diff --git a/src/api/controllers/movedate.controller.ts b/src/api/controllers/movedate.controller.ts index c40be11..c168d90 100644 --- a/src/api/controllers/movedate.controller.ts +++ b/src/api/controllers/movedate.controller.ts @@ -10,7 +10,7 @@ export default class MovedateController { const { order, direction, offset, limit, q, filters } = req.query as any; const query = Movedate.query() .withGraphJoined( - '[hive, apiary, creator(identifier), editor(identifier)]', + '[hive, apiary, creator(identifier), editor(identifier), movedate_previous_apiary]', ) .where({ 'apiary.user_id': req.session.user.user_id, diff --git a/src/api/controllers/queen.controller.ts b/src/api/controllers/queen.controller.ts index 3b1253a..780f63e 100644 --- a/src/api/controllers/queen.controller.ts +++ b/src/api/controllers/queen.controller.ts @@ -5,6 +5,7 @@ import { QueenDuration } from '../models/queen_duration.model.js'; import { Checkup } from '../models/checkup.model.js'; import { Harvest } from '../models/harvest.model.js'; import { FastifyReply, FastifyRequest } from 'fastify'; +import Objection from 'objection'; export default class QueenController { static async get(req: FastifyRequest, reply: FastifyReply) { @@ -245,6 +246,9 @@ export default class QueenController { user_id: req.session.user.user_id, bee_id: req.session.user.bee_id, }); + if (hive_id && body.move_date) { + await inactivateOtherQueens(trx, hive_id, body.move_date); + } result.push(res.id); } return result; @@ -256,14 +260,19 @@ export default class QueenController { const body = req.body as any; const ids = body.ids; const insert = { ...body.data }; + console.log(insert); if (insert.hive_id) { insert.hive_id = insert.hive_id !== 'empty' ? insert.hive_id : null; } const result = await Queen.transaction(async (trx) => { - return await Queen.query(trx) + const res = await Queen.query(trx) .patch({ ...insert, edit_id: req.session.user.bee_id }) .findByIds(ids) .where('user_id', req.session.user.user_id); + if (insert.hive_id) { + await inactivateOtherQueens(trx, insert.hive_id, insert.move_date); + } + return res; }); return result; } @@ -331,3 +340,33 @@ export default class QueenController { return result; } } + +/** + * @description If a new queen is added to a hive all other queens in the hive should be inactivated (RIP) + */ +async function inactivateOtherQueens( + trx: Objection.Transaction, + hive_id: number, + move_date: string, +) { + let lastMoveDate = new Date(move_date); + const queens = await Queen.query(trx) + .where('hive_id', hive_id) + .orderBy('move_date', 'desc'); + + for (const queen of queens) { + if (!queen.move_date) continue; + const curMoveDate = new Date(queen.move_date); + if (queen.modus && curMoveDate < lastMoveDate) { + await Queen.query(trx) + .patch({ + modus: false, + modus_date: lastMoveDate.toISOString().split('T')[0], + }) + .where('id', queen.id); + } + if (curMoveDate < lastMoveDate) { + lastMoveDate = curMoveDate; + } + } +} diff --git a/src/api/models/movedate.model.ts b/src/api/models/movedate.model.ts index 8ad0026..ad51b0a 100644 --- a/src/api/models/movedate.model.ts +++ b/src/api/models/movedate.model.ts @@ -4,10 +4,11 @@ import { Model } from 'objection'; import { Apiary } from './apiary.model.js'; import { Hive } from './hive.model.js'; import { MovedateCount } from './movedate_count.model.js'; +import { MovedatePreviousApiary } from './movedate_previous_apiary.model.js'; export class Movedate extends ExtModel { id!: number; - date!: Date; + date!: string; apiary_id!: number; hive_id!: number; edit_id!: number; @@ -27,7 +28,7 @@ export class Movedate extends ExtModel { required: ['date', 'apiary_id', 'hive_id'], properties: { id: { type: 'integer' }, - date: { type: 'string', format: 'date' }, + date: { type: 'string', format: 'iso-date-time' }, edit_id: { type: 'integer' }, apiary_id: { type: 'integer' }, // Apiary FK hive_id: { type: 'integer' }, // Hive FK @@ -59,6 +60,14 @@ export class Movedate extends ExtModel { to: 'movedates_counts.hive_id', }, }, + movedate_previous_apiary: { + relation: Model.BelongsToOneRelation, + modelClass: MovedatePreviousApiary, + join: { + from: 'movedates.id', + to: 'movedates_previous_apiary.current_move_id', + }, + }, creator: { relation: ExtModel.HasOneRelation, modelClass: User, diff --git a/src/api/models/movedate_previous_apiary.model.ts b/src/api/models/movedate_previous_apiary.model.ts new file mode 100644 index 0000000..96ce98a --- /dev/null +++ b/src/api/models/movedate_previous_apiary.model.ts @@ -0,0 +1,26 @@ +import { Model } from 'objection'; +import { Movedate } from './movedate.model.js'; + +export class MovedatePreviousApiary extends Model { + current_move_id!: number; + current_move_date!: string; + hive_id!: number; + previous_apiary_id!: number; + previous_apiary_name!: string; + + movedate?: Movedate; + + static tableName = 'movedates_previous_apiary'; + static idColumn = 'current_move_id'; + + static relationMappings = () => ({ + movedate: { + relation: Model.BelongsToOneRelation, + modelClass: Movedate, + join: { + from: 'movedates_previous_apiary.current_move_id', + to: 'movedates.id', + }, + }, + }); +} diff --git a/src/api/models/queen.model.ts b/src/api/models/queen.model.ts index 824bfc0..4aa545e 100644 --- a/src/api/models/queen.model.ts +++ b/src/api/models/queen.model.ts @@ -50,7 +50,7 @@ export class Queen extends ExtModel { mark_colour: { type: 'string', maxLength: 24 }, mother: { type: 'string', maxLength: 24 }, date: { type: 'string', format: 'date' }, - move_date: { type: 'string', format: 'date' }, + move_date: { type: 'string', format: 'iso-date-time' }, url: { type: 'string', maxLength: 512 }, note: { type: 'string', maxLength: 2000 }, diff --git a/src/api/routes/v1/movedate.route.ts b/src/api/routes/v1/movedate.route.ts index 9216803..a5b0362 100644 --- a/src/api/routes/v1/movedate.route.ts +++ b/src/api/routes/v1/movedate.route.ts @@ -43,6 +43,7 @@ export default function routes( schema: { body: z.object({ ids: z.array(numberSchema), + data: z.object({}).passthrough(), }), }, }, diff --git a/src/api/utils/stripe.util.ts b/src/api/utils/stripe.util.ts index 9000d25..ad56cb3 100644 --- a/src/api/utils/stripe.util.ts +++ b/src/api/utils/stripe.util.ts @@ -25,11 +25,12 @@ export async function createOrder( }, ], mode: 'payment', - success_url: `${frontend}/premium?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${frontend}/premium`, + success_url: `${frontend}/premium?session_id={CHECKOUT_SESSION_ID}&server=${process.env.SERVER}`, + cancel_url: `${frontend}/premium?server=${process.env.SERVER}`, client_reference_id: JSON.stringify({ user_id: user_id, quantity: quantity, + server: process.env.SERVER, }), }); return session; diff --git a/src/config/environment.config.ts b/src/config/environment.config.ts index 3edffda..77399bc 100644 --- a/src/config/environment.config.ts +++ b/src/config/environment.config.ts @@ -193,7 +193,9 @@ const openAI = { }; const serverLocations = ['eu', 'us']; -const serverLocation = serverLocations.includes(process.env.SERVER_LOCATION) +const isServerLocationValid = (server: string) => + serverLocations.includes(server); +const serverLocation = isServerLocationValid(process.env.SERVER_LOCATION) ? process.env.SERVER_LOCATION : 'eu'; @@ -226,5 +228,6 @@ export { openAI, isContainer, isChild, + isServerLocationValid, serverLocation, }; diff --git a/test/e2e/09-movedate-routes.e2e.test.cjs b/test/e2e/09-movedate-routes.e2e.test.cjs index aa25f82..7b4313b 100644 --- a/test/e2e/09-movedate-routes.e2e.test.cjs +++ b/test/e2e/09-movedate-routes.e2e.test.cjs @@ -1,7 +1,8 @@ const request = require('supertest'); const { expect } = require('chai'); -const { doRequest, expectations, doQueryRequest } = require(process.cwd() + - '/test/utils/index.cjs'); +const { doRequest, expectations, doQueryRequest } = require( + process.cwd() + '/test/utils/index.cjs', +); const testInsert = { hive_ids: [2, 3], @@ -81,7 +82,7 @@ describe('Movedate routes', function () { route, null, null, - { ids: [insertId] }, + { ids: [insertId], data: {} }, function (err, res) { expect(res.statusCode).to.eqls(401); expect(res.errors, 'JsonWebTokenError');