From 777e650789e9d5c1274a22c943c1fb1d2daa4d87 Mon Sep 17 00:00:00 2001 From: victorzarzar Date: Thu, 12 Mar 2026 15:51:12 -0300 Subject: [PATCH] Chore: Remake --- README.md | 85 ++++++- bun.lock | 30 +-- drizzle/0001_nifty_alice.sql | 8 + drizzle/meta/0001_snapshot.json | 221 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/migrations.js | 2 + package.json | 9 +- src/app/(auth)/forgot-password.tsx | 7 +- src/app/(auth)/reset-password.tsx | 7 +- src/app/(auth)/signin.tsx | 7 +- src/app/(auth)/signup.tsx | 7 +- .../components/forgot-password-form.tsx | 22 +- src/shared/components/reset-password-form.tsx | 82 ++++--- src/shared/components/sign-up-form.tsx | 32 ++- src/shared/components/ui/alert.tsx | 85 +++++++ src/shared/db/schema.ts | 7 + src/shared/services/userService.ts | 61 ++++- 17 files changed, 585 insertions(+), 94 deletions(-) create mode 100644 drizzle/0001_nifty_alice.sql create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 src/shared/components/ui/alert.tsx diff --git a/README.md b/README.md index 0ce1149..28112a4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- React Native CRUD + React Native Basic Auth

React Native @@ -13,9 +13,11 @@ Bun

- A full-featured CRUD application built with React Native, Expo Router, NativeWind, Drizzle ORM, and SQLite. Create, read, update, and delete records with a clean, production-ready mobile interface. + A starter boilerplate for authentication in React Native, built with Expo Router, NativeWind, Drizzle ORM, and SQLite. Includes sign up, sign in, sign out, forgot password, and reset password flows — designed as a solid foundation to build on, not a production-ready solution out of the box.

