feat: store user timezone (#7902)

* feat: store user timezone

* tz validation
This commit is contained in:
Hemachandar
2024-11-07 07:36:19 +05:30
committed by GitHub
parent 356b0916fd
commit 62ee075a6f
10 changed files with 66 additions and 2 deletions
+4
View File
@@ -50,6 +50,10 @@ class User extends ParanoidModel {
@observable
notificationSettings: NotificationSettings;
@Field
@observable
timezone?: string;
@observable
email: string;
+6
View File
@@ -225,6 +225,12 @@ export default class AuthStore extends Store<Team> {
});
}
const currTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (data.user.timezone !== currTimezone) {
const user = this.rootStore.users.get(data.user.id)!;
void user.save({ timezone: currTimezone });
}
// Redirect to the correct custom domain or team subdomain if needed
// Occurs when the (sub)domain is changed in admin and the user hits an old url
const { hostname, pathname } = window.location;
@@ -0,0 +1,15 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn("users", "timezone", {
type: Sequelize.STRING,
allowNull: true,
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn("users", "timezone");
},
};
+4
View File
@@ -183,6 +183,10 @@ class User extends ParanoidModel<
@Column(DataType.STRING)
language: keyof typeof locales | null;
@AllowNull
@Column(DataType.STRING)
timezone: string | null;
@AllowNull
@IsUrlOrRelativePath
@Length({ max: 4096, msg: "avatarUrl must be less than 4096 characters" })
@@ -10,6 +10,7 @@ exports[`presents a user 1`] = `
"lastActiveAt": undefined,
"name": "Test User",
"role": "member",
"timezone": undefined,
"updatedAt": undefined,
}
`;
@@ -24,6 +25,7 @@ exports[`presents a user without slack data 1`] = `
"lastActiveAt": undefined,
"name": "Test User",
"role": "member",
"timezone": undefined,
"updatedAt": undefined,
}
`;
+2
View File
@@ -21,6 +21,7 @@ type UserPresentation = {
language?: string;
preferences?: UserPreferences | null;
notificationSettings?: NotificationSettings;
timezone?: string | null;
};
export default function presentUser(
@@ -37,6 +38,7 @@ export default function presentUser(
createdAt: user.createdAt,
updatedAt: user.updatedAt,
lastActiveAt: user.lastActiveAt,
timezone: user.timezone,
};
if (options.includeDetails) {
+2 -1
View File
@@ -2,7 +2,7 @@ import { z } from "zod";
import { NotificationEventType, UserPreference, UserRole } from "@shared/types";
import { locales } from "@shared/utils/date";
import User from "@server/models/User";
import { zodEnumFromObjectKeys } from "@server/utils/zod";
import { zodEnumFromObjectKeys, zodTimezone } from "@server/utils/zod";
import { BaseSchema } from "../schema";
const BaseIdSchema = z.object({
@@ -84,6 +84,7 @@ export const UsersUpdateSchema = BaseSchema.extend({
avatarUrl: z.string().nullish(),
language: zodEnumFromObjectKeys(locales).optional(),
preferences: z.record(z.nativeEnum(UserPreference), z.boolean()).optional(),
timezone: zodTimezone().optional(),
}),
});
+13
View File
@@ -702,6 +702,19 @@ describe("#users.update", () => {
expect(body.data.preferences.rememberLastPath).toBe(true);
});
it("should update user timezone", async () => {
const user = await buildUser();
const res = await server.post("/api/users.update", {
body: {
token: user.getJwtToken(),
timezone: "Asia/Calcutta",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.timezone).toEqual("Asia/Calcutta");
});
it("should require authentication", async () => {
const res = await server.post("/api/users.update");
const body = await res.json();
+5 -1
View File
@@ -209,7 +209,8 @@ router.post(
async (ctx: APIContext<T.UsersUpdateReq>) => {
const { auth, transaction } = ctx.state;
const actor = auth.user;
const { id, name, avatarUrl, language, preferences } = ctx.input.body;
const { id, name, avatarUrl, language, preferences, timezone } =
ctx.input.body;
let user: User | null = actor;
if (id) {
@@ -236,6 +237,9 @@ router.post(
user.setPreference(key, preferences[key] as boolean);
}
}
if (timezone) {
user.timezone = timezone;
}
await Event.createFromContext(ctx, {
name: "users.update",
+13
View File
@@ -15,3 +15,16 @@ export const zodIconType = () =>
z.string().regex(emojiRegex()),
zodEnumFromObjectKeys(IconLibrary.mapping),
]);
export const zodTimezone = () =>
z.string().refine(
(timezone) => {
try {
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return true;
} catch {
return false;
}
},
{ message: "invalid timezone" }
);