mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
User.email and implementation
This commit is contained in:
4
packages/backend/drizzle/0027_volatile_otto_octavius.sql
Normal file
4
packages/backend/drizzle/0027_volatile_otto_octavius.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE "User" ADD COLUMN "email" varchar(255);--> statement-breakpoint
|
||||||
|
UPDATE "User" SET "email" = 'user_' || id || '@placeholder.local' WHERE "email" IS NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "User" ALTER COLUMN "email" SET NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "User" ADD CONSTRAINT "User_email_unique" UNIQUE("email");
|
||||||
1159
packages/backend/drizzle/meta/0027_snapshot.json
Normal file
1159
packages/backend/drizzle/meta/0027_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,13 @@
|
|||||||
"when": 1769615487574,
|
"when": 1769615487574,
|
||||||
"tag": "0026_stale_shocker",
|
"tag": "0026_stale_shocker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 27,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769635016079,
|
||||||
|
"tag": "0027_volatile_otto_octavius",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -97,15 +97,15 @@ const issueComments = [
|
|||||||
|
|
||||||
const passwordHash = await hashPassword("a");
|
const passwordHash = await hashPassword("a");
|
||||||
const users = [
|
const users = [
|
||||||
{ name: "user 1", username: "u1", passwordHash, avatarURL: null },
|
{ name: "user 1", username: "u1", email: "user1@example.com", passwordHash, avatarURL: null },
|
||||||
{ name: "user 2", username: "u2", passwordHash, avatarURL: null },
|
{ name: "user 2", username: "u2", email: "user2@example.com", passwordHash, avatarURL: null },
|
||||||
// anything past here is just to have more users to assign issues to
|
// anything past here is just to have more users to assign issues to
|
||||||
{ name: "user 3", username: "u3", passwordHash, avatarURL: null },
|
{ name: "user 3", username: "u3", email: "user3@example.com", passwordHash, avatarURL: null },
|
||||||
{ name: "user 4", username: "u4", passwordHash, avatarURL: null },
|
{ name: "user 4", username: "u4", email: "user4@example.com", passwordHash, avatarURL: null },
|
||||||
{ name: "user 5", username: "u5", passwordHash, avatarURL: null },
|
{ name: "user 5", username: "u5", email: "user5@example.com", passwordHash, avatarURL: null },
|
||||||
{ name: "user 6", username: "u6", passwordHash, avatarURL: null },
|
{ name: "user 6", username: "u6", email: "user6@example.com", passwordHash, avatarURL: null },
|
||||||
{ name: "user 7", username: "u7", passwordHash, avatarURL: null },
|
{ name: "user 7", username: "u7", email: "user7@example.com", passwordHash, avatarURL: null },
|
||||||
{ name: "user 8", username: "u8", passwordHash, avatarURL: null },
|
{ name: "user 8", username: "u8", email: "user8@example.com", passwordHash, avatarURL: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import { db } from "../client";
|
|||||||
export async function createUser(
|
export async function createUser(
|
||||||
name: string,
|
name: string,
|
||||||
username: string,
|
username: string,
|
||||||
|
email: string,
|
||||||
passwordHash: string,
|
passwordHash: string,
|
||||||
avatarURL?: string | null,
|
avatarURL?: string | null,
|
||||||
) {
|
) {
|
||||||
const [user] = await db.insert(User).values({ name, username, passwordHash, avatarURL }).returning();
|
const [user] = await db
|
||||||
|
.insert(User)
|
||||||
|
.values({ name, username, email, passwordHash, avatarURL })
|
||||||
|
.returning();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +26,11 @@ export async function getUserByUsername(username: string) {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserByEmail(email: string) {
|
||||||
|
const [user] = await db.select().from(User).where(eq(User.email, email));
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateById(
|
export async function updateById(
|
||||||
id: number,
|
id: number,
|
||||||
updates: {
|
updates: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { RegisterRequestSchema } from "@sprint/shared";
|
|||||||
import type { BunRequest } from "bun";
|
import type { BunRequest } from "bun";
|
||||||
import { buildAuthCookie, generateToken, hashPassword } from "../../auth/utils";
|
import { buildAuthCookie, generateToken, hashPassword } from "../../auth/utils";
|
||||||
import { createSession, createUser, getUserByUsername } from "../../db/queries";
|
import { createSession, createUser, getUserByUsername } from "../../db/queries";
|
||||||
|
import { getUserByEmail } from "../../db/queries/users";
|
||||||
import { errorResponse, parseJsonBody } from "../../validation";
|
import { errorResponse, parseJsonBody } from "../../validation";
|
||||||
|
|
||||||
export default async function register(req: BunRequest) {
|
export default async function register(req: BunRequest) {
|
||||||
@@ -12,15 +13,20 @@ export default async function register(req: BunRequest) {
|
|||||||
const parsed = await parseJsonBody(req, RegisterRequestSchema);
|
const parsed = await parseJsonBody(req, RegisterRequestSchema);
|
||||||
if ("error" in parsed) return parsed.error;
|
if ("error" in parsed) return parsed.error;
|
||||||
|
|
||||||
const { name, username, password, avatarURL } = parsed.data;
|
const { name, username, email, password, avatarURL } = parsed.data;
|
||||||
|
|
||||||
const existing = await getUserByUsername(username);
|
const existingUsername = await getUserByUsername(username);
|
||||||
if (existing) {
|
if (existingUsername) {
|
||||||
return errorResponse("username already taken", "USERNAME_TAKEN", 400);
|
return errorResponse("username already taken", "USERNAME_TAKEN", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingEmail = await getUserByEmail(email);
|
||||||
|
if (existingEmail) {
|
||||||
|
return errorResponse("email already registered", "EMAIL_TAKEN", 400);
|
||||||
|
}
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
const user = await createUser(name, username, passwordHash, avatarURL);
|
const user = await createUser(name, username, email, passwordHash, avatarURL);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return errorResponse("failed to create user", "USER_CREATE_ERROR", 500);
|
return errorResponse("failed to create user", "USER_CREATE_ERROR", 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,8 @@ async function handler(req: AuthedRequest) {
|
|||||||
const quantity = Math.max(1, totalMembers - 5);
|
const quantity = Math.max(1, totalMembers - 5);
|
||||||
const priceId = billingPeriod === "annual" ? STRIPE_PRICE_ANNUAL : STRIPE_PRICE_MONTHLY;
|
const priceId = billingPeriod === "annual" ? STRIPE_PRICE_ANNUAL : STRIPE_PRICE_MONTHLY;
|
||||||
|
|
||||||
// build customer data - use username as email if no email field exists
|
// use the user's email from the database
|
||||||
const customerEmail = user.username.includes("@")
|
const customerEmail = user.email;
|
||||||
? user.username
|
|
||||||
: `${user.username}@localhost.local`;
|
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await stripe.checkout.sessions.create({
|
||||||
customer_email: customerEmail,
|
customer_email: customerEmail,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */
|
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */
|
||||||
|
|
||||||
import { USER_NAME_MAX_LENGTH, USER_USERNAME_MAX_LENGTH } from "@sprint/shared";
|
import { USER_EMAIL_MAX_LENGTH, USER_NAME_MAX_LENGTH, USER_USERNAME_MAX_LENGTH } from "@sprint/shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
@@ -36,6 +36,7 @@ export default function LogInForm({
|
|||||||
|
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [avatarURL, setAvatarUrl] = useState<string | null>(null);
|
const [avatarURL, setAvatarUrl] = useState<string | null>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -75,7 +76,7 @@ export default function LogInForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const register = () => {
|
const register = () => {
|
||||||
if (name.trim() === "" || username.trim() === "" || password.trim() === "") {
|
if (name.trim() === "" || username.trim() === "" || email.trim() === "" || password.trim() === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ export default function LogInForm({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name,
|
name,
|
||||||
username,
|
username,
|
||||||
|
email,
|
||||||
password,
|
password,
|
||||||
avatarURL,
|
avatarURL,
|
||||||
}),
|
}),
|
||||||
@@ -129,6 +131,7 @@ export default function LogInForm({
|
|||||||
setError("");
|
setError("");
|
||||||
setSubmitAttempted(false);
|
setSubmitAttempted(false);
|
||||||
setAvatarUrl(null);
|
setAvatarUrl(null);
|
||||||
|
setEmail("");
|
||||||
requestAnimationFrame(() => focusFirstInput());
|
requestAnimationFrame(() => focusFirstInput());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,6 +252,15 @@ export default function LogInForm({
|
|||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
maxLength={USER_NAME_MAX_LENGTH}
|
maxLength={USER_NAME_MAX_LENGTH}
|
||||||
/>
|
/>
|
||||||
|
<Field
|
||||||
|
label="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)}
|
||||||
|
submitAttempted={submitAttempted}
|
||||||
|
spellcheck={false}
|
||||||
|
maxLength={USER_EMAIL_MAX_LENGTH}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Field
|
<Field
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ORG_NAME_MAX_LENGTH,
|
ORG_NAME_MAX_LENGTH,
|
||||||
ORG_SLUG_MAX_LENGTH,
|
ORG_SLUG_MAX_LENGTH,
|
||||||
PROJECT_NAME_MAX_LENGTH,
|
PROJECT_NAME_MAX_LENGTH,
|
||||||
|
USER_EMAIL_MAX_LENGTH,
|
||||||
USER_NAME_MAX_LENGTH,
|
USER_NAME_MAX_LENGTH,
|
||||||
USER_USERNAME_MAX_LENGTH,
|
USER_USERNAME_MAX_LENGTH,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
@@ -40,6 +41,7 @@ export const RegisterRequestSchema = z.object({
|
|||||||
.min(1, "Username is required")
|
.min(1, "Username is required")
|
||||||
.max(USER_USERNAME_MAX_LENGTH)
|
.max(USER_USERNAME_MAX_LENGTH)
|
||||||
.regex(/^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscores, and hyphens"),
|
.regex(/^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscores, and hyphens"),
|
||||||
|
email: z.string().min(1, "Email is required").email("Invalid email address").max(USER_EMAIL_MAX_LENGTH),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, "Password must be at least 8 characters")
|
.min(8, "Password must be at least 8 characters")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const USER_NAME_MAX_LENGTH = 64;
|
export const USER_NAME_MAX_LENGTH = 64;
|
||||||
export const USER_USERNAME_MAX_LENGTH = 32;
|
export const USER_USERNAME_MAX_LENGTH = 32;
|
||||||
|
export const USER_EMAIL_MAX_LENGTH = 256;
|
||||||
|
|
||||||
export const ORG_NAME_MAX_LENGTH = 64;
|
export const ORG_NAME_MAX_LENGTH = 64;
|
||||||
export const ORG_DESCRIPTION_MAX_LENGTH = 1024;
|
export const ORG_DESCRIPTION_MAX_LENGTH = 1024;
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export {
|
|||||||
PROJECT_DESCRIPTION_MAX_LENGTH,
|
PROJECT_DESCRIPTION_MAX_LENGTH,
|
||||||
PROJECT_NAME_MAX_LENGTH,
|
PROJECT_NAME_MAX_LENGTH,
|
||||||
PROJECT_SLUG_MAX_LENGTH,
|
PROJECT_SLUG_MAX_LENGTH,
|
||||||
|
USER_EMAIL_MAX_LENGTH,
|
||||||
USER_NAME_MAX_LENGTH,
|
USER_NAME_MAX_LENGTH,
|
||||||
USER_USERNAME_MAX_LENGTH,
|
USER_USERNAME_MAX_LENGTH,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ORG_NAME_MAX_LENGTH,
|
ORG_NAME_MAX_LENGTH,
|
||||||
ORG_SLUG_MAX_LENGTH,
|
ORG_SLUG_MAX_LENGTH,
|
||||||
PROJECT_NAME_MAX_LENGTH,
|
PROJECT_NAME_MAX_LENGTH,
|
||||||
|
USER_EMAIL_MAX_LENGTH,
|
||||||
USER_NAME_MAX_LENGTH,
|
USER_NAME_MAX_LENGTH,
|
||||||
USER_USERNAME_MAX_LENGTH,
|
USER_USERNAME_MAX_LENGTH,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
@@ -56,6 +57,7 @@ export const User = pgTable("User", {
|
|||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
name: varchar({ length: USER_NAME_MAX_LENGTH }).notNull(),
|
name: varchar({ length: USER_NAME_MAX_LENGTH }).notNull(),
|
||||||
username: varchar({ length: USER_USERNAME_MAX_LENGTH }).notNull().unique(),
|
username: varchar({ length: USER_USERNAME_MAX_LENGTH }).notNull().unique(),
|
||||||
|
email: varchar({ length: USER_EMAIL_MAX_LENGTH }).notNull().unique(),
|
||||||
passwordHash: varchar({ length: 255 }).notNull(),
|
passwordHash: varchar({ length: 255 }).notNull(),
|
||||||
avatarURL: varchar({ length: 512 }),
|
avatarURL: varchar({ length: 512 }),
|
||||||
iconPreference: varchar({ length: 10 }).notNull().default("pixel").$type<IconStyle>(),
|
iconPreference: varchar({ length: 10 }).notNull().default("pixel").$type<IconStyle>(),
|
||||||
|
|||||||
Reference in New Issue
Block a user