diff --git a/server/middlewares/authentication.ts b/server/middlewares/authentication.ts index 577c0ca9b..c1b1da205 100644 --- a/server/middlewares/authentication.ts +++ b/server/middlewares/authentication.ts @@ -123,7 +123,7 @@ export default function auth(options: AuthenticationOptions = {}) { } // not awaiting the promise here so that the request is not blocked - user.updateActiveAt(ctx.request.ip).catch((err) => { + user.updateActiveAt(ctx).catch((err) => { Logger.error("Failed to update user activeAt", err); }); diff --git a/server/models/TeamDomain.ts b/server/models/TeamDomain.ts index 3c2901552..b685aec02 100644 --- a/server/models/TeamDomain.ts +++ b/server/models/TeamDomain.ts @@ -10,8 +10,8 @@ import { BeforeCreate, } from "sequelize-typescript"; import { TeamValidation } from "@shared/validations"; +import env from "@server/env"; import { ValidationError } from "@server/errors"; -import isCloudHosted from "~/utils/isCloudHosted"; import Team from "./Team"; import User from "./User"; import IdModel from "./base/IdModel"; @@ -19,6 +19,8 @@ import Fix from "./decorators/Fix"; import IsFQDN from "./validators/IsFQDN"; import Length from "./validators/Length"; +const isCloudHosted = env.DEPLOYMENT === "hosted"; + @Table({ tableName: "team_domains", modelName: "team_domain" }) @Fix class TeamDomain extends IdModel { diff --git a/server/models/User.ts b/server/models/User.ts index e7ac269c7..23c176bc9 100644 --- a/server/models/User.ts +++ b/server/models/User.ts @@ -1,6 +1,7 @@ import crypto from "crypto"; import { addMinutes, subMinutes } from "date-fns"; import JWT from "jsonwebtoken"; +import { Context } from "koa"; import { Transaction, QueryTypes, SaveOptions, Op } from "sequelize"; import { Table, @@ -51,6 +52,8 @@ import NotContainsUrl from "./validators/NotContainsUrl"; export enum UserFlag { InviteSent = "inviteSent", InviteReminderSent = "inviteReminderSent", + DesktopWeb = "desktopWeb", + MobileWeb = "mobileWeb", } export enum UserRole { @@ -264,8 +267,11 @@ class User extends ParanoidModel { if (!this.flags) { this.flags = {}; } - this.flags[flag] = value ? 1 : 0; - this.changed("flags", true); + const binary = value ? 1 : 0; + if (this.flags[flag] !== binary) { + this.flags[flag] = binary; + this.changed("flags", true); + } return this.flags; }; @@ -350,7 +356,8 @@ class User extends ParanoidModel { .map((c) => c.id); }; - updateActiveAt = async (ip: string, force = false) => { + updateActiveAt = async (ctx: Context, force = false) => { + const { ip } = ctx.request; const fiveMinutesAgo = subMinutes(new Date(), 5); // ensure this is updated only every few minutes otherwise @@ -358,13 +365,20 @@ class User extends ParanoidModel { if (!this.lastActiveAt || this.lastActiveAt < fiveMinutesAgo || force) { this.lastActiveAt = new Date(); this.lastActiveIp = ip; - - return this.save({ - hooks: false, - }); } - return this; + // Track the clients each user is using + if (ctx.userAgent.isMobile) { + this.setFlag(UserFlag.MobileWeb); + } + if (ctx.userAgent.isDesktop) { + this.setFlag(UserFlag.DesktopWeb); + } + + // Save only writes to the database if there are changes + return this.save({ + hooks: false, + }); }; updateSignedIn = (ip: string) => { diff --git a/server/routes/api/developer.ts b/server/routes/api/developer.ts index bddf1b6f1..a0bf673f0 100644 --- a/server/routes/api/developer.ts +++ b/server/routes/api/developer.ts @@ -45,7 +45,7 @@ router.post("developer.create_test_users", dev(), auth(), async (ctx) => { // Convert from invites to active users by marking as active await Promise.all( - response.users.map((user) => user.updateActiveAt(ctx.request.ip, true)) + response.users.map((user) => user.updateActiveAt(ctx, true)) ); ctx.body = { diff --git a/server/routes/api/index.ts b/server/routes/api/index.ts index 2e7201a4f..7b1a890e5 100644 --- a/server/routes/api/index.ts +++ b/server/routes/api/index.ts @@ -1,6 +1,7 @@ -import Koa, { DefaultContext, DefaultState } from "koa"; +import Koa, { BaseContext, DefaultContext, DefaultState } from "koa"; import bodyParser from "koa-body"; import Router from "koa-router"; +import userAgent, { UserAgentContext } from "koa-useragent"; import env from "@server/env"; import { NotFoundError } from "@server/errors"; import errorHandling from "@server/middlewares/errorHandling"; @@ -50,6 +51,7 @@ api.use( }, }) ); +api.use(userAgent); api.use(methodOverride()); api.use(apiWrapper()); api.use(editor()); diff --git a/server/routes/auth/index.ts b/server/routes/auth/index.ts index 62f4c6475..974df0335 100644 --- a/server/routes/auth/index.ts +++ b/server/routes/auth/index.ts @@ -31,7 +31,8 @@ router.get("/redirect", auth(), async (ctx) => { } // ensure that the lastActiveAt on user is updated to prevent replay requests - await user.updateActiveAt(ctx.request.ip, true); + await user.updateActiveAt(ctx, true); + ctx.cookies.set("accessToken", jwtToken, { httpOnly: false, expires: addMonths(new Date(), 3),