Skip to content

Commit 8f5eed5

Browse files
authored
chore: migrate to @db.Timestamptz(3) and remove luxon dependency (hoppscotch#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 147dba6. * chore: cleanup
1 parent 28ce902 commit 8f5eed5

File tree

13 files changed

+128
-89
lines changed

13 files changed

+128
-89
lines changed

packages/hoppscotch-backend/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"graphql-subscriptions": "3.0.0",
6161
"handlebars": "4.7.8",
6262
"io-ts": "2.2.22",
63-
"luxon": "3.7.1",
6463
"morgan": "1.10.1",
6564
"nodemailer": "7.0.5",
6665
"passport": "0.7.0",
@@ -86,7 +85,6 @@
8685
"@types/cookie-parser": "1.4.9",
8786
"@types/express": "5.0.3",
8887
"@types/jest": "30.0.0",
89-
"@types/luxon": "3.6.2",
9088
"@types/node": "24.1.0",
9189
"@types/nodemailer": "6.4.17",
9290
"@types/passport-github2": "1.2.9",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- AlterTable
2+
ALTER TABLE "Account" ALTER COLUMN "loggedIn" SET DATA TYPE TIMESTAMPTZ(3) USING "loggedIn" AT TIME ZONE 'UTC';
3+
4+
-- AlterTable
5+
ALTER TABLE "InfraConfig" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
6+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
7+
8+
-- AlterTable
9+
ALTER TABLE "InfraToken" ALTER COLUMN "expiresOn" SET DATA TYPE TIMESTAMPTZ(3) USING "expiresOn" AT TIME ZONE 'UTC',
10+
ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
11+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
12+
13+
-- AlterTable
14+
ALTER TABLE "InvitedUsers" ALTER COLUMN "invitedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "invitedOn" AT TIME ZONE 'UTC';
15+
16+
-- AlterTable
17+
ALTER TABLE "PersonalAccessToken" ALTER COLUMN "expiresOn" SET DATA TYPE TIMESTAMPTZ(3) USING "expiresOn" AT TIME ZONE 'UTC',
18+
ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
19+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
20+
21+
-- AlterTable
22+
ALTER TABLE "Shortcode" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
23+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
24+
25+
-- AlterTable
26+
ALTER TABLE "TeamCollection" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
27+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
28+
29+
-- AlterTable
30+
ALTER TABLE "TeamRequest" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
31+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
32+
33+
-- AlterTable
34+
ALTER TABLE "User" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
35+
ALTER COLUMN "lastLoggedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "lastLoggedOn" AT TIME ZONE 'UTC',
36+
ALTER COLUMN "lastActiveOn" SET DATA TYPE TIMESTAMPTZ(3) USING "lastActiveOn" AT TIME ZONE 'UTC';
37+
38+
-- AlterTable
39+
ALTER TABLE "UserCollection" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
40+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
41+
42+
-- AlterTable
43+
ALTER TABLE "UserHistory" ALTER COLUMN "executedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "executedOn" AT TIME ZONE 'UTC';
44+
45+
-- AlterTable
46+
ALTER TABLE "UserRequest" ALTER COLUMN "createdOn" SET DATA TYPE TIMESTAMPTZ(3) USING "createdOn" AT TIME ZONE 'UTC',
47+
ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
48+
49+
-- AlterTable
50+
ALTER TABLE "UserSettings" ALTER COLUMN "updatedOn" SET DATA TYPE TIMESTAMPTZ(3) USING "updatedOn" AT TIME ZONE 'UTC';
51+
52+
-- AlterTable
53+
ALTER TABLE "VerificationToken" ALTER COLUMN "expiresOn" SET DATA TYPE TIMESTAMPTZ(3) USING "expiresOn" AT TIME ZONE 'UTC';
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Please do not edit this file manually
2-
# It should be added in your version-control system (i.e. Git)
3-
provider = "postgresql"
2+
# It should be added in your version-control system (e.g., Git)
3+
provider = "postgresql"

packages/hoppscotch-backend/prisma/schema.prisma

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ model TeamCollection {
5151
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
5252
title String
5353
orderIndex Int
54-
createdOn DateTime @default(now()) @db.Timestamp(3)
55-
updatedOn DateTime @updatedAt @db.Timestamp(3)
54+
createdOn DateTime @default(now()) @db.Timestamptz(3)
55+
updatedOn DateTime @updatedAt @db.Timestamptz(3)
5656
}
5757

5858
model TeamRequest {
@@ -64,8 +64,8 @@ model TeamRequest {
6464
title String
6565
request Json
6666
orderIndex Int
67-
createdOn DateTime @default(now()) @db.Timestamp(3)
68-
updatedOn DateTime @updatedAt @db.Timestamp(3)
67+
createdOn DateTime @default(now()) @db.Timestamptz(3)
68+
updatedOn DateTime @updatedAt @db.Timestamptz(3)
6969
}
7070

7171
model Shortcode {
@@ -74,8 +74,8 @@ model Shortcode {
7474
embedProperties Json?
7575
creatorUid String?
7676
User User? @relation(fields: [creatorUid], references: [uid])
77-
createdOn DateTime @default(now())
78-
updatedOn DateTime @default(now()) @updatedAt
77+
createdOn DateTime @default(now()) @db.Timestamptz(3)
78+
updatedOn DateTime @default(now()) @updatedAt @db.Timestamptz(3)
7979
8080
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
8181
}
@@ -104,9 +104,9 @@ model User {
104104
userRequests UserRequest[]
105105
currentRESTSession Json?
106106
currentGQLSession Json?
107-
lastLoggedOn DateTime? @db.Timestamp(3)
108-
lastActiveOn DateTime? @db.Timestamp(3)
109-
createdOn DateTime @default(now()) @db.Timestamp(3)
107+
lastLoggedOn DateTime? @db.Timestamptz(3)
108+
lastActiveOn DateTime? @db.Timestamptz(3)
109+
createdOn DateTime @default(now()) @db.Timestamptz(3)
110110
invitedUsers InvitedUsers[]
111111
shortcodes Shortcode[]
112112
personalAccessTokens PersonalAccessToken[]
@@ -121,7 +121,7 @@ model Account {
121121
providerRefreshToken String?
122122
providerAccessToken String?
123123
providerScope String?
124-
loggedIn DateTime @default(now()) @db.Timestamp(3)
124+
loggedIn DateTime @default(now()) @db.Timestamptz(3)
125125
126126
@@unique(fields: [provider, providerAccountId], name: "verifyProviderAccount")
127127
}
@@ -131,7 +131,7 @@ model VerificationToken {
131131
token String @unique @default(cuid())
132132
userUid String
133133
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
134-
expiresOn DateTime @db.Timestamp(3)
134+
expiresOn DateTime @db.Timestamptz(3)
135135
136136
@@unique(fields: [deviceIdentifier, token], name: "passwordless_deviceIdentifier_tokens")
137137
}
@@ -141,7 +141,7 @@ model UserSettings {
141141
userUid String @unique
142142
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
143143
properties Json
144-
updatedOn DateTime @updatedAt @db.Timestamp(3)
144+
updatedOn DateTime @updatedAt @db.Timestamptz(3)
145145
}
146146

147147
model UserHistory {
@@ -152,7 +152,7 @@ model UserHistory {
152152
request Json
153153
responseMetadata Json
154154
isStarred Boolean
155-
executedOn DateTime @default(now()) @db.Timestamp(3)
155+
executedOn DateTime @default(now()) @db.Timestamptz(3)
156156
}
157157

158158
enum ReqType {
@@ -174,7 +174,7 @@ model InvitedUsers {
174174
user User @relation(fields: [adminUid], references: [uid], onDelete: Cascade)
175175
adminEmail String
176176
inviteeEmail String @unique
177-
invitedOn DateTime @default(now()) @db.Timestamp(3)
177+
invitedOn DateTime @default(now()) @db.Timestamptz(3)
178178
}
179179

180180
model UserRequest {
@@ -187,8 +187,8 @@ model UserRequest {
187187
request Json
188188
type ReqType
189189
orderIndex Int
190-
createdOn DateTime @default(now()) @db.Timestamp(3)
191-
updatedOn DateTime @updatedAt @db.Timestamp(3)
190+
createdOn DateTime @default(now()) @db.Timestamptz(3)
191+
updatedOn DateTime @updatedAt @db.Timestamptz(3)
192192
}
193193

194194
model UserCollection {
@@ -203,8 +203,8 @@ model UserCollection {
203203
data Json?
204204
orderIndex Int
205205
type ReqType
206-
createdOn DateTime @default(now()) @db.Timestamp(3)
207-
updatedOn DateTime @updatedAt @db.Timestamp(3)
206+
createdOn DateTime @default(now()) @db.Timestamptz(3)
207+
updatedOn DateTime @updatedAt @db.Timestamptz(3)
208208
}
209209

210210
enum TeamAccessRole {
@@ -217,10 +217,10 @@ model InfraConfig {
217217
id String @id @default(cuid())
218218
name String @unique
219219
value String?
220-
lastSyncedEnvFileValue String?
220+
lastSyncedEnvFileValue String? // deprecated, use `value` instead
221221
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
222-
createdOn DateTime @default(now()) @db.Timestamp(3)
223-
updatedOn DateTime @updatedAt @db.Timestamp(3)
222+
createdOn DateTime @default(now()) @db.Timestamptz(3)
223+
updatedOn DateTime @updatedAt @db.Timestamptz(3)
224224
}
225225

226226
model PersonalAccessToken {
@@ -229,17 +229,17 @@ model PersonalAccessToken {
229229
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
230230
label String
231231
token String @unique @default(uuid())
232-
expiresOn DateTime? @db.Timestamp(3)
233-
createdOn DateTime @default(now()) @db.Timestamp(3)
234-
updatedOn DateTime @updatedAt @db.Timestamp(3)
232+
expiresOn DateTime? @db.Timestamptz(3)
233+
createdOn DateTime @default(now()) @db.Timestamptz(3)
234+
updatedOn DateTime @updatedAt @db.Timestamptz(3)
235235
}
236236

237237
model InfraToken {
238238
id String @id @default(cuid())
239239
creatorUid String
240240
label String
241241
token String @unique @default(uuid())
242-
expiresOn DateTime? @db.Timestamp(3)
243-
createdOn DateTime @default(now()) @db.Timestamp(3)
244-
updatedOn DateTime @default(now()) @db.Timestamp(3)
242+
expiresOn DateTime? @db.Timestamptz(3)
243+
createdOn DateTime @default(now()) @db.Timestamptz(3)
244+
updatedOn DateTime @default(now()) @db.Timestamptz(3)
245245
}

packages/hoppscotch-backend/src/auth/auth.service.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { MailerService } from 'src/mailer/mailer.service';
33
import { PrismaService } from 'src/prisma/prisma.service';
44
import { UserService } from 'src/user/user.service';
55
import { VerifyMagicDto } from './dto/verify-magic.dto';
6-
import { DateTime } from 'luxon';
76
import * as argon2 from 'argon2';
87
import * as bcrypt from 'bcrypt';
98
import * as O from 'fp-ts/Option';
@@ -52,14 +51,15 @@ export class AuthService {
5251
const salt = await bcrypt.genSalt(
5352
parseInt(this.configService.get('INFRA.TOKEN_SALT_COMPLEXITY')),
5453
);
55-
const expiresOn = DateTime.now()
56-
.plus({
57-
hours: parseInt(
58-
this.configService.get('INFRA.MAGIC_LINK_TOKEN_VALIDITY'),
59-
),
60-
})
61-
.toISO()
62-
.toString();
54+
55+
// Calculate expiration time by adding hours to current time
56+
let validityInHours = parseInt(
57+
this.configService.get('INFRA.MAGIC_LINK_TOKEN_VALIDITY'),
58+
);
59+
if (isNaN(validityInHours)) validityInHours = 24; // Default: 24 hours
60+
61+
const expiresOn = new Date();
62+
expiresOn.setHours(expiresOn.getHours() + validityInHours);
6363

6464
const idToken = await this.prisma.verificationToken.create({
6565
data: {
@@ -296,8 +296,8 @@ export class AuthService {
296296
);
297297
}
298298

299-
const currentTime = DateTime.now().toISO();
300-
if (currentTime > passwordlessTokens.value.expiresOn.toISOString())
299+
const currentTime = new Date();
300+
if (currentTime > passwordlessTokens.value.expiresOn)
301301
return E.left({
302302
message: MAGIC_LINK_EXPIRED,
303303
statusCode: HttpStatus.UNAUTHORIZED,

packages/hoppscotch-backend/src/auth/helper.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { HttpException, HttpStatus } from '@nestjs/common';
2-
import { DateTime } from 'luxon';
32
import { AuthTokens } from 'src/types/AuthTokens';
43
import { Response } from 'express';
54
import * as cookie from 'cookie';
@@ -43,29 +42,29 @@ export const authCookieHandler = (
4342
redirectUrl: string | null,
4443
configService: ConfigService,
4544
) => {
46-
const currentTime = DateTime.now();
47-
const accessTokenValidity = currentTime
48-
.plus({
49-
milliseconds: parseInt(configService.get('INFRA.ACCESS_TOKEN_VALIDITY')),
50-
})
51-
.toMillis();
52-
const refreshTokenValidity = currentTime
53-
.plus({
54-
milliseconds: parseInt(configService.get('INFRA.REFRESH_TOKEN_VALIDITY')),
55-
})
56-
.toMillis();
45+
// Calculate token validity periods in milliseconds
46+
let accessTokenValidityInMs = parseInt(
47+
configService.get('INFRA.ACCESS_TOKEN_VALIDITY'),
48+
);
49+
let refreshTokenValidityInMs = parseInt(
50+
configService.get('INFRA.REFRESH_TOKEN_VALIDITY'),
51+
);
52+
53+
// Set default values if parsing results in NaN
54+
if (isNaN(accessTokenValidityInMs)) accessTokenValidityInMs = 86400000; // Default: 1 day
55+
if (isNaN(refreshTokenValidityInMs)) refreshTokenValidityInMs = 604800000; // Default: 7 days
5756

5857
res.cookie(AuthTokenType.ACCESS_TOKEN, authTokens.access_token, {
5958
httpOnly: true,
6059
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
6160
sameSite: 'lax',
62-
maxAge: accessTokenValidity,
61+
maxAge: Date.now() + accessTokenValidityInMs,
6362
});
6463
res.cookie(AuthTokenType.REFRESH_TOKEN, authTokens.refresh_token, {
6564
httpOnly: true,
6665
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
6766
sameSite: 'lax',
68-
maxAge: refreshTokenValidity,
67+
maxAge: Date.now() + refreshTokenValidityInMs,
6968
});
7069

7170
if (!redirect) {

packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
2626
});
2727
}
2828

29-
async validate(
30-
accessToken: string,
31-
refreshToken: string,
32-
profile,
33-
done,
34-
) {
29+
async validate(accessToken: string, refreshToken: string, profile, done) {
3530
const email = profile?.emails?.[0]?.value;
3631

3732
if (!validateEmail(email))

packages/hoppscotch-backend/src/guards/infra-token.guard.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
UnauthorizedException,
66
} from '@nestjs/common';
77
import { PrismaService } from 'src/prisma/prisma.service';
8-
import { DateTime } from 'luxon';
98
import {
109
INFRA_TOKEN_EXPIRED,
1110
INFRA_TOKEN_HEADER_MISSING,
@@ -37,8 +36,8 @@ export class InfraTokenGuard implements CanActivate {
3736
if (infraToken === null)
3837
throw new UnauthorizedException(INFRA_TOKEN_INVALID_TOKEN);
3938

40-
const currentTime = DateTime.now().toISO();
41-
if (currentTime > infraToken.expiresOn?.toISOString()) {
39+
// Check if token has expired (if expiresOn is set)
40+
if (infraToken.expiresOn && new Date() > infraToken.expiresOn) {
4241
throw new UnauthorizedException(INFRA_TOKEN_EXPIRED);
4342
}
4443

packages/hoppscotch-backend/src/guards/rest-pat-auth.guard.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
import { Request } from 'express';
88
import { AccessTokenService } from 'src/access-token/access-token.service';
99
import * as E from 'fp-ts/Either';
10-
import { DateTime } from 'luxon';
1110
import { ACCESS_TOKEN_EXPIRED, ACCESS_TOKEN_INVALID } from 'src/errors';
1211
import { createCLIErrorResponse } from 'src/access-token/helper';
1312
@Injectable()
@@ -28,15 +27,23 @@ export class PATAuthGuard implements CanActivate {
2827
throw new BadRequestException(
2928
createCLIErrorResponse(ACCESS_TOKEN_INVALID),
3029
);
31-
request.user = userAccessToken.right.user;
3230

31+
request.user = userAccessToken.right.user;
3332
const accessToken = userAccessToken.right;
34-
if (accessToken.expiresOn === null) return true;
3533

36-
const today = DateTime.now().toISO();
37-
if (accessToken.expiresOn.toISOString() > today) return true;
34+
// If token has no expiration, it's valid
35+
if (accessToken.expiresOn === null) {
36+
return true;
37+
}
38+
39+
// Check if token has expired
40+
if (new Date() > accessToken.expiresOn) {
41+
throw new BadRequestException(
42+
createCLIErrorResponse(ACCESS_TOKEN_EXPIRED),
43+
);
44+
}
3845

39-
throw new BadRequestException(createCLIErrorResponse(ACCESS_TOKEN_EXPIRED));
46+
return true;
4047
}
4148

4249
private extractTokenFromHeader(request: Request): string | undefined {

packages/hoppscotch-backend/src/mailer/templates/team-invitation.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@
511511
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
512512
<tr>
513513
<td class="content-cell" align="center">
514-
<p class="f-fallback sub align-center">&copy; 2021 Hoppscotch</p>
514+
<p class="f-fallback sub align-center">&copy; 2025 Hoppscotch</p>
515515
<p class="f-fallback sub align-center">12 New Fetter Lane, London, United Kingdom, EC4A 1JP.</p>
516516
</td>
517517
</tr>

0 commit comments

Comments
 (0)