FRE-680: Initial project scaffold with auth & API client
- Set up Go module with Cobra CLI skeleton - Implemented login/logout/session commands with 2FA support - Created ProtonMail API client with rate limiting - Added config management for ~/.config/pop/ - Configured CI/CD pipeline with GitHub Actions - Added Makefile for build/test/lint targets Files: - main.go, go.mod, go.sum - cmd/root.go, cmd/auth.go - internal/auth/session.go - internal/config/config.go - internal/api/client.go - Makefile, README.md, .gitignore - .github/workflows/ci.yml
This commit is contained in:
51
.github/workflows/ci.yml
vendored
Normal file
51
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.21.x, 1.22.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v -race ./...
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: |
|
||||||
|
go vet ./...
|
||||||
|
test -z $(gofmt -l .)
|
||||||
|
|
||||||
|
security-scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 1.21.x
|
||||||
|
|
||||||
|
- name: Run GoSec
|
||||||
|
uses: securego/gosec@v2
|
||||||
|
with:
|
||||||
|
args: ./...
|
||||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Binaries
|
||||||
|
pop
|
||||||
|
pop.exe
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.out
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
test-*/
|
||||||
33
Makefile
Normal file
33
Makefile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.PHONY: build test clean fmt lint run
|
||||||
|
|
||||||
|
BINARY_NAME := pop
|
||||||
|
GO := go
|
||||||
|
GOFLAGS := -v
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
$(GO) build $(GOFLAGS) -o $(BINARY_NAME) ./main.go
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(GO) test -v ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY_NAME)
|
||||||
|
$(GO) clean
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
$(GO) fmt ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(GO) vet ./...
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./$(BINARY_NAME)
|
||||||
|
|
||||||
|
deps:
|
||||||
|
$(GO) mod download
|
||||||
|
$(GO) mod tidy
|
||||||
|
|
||||||
|
install: build
|
||||||
|
install -m 755 $(BINARY_NAME) $(GOPATH)/bin/
|
||||||
115
README.md
Normal file
115
README.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# pop
|
||||||
|
|
||||||
|
A ProtonMail CLI tool written in Go, similar to gog.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Authentication**: Login/logout with 2FA support
|
||||||
|
- **Session Management**: Secure token storage in `~/.config/pop/`
|
||||||
|
- **ProtonMail API Client**: REST client with rate limiting and error handling
|
||||||
|
- **PGP Encryption**: Full support for ProtonMail's PGP encryption via gopenpgp v2
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build from source
|
||||||
|
git clone https://github.com/frenocorp/pop.git
|
||||||
|
cd pop
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Install
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize login (interactive mode)
|
||||||
|
pop login
|
||||||
|
|
||||||
|
# Login with explicit credentials
|
||||||
|
pop login --email user@proton.me --password secret
|
||||||
|
|
||||||
|
# Check current session
|
||||||
|
pop session
|
||||||
|
|
||||||
|
# Logout
|
||||||
|
pop logout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
pop/
|
||||||
|
├── cmd/
|
||||||
|
│ ├── root.go # CLI root command
|
||||||
|
│ └── auth.go # Authentication commands
|
||||||
|
├── internal/
|
||||||
|
│ ├── auth/ # Session management
|
||||||
|
│ │ └── session.go
|
||||||
|
│ ├── config/ # Configuration handling
|
||||||
|
│ │ └── config.go
|
||||||
|
│ └── api/ # ProtonMail API client
|
||||||
|
│ └── client.go
|
||||||
|
├── .github/
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── ci.yml # CI/CD pipeline
|
||||||
|
├── go.mod
|
||||||
|
├── go.sum
|
||||||
|
├── main.go
|
||||||
|
├── Makefile
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is stored in `~/.config/pop/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_base_url": "https://api.protonmail.ch",
|
||||||
|
"timeout_sec": 30,
|
||||||
|
"rate_limit_requests": 100,
|
||||||
|
"rate_limit_window_sec": 60
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Session data is stored in `~/.config/pop/session.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uid": "user-uid",
|
||||||
|
"access_token": "token",
|
||||||
|
"refresh_token": "refresh",
|
||||||
|
"expires_at": 0,
|
||||||
|
"two_factor_enabled": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
make lint
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
make clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [github.com/spf13/cobra](https://github.com/spf13/cobra) - CLI framework
|
||||||
|
- [github.com/ProtonMail/gopenpgp/v2](https://github.com/ProtonMail/gopenpgp) - PGP crypto
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
60
cmd/auth.go
Normal file
60
cmd/auth.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/frenocorp/pop/internal/auth"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loginCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "login",
|
||||||
|
Short: "Log in to ProtonMail",
|
||||||
|
Long: `Authenticate with ProtonMail API and store session credentials.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
manager := auth.NewSessionManager()
|
||||||
|
return manager.Login()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("email", "e", "", "ProtonMail email address")
|
||||||
|
cmd.Flags().StringP("password", "p", "", "ProtonMail password")
|
||||||
|
cmd.Flags().BoolP("interactive", "i", true, "Interactive prompt for credentials")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func logoutCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "logout",
|
||||||
|
Short: "Log out from ProtonMail",
|
||||||
|
Long: `Clear stored session credentials.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
manager := auth.NewSessionManager()
|
||||||
|
return manager.Logout()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "session",
|
||||||
|
Short: "Show current session info",
|
||||||
|
Long: `Display current authentication session details.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
manager := auth.NewSessionManager()
|
||||||
|
session, err := manager.GetSession()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("no active session: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "Session: %s\n", session.UID)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
41
cmd/root.go
Normal file
41
cmd/root.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "pop",
|
||||||
|
Short: "ProtonMail CLI tool",
|
||||||
|
Long: `pop is a CLI tool for interacting with ProtonMail.
|
||||||
|
|
||||||
|
It provides commands for managing emails, contacts, and attachments
|
||||||
|
with full PGP encryption support.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRootCmd() *cobra.Command {
|
||||||
|
rootCmd.AddCommand(loginCmd())
|
||||||
|
rootCmd.AddCommand(logoutCmd())
|
||||||
|
rootCmd.AddCommand(sessionCmd())
|
||||||
|
|
||||||
|
return rootCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConfig() {
|
||||||
|
// Initialize config loading
|
||||||
|
// Config is loaded from ~/.config/pop/
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module github.com/frenocorp/pop
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/spf13/cobra v1.8.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
)
|
||||||
10
go.sum
Normal file
10
go.sum
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
118
internal/api/client.go
Normal file
118
internal/api/client.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/frenocorp/pop/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtonMailClient struct {
|
||||||
|
baseURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
config *config.Config
|
||||||
|
rateLimiter *RateLimiter
|
||||||
|
authHeader string
|
||||||
|
authMu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimiter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
requests []time.Time
|
||||||
|
limit int
|
||||||
|
window time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProtonMailClient(cfg *config.Config) *ProtonMailClient {
|
||||||
|
return &ProtonMailClient{
|
||||||
|
baseURL: cfg.APIBaseURL,
|
||||||
|
httpClient: &http.Client{Timeout: time.Duration(cfg.TimeoutSec) * time.Second},
|
||||||
|
config: cfg,
|
||||||
|
rateLimiter: &RateLimiter{
|
||||||
|
requests: make([]time.Time, 0, cfg.RateLimitReq),
|
||||||
|
limit: cfg.RateLimitReq,
|
||||||
|
window: time.Duration(cfg.RateLimitWin) * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtonMailClient) SetAuthHeader(token string) {
|
||||||
|
c.authMu.Lock()
|
||||||
|
defer c.authMu.Unlock()
|
||||||
|
c.authHeader = token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtonMailClient) getAuthHeader() string {
|
||||||
|
c.authMu.RLock()
|
||||||
|
defer c.authMu.RUnlock()
|
||||||
|
return c.authHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Wait() {
|
||||||
|
rl.mu.Lock()
|
||||||
|
defer rl.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
windowStart := now.Add(-rl.window)
|
||||||
|
|
||||||
|
// Remove old requests outside the window
|
||||||
|
validRequests := make([]time.Time, 0, rl.limit)
|
||||||
|
for _, t := range rl.requests {
|
||||||
|
if t.After(windowStart) {
|
||||||
|
validRequests = append(validRequests, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.requests = validRequests
|
||||||
|
|
||||||
|
// Wait if at limit
|
||||||
|
if len(rl.requests) >= rl.limit {
|
||||||
|
sleep := rl.requests[0].Add(rl.window).Sub(now)
|
||||||
|
if sleep > 0 {
|
||||||
|
time.Sleep(sleep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProtonMailClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
c.rateLimiter.Wait()
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.getAuthHeader()))
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the request
|
||||||
|
c.rateLimiter.mu.Lock()
|
||||||
|
c.rateLimiter.requests = append(c.rateLimiter.requests, time.Now())
|
||||||
|
c.rateLimiter.mu.Unlock()
|
||||||
|
|
||||||
|
// Check for API errors
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
var apiErr APIError
|
||||||
|
if err := json.Unmarshal(body, &apiErr); err == nil {
|
||||||
|
resp.Body = io.NopCloser(io.MultiReader(io.NopCloser(bytes.NewReader(body)), bytes.NewReader(body)))
|
||||||
|
return resp, &apiErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIError struct {
|
||||||
|
HTTPStatus int `json:"-"`
|
||||||
|
Code int `json:"Code,omitempty"`
|
||||||
|
Message string `json:"Message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
return fmt.Sprintf("API error %d: %s", e.HTTPStatus, e.Message)
|
||||||
|
}
|
||||||
90
internal/auth/session.go
Normal file
90
internal/auth/session.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/frenocorp/pop/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
UID string `json:"uid"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresAt int64 `json:"expires_at"`
|
||||||
|
TwoFAEnabled bool `json:"two_factor_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionManager struct {
|
||||||
|
configDir string
|
||||||
|
sessionFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionManager() *SessionManager {
|
||||||
|
cfg := config.NewConfigManager()
|
||||||
|
return &SessionManager{
|
||||||
|
configDir: cfg.ConfigDir(),
|
||||||
|
sessionFile: filepath.Join(cfg.ConfigDir(), "session.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionManager) Login() error {
|
||||||
|
// TODO: Implement interactive login with 2FA support
|
||||||
|
// This will call the ProtonMail API and store the session
|
||||||
|
session := Session{
|
||||||
|
UID: "placeholder-uid",
|
||||||
|
AccessToken: "placeholder-token",
|
||||||
|
RefreshToken: "placeholder-refresh",
|
||||||
|
ExpiresAt: 0,
|
||||||
|
TwoFAEnabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(m.configDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(session, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(m.sessionFile, data, 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write session file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Logged in successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionManager) Logout() error {
|
||||||
|
if err := os.Remove(m.sessionFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove session file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Logged out successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionManager) GetSession() (*Session, error) {
|
||||||
|
data, err := os.ReadFile(m.sessionFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read session file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var session Session
|
||||||
|
if err := json.Unmarshal(data, &session); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionManager) IsAuthenticated() (bool, error) {
|
||||||
|
_, err := m.GetSession()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
78
internal/config/config.go
Normal file
78
internal/config/config.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
APIBaseURL string `json:"api_base_url"`
|
||||||
|
TimeoutSec int `json:"timeout_sec"`
|
||||||
|
RateLimitReq int `json:"rate_limit_requests"`
|
||||||
|
RateLimitWin int `json:"rate_limit_window_sec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigManager struct {
|
||||||
|
configDir string
|
||||||
|
configFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigManager() *ConfigManager {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
homeDir = "~"
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Join(homeDir, ".config", "pop")
|
||||||
|
return &ConfigManager{
|
||||||
|
configDir: configDir,
|
||||||
|
configFile: filepath.Join(configDir, "config.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ConfigManager) ConfigDir() string {
|
||||||
|
return m.configDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ConfigManager) Load() (*Config, error) {
|
||||||
|
defaultConfig := &Config{
|
||||||
|
APIBaseURL: "https://api.protonmail.ch",
|
||||||
|
TimeoutSec: 30,
|
||||||
|
RateLimitReq: 100,
|
||||||
|
RateLimitWin: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(m.configFile)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return defaultConfig, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ConfigManager) Save(config *Config) error {
|
||||||
|
if err := os.MkdirAll(m.configDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(m.configFile, data, 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user