feat: implement user authentication with login and register

- Add login screen with email/username support
- Add register screen with email validation
- Implement AuthStore with expo-secure-store (native) / localStorage (web)
- Add X-User-Id header authentication (simple auth without JWT)
- Rename displayName to userName across codebase
- Add findByUserName() to UserRepository
- Check for existing email AND username on registration
- Add AuthButton component with shadow effect
- Add logout button to Header
- Add hash-password.js utility script for manual password resets
- Update CORS to allow X-User-Id header
This commit is contained in:
2026-01-10 20:07:35 +01:00
parent 71f84d1cc7
commit 8efe6c304e
20 changed files with 468 additions and 108 deletions

View File

@@ -1,23 +1,38 @@
import { User } from "@caldav/shared";
import { UserRepository, CreateUserData } from "../../services/interfaces";
import { Logged } from "../../logging";
import { UserModel } from "./models";
import { UserModel, UserDocument } from "./models";
function toUser(doc: UserDocument): User {
return {
id: doc._id.toString(),
email: doc.email,
userName: doc.userName,
passwordHash: doc.passwordHash,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
}
@Logged("MongoUserRepository")
export class MongoUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
throw new Error("Not implemented");
const user = await UserModel.findById(id);
return user ? toUser(user) : null;
}
async findByEmail(email: string): Promise<User | null> {
const user = await UserModel.findOne({ email: email.toLowerCase() });
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
return (user?.toJSON() as unknown as User) ?? null;
return user ? toUser(user) : null;
}
async findByUserName(userName: string): Promise<User | null> {
const user = await UserModel.findOne({ userName });
return user ? toUser(user) : null;
}
async create(data: CreateUserData): Promise<User> {
const user = await UserModel.create(data);
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
return user.toJSON() as unknown as User;
return toUser(user);
}
}

View File

@@ -21,7 +21,7 @@ const UserSchema = new Schema<
lowercase: true,
trim: true,
},
displayName: {
userName: {
type: String,
required: true,
trim: true,