- Error boundary with Macedonian fallback screen and retry button wrapping all navigation - Pull-to-refresh on all list/detail screens (home, explore, posts, chat, profile, ad detail, post detail) - Accessibility: labels on all interactive elements, roles on Pressable/Button, selectable text for user data - Deep linking: mojmajstor:// scheme configured in app.json, ad/[id] and chat/[id] routes work via Expo Router auto-routing - Pagination: cursor-based paginated queries for ads, posts, messages, chats, reviews with Load More buttons on frontend - Profile: shows customer posts with PostCard, loading/error states, create-post navigation - Auth error messages in Macedonian (Невалидни акредитиви, итн.) All UI text in Macedonian. TypeScript clean, Convex functions deployed.
118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import { v } from "convex/values";
|
||
import { query, mutation, action } from "./_generated/server";
|
||
|
||
export const getCurrentUser = query({
|
||
args: { token: v.optional(v.string()) },
|
||
handler: async (ctx, args) => {
|
||
if (!args.token) return null;
|
||
const session = await ctx.db
|
||
.query("sessions")
|
||
.withIndex("by_token", (q) => q.eq("token", args.token!))
|
||
.first();
|
||
if (!session) return null;
|
||
|
||
const user = await ctx.db.get(session.userId);
|
||
return user || null;
|
||
},
|
||
});
|
||
|
||
export const login = mutation({
|
||
args: { email: v.string(), passwordHash: v.string() },
|
||
handler: async (ctx, args) => {
|
||
const account = await ctx.db
|
||
.query("accounts")
|
||
.withIndex("by_provider_email", (q) =>
|
||
q.eq("provider", "password").eq("providerId", args.email)
|
||
)
|
||
.first();
|
||
|
||
if (!account) throw new Error("Погрешна е-пошта или лозинка");
|
||
|
||
const user = await ctx.db.get(account.userId);
|
||
if (!user) throw new Error("Корисникот не е пронајден");
|
||
|
||
if (account.secret !== args.passwordHash) throw new Error("Погрешна е-пошта или лозинка");
|
||
|
||
const token = crypto.randomUUID();
|
||
await ctx.db.insert("sessions", {
|
||
userId: user._id,
|
||
token,
|
||
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||
createdAt: Date.now(),
|
||
});
|
||
|
||
return { token, user };
|
||
},
|
||
});
|
||
|
||
export const register = mutation({
|
||
args: {
|
||
name: v.string(),
|
||
email: v.string(),
|
||
passwordHash: v.string(),
|
||
role: v.string(),
|
||
phone: v.optional(v.string()),
|
||
},
|
||
handler: async (ctx, args) => {
|
||
const existing = await ctx.db
|
||
.query("accounts")
|
||
.withIndex("by_provider_email", (q) =>
|
||
q.eq("provider", "password").eq("providerId", args.email)
|
||
)
|
||
.first();
|
||
|
||
if (existing) throw new Error("Е-поштата е веќе регистрирана");
|
||
|
||
const userId = await ctx.db.insert("users", {
|
||
name: args.name,
|
||
email: args.email,
|
||
phone: args.phone,
|
||
role: args.role,
|
||
reviewCount: 0,
|
||
createdAt: Date.now(),
|
||
});
|
||
|
||
await ctx.db.insert("accounts", {
|
||
userId,
|
||
provider: "password",
|
||
providerId: args.email,
|
||
secret: args.passwordHash,
|
||
createdAt: Date.now(),
|
||
});
|
||
|
||
const token = crypto.randomUUID();
|
||
await ctx.db.insert("sessions", {
|
||
userId,
|
||
token,
|
||
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||
createdAt: Date.now(),
|
||
});
|
||
|
||
const user = await ctx.db.get(userId);
|
||
return { token, user };
|
||
},
|
||
});
|
||
|
||
export const logout = mutation({
|
||
args: { token: v.string() },
|
||
handler: async (ctx, args) => {
|
||
const session = await ctx.db
|
||
.query("sessions")
|
||
.withIndex("by_token", (q) => q.eq("token", args.token))
|
||
.first();
|
||
if (session) {
|
||
await ctx.db.delete(session._id);
|
||
}
|
||
},
|
||
});
|
||
|
||
export const hashPassword = action({
|
||
args: { password: v.string() },
|
||
handler: async (_ctx, args) => {
|
||
const encoder = new TextEncoder();
|
||
const data = encoder.encode(args.password);
|
||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
||
},
|
||
}); |