278 lines
7.1 KiB
Go
278 lines
7.1 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/orohi/vpn-panel/internal/models"
|
|
"github.com/orohi/vpn-panel/internal/secretbox"
|
|
)
|
|
|
|
func scanClient(scanner interface {
|
|
Scan(dest ...any) error
|
|
}) (*models.Client, error) {
|
|
c := &models.Client{}
|
|
var expire sql.NullTime
|
|
var status string
|
|
err := scanner.Scan(
|
|
&c.ID, &c.Username, &c.Email, &c.UUID, &status,
|
|
&c.TrafficLimitBytes, &c.TrafficUsedBytes, &expire, &c.SubToken, &c.Note,
|
|
&c.CreatedAt, &c.UpdatedAt, &c.InboundCount,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.Status = models.ClientStatus(status)
|
|
if expire.Valid {
|
|
t := expire.Time
|
|
c.ExpireAt = &t
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
const clientColumns = `
|
|
c.id, c.username, c.email, c.uuid, c.status,
|
|
c.traffic_limit_bytes, c.traffic_used_bytes, c.expire_at, c.sub_token, c.note,
|
|
c.created_at, c.updated_at,
|
|
(SELECT COUNT(*) FROM client_inbounds ci WHERE ci.client_id = c.id)`
|
|
|
|
func CreateClient(db *sql.DB, c *models.Client, inboundIDs []uuid.UUID) error {
|
|
if c.ID == uuid.Nil {
|
|
c.ID = uuid.New()
|
|
}
|
|
if c.UUID == uuid.Nil {
|
|
c.UUID = uuid.New()
|
|
}
|
|
if c.SubToken == "" {
|
|
tok, err := secretbox.RandomToken(24)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.SubToken = tok
|
|
}
|
|
if c.Status == "" {
|
|
c.Status = models.ClientStatusActive
|
|
}
|
|
now := time.Now().UTC()
|
|
c.CreatedAt = now
|
|
c.UpdatedAt = now
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
_, err = tx.Exec(`
|
|
INSERT INTO clients (
|
|
id, username, email, uuid, status, traffic_limit_bytes, traffic_used_bytes,
|
|
expire_at, sub_token, note, created_at, updated_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`,
|
|
c.ID, c.Username, c.Email, c.UUID, string(c.Status), c.TrafficLimitBytes, c.TrafficUsedBytes,
|
|
c.ExpireAt, c.SubToken, c.Note, c.CreatedAt, c.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, iid := range inboundIDs {
|
|
if _, err := tx.Exec(`INSERT INTO client_inbounds (client_id, inbound_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, c.ID, iid); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func UpdateClient(db *sql.DB, c *models.Client, inboundIDs []uuid.UUID) error {
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
_, err = tx.Exec(`
|
|
UPDATE clients SET
|
|
username = $2, email = $3, status = $4, traffic_limit_bytes = $5,
|
|
expire_at = $6, note = $7, updated_at = NOW()
|
|
WHERE id = $1`,
|
|
c.ID, c.Username, c.Email, string(c.Status), c.TrafficLimitBytes, c.ExpireAt, c.Note,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(`DELETE FROM client_inbounds WHERE client_id = $1`, c.ID); err != nil {
|
|
return err
|
|
}
|
|
for _, iid := range inboundIDs {
|
|
if _, err := tx.Exec(`INSERT INTO client_inbounds (client_id, inbound_id) VALUES ($1, $2)`, c.ID, iid); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func GetClient(db *sql.DB, id uuid.UUID) (*models.Client, error) {
|
|
row := db.QueryRow(`SELECT `+clientColumns+` FROM clients c WHERE c.id = $1`, id)
|
|
c, err := scanClient(row)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids, err := ListClientInboundIDs(db, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.InboundIDs = ids
|
|
return c, nil
|
|
}
|
|
|
|
func GetClientBySubToken(db *sql.DB, token string) (*models.Client, error) {
|
|
row := db.QueryRow(`SELECT `+clientColumns+` FROM clients c WHERE c.sub_token = $1`, token)
|
|
c, err := scanClient(row)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids, err := ListClientInboundIDs(db, c.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.InboundIDs = ids
|
|
return c, nil
|
|
}
|
|
|
|
func ListClients(db *sql.DB) ([]models.Client, error) {
|
|
rows, err := db.Query(`SELECT ` + clientColumns + ` FROM clients c ORDER BY c.created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var list []models.Client
|
|
for rows.Next() {
|
|
c, err := scanClient(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
list = append(list, *c)
|
|
}
|
|
return list, rows.Err()
|
|
}
|
|
|
|
func ListClientInboundIDs(db *sql.DB, clientID uuid.UUID) ([]uuid.UUID, error) {
|
|
rows, err := db.Query(`SELECT inbound_id FROM client_inbounds WHERE client_id = $1`, clientID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var ids []uuid.UUID
|
|
for rows.Next() {
|
|
var id uuid.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
return ids, rows.Err()
|
|
}
|
|
|
|
func SetClientStatus(db *sql.DB, id uuid.UUID, status models.ClientStatus) error {
|
|
_, err := db.Exec(`UPDATE clients SET status = $2, updated_at = NOW() WHERE id = $1`, id, string(status))
|
|
return err
|
|
}
|
|
|
|
func DeleteClient(db *sql.DB, id uuid.UUID) error {
|
|
res, err := db.Exec(`DELETE FROM clients WHERE id = $1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("client not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ListAllInbounds(db *sql.DB) ([]models.Inbound, error) {
|
|
rows, err := db.Query(`
|
|
SELECT ` + inboundColumns + `
|
|
FROM inbounds i
|
|
JOIN protocols p ON p.id = i.protocol_id
|
|
WHERE i.enabled = TRUE
|
|
ORDER BY i.sort_order ASC, i.tag ASC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var list []models.Inbound
|
|
for rows.Next() {
|
|
in, err := scanInbound(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
list = append(list, *in)
|
|
}
|
|
return list, rows.Err()
|
|
}
|
|
|
|
// ClientSubscriptionLinks builds basic share links using online nodes that have the inbound enabled.
|
|
func ClientSubscriptionLinks(db *sql.DB, client *models.Client) ([]string, error) {
|
|
if client == nil || len(client.InboundIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
rows, err := db.Query(`
|
|
SELECT DISTINCT i.tag, p.code, i.port, i.network, i.security, n.host, n.name
|
|
FROM client_inbounds ci
|
|
JOIN inbounds i ON i.id = ci.inbound_id AND i.enabled = TRUE
|
|
JOIN protocols p ON p.id = i.protocol_id
|
|
JOIN node_inbounds ni ON ni.inbound_id = i.id AND ni.enabled = TRUE
|
|
JOIN nodes n ON n.id = ni.node_id AND n.status = 'online' AND n.profile_id = i.profile_id
|
|
WHERE ci.client_id = $1
|
|
ORDER BY n.name, i.tag`, client.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var links []string
|
|
uid := client.UUID.String()
|
|
for rows.Next() {
|
|
var tag, code, network, security, host, nodeName string
|
|
var port int
|
|
if err := rows.Scan(&tag, &code, &port, &network, &security, &host, &nodeName); err != nil {
|
|
return nil, err
|
|
}
|
|
name := client.Username + "-" + tag + "@" + nodeName
|
|
switch code {
|
|
case "vless":
|
|
links = append(links, fmt.Sprintf(
|
|
"vless://%s@%s:%d?encryption=none&type=%s&security=%s#%s",
|
|
uid, host, port, network, security, name,
|
|
))
|
|
case "vmess":
|
|
links = append(links, fmt.Sprintf(
|
|
"vmess://%s@%s:%d?type=%s&security=%s#%s",
|
|
uid, host, port, network, security, name,
|
|
))
|
|
case "trojan":
|
|
links = append(links, fmt.Sprintf(
|
|
"trojan://%s@%s:%d?type=%s&security=%s#%s",
|
|
uid, host, port, network, security, name,
|
|
))
|
|
case "shadowsocks":
|
|
links = append(links, fmt.Sprintf(
|
|
"ss://%s@%s:%d#%s",
|
|
uid, host, port, name,
|
|
))
|
|
default:
|
|
links = append(links, fmt.Sprintf("%s://%s@%s:%d?type=%s&security=%s#%s", code, uid, host, port, network, security, name))
|
|
}
|
|
}
|
|
return links, rows.Err()
|
|
}
|