Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.

Commit 34c156e

Browse files
committed
fix(auth): Handle CockroachDB BIGINT IDs as strings to prevent precision loss
When migrating from MSSQL to CockroachDB, user IDs changed from sequential INT to distributed BIGINT values generated via unique_rowid(). ColdFusion's double precision cannot safely represent all 64-bit integers, causing silent ID corruption that manifested as: - Empty user_id in token records after registration - Random "user not found" errors - Token verification failures Changes: - User.cfc: Set id and roleId properties to datatype="string" - UserToken.cfc: Set id and user_id properties to datatype="string" - AuthController.saveUser(): Add ID retrieval fallback and extensive logging to debug Wheels ID population with CockroachDB This ensures BIGINT IDs are treated as opaque string identifiers throughout the application layer while remaining INT8 in the database. PostgreSQL/ CockroachDB automatically handle string-to-BIGINT conversion in queries. Fixes user registration flow where tokens were being created with empty user_id values.
1 parent a25ebcf commit 34c156e

3 files changed

Lines changed: 104 additions & 47 deletions

File tree

app/controllers/web/AuthController.cfc

Lines changed: 93 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -619,10 +619,10 @@ component extends="app.Controllers.Controller" {
619619
return user;
620620
}
621621

622-
private function saveUser(required struct userData) {
623-
var message = "";
622+
623+
private string function saveUser(required struct userData) {
624624
try {
625-
// check required fields
625+
// Validate required fields
626626
if (!structKeyExists(userData, "firstname") || !len(trim(userData.firstname))) {
627627
return "First name is required.";
628628
}
@@ -638,41 +638,96 @@ component extends="app.Controllers.Controller" {
638638
if (!structKeyExists(userData, "passwordHash") || !len(trim(userData.passwordHash))) {
639639
return "Password is required.";
640640
}
641-
// Check if a user with the same email already exists
642-
var existingUser = model("User").findFirst( where="email = '#userData.email#'");
643-
if (!isObject(existingUser)) {
644-
// Create a new user
645-
var newUser = model("User").new();
646-
newUser.firstname = userData.firstname;
647-
newUser.lastname = userData.lastname;
648-
newUser.email = userData.email;
649-
newUser.passwordhash = bCryptHashPW(userData.passwordHash, bCryptGenSalt());
650-
newUser.roleid = GetUserRoleId(); // user role
651-
newUser.status = SetInactive(); // set inactive
652-
if(structKeyExists(userData, "newsletter")){
653-
newUser.newsletter = true;
654-
}
655-
if(newUser.save()){
656-
// Generate a unique verification token
657-
var verificationToken = Hash(createUUID());
658-
// Save token to the user_tokens table
659-
var newToken = model("UserToken").new();
660-
newToken.token = verificationToken;
661-
newToken.user_id = newUser.id;
662-
newToken.status = false; // Not verified
663-
newToken.save();
664-
// Send verification email
665-
if(sendVerificationEmail(newUser.email, verificationToken)){
666-
message = "Registration successful. Please check your email to verify your account.";
667-
}else{
668-
message = "Unable to send verification email. Please try again or contact support.";
641+
if (len(userData.passwordHash) < 8) {
642+
return "Password must be at least 8 characters long.";
643+
}
644+
645+
// Check for duplicate email - CORRECTED: Use Dynamic Finder
646+
var existingUser = model("User").findOneByEmail(userData.email);
647+
if (isObject(existingUser)) {
648+
return message = "An account with this email address already exists.";
649+
}
650+
651+
// Create a new user
652+
var newUser = model("User").new();
653+
newUser.firstname = userData.firstname;
654+
newUser.lastname = userData.lastname;
655+
newUser.email = userData.email;
656+
newUser.passwordhash = bCryptHashPW(userData.passwordHash, bCryptGenSalt());
657+
newUser.roleid = toString(GetUserRoleId()); // Convert to string
658+
newUser.status = SetInactive();
659+
660+
if (structKeyExists(userData, "newsletter")) {
661+
newUser.newsletter = true;
662+
}
663+
664+
if (!newUser.save()) {
665+
model("Log").log(
666+
category = "wheels.auth",
667+
level = "ERROR",
668+
message = "Failed to save new user",
669+
details = {
670+
"email": userData.email,
671+
"errors": newUser.allErrors()
669672
}
670-
}else{
671-
message = "Unable to create user account. Please try again or contact support.";
673+
);
674+
return "Unable to create user account. Please try again or contact support.";
675+
}
676+
677+
// After save, ID should now be populated as a string
678+
var userId = newUser.id; // Already a string due to model config
679+
680+
// Validate we got a proper ID
681+
if (!len(userId)) {
682+
// Fallback: re-fetch from database
683+
var savedUser = model("User").findOneByEmail(userData.email);
684+
if (isObject(savedUser)) {
685+
userId = savedUser.id;
686+
} else {
687+
model("Log").log(
688+
category = "wheels.auth",
689+
level = "ERROR",
690+
message = "User saved but ID is empty and cannot re-fetch",
691+
details = {"email": userData.email}
692+
);
693+
return "Registration error. Please contact support.";
672694
}
673-
} else {
674-
message = "An account with this email address already exists.";
675695
}
696+
697+
// Generate and save verification token
698+
var verificationToken = Hash(createUUID());
699+
700+
var newToken = model("UserToken").new();
701+
newToken.token = verificationToken;
702+
newToken.user_id = userId; // Now a string, safe
703+
newToken.status = false;
704+
705+
if (!newToken.save()) {
706+
model("Log").log(
707+
category = "wheels.auth",
708+
level = "ERROR",
709+
message = "Failed to save verification token",
710+
details = {
711+
"user_id": userId,
712+
"errors": newToken.allErrors()
713+
}
714+
);
715+
return "Registration error. Please contact support.";
716+
}
717+
718+
// Send verification email
719+
if (!sendVerificationEmail(userData.email, verificationToken)) {
720+
model("Log").log(
721+
category = "wheels.auth",
722+
level = "WARN",
723+
message = "User created but verification email failed",
724+
details = {"user_id": userId, "email": userData.email}
725+
);
726+
return "Registration completed but we couldn't send the verification email. Please contact support to resend the verification link.";
727+
}
728+
729+
return "Registration successful. Please check your email to verify your account.";
730+
676731
} catch (any e) {
677732
// Log internal error, but return generic message
678733
model("Log").log(
@@ -681,12 +736,12 @@ component extends="app.Controllers.Controller" {
681736
message = "Exception in saveUser",
682737
details = {
683738
"error_message": e.message,
684-
"error_detail": e.detail
739+
"error_detail": e.detail,
740+
"email": structKeyExists(userData, "email") ? userData.email : ""
685741
}
686742
);
687-
message = "An error occurred while creating your account. Please try again later.";
743+
return "An error occurred while creating your account. Please try again later.";
688744
}
689-
return message;
690745
}
691746

692747
private function sendVerificationEmail(required string email, required string token) {

app/models/User.cfc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ component extends="app.Models.Model" {
22
function config() {
33
table("users");
44

5+
// Tell Wheels to treat the ID as a string, not an integer -- CockroachDB is storing INT8 (BIGINT), which is a 64-bit integer that ColdFusion cannot safely represent as a number.
56
// ID Property
67
property(
78
name="id",
89
column="id",
9-
dataType="integer",
10+
dataType="string",
1011
automaticValidations=false
1112
);
1213

@@ -152,8 +153,8 @@ component extends="app.Models.Model" {
152153
label="WordPress ID"
153154
);
154155

155-
property(name="website", column="website", dataType="string", default="");
156-
property(name="ip", column="ip", dataType="string", default="");
156+
property(name="website", column="website", dataType="string", defaultValue = "");
157+
property(name="ip", column="ip", dataType="string", defaultValue = "");
157158

158159
// Relationships
159160
belongsTo(name="Role", foreignKey="roleId");

app/models/UserToken.cfc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ component extends="app.Models.Model" {
22
function config() {
33
table("user_tokens");
44

5-
property(name="id", column="id", type="integer", required=true, primarykey=true);
6-
property(name="token", column="token", type="string", required=false, default="");
7-
property(name="createdAt", column="createdat", type="datetime", required=false, default="");
8-
property(name="updatedAt", column="updatedat", type="datetime", required=false, default="");
9-
property(name="deletedAt", column="deletedat", type="datetime", required=false, default="");
10-
property(name="user_id", column="user_id", type="integer", required=true, foreignkey=true, references="User(id)");
5+
// Tell Wheels to treat the ID as a string, not an integer -- CockroachDB is storing INT8 (BIGINT), which is a 64-bit integer that ColdFusion cannot safely represent as a number.
6+
property(name="id", column="id", dataType ="string", primarykey=true);
7+
property(name="token", column="token", dataType ="string", defaultValue = "");
8+
property(name="createdAt", column="createdat", dataType ="datetime", defaultValue = "");
9+
property(name="updatedAt", column="updatedat", dataType ="datetime", defaultValue = "");
10+
property(name="deletedAt", column="deletedat", dataType ="datetime", defaultValue = "");
11+
property(name="user_id", column="user_id", dataType ="string");
1112

1213
belongsTo(name="User", foreignKey="user_id");
1314
}

0 commit comments

Comments
 (0)