diff --git a/.gitignore b/.gitignore index f3b938a..4dbaaf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /*.db +.env diff --git a/api/go.mod b/api/go.mod index c67c318..9f39987 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,6 +3,10 @@ module archive.local go 1.25.4 require ( - github.com/jmoiron/sqlx v1.4.0 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/jmoiron/sqlx v1.4.0 + github.com/joho/godotenv v1.5.1 + github.com/mattn/go-sqlite3 v1.14.32 + golang.org/x/crypto v0.46.0 ) + +require github.com/golang-jwt/jwt/v5 v5.3.0 diff --git a/api/go.sum b/api/go.sum index 320de6a..0dc7696 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,8 +1,17 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= diff --git a/api/main.go b/api/main.go index d8d6269..e2bbbe0 100644 --- a/api/main.go +++ b/api/main.go @@ -1,12 +1,18 @@ package main import ( + "encoding/json" "flag" "fmt" "log" "net/http" + "os" + "time" + "github.com/golang-jwt/jwt/v5" "github.com/jmoiron/sqlx" + "github.com/joho/godotenv" + "golang.org/x/crypto/bcrypt" _ "github.com/mattn/go-sqlite3" ) @@ -15,6 +21,76 @@ var ( addr = flag.String("addr", ":5000", "http server address") ) +type User struct { + ID int64 `json:"id" db:"id"` + Email string `json:"email" db:"email"` + Password string `json:"-" db:"password"` + CreatedAt string `json:"created_at" db:"created_at"` + UpdatedAt string `json:"updated_at" db:"updated_at"` +} + +func readJSON(r *http.Request, s any) error { + return json.NewDecoder(r.Body).Decode(s) +} + +func sendJSON(w http.ResponseWriter, data any, status int) error { + w.WriteHeader(status) + return json.NewEncoder(w).Encode(data) +} + +func sendApiError(w http.ResponseWriter, msg string, err error, status int) { + var e struct { + Message string `json:"message"` + Error string `json:"error,omitempty"` + } + e.Message = msg + if err != nil { + e.Error = err.Error() + } + sendJSON(w, e, status) +} + +var jwtSecretKey []byte + +func init() { + if err := godotenv.Load(); err != nil { + panic("couldn't parse env") + } + + s := os.Getenv("JWT_SECRET") + if s == "" { + panic("JWT_SECRET env doesn't exist") + } + jwtSecretKey = []byte(s) +} + +func generateAccessToken(userId int64) (string, error) { + exp := time.Now().Add(time.Hour * 24 * 7) + claims := jwt.MapClaims{ + "id": userId, + "exp": exp.Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecretKey) +} + +type AuthResponse struct { + AccessToken string `json:"access_token"` +} + +func handler(mux *http.ServeMux) http.HandlerFunc { + fmt.Printf("starting http server at %s\n", *addr) + + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") + w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type,Content-Length") + w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS") + + mux.ServeHTTP(w, r) + } +} + func main() { flag.Parse() @@ -30,8 +106,96 @@ func main() { mux := http.NewServeMux() - fmt.Printf("starting http server at %s\n", *addr) - if err := http.ListenAndServe(*addr, mux); err != nil { + mux.HandleFunc("OPTIONS /", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + fmt.Fprintf(w, "ok") + }) + + mux.HandleFunc("POST /auth/register", func(w http.ResponseWriter, r *http.Request) { + var body struct { + Email string `json:"email"` + Password string `json:"password"` + } + if err := readJSON(r, &body); err != nil { + sendApiError(w, "couldn't parse body", err, 500) + return + } + + if body.Email == "" || body.Password == "" { + sendApiError(w, "invalid request", nil, 400) + return + } + + p, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10) + if err != nil { + sendApiError(w, "couldn't hash password", err, 500) + return + } + + res, err := db.Exec("INSERT INTO users (email, password) VALUES (?, ?)", body.Email, string(p)) + if err != nil { + sendApiError(w, "couldn't create user", err, 500) + return + } + _ = res + + id, err := res.LastInsertId() + if err != nil { + sendApiError(w, "couldn't get user id", err, 500) + return + } + + token, err := generateAccessToken(id) + if err != nil { + sendApiError(w, "couldn't generate token", err, 500) + return + } + + sendJSON(w, AuthResponse{token}, 201) + }) + + mux.HandleFunc("POST /auth/login", func(w http.ResponseWriter, r *http.Request) { + var body struct { + Email string `json:"email"` + Password string `json:"password"` + } + if err := readJSON(r, &body); err != nil { + sendApiError(w, "couldn't parse body", err, 500) + return + } + + if body.Email == "" || body.Password == "" { + sendApiError(w, "invalid request", nil, 400) + return + } + + row := db.QueryRowx("SELECT * FROM users WHERE email = ?", body.Email) + if row.Err() != nil { + sendApiError(w, "couldn't find user", err, 404) + return + } + + var user User + if err := row.StructScan(&user); err != nil { + sendApiError(w, "couldn't find user", err, 404) + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password)); err != nil { + sendApiError(w, "invalid password", nil, 400) + return + } + + token, err := generateAccessToken(user.ID) + if err != nil { + sendApiError(w, "couldn't generate token", err, 500) + return + } + + sendJSON(w, AuthResponse{token}, 200) + }) + + if err := http.ListenAndServe(*addr, handler(mux)); err != nil { log.Fatalf("failed to start http server: %v\n", err) } } diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..15cd775 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,4 @@ +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= diff --git a/web/bun.lock b/web/bun.lock index 7310487..7da4bc9 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -5,9 +5,14 @@ "": { "name": "web", "dependencies": { + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.2", + "clsx": "^2.1.1", + "dayjs": "^1.11.19", "next": "16.0.10", "react": "19.2.1", "react-dom": "19.2.1", + "react-hook-form": "^7.68.0", }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -213,6 +218,10 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -317,10 +326,14 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], + "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], @@ -349,10 +362,14 @@ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -369,6 +386,8 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], @@ -377,6 +396,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], @@ -463,8 +484,12 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], @@ -643,6 +668,10 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -703,6 +732,8 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -711,6 +742,8 @@ "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], + "react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], diff --git a/web/package.json b/web/package.json index 7b1be0c..33ade29 100644 --- a/web/package.json +++ b/web/package.json @@ -9,9 +9,14 @@ "lint": "eslint" }, "dependencies": { + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.2", + "clsx": "^2.1.1", + "dayjs": "^1.11.19", "next": "16.0.10", "react": "19.2.1", - "react-dom": "19.2.1" + "react-dom": "19.2.1", + "react-hook-form": "^7.68.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/web/src/api/auth/useLoginMutation.ts b/web/src/api/auth/useLoginMutation.ts new file mode 100644 index 0000000..4fbeb42 --- /dev/null +++ b/web/src/api/auth/useLoginMutation.ts @@ -0,0 +1,19 @@ +import { $axios } from "@/lib/axios"; +import { useMutation } from "@tanstack/react-query"; + +export type LoginData = { + email: string; + password: string; +}; + +export const useLoginMutation = () => { + return useMutation({ + mutationFn: async (data: LoginData) => { + const resp = await $axios.post<{ access_token: string }>( + "/auth/login", + data, + ); + return resp.data; + }, + }); +}; diff --git a/web/src/api/auth/useRegisterMutation.ts b/web/src/api/auth/useRegisterMutation.ts new file mode 100644 index 0000000..7a66259 --- /dev/null +++ b/web/src/api/auth/useRegisterMutation.ts @@ -0,0 +1,19 @@ +import { $axios } from "@/lib/axios"; +import { useMutation } from "@tanstack/react-query"; + +export type RegisterData = { + email: string; + password: string; +}; + +export const useRegisterMutation = () => { + return useMutation({ + mutationFn: async (data: RegisterData) => { + const resp = await $axios.post<{ access_token: string }>( + "/auth/register", + data, + ); + return resp.data; + }, + }); +}; diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 5b09098..24823f2 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import "./globals.css"; +import { Providers } from "@/app/providers"; +import { ReactNode } from "react"; export const metadata: Metadata = { title: "archive.local", @@ -9,11 +11,13 @@ export const metadata: Metadata = { export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: ReactNode; }>) { return ( - -
{children} - +