Skip to content
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
TW_ENV=development
TW_LOG_LEVEL=INFO

TW_OTP_PROVIDER=mock
TW_ALLOWED_EMAIL_DOMAINS=@schools.gov.sg
TW_MOCK_ALLOWED_EMAILS=tracy_lim@schools.gov.sg

# TW_VITE_DEV_SERVER_URL=http://localhost:5173
# TW_BUNDLE_DIRECTORY=dist

Expand Down
19 changes: 18 additions & 1 deletion server/cmd/tw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/String-sg/teacher-workspace/server/internal/config"
"github.com/String-sg/teacher-workspace/server/internal/handler"
"github.com/String-sg/teacher-workspace/server/internal/middleware"
"github.com/String-sg/teacher-workspace/server/internal/otp"
"github.com/String-sg/teacher-workspace/server/pkg/dotenv"
"golang.org/x/sync/errgroup"
)
Expand Down Expand Up @@ -58,7 +59,23 @@ func main() {
}

func run(ctx context.Context, cfg *config.Config) error {
h, err := handler.New(cfg)
var otpProvider otp.Provider
switch cfg.OTPProvider {
case config.ProviderMock:
otpProvider = otp.NewMockProvider(cfg.Mock.AllowedEmails)
case config.ProviderOTPaaS:
otpProvider = otp.NewOTPaaSProvider(
cfg.OTPaaS.Host,
cfg.OTPaaS.ID,
cfg.OTPaaS.Namespace,
cfg.OTPaaS.Secret,
cfg.OTPaaS.Timeout,
)
default:
return fmt.Errorf("unsupported OTP provider: %q", cfg.OTPProvider)
}

h, err := handler.New(cfg, otpProvider)
if err != nil {
return fmt.Errorf("create handler: %w", err)
}
Expand Down
45 changes: 44 additions & 1 deletion server/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,30 @@ import (
)

type Environment string
type Provider string

const (
EnvironmentDevelopment Environment = "development"
EnvironmentProduction Environment = "production"

ProviderOTPaaS Provider = "otpaas"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ProviderOTPaaS Provider = "otpaas"
OTPProviderOTPaaS Provider = "otpaas"

I think we should prefix with OTP because there will be a lot of different provider services in the future.

ProviderMock Provider = "mock"
)

// Config is the main configuration for the application.
type Config struct {
Environment Environment `dotenv:"TW_ENV"`
LogLevel slog.Level `dotenv:"TW_LOG_LEVEL"`

OTPProvider Provider `dotenv:"TW_OTP_PROVIDER"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BOTP service only focusing on OTP operations so the configurations do not need to be nested. TW will have a lot of different kind of configurations so I think we should nest OTP related configurations into a struct.

For example:

type Config struct {
    ...

    OTP OTPConfig `dotenv:",squash"`
}

type OTPConfig struct {
    Provider OTPProvider `dotenv:"TW_OTP_PROVIDER"`
    AllowedEmailDomains []string `dotenv:"TW_OTP_ALLOWED_EMAIL_DOMAINS"`

    OTPaaS OTPaaSConfig `dotenv:",squash"`
    Mock     MockConfig      `dotenv:",squash"`
}

Then we add comments to OTPaaSConfig and MockConfig:

// OTPaaSConfig is the configuration for the OTPaaS OTP provider.
type OTPaaSConfig struct {
    ...
}

// MockConfig is the configuration for the mock OTP provider.
type MockConfig struct {
    ...
}

WDYT?

AllowedEmailDomains []string `dotenv:"TW_ALLOWED_EMAIL_DOMAINS"`

ViteDevServerURL *url.URL `dotenv:"TW_VITE_DEV_SERVER_URL"`
BundleDirectory string `dotenv:"TW_BUNDLE_DIRECTORY"`

Server ServerConfig `dotenv:",squash"`
OTPaaS OTPaaSConfig `dotenv:",squash"`
Mock MockConfig `dotenv:",squash"`
}

// ServerConfig represents the configuration for the HTTP server.
Expand All @@ -47,12 +55,19 @@ type OTPaaSConfig struct {
Timeout time.Duration `dotenv:"TW_OTPAAS_TIMEOUT"`
}

type MockConfig struct {
AllowedEmails []string `dotenv:"TW_MOCK_ALLOWED_EMAILS"`
}

// Default returns the default configuration for the application.
func Default() *Config {
return &Config{
Environment: EnvironmentDevelopment,
LogLevel: slog.LevelInfo,

OTPProvider: ProviderOTPaaS,
AllowedEmailDomains: []string{"@schools.gov.sg"},

ViteDevServerURL: must(url.Parse("http://localhost:5173")),
BundleDirectory: "dist",

Expand All @@ -72,6 +87,9 @@ func Default() *Config {
Secret: "",
Timeout: 10 * time.Second,
},
Mock: MockConfig{
AllowedEmails: nil,
},
}
}

Expand All @@ -82,6 +100,12 @@ func (cfg *Config) Validate() error {
errs = append(errs, fmt.Errorf("TW_ENV must be one of %q or %q; got %q", EnvironmentDevelopment, EnvironmentProduction, cfg.Environment))
}

if len(cfg.AllowedEmailDomains) == 0 {
errs = append(errs, errors.New("TW_ALLOWED_EMAIL_DOMAINS is required"))
}

errs = append(errs, cfg.Server.validate())

switch cfg.Environment {
case EnvironmentDevelopment:
if cfg.ViteDevServerURL.Scheme != "http" && cfg.ViteDevServerURL.Scheme != "https" {
Expand All @@ -100,7 +124,16 @@ func (cfg *Config) Validate() error {
}
}

return errors.Join(append(errs, cfg.Server.validate(), cfg.OTPaaS.validate())...)
switch cfg.OTPProvider {
case ProviderOTPaaS:
errs = append(errs, cfg.OTPaaS.validate())
case ProviderMock:
errs = append(errs, cfg.Mock.validate())
default:
errs = append(errs, fmt.Errorf("TW_OTP_PROVIDER must be one of %q or %q; got %q", ProviderOTPaaS, ProviderMock, cfg.OTPProvider))
}

return errors.Join(errs...)
}

func (c ServerConfig) validate() error {
Expand Down Expand Up @@ -147,6 +180,16 @@ func (c OTPaaSConfig) validate() error {
return errors.Join(errs...)
}

func (c MockConfig) validate() error {
var errs []error

if len(c.AllowedEmails) == 0 {
errs = append(errs, errors.New("TW_MOCK_ALLOWED_EMAILS is required"))
}

return errors.Join(errs...)
}

func must[T any](value T, err error) T {
if err != nil {
panic(err)
Expand Down
Loading
Loading