diff --git a/.scripts/update.sh b/.scripts/update.sh index efcdebc..552e740 100755 --- a/.scripts/update.sh +++ b/.scripts/update.sh @@ -38,6 +38,4 @@ if [ "${updated}" = true ]; then nix-update --flake --version=skip --subpackage client default git add flake.nix git commit -m "build(nix): updated nix hashes" -else - echo "nothing to update" fi \ No newline at end of file diff --git a/.treli.yaml b/.treli.yaml index f5ce22a..2dcd377 100644 --- a/.treli.yaml +++ b/.treli.yaml @@ -21,15 +21,15 @@ apps: dir: server exts: - go - onstart: go build -o ./tmp/app -tags dev && ./tmp/app - onchange: go build -o ./tmp/app -tags dev && ./tmp/app + onstart: go build -o ./tmp/app -tags dev cmd/trevstack/main.go && ./tmp/app + onchange: go build -o ./tmp/app -tags dev cmd/trevstack/main.go && ./tmp/app nix: color: "#74c7ec" exts: - nix - onstart: nix fmt - onchange: nix fmt + onstart: nix fmt . + onchange: nix fmt . prettier: color: "#fab387" diff --git a/buf.gen.yaml b/buf.gen.yaml index f19c683..9f8ee3d 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -5,6 +5,9 @@ managed: override: - file_option: go_package_prefix value: github.com/spotdemo4/trevstack/server/internal/connect + disable: + - file_option: go_package + module: buf.build/bufbuild/protovalidate plugins: - local: protoc-gen-go @@ -18,6 +21,7 @@ plugins: - local: protoc-gen-es out: client/src/lib/connect opt: target=ts + include_imports: true - local: protoc-gen-connect-openapi out: client/static/openapi diff --git a/buf.lock b/buf.lock new file mode 100644 index 0000000..ee39cd6 --- /dev/null +++ b/buf.lock @@ -0,0 +1,6 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 8976f5be98c146529b1cc15cd2012b60 + digest: b5:5d513af91a439d9e78cacac0c9455c7cb885a8737d30405d0b91974fe05276d19c07a876a51a107213a3d01b83ecc912996cdad4cddf7231f91379079cf7488d diff --git a/buf.yaml b/buf.yaml index afda648..f98c828 100644 --- a/buf.yaml +++ b/buf.yaml @@ -2,3 +2,5 @@ version: v2 modules: - path: proto +deps: + - buf.build/bufbuild/protovalidate diff --git a/flake.lock b/flake.lock index 44b95dc..e29da96 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1744463964, - "narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=", + "lastModified": 1746663147, + "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650", + "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", "type": "github" }, "original": { @@ -43,11 +43,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1744776944, - "narHash": "sha256-wdmVDDyz6aZpFai2P0PBb2sMRBTgnaURylJw8wis6ks=", + "lastModified": 1744951889, + "narHash": "sha256-SXVCC/3rmTRJ4qhi63F4A9hgyGya0CTwA+EhPcow9rU=", "owner": "spotdemo4", "repo": "treli", - "rev": "eefe10c1ba62a577592ff25e6e5b274215c1d8db", + "rev": "6f1413a2e7324f44eb5f1dc4a2fc506c7dae4fb4", "type": "github" }, "original": { diff --git a/proto/item/v1/item.proto b/proto/item/v1/item.proto index 4b728d3..4a47d88 100644 --- a/proto/item/v1/item.proto +++ b/proto/item/v1/item.proto @@ -2,11 +2,14 @@ syntax = "proto3"; package item.v1; +import "buf/validate/validate.proto"; import "google/protobuf/timestamp.proto"; message Item { int64 id = 1; - string name = 2; + string name = 2 [ + (buf.validate.field).string.min_len = 3 + ]; google.protobuf.Timestamp added = 3; string description = 4; float price = 5; diff --git a/proto/user/v1/auth.proto b/proto/user/v1/auth.proto index e4a249e..130905d 100644 --- a/proto/user/v1/auth.proto +++ b/proto/user/v1/auth.proto @@ -2,17 +2,21 @@ syntax = "proto3"; package user.v1; +import "buf/validate/validate.proto"; + service AuthService { rpc Login (LoginRequest) returns (LoginResponse) {} rpc SignUp (SignUpRequest) returns (SignUpResponse) {} rpc Logout (LogoutRequest) returns (LogoutResponse) {} - // rpc GetPasskeyIDs (GetPasskeyIDsRequest) returns (GetPasskeyIDsResponse) {} - // rpc BeginPasskeyLogin (BeginPasskeyLoginRequest) returns (BeginPasskeyLoginResponse) {} - // rpc FinishPasskeyLogin (FinishPasskeyLoginRequest) returns (FinishPasskeyLoginResponse) {} + + rpc BeginPasskeyLogin (BeginPasskeyLoginRequest) returns (BeginPasskeyLoginResponse) {} + rpc FinishPasskeyLogin (FinishPasskeyLoginRequest) returns (FinishPasskeyLoginResponse) {} } message LoginRequest { - string username = 1; + string username = 1 [ + (buf.validate.field).string.min_len = 3 + ]; string password = 2; } message LoginResponse { @@ -20,7 +24,9 @@ message LoginResponse { } message SignUpRequest { - string username = 1; + string username = 1 [ + (buf.validate.field).string.min_len = 3 + ]; string password = 2; string confirm_password = 3; } @@ -29,15 +35,17 @@ message SignUpResponse {} message LogoutRequest {} message LogoutResponse {} -// message GetPasskeyIDsRequest { -// string username = 1; -// } -// message GetPasskeyIDsResponse { -// repeated string passkey_ids = 1; -// } +message BeginPasskeyLoginRequest { + string username = 1; +} +message BeginPasskeyLoginResponse { + string options_json = 1; +} -// message BeginPasskeyLoginRequest {} -// message BeginPasskeyLoginResponse {} - -// message FinishPasskeyLoginRequest {} -// message FinishPasskeyLoginResponse {} \ No newline at end of file +message FinishPasskeyLoginRequest { + string username = 1; + string attestation = 2; +} +message FinishPasskeyLoginResponse { + string token = 1; +} \ No newline at end of file diff --git a/proto/user/v1/user.proto b/proto/user/v1/user.proto index 3d41da1..5de8348 100644 --- a/proto/user/v1/user.proto +++ b/proto/user/v1/user.proto @@ -13,8 +13,9 @@ service UserService { rpc UpdatePassword (UpdatePasswordRequest) returns (UpdatePasswordResponse) {} rpc GetAPIKey (GetAPIKeyRequest) returns (GetAPIKeyResponse) {} rpc UpdateProfilePicture (UpdateProfilePictureRequest) returns (UpdateProfilePictureResponse) {} - // rpc BeginPasskeyRegistration (BeginPasskeyRegistrationRequest) returns (BeginPasskeyRegistrationResponse) {} - // rpc FinishPasskeyRegistration (FinishPasskeyRegistrationRequest) returns (FinishPasskeyRegistrationResponse) {} + + rpc BeginPasskeyRegistration (BeginPasskeyRegistrationRequest) returns (BeginPasskeyRegistrationResponse) {} + rpc FinishPasskeyRegistration (FinishPasskeyRegistrationRequest) returns (FinishPasskeyRegistrationResponse) {} } message GetUserRequest {} @@ -47,6 +48,16 @@ message UpdateProfilePictureResponse { User user = 1; } +message BeginPasskeyRegistrationRequest {} +message BeginPasskeyRegistrationResponse { + string options_json = 1; +} + +message FinishPasskeyRegistrationRequest { + string attestation = 1; +} +message FinishPasskeyRegistrationResponse {} + // message BeginPasskeyRegistrationRequest {} // message BeginPasskeyRegistrationResponse {} diff --git a/server/cmd/trevstack/main.go b/server/cmd/trevstack/main.go index 87e40d8..4420cc1 100644 --- a/server/cmd/trevstack/main.go +++ b/server/cmd/trevstack/main.go @@ -8,11 +8,14 @@ import ( "log" "log/slog" "net/http" + "net/url" "os" "os/signal" "syscall" "time" + "connectrpc.com/validate" + "github.com/go-webauthn/webauthn/webauthn" "github.com/joho/godotenv" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -48,11 +51,27 @@ func main() { log.Fatalf("failed to connect to database: %s", err.Error()) } + // Create webauthn + webAuthn, err := webauthn.New(&webauthn.Config{ + RPDisplayName: env.Name, + RPID: env.URL.Hostname(), + RPOrigins: []string{env.URL.String()}, + }) + if err != nil { + log.Fatalf("failed to create webauthn: %s", err.Error()) + } + + // Create validate interceptor + vi, err := validate.NewInterceptor() + if err != nil { + log.Fatalf("failed to create validator: %s", err.Error()) + } + // Serve GRPC Handlers api := http.NewServeMux() - api.Handle(interceptors.WithCORS(user.NewAuthHandler(sqlc, env.Key))) - api.Handle(interceptors.WithCORS(user.NewHandler(sqlc, env.Key))) - api.Handle(interceptors.WithCORS(item.NewHandler(sqlc, env.Key))) + api.Handle(interceptors.WithCORS(user.NewAuthHandler(vi, sqlc, webAuthn, env.Name, env.Key))) + api.Handle(interceptors.WithCORS(user.NewHandler(vi, sqlc, webAuthn, env.Key))) + api.Handle(interceptors.WithCORS(item.NewHandler(vi, sqlc, env.Key))) // Serve web interface mux := http.NewServeMux() @@ -93,6 +112,8 @@ func main() { type env struct { Port string Key string + Name string + URL *url.URL DatabaseURL string } @@ -106,19 +127,36 @@ func getEnv() (*env, error) { env := env{ Port: os.Getenv("PORT"), Key: os.Getenv("KEY"), + Name: os.Getenv("NAME"), DatabaseURL: os.Getenv("DATABASE_URL"), } // Validate if env.Port == "" { env.Port = "8080" + log.Printf("env 'PORT' not found, defaulting to %s\n", env.Port) } if env.Key == "" { - return nil, errors.New("env 'key' not found") + return nil, errors.New("env 'KEY' not found") + } + if env.Name == "" { + env.Name = "trevstack" + log.Printf("env 'NAME' not found, defaulting to %s\n", env.Name) } if env.DatabaseURL == "" { return nil, errors.New("env 'DATABASE_URL' not found") } + // Parse URL + if os.Getenv("URL") == "" { + env.URL, _ = url.Parse("http://localhost:" + env.Port) + log.Printf("env 'URL' not found, defaulting to %s\n", env.URL.String()) + } else { + env.URL, err = url.Parse(os.Getenv("URL")) + if err != nil { + return nil, err + } + } + return &env, nil } diff --git a/server/db/migrations/20250418055807_passkeys.sql b/server/db/migrations/20250418055807_passkeys.sql new file mode 100644 index 0000000..30f9eca --- /dev/null +++ b/server/db/migrations/20250418055807_passkeys.sql @@ -0,0 +1,22 @@ +-- migrate:up +CREATE TABLE credential ( + cred_id TEXT PRIMARY KEY NOT NULL, + cred_public_key BLOB NOT NULL, + sign_count INTEGER NOT NULL, + transports TEXT, + user_verified BOOLEAN, + backup_eligible BOOLEAN, + backup_state BOOLEAN, + attestation_object BLOB, + attestation_client_data BLOB, + created_at DATETIME NOT NULL, + last_used DATETIME NOT NULL, + user_id INTEGER NOT NULL, + + FOREIGN KEY (user_id) REFERENCES user (id) +); +ALTER TABLE user ADD webauthn_id TEXT NOT NULL; + +-- migrate:down +DROP TABLE credential; +ALTER TABLE user DROP COLUMN webauthn_id; diff --git a/server/db/queries/credential.sql b/server/db/queries/credential.sql new file mode 100644 index 0000000..a848756 --- /dev/null +++ b/server/db/queries/credential.sql @@ -0,0 +1,83 @@ +-- name: GetCredential :one +SELECT + cred_id, + cred_public_key, + sign_count, + transports, + user_verified, + backup_eligible, + backup_state, + attestation_object, + attestation_client_data, + created_at, + last_used, + user_id +FROM credential +WHERE + cred_id = @id + AND + user_id = @user_id +LIMIT 1; + +-- name: GetCredentials :many +SELECT + cred_id, + cred_public_key, + sign_count, + transports, + user_verified, + backup_eligible, + backup_state, + attestation_object, + attestation_client_data, + created_at, + last_used, + user_id +FROM credential +WHERE user_id = @user_id; + +-- name: InsertCredential :exec +INSERT INTO credential ( + cred_id, + cred_public_key, + sign_count, + transports, + user_verified, + backup_eligible, + backup_state, + attestation_object, + attestation_client_data, + created_at, + last_used, + user_id +) VALUES ( + @cred_id, + @cred_public_key, + @sign_count, + @transports, + @user_verified, + @backup_eligible, + @backup_state, + @attestation_object, + @attestation_client_data, + @created_at, + @last_used, + @user_id +); + +-- name: UpdateCredential :exec +UPDATE credential +SET + last_used = COALESCE(sqlc.narg('last_used'), last_used), + sign_count = COALESCE(sqlc.narg('sign_count'), sign_count) +WHERE + cred_id = @id + AND + user_id = @user_id; + +-- name: DeleteCredential :exec +DELETE FROM credential +WHERE + cred_id = @id + AND + user_id = @user_id; diff --git a/server/db/queries/item.sql b/server/db/queries/item.sql index 0e860be..d9d5e3b 100644 --- a/server/db/queries/item.sql +++ b/server/db/queries/item.sql @@ -34,6 +34,7 @@ WHERE AND (added <= sqlc.narg('end') OR sqlc.narg('end') IS NULL) ) +ORDER BY added DESC LIMIT @limit OFFSET diff --git a/server/db/queries/user.sql b/server/db/queries/user.sql index 5153da1..da5eca8 100644 --- a/server/db/queries/user.sql +++ b/server/db/queries/user.sql @@ -3,7 +3,8 @@ SELECT id, username, password, - profile_picture_id + profile_picture_id, + webauthn_id FROM user WHERE id = @id @@ -14,7 +15,8 @@ SELECT id, username, password, - profile_picture_id + profile_picture_id, + webauthn_id FROM user WHERE username = @username @@ -23,10 +25,12 @@ LIMIT 1; -- name: InsertUser :one INSERT INTO user ( username, - password + password, + webauthn_id ) VALUES ( @username, - @password + @password, + @webauthn_id ) RETURNING id; diff --git a/server/db/schema.sql b/server/db/schema.sql index 7575031..b001548 100644 --- a/server/db/schema.sql +++ b/server/db/schema.sql @@ -3,7 +3,7 @@ CREATE TABLE user ( id INTEGER PRIMARY KEY NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, - profile_picture_id INTEGER, + profile_picture_id INTEGER, webauthn_id TEXT NOT NULL, FOREIGN KEY (profile_picture_id) REFERENCES file (id) ); @@ -26,6 +26,23 @@ CREATE TABLE item ( FOREIGN KEY (user_id) REFERENCES user (id) ); +CREATE TABLE credential ( + cred_id TEXT PRIMARY KEY NOT NULL, + cred_public_key BLOB NOT NULL, + sign_count INTEGER NOT NULL, + transports TEXT, + user_verified BOOLEAN, + backup_eligible BOOLEAN, + backup_state BOOLEAN, + attestation_object BLOB, + attestation_client_data BLOB, + created_at DATETIME NOT NULL, + last_used DATETIME NOT NULL, + user_id INTEGER NOT NULL, + + FOREIGN KEY (user_id) REFERENCES user (id) +); -- Dbmate schema migrations INSERT INTO "schema_migrations" (version) VALUES - ('20250410195416'); + ('20250410195416'), + ('20250418055807'); diff --git a/server/go.mod b/server/go.mod index ec4f735..ebd7706 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,31 +3,47 @@ module github.com/spotdemo4/trevstack/server go 1.24.1 require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 connectrpc.com/connect v1.18.1 connectrpc.com/cors v0.1.0 - github.com/amacneil/dbmate/v2 v2.26.0 + connectrpc.com/validate v0.3.0 + github.com/amacneil/dbmate/v2 v2.27.0 + github.com/go-webauthn/webauthn v0.12.3 github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/rs/cors v1.11.1 github.com/spotdemo4/dbmate-sqlite-modernc v0.0.2 - golang.org/x/crypto v0.37.0 - golang.org/x/net v0.39.0 + golang.org/x/crypto v0.38.0 + golang.org/x/net v0.40.0 golang.org/x/time v0.11.0 google.golang.org/protobuf v1.36.6 modernc.org/sqlite v1.37.0 ) require ( + buf.build/go/protovalidate v0.12.0 // indirect + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/go-webauthn/x v0.1.21 // indirect + github.com/google/cel-go v0.25.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-tpm v0.9.5 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - modernc.org/libc v1.62.1 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect + modernc.org/libc v1.65.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.10.0 // indirect ) diff --git a/server/go.sum b/server/go.sum index eae5bd7..2fd1216 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,21 +1,42 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= +buf.build/go/protovalidate v0.12.0 h1:4GKJotbspQjRCcqZMGVSuC8SjwZ/FmgtSuKDpKUTZew= +buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ= connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg= +connectrpc.com/validate v0.3.0 h1:eMPASBQM+ztVzuLSXddB61zwJKzvWWZ6RLdIwTgh9Wo= +connectrpc.com/validate v0.3.0/go.mod h1:QLGN/m+oDeI4zaDAANK1L1G5K4i8gg6CUUwyl3HAG4A= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/amacneil/dbmate/v2 v2.26.0 h1:74ykEWh0V41BU3wesgmTowMn/2x9rUfIxIG+Q+vuUm0= -github.com/amacneil/dbmate/v2 v2.26.0/go.mod h1:cnjZKm5x/gKMLPfExXbEDUUQi1Sv5UqY3AIgbnxql84= +github.com/amacneil/dbmate/v2 v2.27.0 h1:A9JCrHD2z7bbPashxSdS17Xhfzzpu/2oB67P6j/xTVY= +github.com/amacneil/dbmate/v2 v2.27.0/go.mod h1:3OcOFCWRyY5VhRPTGaFq6Siijgzecoe5+0A3oZbaHIc= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= -github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE= +github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY= +github.com/go-webauthn/x v0.1.21 h1:nFbckQxudvHEJn2uy1VEi713MeSpApoAv9eRqsb9AdQ= +github.com/go-webauthn/x v0.1.21/go.mod h1:sEYohtg1zL4An1TXIUIQ5csdmoO+WO0R4R2pGKaHYKA= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= +github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -26,8 +47,10 @@ 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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -38,43 +61,59 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/spotdemo4/dbmate-sqlite-modernc v0.0.2 h1:537TA0HvjoWJxPzAhP+t2Gt82NpxGhCdmDsJpta7sdc= github.com/spotdemo4/dbmate-sqlite-modernc v0.0.2/go.mod h1:JwuvDDgb1VLGyHgSsQN3I+dx6QkEGk86o6Yc8vxzG/4= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w= github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= +google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= -modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= -modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss= +modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= -modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/libc v1.65.3 h1:umuvKKemW1RATk14y2f0IMPYa/Bi8NB+iL+kOQYNWAw= +modernc.org/libc v1.65.3/go.mod h1:VI3V2S5mNka4deJErQ0jsMXe7jgxojE2fOB/mWoHlbc= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= diff --git a/server/internal/auth/creds.go b/server/internal/auth/creds.go new file mode 100644 index 0000000..53e17de --- /dev/null +++ b/server/internal/auth/creds.go @@ -0,0 +1,49 @@ +package auth + +import ( + "strings" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/spotdemo4/trevstack/server/internal/sqlc" +) + +func NewCreds(creds []sqlc.Credential) []webauthn.Credential { + webauthnCreds := []webauthn.Credential{} + + for _, c := range creds { + transports := []protocol.AuthenticatorTransport{} + if c.Transports != nil { + for t := range strings.SplitSeq(*c.Transports, " ") { + transports = append(transports, protocol.AuthenticatorTransport(t)) + } + } + + flags := webauthn.CredentialFlags{} + if c.UserVerified != nil { + flags.UserVerified = *c.UserVerified + } + if c.BackupEligible != nil { + flags.BackupEligible = *c.BackupEligible + } + if c.BackupState != nil { + flags.BackupState = *c.BackupState + } + + webauthnCreds = append(webauthnCreds, webauthn.Credential{ + ID: []byte(c.CredID), + PublicKey: c.CredPublicKey, + Authenticator: webauthn.Authenticator{ + SignCount: uint32(c.SignCount), + }, + Transport: transports, + Flags: flags, + Attestation: webauthn.CredentialAttestation{ + Object: c.AttestationObject, + ClientDataJSON: c.AttestationClientData, + }, + }) + } + + return webauthnCreds +} diff --git a/server/internal/auth/user.go b/server/internal/auth/user.go new file mode 100644 index 0000000..6baaf45 --- /dev/null +++ b/server/internal/auth/user.go @@ -0,0 +1,33 @@ +package auth + +import "github.com/go-webauthn/webauthn/webauthn" + +type User struct { + id string + username string + credentials []webauthn.Credential +} + +func NewUser(id string, username string, credentials []webauthn.Credential) User { + return User{ + id: id, + username: username, + credentials: credentials, + } +} + +func (u User) WebAuthnID() []byte { + return []byte(u.id) +} + +func (u User) WebAuthnName() string { + return u.username +} + +func (u User) WebAuthnDisplayName() string { + return u.username +} + +func (u User) WebAuthnCredentials() []webauthn.Credential { + return u.credentials +} diff --git a/server/internal/connect/item/v1/item.pb.go b/server/internal/connect/item/v1/item.pb.go index 27b1954..2832c1f 100644 --- a/server/internal/connect/item/v1/item.pb.go +++ b/server/internal/connect/item/v1/item.pb.go @@ -7,6 +7,7 @@ package itemv1 import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" @@ -638,10 +639,10 @@ var File_item_v1_item_proto protoreflect.FileDescriptor const file_item_v1_item_proto_rawDesc = "" + "\n" + - "\x12item/v1/item.proto\x12\aitem.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb0\x01\n" + + "\x12item/v1/item.proto\x12\aitem.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb9\x01\n" + "\x04Item\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x120\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1b\n" + + "\x04name\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x03R\x04name\x120\n" + "\x05added\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x05added\x12 \n" + "\vdescription\x18\x04 \x01(\tR\vdescription\x12\x14\n" + "\x05price\x18\x05 \x01(\x02R\x05price\x12\x1a\n" + diff --git a/server/internal/connect/user/v1/auth.pb.go b/server/internal/connect/user/v1/auth.pb.go index 831c880..bf635a6 100644 --- a/server/internal/connect/user/v1/auth.pb.go +++ b/server/internal/connect/user/v1/auth.pb.go @@ -7,6 +7,7 @@ package userv1 import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -285,27 +286,222 @@ func (*LogoutResponse) Descriptor() ([]byte, []int) { return file_user_v1_auth_proto_rawDescGZIP(), []int{5} } +type BeginPasskeyLoginRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginPasskeyLoginRequest) Reset() { + *x = BeginPasskeyLoginRequest{} + mi := &file_user_v1_auth_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginPasskeyLoginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginPasskeyLoginRequest) ProtoMessage() {} + +func (x *BeginPasskeyLoginRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_auth_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BeginPasskeyLoginRequest.ProtoReflect.Descriptor instead. +func (*BeginPasskeyLoginRequest) Descriptor() ([]byte, []int) { + return file_user_v1_auth_proto_rawDescGZIP(), []int{6} +} + +func (x *BeginPasskeyLoginRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type BeginPasskeyLoginResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + OptionsJson string `protobuf:"bytes,1,opt,name=options_json,json=optionsJson,proto3" json:"options_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginPasskeyLoginResponse) Reset() { + *x = BeginPasskeyLoginResponse{} + mi := &file_user_v1_auth_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginPasskeyLoginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginPasskeyLoginResponse) ProtoMessage() {} + +func (x *BeginPasskeyLoginResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_auth_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BeginPasskeyLoginResponse.ProtoReflect.Descriptor instead. +func (*BeginPasskeyLoginResponse) Descriptor() ([]byte, []int) { + return file_user_v1_auth_proto_rawDescGZIP(), []int{7} +} + +func (x *BeginPasskeyLoginResponse) GetOptionsJson() string { + if x != nil { + return x.OptionsJson + } + return "" +} + +type FinishPasskeyLoginRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Attestation string `protobuf:"bytes,2,opt,name=attestation,proto3" json:"attestation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishPasskeyLoginRequest) Reset() { + *x = FinishPasskeyLoginRequest{} + mi := &file_user_v1_auth_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishPasskeyLoginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishPasskeyLoginRequest) ProtoMessage() {} + +func (x *FinishPasskeyLoginRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_auth_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishPasskeyLoginRequest.ProtoReflect.Descriptor instead. +func (*FinishPasskeyLoginRequest) Descriptor() ([]byte, []int) { + return file_user_v1_auth_proto_rawDescGZIP(), []int{8} +} + +func (x *FinishPasskeyLoginRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *FinishPasskeyLoginRequest) GetAttestation() string { + if x != nil { + return x.Attestation + } + return "" +} + +type FinishPasskeyLoginResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishPasskeyLoginResponse) Reset() { + *x = FinishPasskeyLoginResponse{} + mi := &file_user_v1_auth_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishPasskeyLoginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishPasskeyLoginResponse) ProtoMessage() {} + +func (x *FinishPasskeyLoginResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_auth_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishPasskeyLoginResponse.ProtoReflect.Descriptor instead. +func (*FinishPasskeyLoginResponse) Descriptor() ([]byte, []int) { + return file_user_v1_auth_proto_rawDescGZIP(), []int{9} +} + +func (x *FinishPasskeyLoginResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + var File_user_v1_auth_proto protoreflect.FileDescriptor const file_user_v1_auth_proto_rawDesc = "" + "\n" + - "\x12user/v1/auth.proto\x12\auser.v1\"F\n" + - "\fLoginRequest\x12\x1a\n" + - "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\x12user/v1/auth.proto\x12\auser.v1\x1a\x1bbuf/validate/validate.proto\"O\n" + + "\fLoginRequest\x12#\n" + + "\busername\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x03R\busername\x12\x1a\n" + "\bpassword\x18\x02 \x01(\tR\bpassword\"%\n" + "\rLoginResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\"r\n" + - "\rSignUpRequest\x12\x1a\n" + - "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"{\n" + + "\rSignUpRequest\x12#\n" + + "\busername\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x03R\busername\x12\x1a\n" + "\bpassword\x18\x02 \x01(\tR\bpassword\x12)\n" + "\x10confirm_password\x18\x03 \x01(\tR\x0fconfirmPassword\"\x10\n" + "\x0eSignUpResponse\"\x0f\n" + "\rLogoutRequest\"\x10\n" + - "\x0eLogoutResponse2\xc1\x01\n" + + "\x0eLogoutResponse\"6\n" + + "\x18BeginPasskeyLoginRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\">\n" + + "\x19BeginPasskeyLoginResponse\x12!\n" + + "\foptions_json\x18\x01 \x01(\tR\voptionsJson\"Y\n" + + "\x19FinishPasskeyLoginRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + + "\vattestation\x18\x02 \x01(\tR\vattestation\"2\n" + + "\x1aFinishPasskeyLoginResponse\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token2\x80\x03\n" + "\vAuthService\x128\n" + "\x05Login\x12\x15.user.v1.LoginRequest\x1a\x16.user.v1.LoginResponse\"\x00\x12;\n" + "\x06SignUp\x12\x16.user.v1.SignUpRequest\x1a\x17.user.v1.SignUpResponse\"\x00\x12;\n" + - "\x06Logout\x12\x16.user.v1.LogoutRequest\x1a\x17.user.v1.LogoutResponse\"\x00B\x9c\x01\n" + + "\x06Logout\x12\x16.user.v1.LogoutRequest\x1a\x17.user.v1.LogoutResponse\"\x00\x12\\\n" + + "\x11BeginPasskeyLogin\x12!.user.v1.BeginPasskeyLoginRequest\x1a\".user.v1.BeginPasskeyLoginResponse\"\x00\x12_\n" + + "\x12FinishPasskeyLogin\x12\".user.v1.FinishPasskeyLoginRequest\x1a#.user.v1.FinishPasskeyLoginResponse\"\x00B\x9c\x01\n" + "\vcom.user.v1B\tAuthProtoP\x01ZEgithub.com/spotdemo4/trevstack/server/internal/connect/user/v1;userv1\xa2\x02\x03UXX\xaa\x02\aUser.V1\xca\x02\aUser\\V1\xe2\x02\x13User\\V1\\GPBMetadata\xea\x02\bUser::V1b\x06proto3" var ( @@ -320,24 +516,32 @@ func file_user_v1_auth_proto_rawDescGZIP() []byte { return file_user_v1_auth_proto_rawDescData } -var file_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_user_v1_auth_proto_goTypes = []any{ - (*LoginRequest)(nil), // 0: user.v1.LoginRequest - (*LoginResponse)(nil), // 1: user.v1.LoginResponse - (*SignUpRequest)(nil), // 2: user.v1.SignUpRequest - (*SignUpResponse)(nil), // 3: user.v1.SignUpResponse - (*LogoutRequest)(nil), // 4: user.v1.LogoutRequest - (*LogoutResponse)(nil), // 5: user.v1.LogoutResponse + (*LoginRequest)(nil), // 0: user.v1.LoginRequest + (*LoginResponse)(nil), // 1: user.v1.LoginResponse + (*SignUpRequest)(nil), // 2: user.v1.SignUpRequest + (*SignUpResponse)(nil), // 3: user.v1.SignUpResponse + (*LogoutRequest)(nil), // 4: user.v1.LogoutRequest + (*LogoutResponse)(nil), // 5: user.v1.LogoutResponse + (*BeginPasskeyLoginRequest)(nil), // 6: user.v1.BeginPasskeyLoginRequest + (*BeginPasskeyLoginResponse)(nil), // 7: user.v1.BeginPasskeyLoginResponse + (*FinishPasskeyLoginRequest)(nil), // 8: user.v1.FinishPasskeyLoginRequest + (*FinishPasskeyLoginResponse)(nil), // 9: user.v1.FinishPasskeyLoginResponse } var file_user_v1_auth_proto_depIdxs = []int32{ 0, // 0: user.v1.AuthService.Login:input_type -> user.v1.LoginRequest 2, // 1: user.v1.AuthService.SignUp:input_type -> user.v1.SignUpRequest 4, // 2: user.v1.AuthService.Logout:input_type -> user.v1.LogoutRequest - 1, // 3: user.v1.AuthService.Login:output_type -> user.v1.LoginResponse - 3, // 4: user.v1.AuthService.SignUp:output_type -> user.v1.SignUpResponse - 5, // 5: user.v1.AuthService.Logout:output_type -> user.v1.LogoutResponse - 3, // [3:6] is the sub-list for method output_type - 0, // [0:3] is the sub-list for method input_type + 6, // 3: user.v1.AuthService.BeginPasskeyLogin:input_type -> user.v1.BeginPasskeyLoginRequest + 8, // 4: user.v1.AuthService.FinishPasskeyLogin:input_type -> user.v1.FinishPasskeyLoginRequest + 1, // 5: user.v1.AuthService.Login:output_type -> user.v1.LoginResponse + 3, // 6: user.v1.AuthService.SignUp:output_type -> user.v1.SignUpResponse + 5, // 7: user.v1.AuthService.Logout:output_type -> user.v1.LogoutResponse + 7, // 8: user.v1.AuthService.BeginPasskeyLogin:output_type -> user.v1.BeginPasskeyLoginResponse + 9, // 9: user.v1.AuthService.FinishPasskeyLogin:output_type -> user.v1.FinishPasskeyLoginResponse + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -354,7 +558,7 @@ func file_user_v1_auth_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_user_v1_auth_proto_rawDesc), len(file_user_v1_auth_proto_rawDesc)), NumEnums: 0, - NumMessages: 6, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/server/internal/connect/user/v1/user.pb.go b/server/internal/connect/user/v1/user.pb.go index bf6acda..fc92715 100644 --- a/server/internal/connect/user/v1/user.pb.go +++ b/server/internal/connect/user/v1/user.pb.go @@ -457,6 +457,166 @@ func (x *UpdateProfilePictureResponse) GetUser() *User { return nil } +type BeginPasskeyRegistrationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginPasskeyRegistrationRequest) Reset() { + *x = BeginPasskeyRegistrationRequest{} + mi := &file_user_v1_user_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginPasskeyRegistrationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginPasskeyRegistrationRequest) ProtoMessage() {} + +func (x *BeginPasskeyRegistrationRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BeginPasskeyRegistrationRequest.ProtoReflect.Descriptor instead. +func (*BeginPasskeyRegistrationRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{9} +} + +type BeginPasskeyRegistrationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + OptionsJson string `protobuf:"bytes,1,opt,name=options_json,json=optionsJson,proto3" json:"options_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginPasskeyRegistrationResponse) Reset() { + *x = BeginPasskeyRegistrationResponse{} + mi := &file_user_v1_user_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginPasskeyRegistrationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginPasskeyRegistrationResponse) ProtoMessage() {} + +func (x *BeginPasskeyRegistrationResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BeginPasskeyRegistrationResponse.ProtoReflect.Descriptor instead. +func (*BeginPasskeyRegistrationResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{10} +} + +func (x *BeginPasskeyRegistrationResponse) GetOptionsJson() string { + if x != nil { + return x.OptionsJson + } + return "" +} + +type FinishPasskeyRegistrationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Attestation string `protobuf:"bytes,1,opt,name=attestation,proto3" json:"attestation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishPasskeyRegistrationRequest) Reset() { + *x = FinishPasskeyRegistrationRequest{} + mi := &file_user_v1_user_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishPasskeyRegistrationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishPasskeyRegistrationRequest) ProtoMessage() {} + +func (x *FinishPasskeyRegistrationRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishPasskeyRegistrationRequest.ProtoReflect.Descriptor instead. +func (*FinishPasskeyRegistrationRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{11} +} + +func (x *FinishPasskeyRegistrationRequest) GetAttestation() string { + if x != nil { + return x.Attestation + } + return "" +} + +type FinishPasskeyRegistrationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishPasskeyRegistrationResponse) Reset() { + *x = FinishPasskeyRegistrationResponse{} + mi := &file_user_v1_user_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishPasskeyRegistrationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishPasskeyRegistrationResponse) ProtoMessage() {} + +func (x *FinishPasskeyRegistrationResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishPasskeyRegistrationResponse.ProtoReflect.Descriptor instead. +func (*FinishPasskeyRegistrationResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{12} +} + var File_user_v1_user_proto protoreflect.FileDescriptor const file_user_v1_user_proto_rawDesc = "" + @@ -485,12 +645,20 @@ const file_user_v1_user_proto_rawDesc = "" + "\tfile_name\x18\x01 \x01(\tR\bfileName\x12\x12\n" + "\x04data\x18\x02 \x01(\fR\x04data\"A\n" + "\x1cUpdateProfilePictureResponse\x12!\n" + - "\x04user\x18\x01 \x01(\v2\r.user.v1.UserR\x04user2\xcf\x02\n" + + "\x04user\x18\x01 \x01(\v2\r.user.v1.UserR\x04user\"!\n" + + "\x1fBeginPasskeyRegistrationRequest\"E\n" + + " BeginPasskeyRegistrationResponse\x12!\n" + + "\foptions_json\x18\x01 \x01(\tR\voptionsJson\"D\n" + + " FinishPasskeyRegistrationRequest\x12 \n" + + "\vattestation\x18\x01 \x01(\tR\vattestation\"#\n" + + "!FinishPasskeyRegistrationResponse2\xb8\x04\n" + "\vUserService\x12>\n" + "\aGetUser\x12\x17.user.v1.GetUserRequest\x1a\x18.user.v1.GetUserResponse\"\x00\x12S\n" + "\x0eUpdatePassword\x12\x1e.user.v1.UpdatePasswordRequest\x1a\x1f.user.v1.UpdatePasswordResponse\"\x00\x12D\n" + "\tGetAPIKey\x12\x19.user.v1.GetAPIKeyRequest\x1a\x1a.user.v1.GetAPIKeyResponse\"\x00\x12e\n" + - "\x14UpdateProfilePicture\x12$.user.v1.UpdateProfilePictureRequest\x1a%.user.v1.UpdateProfilePictureResponse\"\x00B\x9c\x01\n" + + "\x14UpdateProfilePicture\x12$.user.v1.UpdateProfilePictureRequest\x1a%.user.v1.UpdateProfilePictureResponse\"\x00\x12q\n" + + "\x18BeginPasskeyRegistration\x12(.user.v1.BeginPasskeyRegistrationRequest\x1a).user.v1.BeginPasskeyRegistrationResponse\"\x00\x12t\n" + + "\x19FinishPasskeyRegistration\x12).user.v1.FinishPasskeyRegistrationRequest\x1a*.user.v1.FinishPasskeyRegistrationResponse\"\x00B\x9c\x01\n" + "\vcom.user.v1B\tUserProtoP\x01ZEgithub.com/spotdemo4/trevstack/server/internal/connect/user/v1;userv1\xa2\x02\x03UXX\xaa\x02\aUser.V1\xca\x02\aUser\\V1\xe2\x02\x13User\\V1\\GPBMetadata\xea\x02\bUser::V1b\x06proto3" var ( @@ -505,35 +673,43 @@ func file_user_v1_user_proto_rawDescGZIP() []byte { return file_user_v1_user_proto_rawDescData } -var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_user_v1_user_proto_goTypes = []any{ - (*User)(nil), // 0: user.v1.User - (*GetUserRequest)(nil), // 1: user.v1.GetUserRequest - (*GetUserResponse)(nil), // 2: user.v1.GetUserResponse - (*UpdatePasswordRequest)(nil), // 3: user.v1.UpdatePasswordRequest - (*UpdatePasswordResponse)(nil), // 4: user.v1.UpdatePasswordResponse - (*GetAPIKeyRequest)(nil), // 5: user.v1.GetAPIKeyRequest - (*GetAPIKeyResponse)(nil), // 6: user.v1.GetAPIKeyResponse - (*UpdateProfilePictureRequest)(nil), // 7: user.v1.UpdateProfilePictureRequest - (*UpdateProfilePictureResponse)(nil), // 8: user.v1.UpdateProfilePictureResponse + (*User)(nil), // 0: user.v1.User + (*GetUserRequest)(nil), // 1: user.v1.GetUserRequest + (*GetUserResponse)(nil), // 2: user.v1.GetUserResponse + (*UpdatePasswordRequest)(nil), // 3: user.v1.UpdatePasswordRequest + (*UpdatePasswordResponse)(nil), // 4: user.v1.UpdatePasswordResponse + (*GetAPIKeyRequest)(nil), // 5: user.v1.GetAPIKeyRequest + (*GetAPIKeyResponse)(nil), // 6: user.v1.GetAPIKeyResponse + (*UpdateProfilePictureRequest)(nil), // 7: user.v1.UpdateProfilePictureRequest + (*UpdateProfilePictureResponse)(nil), // 8: user.v1.UpdateProfilePictureResponse + (*BeginPasskeyRegistrationRequest)(nil), // 9: user.v1.BeginPasskeyRegistrationRequest + (*BeginPasskeyRegistrationResponse)(nil), // 10: user.v1.BeginPasskeyRegistrationResponse + (*FinishPasskeyRegistrationRequest)(nil), // 11: user.v1.FinishPasskeyRegistrationRequest + (*FinishPasskeyRegistrationResponse)(nil), // 12: user.v1.FinishPasskeyRegistrationResponse } var file_user_v1_user_proto_depIdxs = []int32{ - 0, // 0: user.v1.GetUserResponse.user:type_name -> user.v1.User - 0, // 1: user.v1.UpdatePasswordResponse.user:type_name -> user.v1.User - 0, // 2: user.v1.UpdateProfilePictureResponse.user:type_name -> user.v1.User - 1, // 3: user.v1.UserService.GetUser:input_type -> user.v1.GetUserRequest - 3, // 4: user.v1.UserService.UpdatePassword:input_type -> user.v1.UpdatePasswordRequest - 5, // 5: user.v1.UserService.GetAPIKey:input_type -> user.v1.GetAPIKeyRequest - 7, // 6: user.v1.UserService.UpdateProfilePicture:input_type -> user.v1.UpdateProfilePictureRequest - 2, // 7: user.v1.UserService.GetUser:output_type -> user.v1.GetUserResponse - 4, // 8: user.v1.UserService.UpdatePassword:output_type -> user.v1.UpdatePasswordResponse - 6, // 9: user.v1.UserService.GetAPIKey:output_type -> user.v1.GetAPIKeyResponse - 8, // 10: user.v1.UserService.UpdateProfilePicture:output_type -> user.v1.UpdateProfilePictureResponse - 7, // [7:11] is the sub-list for method output_type - 3, // [3:7] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 0, // 0: user.v1.GetUserResponse.user:type_name -> user.v1.User + 0, // 1: user.v1.UpdatePasswordResponse.user:type_name -> user.v1.User + 0, // 2: user.v1.UpdateProfilePictureResponse.user:type_name -> user.v1.User + 1, // 3: user.v1.UserService.GetUser:input_type -> user.v1.GetUserRequest + 3, // 4: user.v1.UserService.UpdatePassword:input_type -> user.v1.UpdatePasswordRequest + 5, // 5: user.v1.UserService.GetAPIKey:input_type -> user.v1.GetAPIKeyRequest + 7, // 6: user.v1.UserService.UpdateProfilePicture:input_type -> user.v1.UpdateProfilePictureRequest + 9, // 7: user.v1.UserService.BeginPasskeyRegistration:input_type -> user.v1.BeginPasskeyRegistrationRequest + 11, // 8: user.v1.UserService.FinishPasskeyRegistration:input_type -> user.v1.FinishPasskeyRegistrationRequest + 2, // 9: user.v1.UserService.GetUser:output_type -> user.v1.GetUserResponse + 4, // 10: user.v1.UserService.UpdatePassword:output_type -> user.v1.UpdatePasswordResponse + 6, // 11: user.v1.UserService.GetAPIKey:output_type -> user.v1.GetAPIKeyResponse + 8, // 12: user.v1.UserService.UpdateProfilePicture:output_type -> user.v1.UpdateProfilePictureResponse + 10, // 13: user.v1.UserService.BeginPasskeyRegistration:output_type -> user.v1.BeginPasskeyRegistrationResponse + 12, // 14: user.v1.UserService.FinishPasskeyRegistration:output_type -> user.v1.FinishPasskeyRegistrationResponse + 9, // [9:15] is the sub-list for method output_type + 3, // [3:9] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_user_v1_user_proto_init() } @@ -548,7 +724,7 @@ func file_user_v1_user_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_user_v1_user_proto_rawDesc), len(file_user_v1_user_proto_rawDesc)), NumEnums: 0, - NumMessages: 9, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/server/internal/connect/user/v1/userv1connect/auth.connect.go b/server/internal/connect/user/v1/userv1connect/auth.connect.go index 23dcc78..7941001 100644 --- a/server/internal/connect/user/v1/userv1connect/auth.connect.go +++ b/server/internal/connect/user/v1/userv1connect/auth.connect.go @@ -39,6 +39,12 @@ const ( AuthServiceSignUpProcedure = "/user.v1.AuthService/SignUp" // AuthServiceLogoutProcedure is the fully-qualified name of the AuthService's Logout RPC. AuthServiceLogoutProcedure = "/user.v1.AuthService/Logout" + // AuthServiceBeginPasskeyLoginProcedure is the fully-qualified name of the AuthService's + // BeginPasskeyLogin RPC. + AuthServiceBeginPasskeyLoginProcedure = "/user.v1.AuthService/BeginPasskeyLogin" + // AuthServiceFinishPasskeyLoginProcedure is the fully-qualified name of the AuthService's + // FinishPasskeyLogin RPC. + AuthServiceFinishPasskeyLoginProcedure = "/user.v1.AuthService/FinishPasskeyLogin" ) // AuthServiceClient is a client for the user.v1.AuthService service. @@ -46,6 +52,8 @@ type AuthServiceClient interface { Login(context.Context, *connect.Request[v1.LoginRequest]) (*connect.Response[v1.LoginResponse], error) SignUp(context.Context, *connect.Request[v1.SignUpRequest]) (*connect.Response[v1.SignUpResponse], error) Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error) + BeginPasskeyLogin(context.Context, *connect.Request[v1.BeginPasskeyLoginRequest]) (*connect.Response[v1.BeginPasskeyLoginResponse], error) + FinishPasskeyLogin(context.Context, *connect.Request[v1.FinishPasskeyLoginRequest]) (*connect.Response[v1.FinishPasskeyLoginResponse], error) } // NewAuthServiceClient constructs a client for the user.v1.AuthService service. By default, it uses @@ -77,14 +85,28 @@ func NewAuthServiceClient(httpClient connect.HTTPClient, baseURL string, opts .. connect.WithSchema(authServiceMethods.ByName("Logout")), connect.WithClientOptions(opts...), ), + beginPasskeyLogin: connect.NewClient[v1.BeginPasskeyLoginRequest, v1.BeginPasskeyLoginResponse]( + httpClient, + baseURL+AuthServiceBeginPasskeyLoginProcedure, + connect.WithSchema(authServiceMethods.ByName("BeginPasskeyLogin")), + connect.WithClientOptions(opts...), + ), + finishPasskeyLogin: connect.NewClient[v1.FinishPasskeyLoginRequest, v1.FinishPasskeyLoginResponse]( + httpClient, + baseURL+AuthServiceFinishPasskeyLoginProcedure, + connect.WithSchema(authServiceMethods.ByName("FinishPasskeyLogin")), + connect.WithClientOptions(opts...), + ), } } // authServiceClient implements AuthServiceClient. type authServiceClient struct { - login *connect.Client[v1.LoginRequest, v1.LoginResponse] - signUp *connect.Client[v1.SignUpRequest, v1.SignUpResponse] - logout *connect.Client[v1.LogoutRequest, v1.LogoutResponse] + login *connect.Client[v1.LoginRequest, v1.LoginResponse] + signUp *connect.Client[v1.SignUpRequest, v1.SignUpResponse] + logout *connect.Client[v1.LogoutRequest, v1.LogoutResponse] + beginPasskeyLogin *connect.Client[v1.BeginPasskeyLoginRequest, v1.BeginPasskeyLoginResponse] + finishPasskeyLogin *connect.Client[v1.FinishPasskeyLoginRequest, v1.FinishPasskeyLoginResponse] } // Login calls user.v1.AuthService.Login. @@ -102,11 +124,23 @@ func (c *authServiceClient) Logout(ctx context.Context, req *connect.Request[v1. return c.logout.CallUnary(ctx, req) } +// BeginPasskeyLogin calls user.v1.AuthService.BeginPasskeyLogin. +func (c *authServiceClient) BeginPasskeyLogin(ctx context.Context, req *connect.Request[v1.BeginPasskeyLoginRequest]) (*connect.Response[v1.BeginPasskeyLoginResponse], error) { + return c.beginPasskeyLogin.CallUnary(ctx, req) +} + +// FinishPasskeyLogin calls user.v1.AuthService.FinishPasskeyLogin. +func (c *authServiceClient) FinishPasskeyLogin(ctx context.Context, req *connect.Request[v1.FinishPasskeyLoginRequest]) (*connect.Response[v1.FinishPasskeyLoginResponse], error) { + return c.finishPasskeyLogin.CallUnary(ctx, req) +} + // AuthServiceHandler is an implementation of the user.v1.AuthService service. type AuthServiceHandler interface { Login(context.Context, *connect.Request[v1.LoginRequest]) (*connect.Response[v1.LoginResponse], error) SignUp(context.Context, *connect.Request[v1.SignUpRequest]) (*connect.Response[v1.SignUpResponse], error) Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error) + BeginPasskeyLogin(context.Context, *connect.Request[v1.BeginPasskeyLoginRequest]) (*connect.Response[v1.BeginPasskeyLoginResponse], error) + FinishPasskeyLogin(context.Context, *connect.Request[v1.FinishPasskeyLoginRequest]) (*connect.Response[v1.FinishPasskeyLoginResponse], error) } // NewAuthServiceHandler builds an HTTP handler from the service implementation. It returns the path @@ -134,6 +168,18 @@ func NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption connect.WithSchema(authServiceMethods.ByName("Logout")), connect.WithHandlerOptions(opts...), ) + authServiceBeginPasskeyLoginHandler := connect.NewUnaryHandler( + AuthServiceBeginPasskeyLoginProcedure, + svc.BeginPasskeyLogin, + connect.WithSchema(authServiceMethods.ByName("BeginPasskeyLogin")), + connect.WithHandlerOptions(opts...), + ) + authServiceFinishPasskeyLoginHandler := connect.NewUnaryHandler( + AuthServiceFinishPasskeyLoginProcedure, + svc.FinishPasskeyLogin, + connect.WithSchema(authServiceMethods.ByName("FinishPasskeyLogin")), + connect.WithHandlerOptions(opts...), + ) return "/user.v1.AuthService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case AuthServiceLoginProcedure: @@ -142,6 +188,10 @@ func NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption authServiceSignUpHandler.ServeHTTP(w, r) case AuthServiceLogoutProcedure: authServiceLogoutHandler.ServeHTTP(w, r) + case AuthServiceBeginPasskeyLoginProcedure: + authServiceBeginPasskeyLoginHandler.ServeHTTP(w, r) + case AuthServiceFinishPasskeyLoginProcedure: + authServiceFinishPasskeyLoginHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -162,3 +212,11 @@ func (UnimplementedAuthServiceHandler) SignUp(context.Context, *connect.Request[ func (UnimplementedAuthServiceHandler) Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.AuthService.Logout is not implemented")) } + +func (UnimplementedAuthServiceHandler) BeginPasskeyLogin(context.Context, *connect.Request[v1.BeginPasskeyLoginRequest]) (*connect.Response[v1.BeginPasskeyLoginResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.AuthService.BeginPasskeyLogin is not implemented")) +} + +func (UnimplementedAuthServiceHandler) FinishPasskeyLogin(context.Context, *connect.Request[v1.FinishPasskeyLoginRequest]) (*connect.Response[v1.FinishPasskeyLoginResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.AuthService.FinishPasskeyLogin is not implemented")) +} diff --git a/server/internal/connect/user/v1/userv1connect/user.connect.go b/server/internal/connect/user/v1/userv1connect/user.connect.go index 3f61b1c..89913d6 100644 --- a/server/internal/connect/user/v1/userv1connect/user.connect.go +++ b/server/internal/connect/user/v1/userv1connect/user.connect.go @@ -43,6 +43,12 @@ const ( // UserServiceUpdateProfilePictureProcedure is the fully-qualified name of the UserService's // UpdateProfilePicture RPC. UserServiceUpdateProfilePictureProcedure = "/user.v1.UserService/UpdateProfilePicture" + // UserServiceBeginPasskeyRegistrationProcedure is the fully-qualified name of the UserService's + // BeginPasskeyRegistration RPC. + UserServiceBeginPasskeyRegistrationProcedure = "/user.v1.UserService/BeginPasskeyRegistration" + // UserServiceFinishPasskeyRegistrationProcedure is the fully-qualified name of the UserService's + // FinishPasskeyRegistration RPC. + UserServiceFinishPasskeyRegistrationProcedure = "/user.v1.UserService/FinishPasskeyRegistration" ) // UserServiceClient is a client for the user.v1.UserService service. @@ -51,6 +57,8 @@ type UserServiceClient interface { UpdatePassword(context.Context, *connect.Request[v1.UpdatePasswordRequest]) (*connect.Response[v1.UpdatePasswordResponse], error) GetAPIKey(context.Context, *connect.Request[v1.GetAPIKeyRequest]) (*connect.Response[v1.GetAPIKeyResponse], error) UpdateProfilePicture(context.Context, *connect.Request[v1.UpdateProfilePictureRequest]) (*connect.Response[v1.UpdateProfilePictureResponse], error) + BeginPasskeyRegistration(context.Context, *connect.Request[v1.BeginPasskeyRegistrationRequest]) (*connect.Response[v1.BeginPasskeyRegistrationResponse], error) + FinishPasskeyRegistration(context.Context, *connect.Request[v1.FinishPasskeyRegistrationRequest]) (*connect.Response[v1.FinishPasskeyRegistrationResponse], error) } // NewUserServiceClient constructs a client for the user.v1.UserService service. By default, it uses @@ -88,15 +96,29 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts .. connect.WithSchema(userServiceMethods.ByName("UpdateProfilePicture")), connect.WithClientOptions(opts...), ), + beginPasskeyRegistration: connect.NewClient[v1.BeginPasskeyRegistrationRequest, v1.BeginPasskeyRegistrationResponse]( + httpClient, + baseURL+UserServiceBeginPasskeyRegistrationProcedure, + connect.WithSchema(userServiceMethods.ByName("BeginPasskeyRegistration")), + connect.WithClientOptions(opts...), + ), + finishPasskeyRegistration: connect.NewClient[v1.FinishPasskeyRegistrationRequest, v1.FinishPasskeyRegistrationResponse]( + httpClient, + baseURL+UserServiceFinishPasskeyRegistrationProcedure, + connect.WithSchema(userServiceMethods.ByName("FinishPasskeyRegistration")), + connect.WithClientOptions(opts...), + ), } } // userServiceClient implements UserServiceClient. type userServiceClient struct { - getUser *connect.Client[v1.GetUserRequest, v1.GetUserResponse] - updatePassword *connect.Client[v1.UpdatePasswordRequest, v1.UpdatePasswordResponse] - getAPIKey *connect.Client[v1.GetAPIKeyRequest, v1.GetAPIKeyResponse] - updateProfilePicture *connect.Client[v1.UpdateProfilePictureRequest, v1.UpdateProfilePictureResponse] + getUser *connect.Client[v1.GetUserRequest, v1.GetUserResponse] + updatePassword *connect.Client[v1.UpdatePasswordRequest, v1.UpdatePasswordResponse] + getAPIKey *connect.Client[v1.GetAPIKeyRequest, v1.GetAPIKeyResponse] + updateProfilePicture *connect.Client[v1.UpdateProfilePictureRequest, v1.UpdateProfilePictureResponse] + beginPasskeyRegistration *connect.Client[v1.BeginPasskeyRegistrationRequest, v1.BeginPasskeyRegistrationResponse] + finishPasskeyRegistration *connect.Client[v1.FinishPasskeyRegistrationRequest, v1.FinishPasskeyRegistrationResponse] } // GetUser calls user.v1.UserService.GetUser. @@ -119,12 +141,24 @@ func (c *userServiceClient) UpdateProfilePicture(ctx context.Context, req *conne return c.updateProfilePicture.CallUnary(ctx, req) } +// BeginPasskeyRegistration calls user.v1.UserService.BeginPasskeyRegistration. +func (c *userServiceClient) BeginPasskeyRegistration(ctx context.Context, req *connect.Request[v1.BeginPasskeyRegistrationRequest]) (*connect.Response[v1.BeginPasskeyRegistrationResponse], error) { + return c.beginPasskeyRegistration.CallUnary(ctx, req) +} + +// FinishPasskeyRegistration calls user.v1.UserService.FinishPasskeyRegistration. +func (c *userServiceClient) FinishPasskeyRegistration(ctx context.Context, req *connect.Request[v1.FinishPasskeyRegistrationRequest]) (*connect.Response[v1.FinishPasskeyRegistrationResponse], error) { + return c.finishPasskeyRegistration.CallUnary(ctx, req) +} + // UserServiceHandler is an implementation of the user.v1.UserService service. type UserServiceHandler interface { GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.GetUserResponse], error) UpdatePassword(context.Context, *connect.Request[v1.UpdatePasswordRequest]) (*connect.Response[v1.UpdatePasswordResponse], error) GetAPIKey(context.Context, *connect.Request[v1.GetAPIKeyRequest]) (*connect.Response[v1.GetAPIKeyResponse], error) UpdateProfilePicture(context.Context, *connect.Request[v1.UpdateProfilePictureRequest]) (*connect.Response[v1.UpdateProfilePictureResponse], error) + BeginPasskeyRegistration(context.Context, *connect.Request[v1.BeginPasskeyRegistrationRequest]) (*connect.Response[v1.BeginPasskeyRegistrationResponse], error) + FinishPasskeyRegistration(context.Context, *connect.Request[v1.FinishPasskeyRegistrationRequest]) (*connect.Response[v1.FinishPasskeyRegistrationResponse], error) } // NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path @@ -158,6 +192,18 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption connect.WithSchema(userServiceMethods.ByName("UpdateProfilePicture")), connect.WithHandlerOptions(opts...), ) + userServiceBeginPasskeyRegistrationHandler := connect.NewUnaryHandler( + UserServiceBeginPasskeyRegistrationProcedure, + svc.BeginPasskeyRegistration, + connect.WithSchema(userServiceMethods.ByName("BeginPasskeyRegistration")), + connect.WithHandlerOptions(opts...), + ) + userServiceFinishPasskeyRegistrationHandler := connect.NewUnaryHandler( + UserServiceFinishPasskeyRegistrationProcedure, + svc.FinishPasskeyRegistration, + connect.WithSchema(userServiceMethods.ByName("FinishPasskeyRegistration")), + connect.WithHandlerOptions(opts...), + ) return "/user.v1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case UserServiceGetUserProcedure: @@ -168,6 +214,10 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption userServiceGetAPIKeyHandler.ServeHTTP(w, r) case UserServiceUpdateProfilePictureProcedure: userServiceUpdateProfilePictureHandler.ServeHTTP(w, r) + case UserServiceBeginPasskeyRegistrationProcedure: + userServiceBeginPasskeyRegistrationHandler.ServeHTTP(w, r) + case UserServiceFinishPasskeyRegistrationProcedure: + userServiceFinishPasskeyRegistrationHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -192,3 +242,11 @@ func (UnimplementedUserServiceHandler) GetAPIKey(context.Context, *connect.Reque func (UnimplementedUserServiceHandler) UpdateProfilePicture(context.Context, *connect.Request[v1.UpdateProfilePictureRequest]) (*connect.Response[v1.UpdateProfilePictureResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.UpdateProfilePicture is not implemented")) } + +func (UnimplementedUserServiceHandler) BeginPasskeyRegistration(context.Context, *connect.Request[v1.BeginPasskeyRegistrationRequest]) (*connect.Response[v1.BeginPasskeyRegistrationResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.BeginPasskeyRegistration is not implemented")) +} + +func (UnimplementedUserServiceHandler) FinishPasskeyRegistration(context.Context, *connect.Request[v1.FinishPasskeyRegistrationRequest]) (*connect.Response[v1.FinishPasskeyRegistrationResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("user.v1.UserService.FinishPasskeyRegistration is not implemented")) +} diff --git a/server/internal/database/migrate.go b/server/internal/database/migrate.go index d1dfbb0..ce42c72 100644 --- a/server/internal/database/migrate.go +++ b/server/internal/database/migrate.go @@ -2,7 +2,6 @@ package database import ( "embed" - "io" "log" "net/url" @@ -11,8 +10,8 @@ import ( ) func Migrate(dsn string, dbFS embed.FS) error { - _, err := dbFS.ReadDir(".") - if err == io.EOF { + entries, err := dbFS.ReadDir(".") + if err != nil || len(entries) == 0 { return nil } @@ -35,7 +34,7 @@ func Migrate(dsn string, dbFS embed.FS) error { log.Println(m.Version, m.FilePath) } - log.Println("\nApplying...") + log.Println("Applying...") err = db.CreateAndMigrate() if err != nil { return err diff --git a/server/internal/handlers/client/client.go b/server/internal/handlers/client/client.go index 9d75c0b..2d56e84 100644 --- a/server/internal/handlers/client/client.go +++ b/server/internal/handlers/client/client.go @@ -2,7 +2,6 @@ package client import ( "embed" - "io" "io/fs" "net/http" @@ -10,8 +9,8 @@ import ( ) func NewClientHandler(key string, clientFS embed.FS) http.Handler { - _, err := clientFS.ReadDir(".") - if err == io.EOF { + entries, err := clientFS.ReadDir(".") + if err != nil || len(entries) == 0 { return http.NotFoundHandler() } diff --git a/server/internal/handlers/item/v1/item.go b/server/internal/handlers/item/v1/item.go index 5fe51ce..a4a0765 100644 --- a/server/internal/handlers/item/v1/item.go +++ b/server/internal/handlers/item/v1/item.go @@ -8,6 +8,7 @@ import ( "time" "connectrpc.com/connect" + "connectrpc.com/validate" itemv1 "github.com/spotdemo4/trevstack/server/internal/connect/item/v1" "github.com/spotdemo4/trevstack/server/internal/connect/item/v1/itemv1connect" "github.com/spotdemo4/trevstack/server/internal/interceptors" @@ -190,8 +191,8 @@ func (h *Handler) DeleteItem(ctx context.Context, req *connect.Request[itemv1.De return res, nil } -func NewHandler(db *sqlc.Queries, key string) (string, http.Handler) { - interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key)) +func NewHandler(vi *validate.Interceptor, db *sqlc.Queries, key string) (string, http.Handler) { + interceptors := connect.WithInterceptors(vi, interceptors.NewAuthInterceptor(key)) return itemv1connect.NewItemServiceHandler( &Handler{ diff --git a/server/internal/handlers/user/v1/auth.go b/server/internal/handlers/user/v1/auth.go index edc7a29..42c5414 100644 --- a/server/internal/handlers/user/v1/auth.go +++ b/server/internal/handlers/user/v1/auth.go @@ -3,15 +3,20 @@ package user import ( "context" "database/sql" + "encoding/json" "errors" "net/http" "strconv" + "sync" "time" - _ "crypto/sha256" // Crypto - "connectrpc.com/connect" + "connectrpc.com/validate" + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/spotdemo4/trevstack/server/internal/auth" userv1 "github.com/spotdemo4/trevstack/server/internal/connect/user/v1" "github.com/spotdemo4/trevstack/server/internal/connect/user/v1/userv1connect" "github.com/spotdemo4/trevstack/server/internal/interceptors" @@ -20,8 +25,13 @@ import ( ) type AuthHandler struct { - db *sqlc.Queries - key []byte + db *sqlc.Queries + webAuthn *webauthn.WebAuthn + key []byte + name string + + sessions *map[string]*webauthn.SessionData + mu sync.Mutex } func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) { @@ -40,37 +50,18 @@ func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.Log return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password")) } - // Generate JWT - t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ - Issuer: "trevstack", - Subject: strconv.FormatInt(user.ID, 10), - IssuedAt: &jwt.NumericDate{ - Time: time.Now(), - }, - ExpiresAt: &jwt.NumericDate{ - Time: time.Now().Add(time.Hour * 24), - }, - }) - ss, err := t.SignedString(h.key) + // Create JWT + token, cookie, err := h.createJWT(user.ID) if err != nil { return nil, connect.NewError(connect.CodeInternal, err) } - // Create cookie - cookie := http.Cookie{ - Name: "token", - Value: ss, - Path: "/", - MaxAge: 86400, - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteStrictMode, - } - + // Create response res := connect.NewResponse(&userv1.LoginResponse{ - Token: ss, + Token: token, }) res.Header().Set("Set-Cookie", cookie.String()) + return res, nil } @@ -82,7 +73,7 @@ func (h *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.Si return nil, connect.NewError(connect.CodeInternal, err) } } else { - return nil, connect.NewError(connect.CodeAlreadyExists, err) + return nil, connect.NewError(connect.CodeAlreadyExists, errors.New("user already exists")) } // Check if confirmation passwords match @@ -98,8 +89,9 @@ func (h *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.Si // Create user _, err = h.db.InsertUser(ctx, sqlc.InsertUserParams{ - Username: req.Msg.Username, - Password: string(hash), + Username: req.Msg.Username, + Password: string(hash), + WebauthnID: uuid.New().String(), }) if err != nil { return nil, connect.NewError(connect.CodeInternal, err) @@ -123,104 +115,173 @@ func (h *AuthHandler) Logout(_ context.Context, _ *connect.Request[userv1.Logout res := connect.NewResponse(&userv1.LogoutResponse{}) res.Header().Set("Set-Cookie", cookie.String()) + return res, nil } -// func (h *AuthHandler) GetPasskeyIDs(_ context.Context, req *connect.Request[userv1.GetPasskeyIDsRequest]) (*connect.Response[userv1.GetPasskeyIDsResponse], error) { -// // Get user -// user := models.User{} -// if err := h.db.Preload("Passkeys").First(&user, "username = ?", req.Msg.Username).Error; err != nil { -// return nil, connect.NewError(connect.CodeNotFound, err) -// } +func (h *AuthHandler) BeginPasskeyLogin(ctx context.Context, req *connect.Request[userv1.BeginPasskeyLoginRequest]) (*connect.Response[userv1.BeginPasskeyLoginResponse], error) { + // Get user + pUser, _, err := h.getPasskeyUser(ctx, req.Msg.Username) + if err != nil { + return nil, connect.NewError(connect.CodeUnauthenticated, err) + } -// // Get IDs -// ids := []string{} -// for _, passkey := range user.Passkeys { -// ids = append(ids, passkey.ID) -// } + // Get options for user + options, session, err := h.webAuthn.BeginLogin(pUser) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// return connect.NewResponse(&userv1.GetPasskeyIDsResponse{ -// PasskeyIds: ids, -// }), nil -// } + // Turn the options into json + optionsJSON, err := json.Marshal(options) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } -// func (h *AuthHandler) PasskeyLogin(_ context.Context, req *connect.Request[userv1.PasskeyLoginRequest]) (*connect.Response[userv1.PasskeyLoginResponse], error) { -// // Get passkey -// passkey := models.Passkey{} -// if err := h.db.First(&passkey, "id = ?", req.Msg.Id).Error; err != nil { -// return nil, connect.NewError(connect.CodeNotFound, err) -// } + // Set session for validation later + h.setSession(req.Msg.Username, session) -// // create a verifier from a trusted private key -// var verifier cose.Verifier -// var err error -// switch req.Msg.Algorithm { -// case -7: -// verifier, err = cose.NewVerifier(cose.AlgorithmES256, passkey.PublicKey) + return connect.NewResponse(&userv1.BeginPasskeyLoginResponse{ + OptionsJson: string(optionsJSON), + }), nil +} -// case -257: -// verifier, err = cose.NewVerifier(cose.AlgorithmRS256, passkey.PublicKey) +func (h *AuthHandler) FinishPasskeyLogin(ctx context.Context, req *connect.Request[userv1.FinishPasskeyLoginRequest]) (*connect.Response[userv1.FinishPasskeyLoginResponse], error) { + // Get user + pUser, userID, err := h.getPasskeyUser(ctx, req.Msg.Username) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// default: -// return nil, connect.NewError(connect.CodeInternal, errors.New("decode algorithm not implemented")) -// } -// if err != nil { -// return nil, connect.NewError(connect.CodeInternal, err) -// } + // Get the session data previously set + session, err := h.getSession(req.Msg.Username) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// // create a sign message from a raw signature payload -// var msg cose.Sign1Message -// if err = msg.UnmarshalCBOR(req.Msg.Signature); err != nil { -// return nil, connect.NewError(connect.CodeInternal, err) -// } + // Parse the attestation response + parsedResponse, err := protocol.ParseCredentialRequestResponseBytes([]byte(req.Msg.Attestation)) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// // Validate passkey -// err = msg.Verify(nil, verifier) -// if err != nil { -// return nil, connect.NewError(connect.CodeUnauthenticated, err) -// } + // Validate the login + credential, err := h.webAuthn.ValidateLogin(pUser, *session, parsedResponse) + if err != nil { + return nil, connect.NewError(connect.CodeUnauthenticated, err) + } -// // Generate JWT -// t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ -// Issuer: "trevstack", -// Subject: strconv.FormatUint(uint64(passkey.UserID), 10), -// IssuedAt: &jwt.NumericDate{ -// Time: time.Now(), -// }, -// ExpiresAt: &jwt.NumericDate{ -// Time: time.Now().Add(time.Hour * 24), -// }, -// }) -// ss, err := t.SignedString(h.key) -// if err != nil { -// return nil, connect.NewError(connect.CodeInternal, err) -// } + // Update the credential in the database + lastUsed := time.Now() + signCount := int64(credential.Authenticator.SignCount) + err = h.db.UpdateCredential(ctx, sqlc.UpdateCredentialParams{ + // set + LastUsed: &lastUsed, + SignCount: &signCount, -// // Create cookie -// cookie := http.Cookie{ -// Name: "token", -// Value: ss, -// Path: "/", -// MaxAge: 86400, -// HttpOnly: true, -// Secure: true, -// SameSite: http.SameSiteStrictMode, -// } + // where + ID: string(credential.ID), + UserID: userID, + }) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } -// res := connect.NewResponse(&userv1.PasskeyLoginResponse{ -// Token: ss, -// }) -// res.Header().Set("Set-Cookie", cookie.String()) -// return res, nil -// } + // Create JWT + token, cookie, err := h.createJWT(userID) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } -func NewAuthHandler(db *sqlc.Queries, key string) (string, http.Handler) { - interceptors := connect.WithInterceptors(interceptors.NewRateLimitInterceptor(key)) + // Create response + resp := connect.NewResponse(&userv1.FinishPasskeyLoginResponse{ + Token: token, + }) + resp.Header().Set("Set-Cookie", cookie.String()) + return resp, nil +} + +func (h *AuthHandler) getPasskeyUser(ctx context.Context, username string) (*auth.User, int64, error) { + user, err := h.db.GetUserbyUsername(ctx, username) + if err != nil { + return nil, 0, err + } + + creds, err := h.db.GetCredentials(ctx, user.ID) + if err != nil { + return nil, 0, err + } + + webCreds := auth.NewCreds(creds) + webUser := auth.NewUser(user.WebauthnID, user.Username, webCreds) + + return &webUser, user.ID, nil +} + +func (h *AuthHandler) getSession(username string) (*webauthn.SessionData, error) { + h.mu.Lock() + defer h.mu.Unlock() + + session, ok := (*h.sessions)[username] + if !ok { + return nil, errors.New("session does not exist") + } + + delete(*h.sessions, username) + return session, nil +} + +func (h *AuthHandler) setSession(username string, data *webauthn.SessionData) { + h.mu.Lock() + defer h.mu.Unlock() + + (*h.sessions)[username] = data +} + +func (h *AuthHandler) createJWT(userid int64) (string, *http.Cookie, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + Issuer: h.name, + Subject: strconv.Itoa(int(userid)), + IssuedAt: &jwt.NumericDate{ + Time: time.Now(), + }, + ExpiresAt: &jwt.NumericDate{ + Time: time.Now().Add(time.Hour * 24), + }, + }) + + tokenString, err := token.SignedString(h.key) + if err != nil { + return "", nil, err + } + + cookie := http.Cookie{ + Name: "token", + Value: tokenString, + Path: "/", + MaxAge: 86400, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + } + + return tokenString, &cookie, nil +} + +func NewAuthHandler(vi *validate.Interceptor, db *sqlc.Queries, webauth *webauthn.WebAuthn, name string, key string) (string, http.Handler) { + interceptors := connect.WithInterceptors(vi, interceptors.NewRateLimitInterceptor(key)) + + sd := map[string]*webauthn.SessionData{} return userv1connect.NewAuthServiceHandler( &AuthHandler{ - db: db, - key: []byte(key), + db: db, + webAuthn: webauth, + name: name, + key: []byte(key), + + sessions: &sd, + mu: sync.Mutex{}, }, interceptors, ) diff --git a/server/internal/handlers/user/v1/user.go b/server/internal/handlers/user/v1/user.go index 930a47e..b81a6b0 100644 --- a/server/internal/handlers/user/v1/user.go +++ b/server/internal/handlers/user/v1/user.go @@ -3,13 +3,19 @@ package user import ( "context" "database/sql" + "encoding/json" "errors" "net/http" "strconv" + "sync" "time" "connectrpc.com/connect" + "connectrpc.com/validate" + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" "github.com/golang-jwt/jwt/v5" + "github.com/spotdemo4/trevstack/server/internal/auth" userv1 "github.com/spotdemo4/trevstack/server/internal/connect/user/v1" "github.com/spotdemo4/trevstack/server/internal/connect/user/v1/userv1connect" "github.com/spotdemo4/trevstack/server/internal/interceptors" @@ -27,8 +33,12 @@ func userToConnect(item sqlc.User) *userv1.User { } type Handler struct { - db *sqlc.Queries - key []byte + db *sqlc.Queries + webAuthn *webauthn.WebAuthn + key []byte + + sessions *map[int64]*webauthn.SessionData + mu sync.Mutex } func (h *Handler) GetUser(ctx context.Context, _ *connect.Request[userv1.GetUserRequest]) (*connect.Response[userv1.GetUserResponse], error) { @@ -200,75 +210,149 @@ func (h *Handler) UpdateProfilePicture(ctx context.Context, req *connect.Request return res, nil } -// func (h *Handler) BeginPasskeyRegistration(ctx context.Context, req *connect.Request[userv1.BeginPasskeyRegistrationRequest]) (*connect.Response[userv1.BeginPasskeyRegistrationResponse], error) { -// // Get user ID from context -// userid, ok := interceptors.GetUserContext(ctx) -// if !ok { -// return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not authenticated")) -// } +func (h *Handler) BeginPasskeyRegistration(ctx context.Context, _ *connect.Request[userv1.BeginPasskeyRegistrationRequest]) (*connect.Response[userv1.BeginPasskeyRegistrationResponse], error) { + userid, ok := interceptors.GetUserContext(ctx) + if !ok { + return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) + } -// // Get user -// user := models.User{} -// if err := h.db.First(&user, "id = ?", userid).Error; err != nil { -// return nil, connect.NewError(connect.CodeInternal, err) -// } + // Get user + pUser, err := h.getPasskeyUser(ctx, userid) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// return connect.NewResponse(&userv1.BeginPasskeyRegistrationResponse{}), nil -// } + // Get options for user + options, session, err := h.webAuthn.BeginRegistration(pUser) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// func (h *Handler) FinishPasskeyRegistration(ctx context.Context, req *connect.Request[userv1.FinishPasskeyRegistrationRequest]) (*connect.Response[userv1.FinishPasskeyRegistrationResponse], error) { -// // Get user ID from context -// userid, ok := interceptors.GetUserContext(ctx) -// if !ok { -// return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not authenticated")) -// } + // Turn options into json + optionsJSON, err := json.Marshal(options) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } -// // Get user -// user := models.User{} -// if err := h.db.First(&user, "id = ?", userid).Error; err != nil { -// return nil, connect.NewError(connect.CodeInternal, err) -// } + // Set session for validation later + h.setSession(userid, session) -// return connect.NewResponse(&userv1.FinishPasskeyRegistrationResponse{}), nil -// } + return connect.NewResponse(&userv1.BeginPasskeyRegistrationResponse{ + OptionsJson: string(optionsJSON), + }), nil +} -// func BeginRegistration(ctx context.Context) error { -// userid, ok := interceptors.GetUserContext(ctx) -// if !ok { -// return nil -// } +func (h *Handler) FinishPasskeyRegistration(ctx context.Context, req *connect.Request[userv1.FinishPasskeyRegistrationRequest]) (*connect.Response[userv1.FinishPasskeyRegistrationResponse], error) { + userid, ok := interceptors.GetUserContext(ctx) + if !ok { + return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated")) + } -// wconfig := &webauthn.Config{ -// RPDisplayName: "Go Webauthn", // Display Name for your site -// RPID: "go-webauthn.local", // Generally the FQDN for your site -// RPOrigins: []string{"https://login.go-webauthn.local"}, // The origin URLs allowed for WebAuthn requests -// } -// webAuthn, err := webauthn.New(wconfig) -// if err != nil { -// return nil -// } + // Get user + pUser, err := h.getPasskeyUser(ctx, userid) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// var user webauthn.User -// user.WebAuthnCredentials() + // Get the session data previously set + session, err := h.getSession(userid) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// var cred webauthn.Credential -// cred.Verify() + // Parse the attestation response + parsedResponse, err := protocol.ParseCredentialCreationResponseBytes([]byte(req.Msg.Attestation)) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } -// var test metadata.Provider -// test. + // Create the credential + credential, err := h.webAuthn.CreateCredential(pUser, *session, parsedResponse) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } -// options, session, err := webAuthn.BeginRegistration(user) + transports := transportsToString(credential.Transport) -// return nil -// } + // Save the credential + err = h.db.InsertCredential(ctx, sqlc.InsertCredentialParams{ + CredID: string(credential.ID), + CredPublicKey: credential.PublicKey, + SignCount: int64(credential.Authenticator.SignCount), + Transports: &transports, + UserVerified: &credential.Flags.UserVerified, + BackupEligible: &credential.Flags.BackupEligible, + BackupState: &credential.Flags.BackupState, + AttestationObject: credential.Attestation.Object, + AttestationClientData: credential.Attestation.ClientDataJSON, + CreatedAt: time.Now(), + LastUsed: time.Now(), + UserID: userid, + }) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } -func NewHandler(db *sqlc.Queries, key string) (string, http.Handler) { - interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key)) + return connect.NewResponse(&userv1.FinishPasskeyRegistrationResponse{}), nil +} +func (h *Handler) getPasskeyUser(ctx context.Context, userid int64) (*auth.User, error) { + user, err := h.db.GetUser(ctx, userid) + if err != nil { + return nil, err + } + + creds, err := h.db.GetCredentials(ctx, user.ID) + if err != nil { + return nil, err + } + + webCreds := auth.NewCreds(creds) + webUser := auth.NewUser(user.WebauthnID, user.Username, webCreds) + + return &webUser, nil +} + +func (h *Handler) getSession(userid int64) (*webauthn.SessionData, error) { + h.mu.Lock() + defer h.mu.Unlock() + + session, ok := (*h.sessions)[userid] + if !ok { + return nil, errors.New("session does not exist") + } + + delete(*h.sessions, userid) + return session, nil +} + +func (h *Handler) setSession(userid int64, data *webauthn.SessionData) { + h.mu.Lock() + defer h.mu.Unlock() + + (*h.sessions)[userid] = data +} + +func transportsToString(transports []protocol.AuthenticatorTransport) string { + s := "" + for _, transport := range transports { + s += string(transport) + ", " + } + return s +} + +func NewHandler(vi *validate.Interceptor, db *sqlc.Queries, webauth *webauthn.WebAuthn, key string) (string, http.Handler) { + interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key), vi) + + sd := map[int64]*webauthn.SessionData{} return userv1connect.NewUserServiceHandler( &Handler{ - db: db, - key: []byte(key), + db: db, + webAuthn: webauth, + key: []byte(key), + + sessions: &sd, + mu: sync.Mutex{}, }, interceptors, ) diff --git a/server/internal/interceptors/auth.go b/server/internal/interceptors/auth.go index 43dbdb0..4fbc156 100644 --- a/server/internal/interceptors/auth.go +++ b/server/internal/interceptors/auth.go @@ -96,7 +96,13 @@ func (i *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { ctx, err = newUserContext(ctx, subject) if err == nil { return next(ctx, req) + } else { + log.Println("huh") + log.Println(err) } + } else { + log.Println("what") + log.Println(err) } } } @@ -176,7 +182,7 @@ func getCookies(rawCookies string) []*http.Cookie { } func validateToken(tokenString string, key string) (subject string, err error) { - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { // Don't forget to validate the alg is what you expect: if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) diff --git a/server/internal/sqlc/credential.sql.go b/server/internal/sqlc/credential.sql.go new file mode 100644 index 0000000..08f808a --- /dev/null +++ b/server/internal/sqlc/credential.sql.go @@ -0,0 +1,221 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: credential.sql + +package sqlc + +import ( + "context" + "time" +) + +const deleteCredential = `-- name: DeleteCredential :exec +DELETE FROM credential +WHERE + cred_id = ?1 + AND + user_id = ?2 +` + +type DeleteCredentialParams struct { + ID string + UserID int64 +} + +func (q *Queries) DeleteCredential(ctx context.Context, arg DeleteCredentialParams) error { + _, err := q.db.ExecContext(ctx, deleteCredential, arg.ID, arg.UserID) + return err +} + +const getCredential = `-- name: GetCredential :one +SELECT + cred_id, + cred_public_key, + sign_count, + transports, + user_verified, + backup_eligible, + backup_state, + attestation_object, + attestation_client_data, + created_at, + last_used, + user_id +FROM credential +WHERE + cred_id = ?1 + AND + user_id = ?2 +LIMIT 1 +` + +type GetCredentialParams struct { + ID string + UserID int64 +} + +func (q *Queries) GetCredential(ctx context.Context, arg GetCredentialParams) (Credential, error) { + row := q.db.QueryRowContext(ctx, getCredential, arg.ID, arg.UserID) + var i Credential + err := row.Scan( + &i.CredID, + &i.CredPublicKey, + &i.SignCount, + &i.Transports, + &i.UserVerified, + &i.BackupEligible, + &i.BackupState, + &i.AttestationObject, + &i.AttestationClientData, + &i.CreatedAt, + &i.LastUsed, + &i.UserID, + ) + return i, err +} + +const getCredentials = `-- name: GetCredentials :many +SELECT + cred_id, + cred_public_key, + sign_count, + transports, + user_verified, + backup_eligible, + backup_state, + attestation_object, + attestation_client_data, + created_at, + last_used, + user_id +FROM credential +WHERE user_id = ?1 +` + +func (q *Queries) GetCredentials(ctx context.Context, userID int64) ([]Credential, error) { + rows, err := q.db.QueryContext(ctx, getCredentials, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Credential + for rows.Next() { + var i Credential + if err := rows.Scan( + &i.CredID, + &i.CredPublicKey, + &i.SignCount, + &i.Transports, + &i.UserVerified, + &i.BackupEligible, + &i.BackupState, + &i.AttestationObject, + &i.AttestationClientData, + &i.CreatedAt, + &i.LastUsed, + &i.UserID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertCredential = `-- name: InsertCredential :exec +INSERT INTO credential ( + cred_id, + cred_public_key, + sign_count, + transports, + user_verified, + backup_eligible, + backup_state, + attestation_object, + attestation_client_data, + created_at, + last_used, + user_id +) VALUES ( + ?1, + ?2, + ?3, + ?4, + ?5, + ?6, + ?7, + ?8, + ?9, + ?10, + ?11, + ?12 +) +` + +type InsertCredentialParams struct { + CredID string + CredPublicKey []byte + SignCount int64 + Transports *string + UserVerified *bool + BackupEligible *bool + BackupState *bool + AttestationObject []byte + AttestationClientData []byte + CreatedAt time.Time + LastUsed time.Time + UserID int64 +} + +func (q *Queries) InsertCredential(ctx context.Context, arg InsertCredentialParams) error { + _, err := q.db.ExecContext(ctx, insertCredential, + arg.CredID, + arg.CredPublicKey, + arg.SignCount, + arg.Transports, + arg.UserVerified, + arg.BackupEligible, + arg.BackupState, + arg.AttestationObject, + arg.AttestationClientData, + arg.CreatedAt, + arg.LastUsed, + arg.UserID, + ) + return err +} + +const updateCredential = `-- name: UpdateCredential :exec +UPDATE credential +SET + last_used = COALESCE(?1, last_used), + sign_count = COALESCE(?2, sign_count) +WHERE + cred_id = ?3 + AND + user_id = ?4 +` + +type UpdateCredentialParams struct { + LastUsed *time.Time + SignCount *int64 + ID string + UserID int64 +} + +func (q *Queries) UpdateCredential(ctx context.Context, arg UpdateCredentialParams) error { + _, err := q.db.ExecContext(ctx, updateCredential, + arg.LastUsed, + arg.SignCount, + arg.ID, + arg.UserID, + ) + return err +} diff --git a/server/internal/sqlc/db.go b/server/internal/sqlc/db.go index 8277a70..e4d7828 100644 --- a/server/internal/sqlc/db.go +++ b/server/internal/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package sqlc diff --git a/server/internal/sqlc/file.sql.go b/server/internal/sqlc/file.sql.go index 843e345..04d2c4b 100644 --- a/server/internal/sqlc/file.sql.go +++ b/server/internal/sqlc/file.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: file.sql package sqlc diff --git a/server/internal/sqlc/item.sql.go b/server/internal/sqlc/item.sql.go index 6074ad8..95468b3 100644 --- a/server/internal/sqlc/item.sql.go +++ b/server/internal/sqlc/item.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: item.sql package sqlc @@ -85,6 +85,7 @@ WHERE AND (added <= ?4 OR ?4 IS NULL) ) +ORDER BY added DESC LIMIT ?6 OFFSET diff --git a/server/internal/sqlc/models.go b/server/internal/sqlc/models.go index d5cbf77..aec96c7 100644 --- a/server/internal/sqlc/models.go +++ b/server/internal/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package sqlc @@ -8,6 +8,21 @@ import ( "time" ) +type Credential struct { + CredID string + CredPublicKey []byte + SignCount int64 + Transports *string + UserVerified *bool + BackupEligible *bool + BackupState *bool + AttestationObject []byte + AttestationClientData []byte + CreatedAt time.Time + LastUsed time.Time + UserID int64 +} + type File struct { ID int64 Name string @@ -34,4 +49,5 @@ type User struct { Username string Password string ProfilePictureID *int64 + WebauthnID string } diff --git a/server/internal/sqlc/user.sql.go b/server/internal/sqlc/user.sql.go index 885c5cf..6c8d73e 100644 --- a/server/internal/sqlc/user.sql.go +++ b/server/internal/sqlc/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: user.sql package sqlc @@ -24,7 +24,8 @@ SELECT id, username, password, - profile_picture_id + profile_picture_id, + webauthn_id FROM user WHERE id = ?1 @@ -39,6 +40,7 @@ func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) { &i.Username, &i.Password, &i.ProfilePictureID, + &i.WebauthnID, ) return i, err } @@ -48,7 +50,8 @@ SELECT id, username, password, - profile_picture_id + profile_picture_id, + webauthn_id FROM user WHERE username = ?1 @@ -63,6 +66,7 @@ func (q *Queries) GetUserbyUsername(ctx context.Context, username string) (User, &i.Username, &i.Password, &i.ProfilePictureID, + &i.WebauthnID, ) return i, err } @@ -70,21 +74,24 @@ func (q *Queries) GetUserbyUsername(ctx context.Context, username string) (User, const insertUser = `-- name: InsertUser :one INSERT INTO user ( username, - password + password, + webauthn_id ) VALUES ( ?1, - ?2 + ?2, + ?3 ) RETURNING id ` type InsertUserParams struct { - Username string - Password string + Username string + Password string + WebauthnID string } func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (int64, error) { - row := q.db.QueryRowContext(ctx, insertUser, arg.Username, arg.Password) + row := q.db.QueryRowContext(ctx, insertUser, arg.Username, arg.Password, arg.WebauthnID) var id int64 err := row.Scan(&id) return id, err