initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI
This commit is contained in:
113
internal/service/auth.go
Normal file
113
internal/service/auth.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"zeromesh/internal/config"
|
||||
"zeromesh/internal/middleware"
|
||||
"zeromesh/internal/model"
|
||||
"zeromesh/internal/repository"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
userRepo *repository.UserRepo
|
||||
jwtCfg config.JWTConfig
|
||||
}
|
||||
|
||||
func NewAuthService(userRepo *repository.UserRepo, jwtCfg config.JWTConfig) *AuthService {
|
||||
return &AuthService{userRepo: userRepo, jwtCfg: jwtCfg}
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(username, password string) (string, *model.User, error) {
|
||||
user, err := s.userRepo.FindByUsername(username)
|
||||
if err != nil {
|
||||
return "", nil, errors.New("invalid credentials")
|
||||
}
|
||||
// Migrate old SHA256 hashes to bcrypt on login
|
||||
if len(user.Password) < 4 || user.Password[:4] != "$2a$" {
|
||||
if sha256Hash(password) != user.Password {
|
||||
return "", nil, errors.New("invalid credentials")
|
||||
}
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("migrate password: %w", err)
|
||||
}
|
||||
user.Password = string(hashed)
|
||||
_ = s.userRepo.UpdatePassword(user.ID, user.Password)
|
||||
} else if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return "", nil, errors.New("invalid credentials")
|
||||
}
|
||||
token, err := middleware.GenerateToken(s.jwtCfg.Secret, user.ID, user.Username, user.Role, s.jwtCfg.ExpireHours)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("generate token: %w", err)
|
||||
}
|
||||
user.Token = token
|
||||
return token, user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(username, password string) (*model.User, string, error) {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
user := &model.User{
|
||||
Username: username,
|
||||
Password: string(hashed),
|
||||
Role: "user",
|
||||
}
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
token, err := middleware.GenerateToken(s.jwtCfg.Secret, user.ID, user.Username, user.Role, s.jwtCfg.ExpireHours)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("generate token: %w", err)
|
||||
}
|
||||
user.Token = token
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) InitAdmin(username, password string) (*model.User, string, error) {
|
||||
count, err := s.userRepo.Count()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, "", fmt.Errorf("admin already exists")
|
||||
}
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
user := &model.User{
|
||||
Username: username,
|
||||
Password: string(hashed),
|
||||
Role: "admin",
|
||||
QuotaNetworks: -1,
|
||||
QuotaNodes: -1,
|
||||
}
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
token, err := middleware.GenerateToken(s.jwtCfg.Secret, user.ID, user.Username, user.Role, s.jwtCfg.ExpireHours)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("generate token: %w", err)
|
||||
}
|
||||
user.Token = token
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func sha256Hash(password string) string {
|
||||
h := sha256.Sum256([]byte(password))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func (s *AuthService) CheckAdmin() (bool, error) {
|
||||
return s.userRepo.HasAdmin()
|
||||
}
|
||||
|
||||
|
||||
54
internal/service/log.go
Normal file
54
internal/service/log.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"zeromesh/internal/model"
|
||||
"zeromesh/internal/repository"
|
||||
)
|
||||
|
||||
type LogService struct {
|
||||
repo *repository.LogRepo
|
||||
}
|
||||
|
||||
func NewLogService(repo *repository.LogRepo) *LogService {
|
||||
return &LogService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *LogService) ListApiLogs(page, pageSize int, ip, path, method string, statusCode int) ([]model.ApiLog, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 200 {
|
||||
pageSize = 20
|
||||
}
|
||||
return s.repo.ListApiLogs(page, pageSize, ip, path, method, statusCode)
|
||||
}
|
||||
|
||||
func (s *LogService) ListErrorLogs(page, pageSize int, ip, path, method string, statusCode int) ([]model.ErrorLog, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 200 {
|
||||
pageSize = 20
|
||||
}
|
||||
return s.repo.ListErrorLogs(page, pageSize, ip, path, method, statusCode)
|
||||
}
|
||||
|
||||
func (s *LogService) ListApiLogGrouped() ([]map[string]interface{}, error) {
|
||||
return s.repo.ListApiLogGrouped()
|
||||
}
|
||||
|
||||
func (s *LogService) InsertApiLog(log *model.ApiLog) error {
|
||||
return s.repo.InsertApiLog(log)
|
||||
}
|
||||
|
||||
func (s *LogService) InsertErrorLog(log *model.ErrorLog) error {
|
||||
return s.repo.InsertErrorLog(log)
|
||||
}
|
||||
|
||||
func (s *LogService) ClearApiLogs() error {
|
||||
return s.repo.ClearApiLogs()
|
||||
}
|
||||
|
||||
func (s *LogService) ClearErrorLogs() error {
|
||||
return s.repo.ClearErrorLogs()
|
||||
}
|
||||
163
internal/service/network.go
Normal file
163
internal/service/network.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"zeromesh/internal/model"
|
||||
"zeromesh/internal/repository"
|
||||
)
|
||||
|
||||
type NetworkService struct {
|
||||
netRepo *repository.NetworkRepo
|
||||
nodeRepo *repository.NodeRepo
|
||||
userRepo *repository.UserRepo
|
||||
}
|
||||
|
||||
func NewNetworkService(netRepo *repository.NetworkRepo, nodeRepo *repository.NodeRepo, userRepo *repository.UserRepo) *NetworkService {
|
||||
return &NetworkService{netRepo: netRepo, nodeRepo: nodeRepo, userRepo: userRepo}
|
||||
}
|
||||
|
||||
func randomPrivateSubnet() string {
|
||||
const (
|
||||
_10 = iota // 10.x.y.0/24
|
||||
_172 // 172.16-31.y.0/24
|
||||
_192 // 192.168.y.0/24
|
||||
)
|
||||
class := rand.Intn(3)
|
||||
var b1, b2 byte
|
||||
switch class {
|
||||
case _10:
|
||||
b1 = byte(rand.Intn(256))
|
||||
b2 = byte(rand.Intn(256))
|
||||
return fmt.Sprintf("10.%d.%d.0/24", b1, b2)
|
||||
case _172:
|
||||
b1 = byte(16 + rand.Intn(16)) // 16-31
|
||||
b2 = byte(rand.Intn(256))
|
||||
return fmt.Sprintf("172.%d.%d.0/24", b1, b2)
|
||||
default:
|
||||
b2 = byte(rand.Intn(256))
|
||||
return fmt.Sprintf("192.168.%d.0/24", b2)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NetworkService) CreateNetwork(userID uint, name, ipRange string) (*model.Network, error) {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
if user.QuotaNetworks >= 0 {
|
||||
used, _ := s.netRepo.CountByUser(userID)
|
||||
if used >= int64(user.QuotaNetworks) {
|
||||
return nil, fmt.Errorf("network quota exceeded (%d)", user.QuotaNetworks)
|
||||
}
|
||||
}
|
||||
if ipRange == "" {
|
||||
ipRange = randomPrivateSubnet()
|
||||
}
|
||||
id := rand.Uint32()
|
||||
network := &model.Network{
|
||||
UserID: userID,
|
||||
NetworkID: id,
|
||||
Name: name,
|
||||
IPRange: ipRange,
|
||||
MTU: 2800,
|
||||
Multicast: true,
|
||||
Private: true,
|
||||
}
|
||||
if err := s.netRepo.Create(network); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return network, nil
|
||||
}
|
||||
|
||||
func (s *NetworkService) ListNetworks(userID uint) ([]model.Network, error) {
|
||||
if userID == 0 {
|
||||
return s.netRepo.List()
|
||||
}
|
||||
return s.netRepo.ListByUser(userID)
|
||||
}
|
||||
|
||||
func (s *NetworkService) GetNetwork(networkID uint32, userID uint) (*model.Network, error) {
|
||||
if userID == 0 {
|
||||
return s.netRepo.FindByNetworkID(networkID)
|
||||
}
|
||||
return s.netRepo.FindByNetworkIDForUser(networkID, userID)
|
||||
}
|
||||
|
||||
func (s *NetworkService) DeleteNetwork(networkID uint32, userID uint) error {
|
||||
net, err := s.GetNetwork(networkID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.netRepo.Delete(net.NetworkID)
|
||||
}
|
||||
|
||||
func (s *NetworkService) AuthorizeMember(networkID uint32, nodeID string, userID uint) error {
|
||||
network, err := s.GetNetwork(networkID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := s.nodeRepo.FindByNodeID(nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node.UserID != userID && userID != 0 {
|
||||
return fmt.Errorf("node does not belong to user")
|
||||
}
|
||||
ip, err := s.allocateIP(network.IPRange, networkID, nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
member := &model.NetworkMember{
|
||||
NetworkID: networkID,
|
||||
NodeID: nodeID,
|
||||
IPAddress: ip,
|
||||
Authorized: true,
|
||||
Label: node.Name,
|
||||
}
|
||||
if err := s.netRepo.AddMember(member); err != nil {
|
||||
return fmt.Errorf("add member: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NetworkService) DeauthorizeMember(networkID uint32, nodeID string, userID uint) error {
|
||||
_, err := s.GetNetwork(networkID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.netRepo.RemoveMember(networkID, nodeID)
|
||||
}
|
||||
|
||||
func (s *NetworkService) ListMembers(networkID uint32, userID uint) ([]model.NetworkMember, error) {
|
||||
_, err := s.GetNetwork(networkID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.netRepo.FindMembers(networkID)
|
||||
}
|
||||
|
||||
func (s *NetworkService) allocateIP(cidr string, networkID uint32, nodeID string) (string, error) {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ones, bits := ipNet.Mask.Size()
|
||||
ip := ipNet.IP.To4()
|
||||
if ip == nil {
|
||||
return "", fmt.Errorf("IPv4 only")
|
||||
}
|
||||
// simple allocation: use last 2 bytes of nodeID hash
|
||||
h := uint32(0)
|
||||
for _, b := range []byte(nodeID) {
|
||||
h = h*31 + uint32(b)
|
||||
}
|
||||
hostBits := bits - ones
|
||||
maxHosts := (1 << uint(hostBits)) - 2
|
||||
hostOffset := int(h%uint32(maxHosts)) + 1
|
||||
ip[2] = byte(hostOffset >> 8)
|
||||
ip[3] = byte(hostOffset)
|
||||
return ip.String(), nil
|
||||
}
|
||||
89
internal/service/node.go
Normal file
89
internal/service/node.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"zeromesh/internal/model"
|
||||
"zeromesh/internal/repository"
|
||||
)
|
||||
|
||||
type NodeService struct {
|
||||
nodeRepo *repository.NodeRepo
|
||||
userRepo *repository.UserRepo
|
||||
}
|
||||
|
||||
func NewNodeService(nodeRepo *repository.NodeRepo, userRepo *repository.UserRepo) *NodeService {
|
||||
return &NodeService{nodeRepo: nodeRepo, userRepo: userRepo}
|
||||
}
|
||||
|
||||
func (s *NodeService) Register(nodeID, publicKey, name, ipAddress string, port int, version string, userID uint) (*model.Node, error) {
|
||||
if userID > 0 {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
if user.QuotaNodes >= 0 {
|
||||
used, _ := s.nodeRepo.CountByUser(userID)
|
||||
if used >= int64(user.QuotaNodes) {
|
||||
return nil, fmt.Errorf("node quota exceeded (%d)", user.QuotaNodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
node := &model.Node{
|
||||
UserID: userID,
|
||||
NodeID: nodeID,
|
||||
PublicKey: publicKey,
|
||||
Name: name,
|
||||
IPAddress: ipAddress,
|
||||
Port: port,
|
||||
Online: true,
|
||||
LastSeen: now(),
|
||||
Version: version,
|
||||
}
|
||||
if err := s.nodeRepo.Upsert(node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (s *NodeService) SetOnline(nodeID string, online bool) error {
|
||||
return s.nodeRepo.SetOnline(nodeID, online)
|
||||
}
|
||||
|
||||
func (s *NodeService) ListNode(userID uint) ([]model.Node, error) {
|
||||
if userID == 0 {
|
||||
return s.nodeRepo.List()
|
||||
}
|
||||
return s.nodeRepo.ListByUser(userID)
|
||||
}
|
||||
|
||||
func (s *NodeService) ListOnline(userID uint) ([]model.Node, error) {
|
||||
if userID == 0 {
|
||||
return s.nodeRepo.ListOnline()
|
||||
}
|
||||
nodes, err := s.nodeRepo.ListByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var online []model.Node
|
||||
for _, n := range nodes {
|
||||
if n.Online {
|
||||
online = append(online, n)
|
||||
}
|
||||
}
|
||||
return online, nil
|
||||
}
|
||||
|
||||
func (s *NodeService) GetNode(nodeID string) (*model.Node, error) {
|
||||
return s.nodeRepo.FindByNodeID(nodeID)
|
||||
}
|
||||
|
||||
func (s *NodeService) CountByUser(userID uint) (int64, error) {
|
||||
return s.nodeRepo.CountByUser(userID)
|
||||
}
|
||||
|
||||
func now() *time.Time {
|
||||
t := time.Now()
|
||||
return &t
|
||||
}
|
||||
25
internal/service/user.go
Normal file
25
internal/service/user.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"zeromesh/internal/database"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func NewUserService(db *database.DB) *UserService {
|
||||
return &UserService{db: db}
|
||||
}
|
||||
|
||||
func (s *UserService) Count() (int64, error) {
|
||||
var count int64
|
||||
err := s.db.Model(&struct{}{}).Table("users").Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *UserService) List(offset, limit int) ([]map[string]interface{}, error) {
|
||||
var users []map[string]interface{}
|
||||
err := s.db.Table("users").Offset(offset).Limit(limit).Order("id desc").Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
Reference in New Issue
Block a user