Profiles page is donegit add -A!
This commit is contained in:
@@ -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.');
|
||||
}
|
||||
},
|
||||
|
4
convex/_generated/api.d.ts
vendored
4
convex/_generated/api.d.ts
vendored
@@ -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,
|
||||
|
@@ -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 };
|
||||
},
|
||||
})
|
||||
|
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
@@ -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
202
convex/statuses.ts
Normal 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);
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user