init
This commit is contained in:
commit
c0cfe20445
|
@ -0,0 +1 @@
|
||||||
|
*.db
|
|
@ -0,0 +1,6 @@
|
||||||
|
run:
|
||||||
|
go run ./cmd/sso/main.go -config ./config/local.yaml
|
||||||
|
gen_proto:
|
||||||
|
protoc -I proto proto/sso/*.proto --go_out=./gen --go_opt=paths=source_relative --go-grpc_out=./gen --go-grpc_opt=paths=source_relative
|
||||||
|
migrate:
|
||||||
|
go run ./cmd/migrator/main.go -migrations-path ./migrations -storage-path ./storage/sso.db -migrations-table migrations
|
|
@ -0,0 +1,44 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var storagePath, migrationsPath, migtrationsTable string
|
||||||
|
|
||||||
|
flag.StringVar(&storagePath, "storage-path", "", "path to db file")
|
||||||
|
flag.StringVar(&migrationsPath, "migrations-path", "", "path to migrations")
|
||||||
|
flag.StringVar(&migtrationsTable, "migrations-table", "migrations", "name of migrations table")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if storagePath == "" {
|
||||||
|
panic("storage-path is required")
|
||||||
|
}
|
||||||
|
if migrationsPath == "" {
|
||||||
|
panic("migrations-path")
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.New("file://"+migrationsPath, fmt.Sprintf("sqlite3://%s?x-migrations-table=%s", storagePath, migtrationsTable))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Up(); err != nil {
|
||||||
|
if errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
fmt.Println("no migrations apply")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("migrations applied successfully")
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sso/internal/app"
|
||||||
|
"sso/internal/config"
|
||||||
|
"sso/internal/lib/logger/slogpretty"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
envLocal = "local"
|
||||||
|
envDev = "dev"
|
||||||
|
envProd = "prod"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.MustLoad()
|
||||||
|
|
||||||
|
// инициализировать логгер
|
||||||
|
log := setupLogger(cfg.Env)
|
||||||
|
|
||||||
|
log.Info("starting application", slog.String("env", cfg.Env))
|
||||||
|
|
||||||
|
application := app.New(log, cfg.GRPC.Port, cfg.StoragePath, cfg.TokenTTL)
|
||||||
|
|
||||||
|
go application.GRPCSrv.MustRun()
|
||||||
|
|
||||||
|
// инициализировать приложение
|
||||||
|
|
||||||
|
// запустить gRPC-сервер приложения
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
|
||||||
|
<-stop
|
||||||
|
|
||||||
|
log.Info("stopping application...")
|
||||||
|
|
||||||
|
application.GRPCSrv.Stop()
|
||||||
|
|
||||||
|
log.Info("application stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupLogger(env string) *slog.Logger {
|
||||||
|
var log *slog.Logger
|
||||||
|
|
||||||
|
switch env {
|
||||||
|
case envLocal:
|
||||||
|
log = setupPrettySlog()
|
||||||
|
case envDev:
|
||||||
|
log = slog.New(
|
||||||
|
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}),
|
||||||
|
)
|
||||||
|
case envProd:
|
||||||
|
log = slog.New(
|
||||||
|
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupPrettySlog() *slog.Logger {
|
||||||
|
opts := slogpretty.PrettyHandlerOptions{
|
||||||
|
SlogOpts: &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := opts.NewPrettyHandler(os.Stdout)
|
||||||
|
|
||||||
|
return slog.New(handler)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
env: "local" # dev, prod
|
||||||
|
storage_path: "./storage/sso.db"
|
||||||
|
token_ttl: 1h
|
||||||
|
grpc:
|
||||||
|
port: 44044
|
||||||
|
timeout: 10m
|
|
@ -0,0 +1,501 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.31.0
|
||||||
|
// protoc v4.25.1
|
||||||
|
// source: sso/sso.proto
|
||||||
|
|
||||||
|
package ssov1
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` // Email of the user to register
|
||||||
|
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // User ID of the registered user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RegisterRequest) Reset() {
|
||||||
|
*x = RegisterRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RegisterRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RegisterRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RegisterRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RegisterRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_sso_sso_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RegisterRequest) GetEmail() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Email
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RegisterRequest) GetPassword() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Password
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // User ID of the registered user.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RegisterResponse) Reset() {
|
||||||
|
*x = RegisterResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RegisterResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RegisterResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RegisterResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RegisterResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_sso_sso_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RegisterResponse) GetUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` // Email of the user to login.
|
||||||
|
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // Password of the user to login.
|
||||||
|
AppId int32 `protobuf:"varint,3,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` // ID of the app to login to.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) Reset() {
|
||||||
|
*x = LoginRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*LoginRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*LoginRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_sso_sso_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetEmail() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Email
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetPassword() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Password
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetAppId() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.AppId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // Auth token of the logged in user.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) Reset() {
|
||||||
|
*x = LoginResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*LoginResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[3]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*LoginResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_sso_sso_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) GetToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Token
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsAdminRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // User ID to validate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IsAdminRequest) Reset() {
|
||||||
|
*x = IsAdminRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IsAdminRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*IsAdminRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *IsAdminRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[4]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use IsAdminRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*IsAdminRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_sso_sso_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IsAdminRequest) GetUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsAdminResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
IsAdmin bool `protobuf:"varint,1,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` // Indicates whether the user is an admin.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IsAdminResponse) Reset() {
|
||||||
|
*x = IsAdminResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IsAdminResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*IsAdminResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *IsAdminResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_sso_sso_proto_msgTypes[5]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use IsAdminResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*IsAdminResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_sso_sso_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IsAdminResponse) GetIsAdmin() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.IsAdmin
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_sso_sso_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_sso_sso_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x0d, 0x73, 0x73, 0x6f, 0x2f, 0x73, 0x73, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
||||||
|
0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x43, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
|
||||||
|
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69,
|
||||||
|
0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a,
|
||||||
|
0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x2b, 0x0a, 0x10, 0x52, 0x65,
|
||||||
|
0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x17,
|
||||||
|
0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||||
|
0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x57, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
|
||||||
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c,
|
||||||
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a,
|
||||||
|
0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70,
|
||||||
|
0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x64,
|
||||||
|
0x22, 0x25, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
|
0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x29, 0x0a, 0x0e, 0x49, 0x73, 0x41, 0x64, 0x6d,
|
||||||
|
0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65,
|
||||||
|
0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
|
||||||
|
0x49, 0x64, 0x22, 0x2c, 0x0a, 0x0f, 0x49, 0x73, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x52, 0x65, 0x73,
|
||||||
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x64, 0x6d, 0x69,
|
||||||
|
0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x64, 0x6d, 0x69, 0x6e,
|
||||||
|
0x32, 0xab, 0x01, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x39, 0x0a, 0x08, 0x52, 0x65, 0x67,
|
||||||
|
0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x52, 0x65, 0x67,
|
||||||
|
0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61,
|
||||||
|
0x75, 0x74, 0x68, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||||
|
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x2e,
|
||||||
|
0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||||
|
0x74, 0x1a, 0x13, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
|
||||||
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x07, 0x49, 0x73, 0x41, 0x64, 0x6d, 0x69,
|
||||||
|
0x6e, 0x12, 0x14, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x49, 0x73, 0x41, 0x64, 0x6d, 0x69, 0x6e,
|
||||||
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x49,
|
||||||
|
0x73, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x13,
|
||||||
|
0x5a, 0x11, 0x79, 0x61, 0x73, 0x68, 0x2e, 0x73, 0x73, 0x6f, 0x2e, 0x76, 0x31, 0x3b, 0x73, 0x73,
|
||||||
|
0x6f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_sso_sso_proto_rawDescOnce sync.Once
|
||||||
|
file_sso_sso_proto_rawDescData = file_sso_sso_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_sso_sso_proto_rawDescGZIP() []byte {
|
||||||
|
file_sso_sso_proto_rawDescOnce.Do(func() {
|
||||||
|
file_sso_sso_proto_rawDescData = protoimpl.X.CompressGZIP(file_sso_sso_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_sso_sso_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_sso_sso_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||||
|
var file_sso_sso_proto_goTypes = []interface{}{
|
||||||
|
(*RegisterRequest)(nil), // 0: auth.RegisterRequest
|
||||||
|
(*RegisterResponse)(nil), // 1: auth.RegisterResponse
|
||||||
|
(*LoginRequest)(nil), // 2: auth.LoginRequest
|
||||||
|
(*LoginResponse)(nil), // 3: auth.LoginResponse
|
||||||
|
(*IsAdminRequest)(nil), // 4: auth.IsAdminRequest
|
||||||
|
(*IsAdminResponse)(nil), // 5: auth.IsAdminResponse
|
||||||
|
}
|
||||||
|
var file_sso_sso_proto_depIdxs = []int32{
|
||||||
|
0, // 0: auth.Auth.Register:input_type -> auth.RegisterRequest
|
||||||
|
2, // 1: auth.Auth.Login:input_type -> auth.LoginRequest
|
||||||
|
4, // 2: auth.Auth.IsAdmin:input_type -> auth.IsAdminRequest
|
||||||
|
1, // 3: auth.Auth.Register:output_type -> auth.RegisterResponse
|
||||||
|
3, // 4: auth.Auth.Login:output_type -> auth.LoginResponse
|
||||||
|
5, // 5: auth.Auth.IsAdmin:output_type -> auth.IsAdminResponse
|
||||||
|
3, // [3:6] is the sub-list for method output_type
|
||||||
|
0, // [0:3] 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_sso_sso_proto_init() }
|
||||||
|
func file_sso_sso_proto_init() {
|
||||||
|
if File_sso_sso_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_sso_sso_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*RegisterRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_sso_sso_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*RegisterResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_sso_sso_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*LoginRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_sso_sso_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*LoginResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_sso_sso_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*IsAdminRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_sso_sso_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*IsAdminResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_sso_sso_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 6,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_sso_sso_proto_goTypes,
|
||||||
|
DependencyIndexes: file_sso_sso_proto_depIdxs,
|
||||||
|
MessageInfos: file_sso_sso_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_sso_sso_proto = out.File
|
||||||
|
file_sso_sso_proto_rawDesc = nil
|
||||||
|
file_sso_sso_proto_goTypes = nil
|
||||||
|
file_sso_sso_proto_depIdxs = nil
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.3.0
|
||||||
|
// - protoc v4.25.1
|
||||||
|
// source: sso/sso.proto
|
||||||
|
|
||||||
|
package ssov1
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
const (
|
||||||
|
Auth_Register_FullMethodName = "/auth.Auth/Register"
|
||||||
|
Auth_Login_FullMethodName = "/auth.Auth/Login"
|
||||||
|
Auth_IsAdmin_FullMethodName = "/auth.Auth/IsAdmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthClient is the client API for Auth service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type AuthClient interface {
|
||||||
|
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error)
|
||||||
|
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
|
||||||
|
IsAdmin(ctx context.Context, in *IsAdminRequest, opts ...grpc.CallOption) (*IsAdminResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthClient(cc grpc.ClientConnInterface) AuthClient {
|
||||||
|
return &authClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *authClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) {
|
||||||
|
out := new(RegisterResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Auth_Register_FullMethodName, in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
|
||||||
|
out := new(LoginResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Auth_Login_FullMethodName, in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *authClient) IsAdmin(ctx context.Context, in *IsAdminRequest, opts ...grpc.CallOption) (*IsAdminResponse, error) {
|
||||||
|
out := new(IsAdminResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Auth_IsAdmin_FullMethodName, in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthServer is the server API for Auth service.
|
||||||
|
// All implementations must embed UnimplementedAuthServer
|
||||||
|
// for forward compatibility
|
||||||
|
type AuthServer interface {
|
||||||
|
Register(context.Context, *RegisterRequest) (*RegisterResponse, error)
|
||||||
|
Login(context.Context, *LoginRequest) (*LoginResponse, error)
|
||||||
|
IsAdmin(context.Context, *IsAdminRequest) (*IsAdminResponse, error)
|
||||||
|
mustEmbedUnimplementedAuthServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedAuthServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedAuthServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedAuthServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Register not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAuthServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAuthServer) IsAdmin(context.Context, *IsAdminRequest) (*IsAdminResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method IsAdmin not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {}
|
||||||
|
|
||||||
|
// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to AuthServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeAuthServer interface {
|
||||||
|
mustEmbedUnimplementedAuthServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) {
|
||||||
|
s.RegisterService(&Auth_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Auth_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RegisterRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AuthServer).Register(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Auth_Register_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AuthServer).Register(ctx, req.(*RegisterRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(LoginRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AuthServer).Login(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Auth_Login_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AuthServer).Login(ctx, req.(*LoginRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Auth_IsAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(IsAdminRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AuthServer).IsAdmin(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Auth_IsAdmin_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AuthServer).IsAdmin(ctx, req.(*IsAdminRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth_ServiceDesc is the grpc.ServiceDesc for Auth service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var Auth_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "auth.Auth",
|
||||||
|
HandlerType: (*AuthServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "Register",
|
||||||
|
Handler: _Auth_Register_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Login",
|
||||||
|
Handler: _Auth_Login_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "IsAdmin",
|
||||||
|
Handler: _Auth_IsAdmin_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "sso/sso.proto",
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
module sso
|
||||||
|
|
||||||
|
go 1.21.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.16.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||||
|
golang.org/x/crypto v0.16.0
|
||||||
|
google.golang.org/grpc v1.59.0
|
||||||
|
google.golang.org/protobuf v1.31.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.26.3 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.16.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.19 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
golang.org/x/net v0.14.0 // indirect
|
||||||
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,76 @@
|
||||||
|
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.26.3 h1:3ljYrjPwsUNAUFdUIr2jVg5EhKdcke/ZLop7uVg1Er8=
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.26.3/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||||
|
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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
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.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||||
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
|
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||||
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||||
|
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||||
|
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
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=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
|
@ -0,0 +1,28 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
grpcapp "sso/internal/app/grpc"
|
||||||
|
"sso/internal/services/auth"
|
||||||
|
"sso/internal/storage/sqlite"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
GRPCSrv *grpcapp.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(log *slog.Logger, grpcPort int, storagePath string, tokenTTL time.Duration) *App {
|
||||||
|
storage, err := sqlite.New(storagePath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authService := auth.New(log, storage, storage, storage, tokenTTL)
|
||||||
|
|
||||||
|
grpcApp := grpcapp.New(log, authService, grpcPort)
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
GRPCSrv: grpcApp,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package grpcapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
authgrpc "sso/internal/grpc/auth"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
log *slog.Logger
|
||||||
|
gRPCServer *grpc.Server
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(log *slog.Logger, authService authgrpc.Auth, port int) *App {
|
||||||
|
gRPCServer := grpc.NewServer()
|
||||||
|
|
||||||
|
authgrpc.Register(gRPCServer, authService)
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
log: log,
|
||||||
|
gRPCServer: gRPCServer,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) MustRun() {
|
||||||
|
if err := a.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Run() error {
|
||||||
|
const op = "grpcapp.Run"
|
||||||
|
|
||||||
|
log := a.log.With(
|
||||||
|
slog.String("op", op),
|
||||||
|
)
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("gRPC server is running", slog.String("addr", l.Addr().String()))
|
||||||
|
|
||||||
|
if err := a.gRPCServer.Serve(l); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Stop() {
|
||||||
|
const op = "grpcapp.Stop"
|
||||||
|
|
||||||
|
a.log.With(slog.String("op", op)).Info("stopping gRPC server")
|
||||||
|
|
||||||
|
a.gRPCServer.GracefulStop()
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ilyakaznacheev/cleanenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Env string `yaml:"env" env-required:"true"`
|
||||||
|
StoragePath string `yaml:"storage_path" env-required:"true"`
|
||||||
|
TokenTTL time.Duration `yaml:"token_ttl" env-default:"1h"`
|
||||||
|
GRPC struct {
|
||||||
|
Port int `yaml:"port" env-default:"44044"`
|
||||||
|
Timeout time.Duration `yaml:"timeout" env-default:"10s"`
|
||||||
|
} `yaml:"grpc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustLoad() *Config {
|
||||||
|
path := fetchConfigPath()
|
||||||
|
if path == "" {
|
||||||
|
panic("config path is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return MustLoadByPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustLoadByPath(configPath string) *Config {
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
panic("config path does not exist: " + configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := cleanenv.ReadConfig(configPath, &cfg); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchConfigPath() string {
|
||||||
|
var res string
|
||||||
|
|
||||||
|
// --config="path/to/config.yaml"
|
||||||
|
flag.StringVar(&res, "config", "", "path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if res == "" {
|
||||||
|
res = os.Getenv("CONFIG_PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Secret string
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int64
|
||||||
|
Email string
|
||||||
|
PassHash []byte
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
ssov1 "sso/gen/sso"
|
||||||
|
"sso/internal/services/auth"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Auth interface {
|
||||||
|
Login(ctx context.Context, email string, password string, appID int) (token string, err error)
|
||||||
|
RegisterNewUser(ctx context.Context, email string, password string) (userID int64, err error)
|
||||||
|
IsAdmin(ctx context.Context, userID int64) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverAPI struct {
|
||||||
|
ssov1.UnimplementedAuthServer
|
||||||
|
auth Auth
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(gRPC *grpc.Server, auth Auth) {
|
||||||
|
ssov1.RegisterAuthServer(gRPC, &serverAPI{auth: auth})
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
emptyValue = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *serverAPI) Login(ctx context.Context, req *ssov1.LoginRequest) (*ssov1.LoginResponse, error) {
|
||||||
|
// validate recieved data
|
||||||
|
if req.GetEmail() == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "email is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GetPassword() == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GetAppId() == emptyValue {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "app_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// login via auth service
|
||||||
|
token, err := s.auth.Login(ctx, req.GetEmail(), req.GetPassword(), int(req.GetAppId()))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, auth.ErrInvalidCredentials) {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ssov1.LoginResponse{
|
||||||
|
Token: token,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverAPI) Register(ctx context.Context, req *ssov1.RegisterRequest) (*ssov1.RegisterResponse, error) {
|
||||||
|
// validate fields
|
||||||
|
if req.GetEmail() == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "email is required")
|
||||||
|
}
|
||||||
|
if req.GetPassword() == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := s.auth.RegisterNewUser(ctx, req.GetEmail(), req.GetPassword())
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, auth.ErrUserExists) {
|
||||||
|
return nil, status.Error(codes.AlreadyExists, "user already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ssov1.RegisterResponse{
|
||||||
|
UserId: userID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverAPI) IsAdmin(ctx context.Context, req *ssov1.IsAdminRequest) (*ssov1.IsAdminResponse, error) {
|
||||||
|
if req.GetUserId() == emptyValue {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "user_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin, err := s.auth.IsAdmin(ctx, req.GetUserId())
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, auth.ErrUserNotFound) {
|
||||||
|
return nil, status.Error(codes.NotFound, "user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ssov1.IsAdminResponse{
|
||||||
|
IsAdmin: isAdmin,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sso/internal/domain/models"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewToken(user models.User, app_id int, secret string, duration time.Duration) (string, error) {
|
||||||
|
token := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["uid"] = user.ID
|
||||||
|
claims["email"] = user.Email
|
||||||
|
claims["exp"] = time.Now().Add(duration).Unix()
|
||||||
|
claims["app_id"] = app_id
|
||||||
|
|
||||||
|
tokenString, err := token.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package sl
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
|
func Err(err error) slog.Attr {
|
||||||
|
return slog.Attr{
|
||||||
|
Key: "error",
|
||||||
|
Value: slog.StringValue(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package slogdiscard
|
||||||
|
|
||||||
|
/*
|
||||||
|
Logger for tests with ignoring input
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDiscardLogger() *slog.Logger {
|
||||||
|
return slog.New(NewDiscardHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscardHandler struct{}
|
||||||
|
|
||||||
|
func NewDiscardHandler() *DiscardHandler {
|
||||||
|
return &DiscardHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error {
|
||||||
|
// Просто игнорируем запись журнала
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler {
|
||||||
|
// Возвращает тот же обработчик, так как нет атрибутов для сохранения
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DiscardHandler) WithGroup(_ string) slog.Handler {
|
||||||
|
// Возвращает тот же обработчик, так как нет группы для сохранения
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool {
|
||||||
|
// Всегда возвращает false, так как запись журнала игнорируется
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package slogpretty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
stdLog "log"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PrettyHandlerOptions struct {
|
||||||
|
SlogOpts *slog.HandlerOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrettyHandler struct {
|
||||||
|
opts PrettyHandlerOptions
|
||||||
|
slog.Handler
|
||||||
|
l *stdLog.Logger
|
||||||
|
attrs []slog.Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts PrettyHandlerOptions) NewPrettyHandler(
|
||||||
|
out io.Writer,
|
||||||
|
) *PrettyHandler {
|
||||||
|
h := &PrettyHandler{
|
||||||
|
Handler: slog.NewJSONHandler(out, opts.SlogOpts),
|
||||||
|
l: stdLog.New(out, "", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error {
|
||||||
|
level := r.Level.String() + ":"
|
||||||
|
|
||||||
|
switch r.Level {
|
||||||
|
case slog.LevelDebug:
|
||||||
|
level = color.MagentaString(level)
|
||||||
|
case slog.LevelInfo:
|
||||||
|
level = color.BlueString(level)
|
||||||
|
case slog.LevelWarn:
|
||||||
|
level = color.YellowString(level)
|
||||||
|
case slog.LevelError:
|
||||||
|
level = color.RedString(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := make(map[string]interface{}, r.NumAttrs())
|
||||||
|
|
||||||
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
|
fields[a.Key] = a.Value.Any()
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, a := range h.attrs {
|
||||||
|
fields[a.Key] = a.Value.Any()
|
||||||
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(fields) > 0 {
|
||||||
|
b, err = json.MarshalIndent(fields, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStr := r.Time.Format("[15:05:05.000]")
|
||||||
|
msg := color.CyanString(r.Message)
|
||||||
|
|
||||||
|
h.l.Println(
|
||||||
|
timeStr,
|
||||||
|
level,
|
||||||
|
msg,
|
||||||
|
color.WhiteString(string(b)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
return &PrettyHandler{
|
||||||
|
Handler: h.Handler,
|
||||||
|
l: h.l,
|
||||||
|
attrs: attrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PrettyHandler) WithGroup(name string) slog.Handler {
|
||||||
|
// TODO: implement
|
||||||
|
return &PrettyHandler{
|
||||||
|
Handler: h.Handler.WithGroup(name),
|
||||||
|
l: h.l,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sso/internal/domain/models"
|
||||||
|
"sso/internal/lib/jwt"
|
||||||
|
"sso/internal/lib/logger/sl"
|
||||||
|
"sso/internal/storage"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
log *slog.Logger
|
||||||
|
usrSaver UserSaver
|
||||||
|
usrProvider UserProvider
|
||||||
|
appProvider AppProvider
|
||||||
|
tokenTTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSaver interface {
|
||||||
|
SaveUser(ctx context.Context, email string, passHash []byte) (uid int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProvider interface {
|
||||||
|
User(ctx context.Context, email string) (models.User, error)
|
||||||
|
IsAdmin(ctx context.Context, userID int64) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppProvider interface {
|
||||||
|
App(ctx context.Context, appID int) (models.App, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||||
|
ErrInvalidAppID = errors.New("invalid app id")
|
||||||
|
ErrUserExists = errors.New("user already exists")
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a new instance of the Auth service.
|
||||||
|
func New(log *slog.Logger, userSaver UserSaver, userProvider UserProvider, appProvider AppProvider, tokenTTL time.Duration) *Auth {
|
||||||
|
return &Auth{
|
||||||
|
log: log,
|
||||||
|
usrSaver: userSaver,
|
||||||
|
usrProvider: userProvider,
|
||||||
|
appProvider: appProvider,
|
||||||
|
tokenTTL: tokenTTL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login checks if user with given credentials exists in the system and returns access token.
|
||||||
|
//
|
||||||
|
// If user exists, but password is incorrect, returns error.
|
||||||
|
// If user doesn't exist, returns error.
|
||||||
|
func (a *Auth) Login(ctx context.Context, email string, password string, appID int) (string, error) {
|
||||||
|
const op = "services.auth.Login"
|
||||||
|
|
||||||
|
log := a.log.With(
|
||||||
|
slog.String("op", op),
|
||||||
|
slog.String("username", email),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Info("attempting to login user")
|
||||||
|
|
||||||
|
user, err := a.usrProvider.User(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrUserNotFound) {
|
||||||
|
a.log.Warn("user not found", sl.Err(err))
|
||||||
|
return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log.Error("failed to get user", sl.Err(err))
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword(user.PassHash, []byte(password)); err != nil {
|
||||||
|
a.log.Info("invalid credentials", sl.Err(err))
|
||||||
|
return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := a.appProvider.App(ctx, appID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("user logged in successfully")
|
||||||
|
|
||||||
|
token, err := jwt.NewToken(user, app.ID, app.Secret, a.tokenTTL)
|
||||||
|
if err != nil {
|
||||||
|
a.log.Error("failed to generate token", sl.Err(err))
|
||||||
|
return "", fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterNewUser registers new user in the system and returns user ID.
|
||||||
|
// If user with given username already exists, returns error.
|
||||||
|
func (a *Auth) RegisterNewUser(ctx context.Context, email string, pass string) (int64, error) {
|
||||||
|
const op = "services.auth.RegisterNewUser"
|
||||||
|
|
||||||
|
log := a.log.With(
|
||||||
|
slog.String("op", op),
|
||||||
|
slog.String("email", email),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Info("registering user")
|
||||||
|
|
||||||
|
passHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to generate password hash", sl.Err(err))
|
||||||
|
return 0, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := a.usrSaver.SaveUser(ctx, email, passHash)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrUserExists) {
|
||||||
|
log.Warn("user already exists", sl.Err(err))
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("%s: %w", op, ErrUserExists)
|
||||||
|
}
|
||||||
|
log.Error("failed to save user", sl.Err(err))
|
||||||
|
return 0, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("user registred")
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdmin checks if user is admin
|
||||||
|
func (a *Auth) IsAdmin(ctx context.Context, userID int64) (bool, error) {
|
||||||
|
const op = "services.auth.IsAdmin"
|
||||||
|
|
||||||
|
log := a.log.With(
|
||||||
|
slog.String("op", op),
|
||||||
|
slog.Int64("user_id", userID),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Info("checking if user is admin")
|
||||||
|
|
||||||
|
isAdmin, err := a.usrProvider.IsAdmin(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrAppNotFound) {
|
||||||
|
log.Warn("app not found", sl.Err(err))
|
||||||
|
|
||||||
|
return false, fmt.Errorf("%s: %w", op, ErrInvalidAppID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("checking if user is admin", slog.Bool("is_admin", isAdmin))
|
||||||
|
|
||||||
|
return isAdmin, nil
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sso/internal/domain/models"
|
||||||
|
"sso/internal/storage"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(storagePath string) (*Storage, error) {
|
||||||
|
const op = "storage.sqlite.New"
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", storagePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Storage{db: db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) SaveUser(ctx context.Context, email string, passHash []byte) (int64, error) {
|
||||||
|
const op = "storage.sqlite.SaveUser"
|
||||||
|
|
||||||
|
stmt, err := s.db.Prepare("insert into users(email, pass_hash) values (?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := stmt.ExecContext(ctx, email, passHash)
|
||||||
|
if err != nil {
|
||||||
|
var sqliteErr sqlite3.Error
|
||||||
|
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||||
|
return 0, fmt.Errorf("%s: %w", op, storage.ErrUserExists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns user by email.
|
||||||
|
func (s *Storage) User(ctx context.Context, email string) (models.User, error) {
|
||||||
|
const op = "storage.sqlite.User"
|
||||||
|
|
||||||
|
stmt, err := s.db.Prepare("select id, email, pass_hash from users where email = ?")
|
||||||
|
if err != nil {
|
||||||
|
return models.User{}, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err = stmt.QueryRowContext(ctx, email).Scan(&user.ID, &user.Email, &user.PassHash)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return models.User{}, fmt.Errorf("%s: %w", op, storage.ErrUserNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.User{}, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) IsAdmin(ctx context.Context, userID int64) (bool, error) {
|
||||||
|
const op = "storage.sqlite.IsAdmin"
|
||||||
|
|
||||||
|
stmt, err := s.db.Prepare("select is_admin from users where id = ?")
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_admin bool
|
||||||
|
err = stmt.QueryRowContext(ctx, userID).Scan(&is_admin)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return false, fmt.Errorf("%s: %w", op, storage.ErrAppNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_admin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// App returns app by id.
|
||||||
|
func (s *Storage) App(ctx context.Context, id int) (models.App, error) {
|
||||||
|
const op = "storage.sqlite.App"
|
||||||
|
|
||||||
|
stmt, err := s.db.Prepare("SELECT id, name, secret FROM apps WHERE id = ?")
|
||||||
|
if err != nil {
|
||||||
|
return models.App{}, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := stmt.QueryRowContext(ctx, id)
|
||||||
|
|
||||||
|
var app models.App
|
||||||
|
err = row.Scan(&app.ID, &app.Name, &app.Secret)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return models.App{}, fmt.Errorf("%s: %w", op, storage.ErrAppNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.App{}, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserExists = errors.New("user already exists")
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
ErrAppNotFound = errors.New("app not found")
|
||||||
|
)
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
DROP TABLE IF EXISTS apps;
|
|
@ -0,0 +1,12 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
pass_hash BLOB NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email ON users (email);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS apps (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
secret TEXT NOT NULL UNIQUE
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE users DROP COLUMN is_admin;
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -0,0 +1,38 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package auth;
|
||||||
|
|
||||||
|
option go_package = "yash.sso.v1;ssov1";
|
||||||
|
|
||||||
|
service Auth {
|
||||||
|
rpc Register (RegisterRequest) returns (RegisterResponse);
|
||||||
|
rpc Login (LoginRequest) returns (LoginResponse);
|
||||||
|
rpc IsAdmin (IsAdminRequest) returns (IsAdminResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterRequest {
|
||||||
|
string email = 1; // Email of the user to register
|
||||||
|
string password = 2; // User ID of the registered user
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterResponse {
|
||||||
|
int64 user_id = 1; // User ID of the registered user.
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string email = 1; // Email of the user to login.
|
||||||
|
string password = 2; // Password of the user to login.
|
||||||
|
int32 app_id = 3; // ID of the app to login to.
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
string token = 1; // Auth token of the logged in user.
|
||||||
|
}
|
||||||
|
|
||||||
|
message IsAdminRequest {
|
||||||
|
int64 user_id = 1; // User ID to validate
|
||||||
|
}
|
||||||
|
|
||||||
|
message IsAdminResponse {
|
||||||
|
bool is_admin = 1; // Indicates whether the user is an admin.
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
ssov1 "sso/gen/sso"
|
||||||
|
"sso/tests/suite"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
emptyAppID = 0
|
||||||
|
appID = 1
|
||||||
|
appSecret = "test-secret"
|
||||||
|
|
||||||
|
passDefaultLen = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegisterLogin_Login_HappyPath(t *testing.T) {
|
||||||
|
ctx, st := suite.New(t)
|
||||||
|
|
||||||
|
email := gofakeit.Email()
|
||||||
|
pass := randomFakePassword()
|
||||||
|
|
||||||
|
respReg, err := st.AuthClient.Register(ctx, &ssov1.RegisterRequest{
|
||||||
|
Email: email,
|
||||||
|
Password: pass,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, respReg.GetUserId())
|
||||||
|
|
||||||
|
respLogin, err := st.AuthClient.Login(ctx, &ssov1.LoginRequest{
|
||||||
|
Email: email,
|
||||||
|
Password: pass,
|
||||||
|
AppId: appID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
loginTime := time.Now()
|
||||||
|
|
||||||
|
token := respLogin.GetToken()
|
||||||
|
require.NotEmpty(t, token)
|
||||||
|
|
||||||
|
tokenParsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(appSecret), err
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
claims, ok := tokenParsed.Claims.(jwt.MapClaims)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
assert.Equal(t, respReg.GetUserId(), int64(claims["uid"].(float64)))
|
||||||
|
assert.Equal(t, email, claims["email"].(string))
|
||||||
|
assert.Equal(t, appID, int(claims["app_id"].(float64)))
|
||||||
|
|
||||||
|
const deltaSeconds = 1
|
||||||
|
|
||||||
|
assert.InDelta(t, loginTime.Add(st.Cfg.TokenTTL).Unix(), claims["exp"].(float64), deltaSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterLogin_DuplicatedRegistration(t *testing.T) {
|
||||||
|
ctx, st := suite.New(t)
|
||||||
|
|
||||||
|
email := gofakeit.Email()
|
||||||
|
pass := randomFakePassword()
|
||||||
|
|
||||||
|
reqFirstReg, err := st.AuthClient.Register(ctx, &ssov1.RegisterRequest{
|
||||||
|
Email: email,
|
||||||
|
Password: pass,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, reqFirstReg.GetUserId())
|
||||||
|
|
||||||
|
reqSecondReg, err := st.AuthClient.Register(ctx, &ssov1.RegisterRequest{
|
||||||
|
Email: email,
|
||||||
|
Password: pass,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Empty(t, reqSecondReg.GetUserId())
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "user already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomFakePassword() string {
|
||||||
|
return gofakeit.Password(true, true, true, true, false, passDefaultLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_FailCases(t *testing.T) {
|
||||||
|
ctx, st := suite.New(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
email string
|
||||||
|
password string
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Register with empty password",
|
||||||
|
email: gofakeit.Email(),
|
||||||
|
password: "",
|
||||||
|
expectedErr: "password is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Register with empty email",
|
||||||
|
email: "",
|
||||||
|
password: randomFakePassword(),
|
||||||
|
expectedErr: "email is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Register with both empty",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
expectedErr: "email is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := st.AuthClient.Register(ctx, &ssov1.RegisterRequest{
|
||||||
|
Email: tt.email,
|
||||||
|
Password: tt.password,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tt.expectedErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogin_FailCases(t *testing.T) {
|
||||||
|
ctx, st := suite.New(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
email string
|
||||||
|
password string
|
||||||
|
appID int32
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Login with Empty Password",
|
||||||
|
email: gofakeit.Email(),
|
||||||
|
password: "",
|
||||||
|
appID: appID,
|
||||||
|
expectedErr: "password is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Login with Empty Email",
|
||||||
|
email: "",
|
||||||
|
password: randomFakePassword(),
|
||||||
|
appID: appID,
|
||||||
|
expectedErr: "email is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Login with Both Empty Email and Password",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
appID: appID,
|
||||||
|
expectedErr: "email is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Login with Non-Matching Password",
|
||||||
|
email: gofakeit.Email(),
|
||||||
|
password: randomFakePassword(),
|
||||||
|
appID: appID,
|
||||||
|
expectedErr: "invalid credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Login without AppID",
|
||||||
|
email: gofakeit.Email(),
|
||||||
|
password: randomFakePassword(),
|
||||||
|
appID: emptyAppID,
|
||||||
|
expectedErr: "app_id is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := st.AuthClient.Register(ctx, &ssov1.RegisterRequest{
|
||||||
|
Email: gofakeit.Email(),
|
||||||
|
Password: randomFakePassword(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = st.AuthClient.Login(ctx, &ssov1.LoginRequest{
|
||||||
|
Email: tt.email,
|
||||||
|
Password: tt.password,
|
||||||
|
AppId: tt.appID,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tt.expectedErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
insert into apps (id, name, secret) values (1, 'test', 'test-secret') ON CONFLICT DO NOTHING;
|
|
@ -0,0 +1,53 @@
|
||||||
|
package suite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
ssov1 "sso/gen/sso"
|
||||||
|
"sso/internal/config"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Suite struct {
|
||||||
|
*testing.T
|
||||||
|
Cfg *config.Config
|
||||||
|
AuthClient ssov1.AuthClient
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
grpcHost = "localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(t *testing.T) (context.Context, *Suite) {
|
||||||
|
t.Helper()
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg := config.MustLoadByPath("../config/local.yaml")
|
||||||
|
|
||||||
|
ctx, cancelCtx := context.WithTimeout(context.Background(), cfg.GRPC.Timeout)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
t.Helper()
|
||||||
|
cancelCtx()
|
||||||
|
})
|
||||||
|
|
||||||
|
cc, err := grpc.DialContext(context.Background(), grpcAddress(cfg),
|
||||||
|
// grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithInsecure(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("grpc server connection failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, &Suite{
|
||||||
|
T: t,
|
||||||
|
Cfg: cfg,
|
||||||
|
AuthClient: ssov1.NewAuthClient(cc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func grpcAddress(cfg *config.Config) string {
|
||||||
|
return net.JoinHostPort(grpcHost, strconv.Itoa(cfg.GRPC.Port))
|
||||||
|
}
|
Loading…
Reference in New Issue