Files
eng-pad-server/internal/grpcserver/interceptors.go
Kyle Isom 7d4e52ae92 Implement Phase 4: gRPC sync service
- Proto definitions (engpad.v1.EngPadSync) with 6 RPCs
- Generated Go gRPC code
- Auth interceptor: username/password from metadata
- SyncNotebook: upsert with full page/stroke replacement in a tx
- DeleteNotebook, ListNotebooks handlers
- Share link RPCs: CreateShareLink, RevokeShareLink, ListShareLinks
- Share link token management (32-byte random, optional expiry)
- gRPC server with TLS 1.3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:52:47 -07:00

47 lines
1.3 KiB
Go

package grpcserver
import (
"context"
"database/sql"
"git.wntrmute.dev/kyle/eng-pad-server/internal/auth"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type contextKey string
const userIDKey contextKey = "user_id"
// UserIDFromContext extracts the authenticated user ID from the context.
func UserIDFromContext(ctx context.Context) (int64, bool) {
id, ok := ctx.Value(userIDKey).(int64)
return id, ok
}
// AuthInterceptor verifies username/password from gRPC metadata.
func AuthInterceptor(database *sql.DB) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
usernames := md.Get("username")
passwords := md.Get("password")
if len(usernames) == 0 || len(passwords) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing credentials")
}
userID, err := auth.AuthenticateUser(database, usernames[0], passwords[0])
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid credentials")
}
ctx = context.WithValue(ctx, userIDKey, userID)
return handler(ctx, req)
}
}