Profiles page is donegit add -A!

This commit is contained in:
2025-09-04 14:23:24 -05:00
parent 500da1f8be
commit 56ea3e0904
13 changed files with 539 additions and 172 deletions

View File

@@ -1,5 +1,6 @@
import { ConvexError } from 'convex/values';
import { Password } from '@convex-dev/auth/providers/Password';
import { validatePassword } from './auth';
import type { DataModel } from './_generated/dataModel';
export default Password<DataModel>({
@@ -10,12 +11,7 @@ export default Password<DataModel>({
};
},
validatePasswordRequirements: (password: string) => {
if (
password.length < 8 ||
!/\d/.test(password) ||
!/[a-z]/.test(password) ||
!/[A-Z]/.test(password)
) {
if (!validatePassword(password)) {
throw new ConvexError('Invalid password.');
}
},

View File

@@ -17,7 +17,7 @@ import type * as CustomPassword from "../CustomPassword.js";
import type * as auth from "../auth.js";
import type * as files from "../files.js";
import type * as http from "../http.js";
import type * as myFunctions from "../myFunctions.js";
import type * as statuses from "../statuses.js";
/**
* A utility for referencing Convex functions in your app's API.
@@ -32,7 +32,7 @@ declare const fullApi: ApiFromModules<{
auth: typeof auth;
files: typeof files;
http: typeof http;
myFunctions: typeof myFunctions;
statuses: typeof statuses;
}>;
export declare const api: FilterApi<
typeof fullApi,

View File

@@ -1,7 +1,13 @@
import { ConvexError, v } from 'convex/values';
import { convexAuth, getAuthUserId } from '@convex-dev/auth/server';
import { mutation, query } from './_generated/server';
import {
convexAuth,
getAuthUserId,
retrieveAccount,
modifyAccountCredentials,
} from '@convex-dev/auth/server';
import { api } from './_generated/api';
import { type Id } from './_generated/dataModel';
import { action, mutation, query } from './_generated/server';
import Password from './CustomPassword';
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
@@ -69,3 +75,44 @@ export const updateUserImage = mutation({
return { success: true };
},
});
export const validatePassword = (password: string): boolean => {
if (
password.length < 8 ||
password.length > 100 ||
!/\d/.test(password) ||
!/[a-z]/.test(password) ||
!/[A-Z]/.test(password)
) {
return false;
}
return true;
};
export const updateUserPassword = action({
args: {
currentPassword: v.string(),
newPassword: v.string(),
},
handler: async (ctx, {currentPassword, newPassword}) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
const user = await ctx.runQuery(api.auth.getUser);
if (!user?.email) throw new ConvexError('User not found.');
const verified = await retrieveAccount(ctx, {
provider: 'password',
account: { id: user.email, secret: currentPassword }
});
if (!verified) throw new ConvexError('Current password is incorrect.');
if (!validatePassword(newPassword))
throw new ConvexError('Invalid password.');
await modifyAccountCredentials(ctx, {
provider: 'password',
account: { id: user.email, secret: newPassword }
});
return { success: true };
},
})

View File

@@ -1,81 +0,0 @@
import { v } from 'convex/values';
import { query, mutation, action } from './_generated/server';
import { api } from './_generated/api';
import { getAuthUserId } from '@convex-dev/auth/server';
// Write your Convex functions in any file inside this directory (`convex`).
// See https://docs.convex.dev/functions for more.
// You can read data from the database via a query:
export const listNumbers = query({
// Validators for arguments.
args: {
count: v.number(),
},
// Query implementation.
handler: async (ctx, args) => {
//// Read the database as many times as you need here.
//// See https://docs.convex.dev/database/reading-data.
const numbers = await ctx.db
.query('numbers')
// Ordered by _creationTime, return most recent
.order('desc')
.take(args.count);
const userId = await getAuthUserId(ctx);
const user = userId === null ? null : await ctx.db.get(userId);
return {
viewer: user?.email ?? null,
numbers: numbers.reverse().map((number) => number.value),
};
},
});
// You can write data to the database via a mutation:
export const addNumber = mutation({
// Validators for arguments.
args: {
value: v.number(),
},
// Mutation implementation.
handler: async (ctx, args) => {
//// Insert or modify documents in the database here.
//// Mutations can also read from the database like queries.
//// See https://docs.convex.dev/database/writing-data.
const id = await ctx.db.insert('numbers', { value: args.value });
console.log('Added new document with id:', id);
// Optionally, return a value from your mutation.
// return id;
},
});
// You can fetch data from and send data to third-party APIs via an action:
export const myAction = action({
// Validators for arguments.
args: {
first: v.number(),
second: v.string(),
},
// Action implementation.
handler: async (ctx, args) => {
//// Use the browser-like `fetch` API to send HTTP requests.
//// See https://docs.convex.dev/functions/actions#calling-third-party-apis-and-using-npm-packages.
// const response = await ctx.fetch("https://api.thirdpartyservice.com");
// const data = await response.json();
//// Query data by running Convex queries.
const data = await ctx.runQuery(api.myFunctions.listNumbers, {
count: 10,
});
console.log(data);
//// Write data by running Convex mutations.
await ctx.runMutation(api.myFunctions.addNumber, {
value: args.first,
});
},
});

View File

@@ -7,7 +7,24 @@ import { authTables } from '@convex-dev/auth/server';
// The schema provides more precise TypeScript types.
export default defineSchema({
...authTables,
numbers: defineTable({
value: v.number(),
}),
users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
currentStatusId: v.optional(v.id('statuses')),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
})
.index("email", ["email"])
.index("phone", ["phone"]),
statuses: defineTable({
userId: v.id('users'),
message: v.string(),
updatedAt: v.number(),
updatedBy: v.id('users'),
})
.index('by_user', ['userId'])
.index('by_user_updatedAt', ['userId', 'updatedAt']),
});

202
convex/statuses.ts Normal file
View File

@@ -0,0 +1,202 @@
import { ConvexError, v } from 'convex/values';
import { getAuthUserId } from '@convex-dev/auth/server';
import { mutation, query } from './_generated/server';
import type { Doc, Id } from './_generated/dataModel';
import { paginationOptsValidator } from 'convex/server';
// NEW: import ctx and data model types
import type { MutationCtx, QueryCtx } from './_generated/server';
// NEW: shared ctx type for helpers
type RWCtx = MutationCtx | QueryCtx;
// CHANGED: typed helpers
const ensureUser = async (
ctx: RWCtx,
userId: Id<'users'>,
) => {
const user = await ctx.db.get(userId);
if (!user) throw new ConvexError('User not found.');
return user;
};
const latestStatusForOwner = async (
ctx: RWCtx,
ownerId: Id<'users'>,
) => {
const [latest] = await ctx.db
.query('statuses')
.withIndex('by_user_updatedAt', q => q.eq('userId', ownerId))
.order('desc')
.take(1);
return latest as Doc<'statuses'> | null;
};
/**
* Create a new status for a single user.
* - Defaults userId to the caller.
* - updatedBy defaults to the caller.
* - Updates the user's currentStatusId pointer.
*/
export const create = mutation({
args: {
message: v.string(),
userId: v.optional(v.id('users')),
updatedBy: v.optional(v.id('users')),
},
handler: async (ctx, args) => {
const authUserId = await getAuthUserId(ctx);
if (!authUserId) throw new ConvexError('Not authenticated.');
const userId = args.userId ?? authUserId;
await ensureUser(ctx, userId);
const updatedBy = args.updatedBy ?? authUserId;
await ensureUser(ctx, updatedBy);
const message = args.message.trim();
if (message.length === 0) {
throw new ConvexError('Message cannot be empty.');
}
const statusId = await ctx.db.insert('statuses', {
message,
userId,
updatedBy,
updatedAt: Date.now(),
});
await ctx.db.patch(userId, { currentStatusId: statusId });
return { statusId };
},
});
/**
* Bulk create the same status for many users.
* - updatedBy defaults to the caller.
* - Updates each user's currentStatusId pointer.
*/
export const bulkCreate = mutation({
args: {
message: v.string(),
ownerIds: v.array(v.id('users')),
updatedBy: v.optional(v.id('users')),
},
handler: async (ctx, args) => {
const authUserId = await getAuthUserId(ctx);
if (!authUserId) throw new ConvexError('Not authenticated.');
if (args.ownerIds.length === 0) return { statusIds: [] };
const updatedBy = args.updatedBy ?? authUserId;
await ensureUser(ctx, updatedBy);
const message = args.message.trim();
if (message.length === 0) {
throw new ConvexError('Message cannot be empty.');
}
const statusIds: Id<'statuses'>[] = [];
const now = Date.now();
// Sequential to keep load predictable; switch to Promise.all
// if your ownerIds lists are small and bounded.
for (const userId of args.ownerIds) {
await ensureUser(ctx, userId);
const statusId = await ctx.db.insert('statuses', {
message,
userId,
updatedBy,
updatedAt: now,
});
await ctx.db.patch(userId, { currentStatusId: statusId });
statusIds.push(statusId);
}
return { statusIds };
},
});
/**
* Current status for a specific user.
* - Uses users.currentStatusId if present,
* otherwise falls back to latest by index.
*/
export const getCurrentForUser = query({
args: { userId: v.id('users') },
handler: async (ctx, { userId }) => {
const user = await ensureUser(ctx, userId);
if (user.currentStatusId) {
const status = await ctx.db.get(user.currentStatusId);
if (status) return status;
}
return await latestStatusForOwner(ctx, userId);
},
});
/**
* Current statuses for all users.
* - Reads each user's currentStatusId pointer.
* - Falls back to latest-by-index if pointer is missing.
*/
export const getCurrentForAll = query({
args: {},
handler: async (ctx) => {
const users = await ctx.db.query('users').collect();
const results = await Promise.all(
users.map(async (u) => {
let status = null;
if (u.currentStatusId) {
status = await ctx.db.get(u.currentStatusId);
}
status ??= await latestStatusForOwner(ctx, u._id);
return {
userId: u._id as Id<'users'>,
status,
};
}),
);
return results;
},
});
/**
* Paginated history for a specific user (newest first).
*/
export const listHistoryByUser = query({
args: {
userId: v.id('users'),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, { userId, paginationOpts }) => {
await ensureUser(ctx, userId);
return await ctx.db
.query('statuses')
.withIndex('by_user_updatedAt', q => q.eq('userId', userId))
.order('desc')
.paginate(paginationOpts);
},
});
/**
* Global paginated history (all users, newest first).
* - Add an index on updatedAt if you want to avoid full-table scans
* when the collection grows large.
*/
export const listHistoryAll = query({
args: { paginationOpts: paginationOptsValidator },
handler: async (ctx, { paginationOpts }) => {
return await ctx.db
.query('statuses')
.order('desc')
.paginate(paginationOpts);
},
});