+> ⚠️ **This is a starting point.** Before shipping to production, review the [Security Considerations](#security-considerations) section below. Several intentional simplifications were made to keep this boilerplate approachable. + ---

@@ -30,6 +32,66 @@ --- +

+ Security Considerations +

+ +This project is intentionally simplified. Before using it as a base for a real-world app, you should address the following: + +### Password Hashing + +Currently, passwords are hashed using `Crypto.CryptoDigestAlgorithm.SHA256` via `expo-crypto`. **SHA-256 is not suitable for password hashing in production** — it is fast by design, which makes brute-force attacks trivial. + +> The `argon2` npm package is **not compatible with React Native** as it relies on Node.js native bindings (C++). Use one of the alternatives below instead. + +**Option 1 — `react-native-argon2`** (recommended, requires `expo prebuild` — exits Expo Go): + +```bash +bun add react-native-argon2 +bun run prebuild +``` + +```ts +import Argon2 from "react-native-argon2"; + +async function hashPassword(password: string, salt: string): Promise { + const { rawHash } = await Argon2.hash(password, salt, { mode: "argon2id" }); + return rawHash; +} +``` + +**Option 2 — `expo-crypto` with SHA-512 + salt** (no eject required, stays in Expo Go): + +```ts +import * as Crypto from "expo-crypto"; + +async function hashPassword(password: string, salt: string): Promise { + return Crypto.digestStringAsync( + Crypto.CryptoDigestAlgorithm.SHA512, + salt + password, + ); +} +``` + +> SHA-512 is still not ideal for password hashing, but significantly better than SHA-256 when combined with a unique per-user salt stored alongside the hash. + +**Option 3 — Backend authentication** (most secure for production): +Move auth entirely to a server (Node.js, Go, etc.) where Argon2id runs natively, and have the app communicate via HTTPS. Services like [Supabase](https://supabase.com) or [Firebase Auth](https://firebase.google.com/products/auth) handle this out of the box. + +> Argon2id is the current recommendation from [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) and [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106). + +### Other Areas to Improve Before Production + +- **Password reset tokens** — currently passed via route params (visible in navigation). Consider storing them only server-side or using a deeper link strategy. +- **Session management** — sessions are stored in SQLite with no expiry. Add a `expiresAt` column and invalidate stale sessions. +- **Input validation** — add stricter password rules (min length, complexity) via Zod schemas. +- **Rate limiting** — no protection against brute-force login attempts exists in a local-first setup; consider adding attempt counters. +- **Token expiry** — reset tokens expire after 15 minutes by default; adjust as needed for your use case. +- **No email verification** — there is no email confirmation step on sign up. +- **Local-only** — this project uses SQLite with no backend. For multi-device or server-side auth, you will need to integrate a backend (e.g., Supabase, Firebase, or a custom API). + +--- +

Installation & Setup

@@ -37,8 +99,8 @@ ### 1. Clone the Repository ```bash -git clone https://github.com/Victor-Zarzar/react-native-crud -cd react-native-crud +git clone https://github.com/Victor-Zarzar/react-native-basic-auth +cd react-native-basic-auth ``` ### 2. Install Dependencies @@ -80,6 +142,7 @@ Open the app: | `bun run ios` | Start on iOS Simulator | | `bun run web` | Start on Web | | `bun run db:generate` | Generate Drizzle ORM migration files | +| `bun run upgrade-deps` | Fix and align dependencies with Expo SDK | | `bun run prebuild` | Rebuild native directories with Expo Prebuild | | `bun run ios:native` | Run native iOS build | | `bun run android:native` | Run native Android build | @@ -100,9 +163,9 @@ Open the app: - **Expo Router** – File-based routing - **TypeScript** – Type-safe development - **NativeWind** – Tailwind CSS for React Native -- **SQLite (Expo SQLite)** – Local persistent storage for CRUD operations +- **SQLite (Expo SQLite)** – Local persistent storage - **Drizzle ORM** – Type-safe ORM for SQLite with migration support -- **Expo Crypto** – Cryptographic utilities for secure ID generation and hashing +- **Expo Crypto** – Cryptographic utilities (SHA-256 for dev; replace with Argon2id for prod) - **React Native Reusables** – Accessible UI component system --- @@ -111,11 +174,11 @@ Open the app: Key Features -- Full CRUD operations — Create, Read, Update, and Delete records +- Complete auth flow — Sign Up, Sign In, Sign Out, Forgot Password, Reset Password - Local data persistence with SQLite via Expo SQLite - Type-safe database queries with Drizzle ORM - Drizzle Studio integration via `expo-drizzle-studio-plugin` for database inspection during development -- Secure ID generation with Expo Crypto +- Password hashing with Expo Crypto (SHA-256 — see [Security Considerations](#security-considerations) for production upgrade path) - Production-ready scalable structure - File-based routing with Expo Router - Dark mode support @@ -146,7 +209,7 @@ Before starting, ensure you have: ``` -react-native-crud/ +react-native-basic-auth/ ├── .expo/ # Expo cache and config ├── .github/ # GitHub Actions & workflows ├── assets/ # Images and fonts @@ -251,7 +314,7 @@ With the development server running (`bun run dev`), press `Shift + M` in the te

- SQL Drizze Studio + SQL Drizzle Studio


@@ -316,6 +379,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file Victor Zarzar - [@Victor-Zarzar](https://github.com/Victor-Zarzar) -Project Link: [https://github.com/Victor-Zarzar/react-native-crud](https://github.com/Victor-Zarzar/react-native-crud) +Project Link: [https://github.com/Victor-Zarzar/react-native-basic-auth](https://github.com/Victor-Zarzar/react-native-basic-auth) --- diff --git a/bun.lock b/bun.lock index cca7f0c..30f07b8 100644 --- a/bun.lock +++ b/bun.lock @@ -20,13 +20,13 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", - "expo": "~55.0.4", + "expo": "~55.0.5", "expo-constants": "~55.0.7", - "expo-crypto": "55.0.8", + "expo-crypto": "~55.0.9", "expo-drizzle-studio-plugin": "^0.2.1", "expo-linking": "~55.0.7", "expo-localization": "~55.0.8", - "expo-router": "~55.0.3", + "expo-router": "~55.0.5", "expo-splash-screen": "~55.0.10", "expo-sqlite": "^55.0.10", "expo-status-bar": "~55.0.4", @@ -328,7 +328,7 @@ "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.24", "", {}, "sha512-1bJ63Yv2Bn8SN2MjrlbwLwUhnC8COOeejd15H88WjCtw5iNErqEPaBnpvmYyqciVYwudGo5drUIdY9C/5yPGbg=="], - "@expo/cli": ["@expo/cli@55.0.14", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.12", "@expo/json-file": "^10.0.12", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "~55.0.9", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.3", "@expo/plist": "^0.5.2", "@expo/prebuild-config": "^55.0.8", "@expo/require-utils": "^55.0.2", "@expo/router-server": "^55.0.9", "@expo/schema-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.2", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.3", "expo-server": "^55.0.6", "fetch-nodeshim": "^0.4.6", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.0", "multitars": "^0.2.3", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-glXPSjjLCIz+KX/ezqLTGIF9eTE1lexiCxunvB3loRZNnGeBDGW3eF++cuPKudW26jeC6bqZkcqBG7Lp0Sp9qg=="], + "@expo/cli": ["@expo/cli@55.0.16", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.12", "@expo/json-file": "^10.0.12", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "~55.0.9", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.3", "@expo/plist": "^0.5.2", "@expo/prebuild-config": "^55.0.8", "@expo/require-utils": "^55.0.2", "@expo/router-server": "^55.0.10", "@expo/schema-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.2", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.3", "expo-server": "^55.0.6", "fetch-nodeshim": "^0.4.6", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.0", "multitars": "^0.2.3", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-rp1mBnA5msGDPTfFuqVl+9RsJOtuA0cXsWSJpHdvsIxcSVg0oJyF/rgvrwsFrNQCLXzkMXm+o3CsY9iL1D/CDA=="], "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.6", "", { "dependencies": { "node-forge": "^1.3.3" } }, "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w=="], @@ -346,7 +346,7 @@ "@expo/env": ["@expo/env@2.1.1", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg=="], - "@expo/fingerprint": ["@expo/fingerprint@0.16.5", "", { "dependencies": { "@expo/env": "^2.0.11", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-mLrcymtgkW9IJ/G1e8MH1Xt2VIb1MOS86ePY0ePcnV3nVyJqm7gfa/AXD1Hk+eZXvf8XhioYz6QZaamBdEzR3A=="], + "@expo/fingerprint": ["@expo/fingerprint@0.16.6", "", { "dependencies": { "@expo/env": "^2.0.11", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-nRITNbnu3RKSHPvKVehrSU4KG2VY9V8nvULOHBw98ukHCAU4bGrU5APvcblOkX3JAap+xEHsg/mZvqlvkLInmQ=="], "@expo/image-utils": ["@expo/image-utils@0.8.12", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A=="], @@ -372,7 +372,7 @@ "@expo/require-utils": ["@expo/require-utils@55.0.2", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0" }, "optionalPeers": ["typescript"] }, "sha512-dV5oCShQ1umKBKagMMT4B/N+SREsQe3lU4Zgmko5AO0rxKV0tynZT6xXs+e2JxuqT4Rz997atg7pki0BnZb4uw=="], - "@expo/router-server": ["@expo/router-server@55.0.9", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "^55.0.6", "expo": "*", "expo-constants": "^55.0.7", "expo-font": "^55.0.4", "expo-router": "*", "expo-server": "^55.0.6", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["@expo/metro-runtime", "expo-router", "react-dom", "react-server-dom-webpack"] }, "sha512-LcCFi+P1qfZOsw0DO4JwNKRxtWt4u2bjTYj0PUe4WVf9NVG/NfUetAXYRbBS6P+gupfM6SC+/bdzdqCWQh7j8g=="], + "@expo/router-server": ["@expo/router-server@55.0.10", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "^55.0.6", "expo": "*", "expo-constants": "^55.0.7", "expo-font": "^55.0.4", "expo-router": "*", "expo-server": "^55.0.6", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["@expo/metro-runtime", "expo-router", "react-dom", "react-server-dom-webpack"] }, "sha512-NZQzHwkaedufNPayVfPxsZGEMngOD3gDvYx9lld4sitRexrKDx5sHmmNHi6IByGbmCb4jwLXub5sIyWh6z1xPQ=="], "@expo/schema-utils": ["@expo/schema-utils@55.0.2", "", {}, "sha512-QZ5WKbJOWkCrMq0/kfhV9ry8te/OaS34YgLVpG8u9y2gix96TlpRTbxM/YATjNcUR2s4fiQmPCOxkGtog4i37g=="], @@ -652,7 +652,7 @@ "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], - "babel-preset-expo": ["babel-preset-expo@55.0.10", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.2", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.2", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-aRtW7qJKohGU2V0LUJ6IeP7py3+kVUo9zcc8+v1Kix8jGGuIvqvpo9S6W1Fmn9VFP2DBwkFDLiyzkCZS85urVA=="], + "babel-preset-expo": ["babel-preset-expo@55.0.11", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.2", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.4", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-ti8t4xufD6gUQQh+qY+b+VT/1zyA0n1PBnwOzCkPUyEDiIVBpaOixR+BzVH68hqu9mH2wDfzoFuGgv+2LfRdqw=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], @@ -824,13 +824,13 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "expo": ["expo@55.0.4", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.14", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devtools": "55.0.2", "@expo/fingerprint": "0.16.5", "@expo/local-build-cache-provider": "55.0.6", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "55.0.9", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.10", "expo-asset": "~55.0.8", "expo-constants": "~55.0.7", "expo-file-system": "~55.0.10", "expo-font": "~55.0.4", "expo-keep-awake": "~55.0.4", "expo-modules-autolinking": "55.0.8", "expo-modules-core": "55.0.13", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.1" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-cbQBPYwmH6FRvh942KR8mSdEcrVdsIMkjdHthtf59zlpzgrk28FabhOdL/Pc9WuS+CsIP3EIQbZqmLkTjv6qPg=="], + "expo": ["expo@55.0.6", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.16", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devtools": "55.0.2", "@expo/fingerprint": "0.16.6", "@expo/local-build-cache-provider": "55.0.6", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "55.0.9", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.11", "expo-asset": "~55.0.8", "expo-constants": "~55.0.7", "expo-file-system": "~55.0.10", "expo-font": "~55.0.4", "expo-keep-awake": "~55.0.4", "expo-modules-autolinking": "55.0.9", "expo-modules-core": "55.0.15", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.1" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-gaF8bh5beWmrptz3d4Gr138CiPoLJtzjNbqNSOQ8kdQm3wMW8lJGT1dsY5NPJTZ7MNJBTN+pcRwshr4BMK4OiA=="], "expo-asset": ["expo-asset@55.0.8", "", { "dependencies": { "@expo/image-utils": "^0.8.12", "expo-constants": "~55.0.7" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-yEz2svDX67R0yiW2skx6dJmcE0q7sj9ECpGMcxBExMCbctc+nMoZCnjUuhzPl5vhClUsO5HFFXS5vIGmf1bgHQ=="], "expo-constants": ["expo-constants@55.0.7", "", { "dependencies": { "@expo/config": "~55.0.8", "@expo/env": "~2.1.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-kdcO4TsQRRqt0USvjaY5vgQMO9H52K3kBZ/ejC7F6rz70mv08GoowrZ1CYOr5O4JpPDRlIpQfZJUucaS/c+KWQ=="], - "expo-crypto": ["expo-crypto@55.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-s4G4l1gRgSv3rS4JeKbybXzobWFUcO/B1F6nffjtoGS4WWU5+DNrmbiMnF9mvW21/ozYKtUu21u3pl2+7cGWnw=="], + "expo-crypto": ["expo-crypto@55.0.9", "", { "peerDependencies": { "expo": "*" } }, "sha512-hYiZYRPMXGQXSgKjp/m84l/6Uq8mTeMts1C7bFZXN5M5TUOiRhrLeqMSYZFXrAlkFpXeO46V+Ts1CFauMBLuCw=="], "expo-drizzle-studio-plugin": ["expo-drizzle-studio-plugin@0.2.1", "", { "peerDependencies": { "expo": ">=53.0.5", "expo-sqlite": ">=15.2.9" } }, "sha512-AjMC7SOutMAv/MkeJSp/26gnHLAA17SG8EtsjIKW6tDcBkq4plDVhjMLusoGe9f0vzYK8KrOvWjEWZ/R+KvIGg=="], @@ -838,9 +838,9 @@ "expo-font": ["expo-font@55.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-ZKeGTFffPygvY5dM/9ATM2p7QDkhsaHopH7wFAWgP2lKzqUMS9B/RxCvw5CaObr9Ro7x9YptyeRKX2HmgmMfrg=="], - "expo-glass-effect": ["expo-glass-effect@55.0.7", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-G7Q9rUaEY0YC36fGE6irDljfsfvzz/y49zagARAKvSJSyQMUSrhR25WOr5LK5Cw7gQNNBEy9U1ctlr7yCay/fQ=="], + "expo-glass-effect": ["expo-glass-effect@55.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-IvUjHb/4t6r2H/LXDjcQ4uDoHrmO2cLOvEb9leLavQ4HX5+P4LRtQrMDMlkWAn5Wo5DkLcG8+1CrQU2nqgogTA=="], - "expo-image": ["expo-image@55.0.5", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-oejmMwy5O9EtC8po9NxkcurWHqND6p8xuJaj9FGNo8NXLt9e+w3cKWx7HuPzkH5y3qFXQ9Od+z+I/wxEci36fw=="], + "expo-image": ["expo-image@55.0.6", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-TKuu0uBmgTZlhd91Glv+V4vSBMlfl0bdQxfl97oKKZUo3OBC13l3eLik7v3VNLJN7PZbiwOAiXkZkqSOBx/Xsw=="], "expo-keep-awake": ["expo-keep-awake@55.0.4", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-vwfdMtMS5Fxaon8gC0AiE70SpxTsHJ+rjeoVJl8kdfdbxczF7OIaVmfjFJ5Gfigd/WZiLqxhfZk34VAkXF4PNg=="], @@ -848,11 +848,11 @@ "expo-localization": ["expo-localization@55.0.8", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-uFmpTsoDT7JE5Nwgt0EQ5gBvFVo7/u458SlY6V9Ep9wY/WPucL0o00VpXoFULaMtKHquKBgVUdHwk6E+JFz4dg=="], - "expo-modules-autolinking": ["expo-modules-autolinking@55.0.8", "", { "dependencies": { "@expo/require-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-nrWB1pkNp7bR8ECUTgYUiJ2Pyh6AvxCBXZ+lyPlfl1TzEIGhwU1Yqr+d78eJDueXaW+9zKeE0HqrTZoLS3ve4A=="], + "expo-modules-autolinking": ["expo-modules-autolinking@55.0.9", "", { "dependencies": { "@expo/require-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-OXIrxSYKlT/1Av1AMyUWeSTW1GChGofWV14sB73o5eFbfuz6ocv18fnKx+Ji67ZC7a0RztDctcZTuEQK84S4iw=="], - "expo-modules-core": ["expo-modules-core@55.0.13", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-DYLQTOJAR7jD3M9S0sH9myZaPEtShdicHrPiWcupIXMeMkQxFzErx+adUI8gZPy4AU45BgeGgtaogRfT25iLfw=="], + "expo-modules-core": ["expo-modules-core@55.0.15", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MAGz1SYSVgQbwVeUysWgPtLh8ozbBwORatXoA4w0NZqZBZzEyBgUQNhuwaroaIi9W8Ir3wy1McmZcDYDJNGmVw=="], - "expo-router": ["expo-router@55.0.3", "", { "dependencies": { "@expo/metro-runtime": "^55.0.6", "@expo/schema-utils": "^55.0.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.10.1", "@react-navigation/native": "^7.1.28", "@react-navigation/native-stack": "^7.10.1", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.7", "expo-image": "^55.0.5", "expo-server": "^55.0.6", "expo-symbols": "^55.0.4", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.7", "@react-navigation/drawer": "^7.7.2", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.7", "expo-linking": "^55.0.7", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-B3MQAeZq9B2SS5kgEybGqXYR0AY7QYM7fQ5E4bJwtvZLJjWPmWhDALhBpD26ovK/i1k0fi9VgW47FKJODxM5Jg=="], + "expo-router": ["expo-router@55.0.5", "", { "dependencies": { "@expo/metro-runtime": "^55.0.6", "@expo/schema-utils": "^55.0.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.10.1", "@react-navigation/native": "^7.1.28", "@react-navigation/native-stack": "^7.10.1", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.8", "expo-image": "^55.0.6", "expo-server": "^55.0.6", "expo-symbols": "^55.0.5", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.7", "@react-navigation/drawer": "^7.7.2", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.7", "expo-linking": "^55.0.7", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-PzN545wLtznKuVQmJXnAKB/JFjSJJIPHatsjJe4Cl6bRADr/MbWv5d2fqOpqFD/C0ZGCRHY1uBalq7mb5IQ3ZQ=="], "expo-server": ["expo-server@55.0.6", "", {}, "sha512-xI72FTm469FfuuBL2R5aNtthgH+GR7ygOpsx/KcPS0K8AZaZd7VjtEExbzn9/qyyYkWW3T+3dAmCDKOMX8gdmQ=="], @@ -862,7 +862,7 @@ "expo-status-bar": ["expo-status-bar@55.0.4", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-BPDjUXKqv1F9j2YNGLRZfkBEZXIEEpqj+t81y4c+4fdSN3Pos7goIHXgcl2ozbKQLgKRZQyNZQtbUgh5UjHYUQ=="], - "expo-symbols": ["expo-symbols@55.0.4", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-w9rxPlpta3gks0G4Tvpq/qQdiMp4R/XOeOzyjSruYUQakmsWbQBKA+Sd/fCVXs7qFJSvVTOGXiOhZm+YJRYZVg=="], + "expo-symbols": ["expo-symbols@55.0.5", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-W/QYRvnYVes947ZYOHtuKL8Gobs7BUjeu9oknzbo4jGnou7Ks6bj1CwdT0ZWNBgaTopbS4/POXumJIkW4cTPSQ=="], "expo-system-ui": ["expo-system-ui@55.0.9", "", { "dependencies": { "@react-native/normalize-colors": "0.83.2", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-8ygP1B0uFAFI8s7eHY2IcGnE83GhFeZYwHBr/fQ4dSXnc7iVT9zp2PvyTyiDiibQ69dBG+fauMQ4KlPcOO51kQ=="], diff --git a/drizzle/0001_nifty_alice.sql b/drizzle/0001_nifty_alice.sql new file mode 100644 index 0000000..de3f559 --- /dev/null +++ b/drizzle/0001_nifty_alice.sql @@ -0,0 +1,8 @@ +CREATE TABLE `password_reset_tokens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `token` text NOT NULL, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `password_reset_tokens_token_unique` ON `password_reset_tokens` (`token`); diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..226c9d4 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,221 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d509af9b-df90-49d7-a4df-8e531445d73c", + "prevId": "a6d42745-9c63-42b7-aac0-76d74e9fff7a", + "tables": { + "items": { + "name": "items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "password_reset_tokens_token_unique": { + "name": "password_reset_tokens_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_resets": { + "name": "password_resets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 574d4a5..b1034df 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1772545231738, "tag": "0000_calm_captain_britain", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1773337960538, + "tag": "0001_nifty_alice", + "breakpoints": true } ] } diff --git a/drizzle/migrations.js b/drizzle/migrations.js index ac28fda..2645e46 100644 --- a/drizzle/migrations.js +++ b/drizzle/migrations.js @@ -1,11 +1,13 @@ // This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo import m0000 from "./0000_calm_captain_britain.sql"; +import m0001 from "./0001_nifty_alice.sql"; import journal from "./meta/_journal.json"; export default { journal, migrations: { m0000, + m0001, }, }; diff --git a/package.json b/package.json index 3610ef3..36e0710 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "lint:fix": "biome check . --write", "format": "biome format . --write", "typecheck": "tsc --noEmit", - "test": "bun test" + "test": "bun test", + "upgrade-deps": "bunx expo install --fix" }, "dependencies": { "@expo/metro-runtime": "~55.0.6", @@ -41,13 +42,13 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", - "expo": "~55.0.4", + "expo": "~55.0.5", "expo-constants": "~55.0.7", - "expo-crypto": "55.0.8", + "expo-crypto": "~55.0.9", "expo-drizzle-studio-plugin": "^0.2.1", "expo-linking": "~55.0.7", "expo-localization": "~55.0.8", - "expo-router": "~55.0.3", + "expo-router": "~55.0.5", "expo-splash-screen": "~55.0.10", "expo-sqlite": "^55.0.10", "expo-status-bar": "~55.0.4", diff --git a/src/app/(auth)/forgot-password.tsx b/src/app/(auth)/forgot-password.tsx index cb94aec..9a5da63 100644 --- a/src/app/(auth)/forgot-password.tsx +++ b/src/app/(auth)/forgot-password.tsx @@ -1,10 +1,5 @@ -import { View } from "react-native"; import { ForgotPasswordForm } from "@/shared/components/forgot-password-form"; export default function ForgotPassword() { - return ( - - - - ); + return ; } diff --git a/src/app/(auth)/reset-password.tsx b/src/app/(auth)/reset-password.tsx index 7e8ed17..5c3ffa6 100644 --- a/src/app/(auth)/reset-password.tsx +++ b/src/app/(auth)/reset-password.tsx @@ -1,10 +1,5 @@ -import { View } from "react-native"; import { ResetPasswordForm } from "@/shared/components/reset-password-form"; export default function ResetPassword() { - return ( - - - - ); + return ; } diff --git a/src/app/(auth)/signin.tsx b/src/app/(auth)/signin.tsx index 7bdd965..52a56ba 100644 --- a/src/app/(auth)/signin.tsx +++ b/src/app/(auth)/signin.tsx @@ -1,10 +1,5 @@ -import { View } from "react-native"; import { SignInForm } from "@/shared/components/sign-in-form"; export default function SignIn() { - return ( - - - - ); + return ; } diff --git a/src/app/(auth)/signup.tsx b/src/app/(auth)/signup.tsx index e20f6dd..40e27ff 100644 --- a/src/app/(auth)/signup.tsx +++ b/src/app/(auth)/signup.tsx @@ -1,10 +1,5 @@ -import { View } from "react-native"; import { SignUpForm } from "@/shared/components/sign-up-form"; export default function SignUp() { - return ( - - - - ); + return ; } diff --git a/src/shared/components/forgot-password-form.tsx b/src/shared/components/forgot-password-form.tsx index 7023e35..d453958 100644 --- a/src/shared/components/forgot-password-form.tsx +++ b/src/shared/components/forgot-password-form.tsx @@ -1,3 +1,5 @@ +import { useRouter } from "expo-router"; +import { useState } from "react"; import { View } from "react-native"; import { Button } from "@/shared/components/ui/button"; import { @@ -10,10 +12,22 @@ import { import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; import { Text } from "@/shared/components/ui/text"; +import { createPasswordResetToken } from "@/shared/services/userService"; export function ForgotPasswordForm() { - function onSubmit() { - // TODO: Submit form and navigate to reset password screen if successful + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + async function onSubmit() { + if (!email) return; + setLoading(true); + try { + const token = await createPasswordResetToken(email); + router.push({ pathname: "/(auth)/reset-password", params: { token } }); + } finally { + setLoading(false); + } } return ( @@ -39,9 +53,11 @@ export function ForgotPasswordForm() { autoCapitalize="none" returnKeyType="send" onSubmitEditing={onSubmit} + value={email} + onChangeText={setEmail} /> - diff --git a/src/shared/components/reset-password-form.tsx b/src/shared/components/reset-password-form.tsx index d4aeadc..d8ea03f 100644 --- a/src/shared/components/reset-password-form.tsx +++ b/src/shared/components/reset-password-form.tsx @@ -1,67 +1,85 @@ -import * as React from "react"; -import { type TextInput, View } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { CheckCircle2, XCircle } from "lucide-react-native"; +import { useState } from "react"; +import { View } from "react-native"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/shared/components/ui/alert"; import { Button } from "@/shared/components/ui/button"; import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from "@/shared/components/ui/card"; import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; import { Text } from "@/shared/components/ui/text"; +import { resetPassword } from "@/shared/services/userService"; export function ResetPasswordForm() { - const codeInputRef = React.useRef(null); - - function onPasswordSubmitEditing() { - codeInputRef.current?.focus(); - } + const { token } = useLocalSearchParams<{ token: string }>(); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [alert, setAlert] = useState<{ + type: "success" | "error"; + message: string; + } | null>(null); + const router = useRouter(); - function onSubmit() { - // TODO: Submit form and navigate to protected screen if successful + async function onSubmit() { + if (!password || !token) return; + setLoading(true); + setAlert(null); + try { + await resetPassword(token, password); + setAlert({ type: "success", message: "Password updated successfully!" }); + setTimeout(() => router.replace("/(auth)/signin"), 2000); + } catch (err) { + setAlert({ type: "error", message: (err as Error).message }); + } finally { + setLoading(false); + } } return ( - + + {alert && ( + + + {alert.type === "success" ? "Success" : "Error"} + + {alert.message} + + )} + Reset password - - Enter the code sent to your email and set a new password - - - - + - - - - - diff --git a/src/shared/components/sign-up-form.tsx b/src/shared/components/sign-up-form.tsx index 7a07354..07b145e 100644 --- a/src/shared/components/sign-up-form.tsx +++ b/src/shared/components/sign-up-form.tsx @@ -1,10 +1,17 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "expo-router"; +import { CheckCircle2, XCircle } from "lucide-react-native"; import * as React from "react"; +import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { Pressable, type TextInput, View } from "react-native"; import { type SignUpSchema, signUpSchema } from "@/shared/auth/schemas"; import { SocialConnections } from "@/shared/components/social-connections"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/shared/components/ui/alert"; import { Button } from "@/shared/components/ui/button"; import { Card, @@ -22,12 +29,15 @@ import { useAuth } from "../hooks/useAuth"; export function SignUpForm() { const router = useRouter(); const { signUp } = useAuth(); + const [alert, setAlert] = useState<{ + type: "success" | "error"; + message: string; + } | null>(null); const passwordInputRef = React.useRef(null); const { control, handleSubmit, - setError, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(signUpSchema), @@ -37,16 +47,30 @@ export function SignUpForm() { async function onSubmit({ email, password }: SignUpSchema) { try { await signUp(email, password); - router.replace("/(auth)/signin"); + setAlert({ type: "success", message: "Account created successfully!" }); + setTimeout(() => router.replace("/(auth)/signin"), 2000); } catch (err) { - setError("root", { + setAlert({ + type: "error", message: err instanceof Error ? err.message : "Something went wrong", }); } } return ( - + + {alert && ( + + + {alert.type === "success" ? "Success" : "Error"} + + {alert.message} + + )} + diff --git a/src/shared/components/ui/alert.tsx b/src/shared/components/ui/alert.tsx new file mode 100644 index 0000000..ba15335 --- /dev/null +++ b/src/shared/components/ui/alert.tsx @@ -0,0 +1,85 @@ +import type { LucideIcon } from "lucide-react-native"; +import * as React from "react"; +import { View, type ViewProps } from "react-native"; +import { Icon } from "@/shared/components/ui/icon"; +import { Text, TextClassContext } from "@/shared/components/ui/text"; +import { cn } from "@/shared/lib/utils"; + +function Alert({ + className, + variant, + children, + icon, + iconClassName, + ...props +}: ViewProps & + React.RefAttributes & { + icon: LucideIcon; + variant?: "default" | "destructive"; + iconClassName?: string; + }) { + return ( + + + + + + {children} + + + ); +} + +function AlertTitle({ + className, + ...props +}: React.ComponentProps & React.RefAttributes) { + return ( + + ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps & React.RefAttributes) { + const textClass = React.useContext(TextClassContext); + return ( + + ); +} + +export { Alert, AlertDescription, AlertTitle }; diff --git a/src/shared/db/schema.ts b/src/shared/db/schema.ts index 341e883..a0ce6be 100644 --- a/src/shared/db/schema.ts +++ b/src/shared/db/schema.ts @@ -21,6 +21,13 @@ export const passwordResets = sqliteTable("password_resets", { expiresAt: integer("expires_at").notNull(), }); +export const passwordResetTokens = sqliteTable("password_reset_tokens", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id").notNull(), + token: text("token").notNull().unique(), + createdAt: integer("created_at").notNull(), +}); + export const items = sqliteTable("items", { id: integer("id").primaryKey().notNull(), title: text("title").notNull(), diff --git a/src/shared/services/userService.ts b/src/shared/services/userService.ts index faaad78..c1c8599 100644 --- a/src/shared/services/userService.ts +++ b/src/shared/services/userService.ts @@ -1,7 +1,7 @@ import { eq } from "drizzle-orm"; import * as Crypto from "expo-crypto"; import { db } from "@/shared/db/client"; -import { sessions, users } from "@/shared/db/schema"; +import { passwordResetTokens, sessions, users } from "@/shared/db/schema"; import type { AuthUser } from "@/shared/types/auth"; async function hashPassword(password: string): Promise { @@ -84,3 +84,62 @@ export async function signInUser( export async function signOutUser(userId: number): Promise { await db.delete(sessions).where(eq(sessions.userId, String(userId))); } + +export async function createPasswordResetToken(email: string): Promise { + const [user] = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (!user) return "ok"; + + const token = await Crypto.digestStringAsync( + Crypto.CryptoDigestAlgorithm.SHA256, + `${email}-${Date.now()}-${Math.random()}`, + ); + + await db + .delete(passwordResetTokens) + .where(eq(passwordResetTokens.userId, String(user.id))); + + await db.insert(passwordResetTokens).values({ + userId: String(user.id), + token, + createdAt: Date.now(), + }); + + return token; +} + +export async function resetPassword( + token: string, + newPassword: string, +): Promise { + const [resetToken] = await db + .select() + .from(passwordResetTokens) + .where(eq(passwordResetTokens.token, token)) + .limit(1); + + if (!resetToken) throw new Error("Invalid or expired token"); + + const FIFTEEN_MINUTES = 15 * 60 * 1000; + if (Date.now() - resetToken.createdAt > FIFTEEN_MINUTES) { + await db + .delete(passwordResetTokens) + .where(eq(passwordResetTokens.token, token)); + throw new Error("Token expired"); + } + + const passwordHash = await hashPassword(newPassword); + + await db + .update(users) + .set({ passwordHash }) + .where(eq(users.id, Number(resetToken.userId))); + + await db + .delete(passwordResetTokens) + .where(eq(passwordResetTokens.token, token)); +}