chore: migrate to @db.Timestamptz(3) and remove luxon dependency (#5283)

* feat: remove deprecated env sync

* feat: using infraConfig in bootstrap

* chore: migrate to TIMESTAMPTZ and remove luxon dependency

* chore: remove luxon deps

* Revert "feat: using infraConfig in bootstrap"

This reverts commit 147dba632f095dad816afdd4e46ed736e8a3b8ff.

* chore: cleanup
This commit is contained in:
Mir Arif Hasan 2025-07-28 21:32:14 +06:00 committed by GitHub
parent 28ce90234f
commit 8f5eed5151
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 128 additions and 89 deletions

View file

@ -60,7 +60,6 @@
"graphql-subscriptions": "3.0.0",
"handlebars": "4.7.8",
"io-ts": "2.2.22",
"luxon": "3.7.1",
"morgan": "1.10.1",
"nodemailer": "7.0.5",
"passport": "0.7.0",
@ -86,7 +85,6 @@
"@types/cookie-parser": "1.4.9",
"@types/express": "5.0.3",
"@types/jest": "30.0.0",
"@types/luxon": "3.6.2",
"@types/node": "24.1.0",
"@types/nodemailer": "6.4.17",
"@types/passport-github2": "1.2.9",

View file

@ -0,0 +1,53 @@
-- AlterTable
ALTER TABLE "Account" ALTER COLUMN "loggedIn" SET DATA TYPE TIMESTAMPTZ(3) USING "loggedIn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "InfraConfig" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "InfraToken" ALTER COLUMN "expiresOn" SET DATA TYPE TIMESTAMPTZ(3) USING "expiresOn" AT TIME ZONE 'UTC',
ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "InvitedUsers" ALTER COLUMN "invitedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "invitedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "PersonalAccessToken" ALTER COLUMN "expiresOn" SET DATA TYPE TIMESTAMPTZ(3) USING "expiresOn" AT TIME ZONE 'UTC',
ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "Shortcode" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "TeamCollection" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "TeamRequest" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "lastLoggedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "lastLoggedOn" AT TIME ZONE 'UTC',
ALTER COLUMN "lastActiveOn" SET DATA TYPE TIMESTAMPTZ(3) USING "lastActiveOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "UserCollection" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "UserHistory" ALTER COLUMN "executedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "executedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "UserRequest" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "UserSettings" ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
-- AlterTable
ALTER TABLE "VerificationToken" ALTER COLUMN "expiresOn" SET DATA TYPE TIMESTAMPTZ(3) USING "expiresOn" AT TIME ZONE 'UTC';

View file

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View file

@ -51,8 +51,8 @@ model TeamCollection {
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @updatedAt @db.Timestamptz(3)
}
model TeamRequest {
@ -64,8 +64,8 @@ model TeamRequest {
title String
request Json
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @updatedAt @db.Timestamptz(3)
}
model Shortcode {
@ -74,8 +74,8 @@ model Shortcode {
embedProperties Json?
creatorUid String?
User User? @relation(fields: [creatorUid], references: [uid])
createdOn DateTime @default(now())
updatedOn DateTime @default(now()) @updatedAt
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @default(now()) @updatedAt @db.Timestamptz(3)
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
}
@ -104,9 +104,9 @@ model User {
userRequests UserRequest[]
currentRESTSession Json?
currentGQLSession Json?
lastLoggedOn DateTime? @db.Timestamp(3)
lastActiveOn DateTime? @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
lastLoggedOn DateTime? @db.Timestamptz(3)
lastActiveOn DateTime? @db.Timestamptz(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
invitedUsers InvitedUsers[]
shortcodes Shortcode[]
personalAccessTokens PersonalAccessToken[]
@ -121,7 +121,7 @@ model Account {
providerRefreshToken String?
providerAccessToken String?
providerScope String?
loggedIn DateTime @default(now()) @db.Timestamp(3)
loggedIn DateTime @default(now()) @db.Timestamptz(3)
@@unique(fields: [provider, providerAccountId], name: "verifyProviderAccount")
}
@ -131,7 +131,7 @@ model VerificationToken {
token String @unique @default(cuid())
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
expiresOn DateTime @db.Timestamp(3)
expiresOn DateTime @db.Timestamptz(3)
@@unique(fields: [deviceIdentifier, token], name: "passwordless_deviceIdentifier_tokens")
}
@ -141,7 +141,7 @@ model UserSettings {
userUid String @unique
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
properties Json
updatedOn DateTime @updatedAt @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamptz(3)
}
model UserHistory {
@ -152,7 +152,7 @@ model UserHistory {
request Json
responseMetadata Json
isStarred Boolean
executedOn DateTime @default(now()) @db.Timestamp(3)
executedOn DateTime @default(now()) @db.Timestamptz(3)
}
enum ReqType {
@ -174,7 +174,7 @@ model InvitedUsers {
user User @relation(fields: [adminUid], references: [uid], onDelete: Cascade)
adminEmail String
inviteeEmail String @unique
invitedOn DateTime @default(now()) @db.Timestamp(3)
invitedOn DateTime @default(now()) @db.Timestamptz(3)
}
model UserRequest {
@ -187,8 +187,8 @@ model UserRequest {
request Json
type ReqType
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @updatedAt @db.Timestamptz(3)
}
model UserCollection {
@ -203,8 +203,8 @@ model UserCollection {
data Json?
orderIndex Int
type ReqType
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @updatedAt @db.Timestamptz(3)
}
enum TeamAccessRole {
@ -217,10 +217,10 @@ model InfraConfig {
id String @id @default(cuid())
name String @unique
value String?
lastSyncedEnvFileValue String?
lastSyncedEnvFileValue String? // deprecated, use `value` instead
isEncrypted Boolean @default(false) // Use case: Let's say, Admin wants to store a Secret Key, but doesn't want to store it in plain text in `value` column
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @updatedAt @db.Timestamptz(3)
}
model PersonalAccessToken {
@ -229,9 +229,9 @@ model PersonalAccessToken {
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
label String
token String @unique @default(uuid())
expiresOn DateTime? @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
expiresOn DateTime? @db.Timestamptz(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @updatedAt @db.Timestamptz(3)
}
model InfraToken {
@ -239,7 +239,7 @@ model InfraToken {
creatorUid String
label String
token String @unique @default(uuid())
expiresOn DateTime? @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @default(now()) @db.Timestamp(3)
expiresOn DateTime? @db.Timestamptz(3)
createdOn DateTime @default(now()) @db.Timestamptz(3)
updatedOn DateTime @default(now()) @db.Timestamptz(3)
}

View file

@ -3,7 +3,6 @@ import { MailerService } from 'src/mailer/mailer.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserService } from 'src/user/user.service';
import { VerifyMagicDto } from './dto/verify-magic.dto';
import { DateTime } from 'luxon';
import * as argon2 from 'argon2';
import * as bcrypt from 'bcrypt';
import * as O from 'fp-ts/Option';
@ -52,14 +51,15 @@ export class AuthService {
const salt = await bcrypt.genSalt(
parseInt(this.configService.get('INFRA.TOKEN_SALT_COMPLEXITY')),
);
const expiresOn = DateTime.now()
.plus({
hours: parseInt(
this.configService.get('INFRA.MAGIC_LINK_TOKEN_VALIDITY'),
),
})
.toISO()
.toString();
// Calculate expiration time by adding hours to current time
let validityInHours = parseInt(
this.configService.get('INFRA.MAGIC_LINK_TOKEN_VALIDITY'),
);
if (isNaN(validityInHours)) validityInHours = 24; // Default: 24 hours
const expiresOn = new Date();
expiresOn.setHours(expiresOn.getHours() + validityInHours);
const idToken = await this.prisma.verificationToken.create({
data: {
@ -296,8 +296,8 @@ export class AuthService {
);
}
const currentTime = DateTime.now().toISO();
if (currentTime > passwordlessTokens.value.expiresOn.toISOString())
const currentTime = new Date();
if (currentTime > passwordlessTokens.value.expiresOn)
return E.left({
message: MAGIC_LINK_EXPIRED,
statusCode: HttpStatus.UNAUTHORIZED,

View file

@ -1,5 +1,4 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AuthTokens } from 'src/types/AuthTokens';
import { Response } from 'express';
import * as cookie from 'cookie';
@ -43,29 +42,29 @@ export const authCookieHandler = (
redirectUrl: string | null,
configService: ConfigService,
) => {
const currentTime = DateTime.now();
const accessTokenValidity = currentTime
.plus({
milliseconds: parseInt(configService.get('INFRA.ACCESS_TOKEN_VALIDITY')),
})
.toMillis();
const refreshTokenValidity = currentTime
.plus({
milliseconds: parseInt(configService.get('INFRA.REFRESH_TOKEN_VALIDITY')),
})
.toMillis();
// Calculate token validity periods in milliseconds
let accessTokenValidityInMs = parseInt(
configService.get('INFRA.ACCESS_TOKEN_VALIDITY'),
);
let refreshTokenValidityInMs = parseInt(
configService.get('INFRA.REFRESH_TOKEN_VALIDITY'),
);
// Set default values if parsing results in NaN
if (isNaN(accessTokenValidityInMs)) accessTokenValidityInMs = 86400000; // Default: 1 day
if (isNaN(refreshTokenValidityInMs)) refreshTokenValidityInMs = 604800000; // Default: 7 days
res.cookie(AuthTokenType.ACCESS_TOKEN, authTokens.access_token, {
httpOnly: true,
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
sameSite: 'lax',
maxAge: accessTokenValidity,
maxAge: Date.now() + accessTokenValidityInMs,
});
res.cookie(AuthTokenType.REFRESH_TOKEN, authTokens.refresh_token, {
httpOnly: true,
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
sameSite: 'lax',
maxAge: refreshTokenValidity,
maxAge: Date.now() + refreshTokenValidityInMs,
});
if (!redirect) {

View file

@ -26,12 +26,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
});
}
async validate(
accessToken: string,
refreshToken: string,
profile,
done,
) {
async validate(accessToken: string, refreshToken: string, profile, done) {
const email = profile?.emails?.[0]?.value;
if (!validateEmail(email))

View file

@ -5,7 +5,6 @@ import {
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { DateTime } from 'luxon';
import {
INFRA_TOKEN_EXPIRED,
INFRA_TOKEN_HEADER_MISSING,
@ -37,8 +36,8 @@ export class InfraTokenGuard implements CanActivate {
if (infraToken === null)
throw new UnauthorizedException(INFRA_TOKEN_INVALID_TOKEN);
const currentTime = DateTime.now().toISO();
if (currentTime > infraToken.expiresOn?.toISOString()) {
// Check if token has expired (if expiresOn is set)
if (infraToken.expiresOn && new Date() > infraToken.expiresOn) {
throw new UnauthorizedException(INFRA_TOKEN_EXPIRED);
}

View file

@ -7,7 +7,6 @@ import {
import { Request } from 'express';
import { AccessTokenService } from 'src/access-token/access-token.service';
import * as E from 'fp-ts/Either';
import { DateTime } from 'luxon';
import { ACCESS_TOKEN_EXPIRED, ACCESS_TOKEN_INVALID } from 'src/errors';
import { createCLIErrorResponse } from 'src/access-token/helper';
@Injectable()
@ -28,15 +27,23 @@ export class PATAuthGuard implements CanActivate {
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKEN_INVALID),
);
request.user = userAccessToken.right.user;
const accessToken = userAccessToken.right;
if (accessToken.expiresOn === null) return true;
const today = DateTime.now().toISO();
if (accessToken.expiresOn.toISOString() > today) return true;
// If token has no expiration, it's valid
if (accessToken.expiresOn === null) {
return true;
}
throw new BadRequestException(createCLIErrorResponse(ACCESS_TOKEN_EXPIRED));
// Check if token has expired
if (new Date() > accessToken.expiresOn) {
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKEN_EXPIRED),
);
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {

View file

@ -511,7 +511,7 @@
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">&copy; 2021 Hoppscotch</p>
<p class="f-fallback sub align-center">&copy; 2025 Hoppscotch</p>
<p class="f-fallback sub align-center">12 New Fetter Lane, London, United Kingdom, EC4A 1JP.</p>
</td>
</tr>

View file

@ -517,7 +517,7 @@
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">&copy; 2021 Hoppscotch</p>
<p class="f-fallback sub align-center">&copy; 2025 Hoppscotch</p>
<p class="f-fallback sub align-center">12 New Fetter Lane, London, United Kingdom, EC4A 1JP.</p>
</td>
</tr>

View file

@ -266,12 +266,12 @@ export function escapeSqlLikeString(str: string) {
/**
* Calculate the expiration date of the token
*
* @param expiresOn Number of days the token is valid for
* @param expiresInDays Number of days the token is valid for
* @returns Date object of the expiration date
*/
export function calculateExpirationDate(expiresOn: null | number) {
if (expiresOn === null) return null;
return new Date(Date.now() + expiresOn * 24 * 60 * 60 * 1000);
export function calculateExpirationDate(expiresInDays: null | number) {
if (expiresInDays === null) return null;
return new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000);
}
/*

View file

@ -241,9 +241,6 @@ importers:
io-ts:
specifier: 2.2.22
version: 2.2.22(fp-ts@2.16.10)
luxon:
specifier: 3.7.1
version: 3.7.1
morgan:
specifier: 1.10.1
version: 1.10.1
@ -314,9 +311,6 @@ importers:
'@types/jest':
specifier: 30.0.0
version: 30.0.0
'@types/luxon':
specifier: 3.6.2
version: 3.6.2
'@types/node':
specifier: 24.1.0
version: 24.1.0
@ -10917,10 +10911,6 @@ packages:
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
engines: {node: '>=12'}
luxon@3.7.1:
resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==}
engines: {node: '>=12'}
magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
@ -26933,8 +26923,6 @@ snapshots:
luxon@3.6.1: {}
luxon@3.7.1: {}
magic-string@0.25.9:
dependencies:
sourcemap-codec: 1.4.8