Compare commits

...

17 Commits

Author SHA1 Message Date
86195e6021 Fix: ca nmsl
All checks were successful
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 10s
Go test / run-go-test (push) Successful in 23s
Go test / cleanup-go-test (push) Successful in 3s
Go test / release-image (push) Successful in 3m30s
2025-12-13 07:04:47 +08:00
7c89c0a144 Chore: trim debug message
All checks were successful
Go test / run-go-vet (push) Successful in 6s
Go test / check-swagger-up-to-date (push) Successful in 11s
Go test / run-go-test (push) Successful in 23s
Go test / cleanup-go-test (push) Successful in 4s
Go test / release-image (push) Successful in 3m37s
2025-12-13 06:35:29 +08:00
47f09b733a Refactor: migrate discordbot
All checks were successful
Go test / run-go-vet (push) Successful in 20s
Go test / check-swagger-up-to-date (push) Successful in 9s
Go test / run-go-test (push) Successful in 47s
Go test / cleanup-go-test (push) Successful in 4s
Go test / release-image (push) Successful in 3m35s
2025-12-13 04:58:12 +08:00
0fc8f1f08c Fix: broken ci and tests
All checks were successful
Go test / run-go-vet (push) Successful in 5s
Go test / run-go-test (push) Successful in 22s
Go test / cleanup-go-test (push) Successful in 4s
Go test / check-swagger-up-to-date (push) Successful in 9s
Go test / release-image (push) Successful in 3m22s
2025-12-13 02:45:33 +08:00
e6c46f29f5 Fix: reset before ci testing
Some checks failed
Go test / run-go-vet (push) Successful in 7s
Go test / check-swagger-up-to-date (push) Successful in 9s
Go test / run-go-test (push) Failing after 31s
Go test / release-image (push) Has been skipped
Go test / cleanup-go-test (push) Successful in 4s
2025-12-13 02:07:45 +08:00
7a108de0ab Fix: makefile target in ci
Some checks failed
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 9s
Go test / run-go-test (push) Failing after 30s
Go test / release-image (push) Has been skipped
Go test / cleanup-go-test (push) Successful in 4s
2025-12-13 01:58:42 +08:00
e8ad9f06e9 Fix: also apply auto delete on delete apis
Some checks failed
Go test / run-go-test (push) Failing after 11s
Go test / release-image (push) Has been skipped
Go test / cleanup-go-test (push) Successful in 3s
Go test / run-go-vet (push) Successful in 6s
Go test / check-swagger-up-to-date (push) Successful in 9s
2025-12-13 01:53:15 +08:00
875db5a8dd Feat: auto delete alias when unused
Some checks failed
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 10s
Go test / run-go-test (push) Failing after 11s
Go test / release-image (push) Has been skipped
Go test / cleanup-go-test (push) Successful in 3s
2025-12-13 01:49:04 +08:00
6b5b7d257d Feat: add makefile target for mac
Some checks failed
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 9s
Go test / run-go-test (push) Failing after 10s
Go test / release-image (push) Has been skipped
Go test / cleanup-go-test (push) Successful in 4s
2025-12-13 01:21:20 +08:00
ea88d8fa43 Fix: no delete modcheck
Some checks failed
Go test / run-go-vet (push) Failing after 5s
Go test / check-swagger-up-to-date (push) Successful in 10s
Go test / run-go-test (push) Has been skipped
Go test / release-image (push) Has been skipped
Go test / cleanup-go-test (push) Successful in 3s
2025-12-13 01:13:03 +08:00
968c0c5658 Fix: image alias put query
All checks were successful
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 8s
Go test / run-go-test (push) Successful in 27s
Go test / cleanup-go-test (push) Successful in 4s
Go test / release-image (push) Successful in 3m34s
2025-12-13 00:57:05 +08:00
44a3bf64c1 Feat: optional reset
All checks were successful
Go test / run-go-vet (push) Successful in 7s
Go test / check-swagger-up-to-date (push) Successful in 10s
Go test / run-go-test (push) Successful in 30s
Go test / cleanup-go-test (push) Successful in 4s
Go test / release-image (push) Successful in 3m36s
2025-12-12 22:38:36 +08:00
e537d83c19 Fix: add project name in ci instead of Makefile
All checks were successful
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 10s
Go test / run-go-test (push) Successful in 25s
Go test / cleanup-go-test (push) Successful in 4s
Go test / release-image (push) Successful in 3m25s
2025-12-12 02:47:29 +08:00
1cf3a9ef0b Fix: cookie timeout
All checks were successful
Go test / run-go-vet (push) Successful in 6s
Go test / check-swagger-up-to-date (push) Successful in 10s
Go test / run-go-test (push) Successful in 36s
Go test / cleanup-go-test (push) Successful in 14s
Go test / release-image (push) Successful in 3m20s
2025-12-12 02:09:46 +08:00
87ae3b76c2 Fix: specify build target
All checks were successful
Go test / run-go-vet (push) Successful in 6s
Go test / check-swagger-up-to-date (push) Successful in 9s
Go test / run-go-test (push) Successful in 25s
Go test / cleanup-go-test (push) Successful in 13s
Go test / release-image (push) Successful in 3m32s
2025-12-09 02:23:55 +08:00
efc3f1ad89 Fix: build binary in dockerfile when running ci
Some checks failed
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 10s
Go test / run-go-test (push) Successful in 27s
Go test / cleanup-go-test (push) Successful in 14s
Go test / release-image (push) Failing after 57s
2025-12-09 02:04:07 +08:00
54c7659ee8 Feat: add release image workflows
Some checks failed
Go test / run-go-vet (push) Successful in 5s
Go test / check-swagger-up-to-date (push) Successful in 9s
Go test / run-go-test (push) Successful in 27s
Go test / cleanup-go-test (push) Successful in 14s
Go test / release-image (push) Failing after 56s
2025-12-09 01:55:15 +08:00
35 changed files with 902 additions and 112 deletions

View File

@@ -10,9 +10,32 @@ jobs:
uses: actions/checkout@v4
- name: Run go vet
run: go vet
run-go-test:
needs: run-go-vet
check-swagger-up-to-date:
runs-on: imgbuilder
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run make swagger
run: make swagger
- name: Check diff
run: |
if ! git diff --exit-code; then
echo "ERROR: swagger introduced uncommitted changes!"
echo "Please commit generated files."
git status
git diff
exit 1
else
echo "No uncommitted changes detected. OpenAPI is up-to-date."
fi
run-go-test:
needs:
- run-go-vet
- check-swagger-up-to-date
runs-on: imgbuilder
env:
COMPOSE_ARGS: '-p go2025-backend'
DOCKER_BUILD_ARGS: '--quiet'
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -29,3 +52,17 @@ jobs:
uses: actions/checkout@v4
- name: Run make docker-clean
run: make docker-clean
release-image:
needs:
- run-go-test
runs-on: imgbuilder
env:
COMPOSE_ARGS: '-p go2025-backend'
steps:
- name: Build and push image
uses: https://gitea.konchin.com/action/docker@main
with:
registry-certificate: ${{ vars.ROOTCA }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
target: build-run

View File

@@ -1,6 +1,24 @@
FROM docker.io/library/debian:13-slim
FROM docker.io/library/golang:1.25 AS build
ADD backend /work/backend
WORKDIR /work
COPY . .
RUN make backend
FROM docker.io/library/debian:13-slim AS build-run
RUN apt-get -y update && apt-get -y upgrade && \
apt-get -y install ca-certificates
COPY --from=build /work/backend /work/backend
WORKDIR /work
ENTRYPOINT ["/work/backend"]
CMD ["serve"]
FROM docker.io/library/debian:13-slim AS native
RUN apt-get -y update && apt-get -y upgrade && \
apt-get -y install ca-certificates
COPY backend /work/backend
WORKDIR /work
ENTRYPOINT ["/work/backend"]

View File

@@ -1,22 +1,38 @@
.PHONY: all swagger install postgres test test-ci \
.PHONY: all swagger install postgres test mac \
docker docker-quiet docker-clean
SWAG ?= go run github.com/swaggo/swag/cmd/swag@v1.16.4
DOCKER ?= docker
COMPOSE ?= $(DOCKER) compose -p go2025-backend --progress plain
DOCKER_BUILD_ARGS +=
COMPOSE_ARGS += --progress plain
COMPOSE ?= $(DOCKER) compose $(COMPOSE_ARGS)
GO_ENV += CGO_ENABLED=0
SOURCE := $(shell find . -type f -name '*.go')
TARGET := backend
all: swagger docker
all: swagger docker-build-native docker
mac: swagger docker-build-run docker
swagger:
$(SWAG) fmt
$(SWAG) init -o docs -g cmds/serve.go -pdl 1
docker: $(TARGET)
$(COMPOSE) up -d --force-recreate --build backend
docker-build-native: $(TARGET)
$(DOCKER) build . $(DOCKER_BUILD_ARGS) \
--target native \
-t go2025/backend:native \
-t go2025/backend:latest
docker-build-run:
$(DOCKER) build . $(DOCKER_BUILD_ARGS) \
--target build-run \
-t go2025/backend:build-run \
-t go2025/backend:latest
docker:
$(COMPOSE) up -d --force-recreate backend dcbot
$(TARGET): $(SOURCE)
$(GO_ENV) go build -o $@
@@ -31,9 +47,7 @@ postgres:
$(COMPOSE) exec postgres psql \
postgres://go2025:go2025@postgres:5432/go2025?sslmode=disable
test-ci: docker-quiet test docker-clean
docker-quiet: $(TARGET)
docker-quiet: docker-build-native
$(COMPOSE) up -d \
--force-recreate --build backend \
--quiet-build --quiet-pull

10
README.md Normal file
View File

@@ -0,0 +1,10 @@
# Golang 2025 Final Project - Backend
## How to setup the development environment
1. clone this project
2. `docker compose up -d`
## How to run
`go run . serve` or `go build && ./backend serve`

168
bot/bot.go Normal file
View File

@@ -0,0 +1,168 @@
package bot
import (
"context"
"log"
"gitea.konchin.com/go2025/backend/tracing"
"github.com/bwmarrin/discordgo"
"github.com/go-resty/resty/v2"
"github.com/spf13/viper"
"go.uber.org/zap"
)
type CommandHandler = func(
*discordgo.Session, *discordgo.InteractionCreate)
type Command interface {
ApplicationCommand() *discordgo.ApplicationCommand
Handler() CommandHandler
}
type Bot struct {
session *discordgo.Session
commands map[string]Command
aliases map[string]int64
Client *resty.Client
}
func New() (*Bot, error) {
// Create Discord session
session, err := discordgo.New(
"Bot " + viper.GetString("discord-bot-token"))
if err != nil {
return nil, err
}
client := resty.New()
client.SetBaseURL(viper.GetString("api-endpoint"))
client.SetAuthToken(viper.GetString("preshared-key"))
bot := &Bot{
session: session,
commands: make(map[string]Command),
Client: client,
}
bot.registerHandlers()
go bot.fetchAliases()
// Set intents - only need guild messages for the ciallo listener
session.Identify.Intents = discordgo.IntentsGuildMessages |
discordgo.IntentsDirectMessages |
discordgo.IntentsMessageContent
return bot, nil
}
func (b *Bot) registerSlashCommands(ctx context.Context) error {
for _, cmd := range b.commands {
_, err := b.session.ApplicationCommandCreate(
b.session.State.User.ID, "", cmd.ApplicationCommand())
if err != nil {
tracing.Logger.Ctx(ctx).
Error("failed to create command",
zap.String("command", cmd.ApplicationCommand().Name),
zap.Error(err))
return err
}
}
return nil
}
func (b *Bot) clearSlashCommands(guildID string) error {
commands, err := b.session.ApplicationCommands(b.session.State.User.ID, guildID)
if err != nil {
return err
}
for _, cmd := range commands {
err := b.session.ApplicationCommandDelete(b.session.State.User.ID, guildID, cmd.ID)
if err != nil {
log.Printf("Failed to delete command %s: %v", cmd.Name, err)
}
}
return nil
}
func (b *Bot) registerHandlers() {
b.session.AddHandler(b.onReady)
b.session.AddHandler(b.onMessageCreate)
b.session.AddHandler(b.onInteractionCreate)
}
func (b *Bot) RegisterCommand(newCommand func(*Bot) Command) {
command := newCommand(b)
b.commands[command.ApplicationCommand().Name] = command
}
func (b *Bot) onReady(
s *discordgo.Session,
event *discordgo.Ready,
) {
ctx := context.Background()
tracing.Logger.Ctx(ctx).
Info("logged in",
zap.String("username", s.State.User.Username),
zap.String("discriminator", s.State.User.Discriminator))
// For development: set your guild ID here for instant updates
// For production: use "" for global commands
guildID := "1377176828833169468" // Replace with your Discord server ID for faster testing
// clear slash commands
if err := b.clearSlashCommands(guildID); err != nil {
log.Printf("Error clearing slash commands: %v", err)
}
// Register slash commands
if err := b.registerSlashCommands(ctx); err != nil {
tracing.Logger.Ctx(ctx).
Error("failed to register slash commands",
zap.Error(err))
return
}
// Set bot status
err := s.UpdateGameStatus(0, "/ping to check status")
if err != nil {
tracing.Logger.Ctx(ctx).
Error("failed to set status",
zap.Error(err))
return
}
}
func (b *Bot) onInteractionCreate(
s *discordgo.Session,
i *discordgo.InteractionCreate,
) {
if i.Type != discordgo.InteractionApplicationCommand {
return
}
command, ok := b.commands[i.ApplicationCommandData().Name]
if ok {
tracing.Logger.Ctx(context.Background()).
Info("run command",
zap.String("name", i.ApplicationCommandData().Name))
command.Handler()(s, i)
} else {
tracing.Logger.Ctx(context.Background()).
Error("command not exist",
zap.String("name", i.ApplicationCommandData().Name))
}
}
func (b *Bot) Start() error {
return b.session.Open()
}
func (b *Bot) Stop() {
if b.session != nil {
b.session.Close()
}
}

53
bot/commands/echo.go Normal file
View File

@@ -0,0 +1,53 @@
package commands
import (
"gitea.konchin.com/go2025/backend/bot"
"github.com/bwmarrin/discordgo"
)
type EchoCommand struct {
bot *bot.Bot
}
func NewEchoCommand(bot *bot.Bot) bot.Command {
return &EchoCommand{bot: bot}
}
func (self *EchoCommand) ApplicationCommand() *discordgo.ApplicationCommand {
return &discordgo.ApplicationCommand{
Name: "echo",
Description: "Bot repeats what you say",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message",
Description: "Message to echo",
Required: true,
},
},
}
}
func (self *EchoCommand) Handler() bot.CommandHandler {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
options := i.ApplicationCommandData().Options
if len(options) == 0 {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "No message provided!",
},
})
return
}
message := options[0].StringValue()
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: message,
},
})
}
}

43
bot/commands/greet.go Normal file
View File

@@ -0,0 +1,43 @@
package commands
import (
"fmt"
"gitea.konchin.com/go2025/backend/bot"
"github.com/bwmarrin/discordgo"
)
type GreetCommand struct {
bot *bot.Bot
}
func NewGreetCommand(bot *bot.Bot) bot.Command {
return &GreetCommand{bot: bot}
}
func (self *GreetCommand) ApplicationCommand() *discordgo.ApplicationCommand {
return &discordgo.ApplicationCommand{
Name: "greet",
Description: "Get a friendly greeting",
}
}
func (self *GreetCommand) Handler() bot.CommandHandler {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
var username string
if i.Member != nil {
username = i.Member.User.Username
} else if i.User != nil {
username = i.User.Username
} else {
username = "Unknown"
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Ciallo, %s!", username),
},
})
}
}

32
bot/commands/ping.go Normal file
View File

@@ -0,0 +1,32 @@
package commands
import (
"gitea.konchin.com/go2025/backend/bot"
"github.com/bwmarrin/discordgo"
)
type PingCommand struct {
bot *bot.Bot
}
func NewPingCommand(bot *bot.Bot) bot.Command {
return &PingCommand{bot: bot}
}
func (self *PingCommand) ApplicationCommand() *discordgo.ApplicationCommand {
return &discordgo.ApplicationCommand{
Name: "ping",
Description: "Check if bot is responsive",
}
}
func (self *PingCommand) Handler() bot.CommandHandler {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "pong",
},
})
}
}

68
bot/commands/web.go Normal file
View File

@@ -0,0 +1,68 @@
package commands
import (
"context"
"net/http"
"gitea.konchin.com/go2025/backend/bot"
"gitea.konchin.com/go2025/backend/handlers/auth"
"gitea.konchin.com/go2025/backend/tracing"
"github.com/bwmarrin/discordgo"
"go.uber.org/zap"
)
type WebCommand struct {
bot *bot.Bot
}
func NewWebCommand(bot *bot.Bot) bot.Command {
return &WebCommand{bot: bot}
}
func (self *WebCommand) ApplicationCommand() *discordgo.ApplicationCommand {
return &discordgo.ApplicationCommand{
Name: "web",
Description: "Get a login link to the web interface",
}
}
func (self *WebCommand) Handler() bot.CommandHandler {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
var userID string
if i.Member != nil {
userID = i.Member.User.ID
} else if i.User != nil {
userID = i.User.ID
}
// Call backend API
var res auth.PostGenLoginUrlOutput
resp, err := self.bot.Client.R().
SetBody(auth.PostGenLoginUrlInput{UserId: userID}).
SetResult(&res).
Post("/bot/auth/gen-login-url")
if err != nil || resp.StatusCode() != http.StatusOK {
tracing.Logger.Ctx(context.Background()).
Error("failed to generate login url",
zap.Error(err))
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "❌ Failed to generate login URL",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
content := "🔗 **Click here to access the web page:**\n"+
res.LoginUrl + "\n\n"
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: content,
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
}

59
bot/onMessageCreate.go Normal file
View File

@@ -0,0 +1,59 @@
package bot
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"gitea.konchin.com/go2025/backend/handlers/api"
"github.com/bwmarrin/discordgo"
"github.com/spf13/viper"
"golang.org/x/exp/rand"
)
func (b *Bot) fetchAliases() {
for {
var res []api.GetAliasesOutputAlias
resp, err := b.Client.R().
SetResult(&res).
Get("/bot/api/aliases")
if err == nil && resp.StatusCode() == http.StatusOK {
aliases := make(map[string]int64)
for _, alias := range res {
aliases[alias.Name] = alias.Id
}
b.aliases = aliases
}
time.Sleep(10 * time.Second)
}
}
func (b *Bot) onMessageCreate(
s *discordgo.Session,
m *discordgo.MessageCreate,
) {
// Ignore messages from the bot itself
if m.Author.ID == s.State.User.ID {
return
}
key := strings.ToLower(strings.TrimSpace(m.Content))
if id, ok := b.aliases[key]; ok {
var res []api.GetImagesOutputImage
resp, err := b.Client.R().
SetResult(&res).
SetQueryParam("aliases", strconv.FormatInt(id, 10)).
Get("/bot/api/images")
if err == nil && resp.StatusCode() == http.StatusOK {
image := res[rand.Intn(len(res))]
s.ChannelMessageSend(m.ChannelID,
fmt.Sprintf("%s/img/%d.%s",
viper.GetString("external-url"),
image.Id, image.Extension))
}
}
}

75
cmds/dcbot.go Normal file
View File

@@ -0,0 +1,75 @@
package cmds
import (
"fmt"
"os"
"os/signal"
"syscall"
"gitea.konchin.com/go2025/backend/bot"
"gitea.konchin.com/go2025/backend/bot/commands"
"gitea.konchin.com/go2025/backend/tracing"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
var dcbotCmd = &cobra.Command{
Use: "dcbot",
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
appname := "go2025-dcbot"
tracing.InitTracer(appname)
if viper.GetString("uptrace-dsn") != "" {
tracing.InitUptrace(appname)
defer tracing.DeferUptrace(ctx)
}
// Initialize bot
discordBot, err := bot.New()
if err != nil {
tracing.Logger.Ctx(ctx).
Panic("failed to create bot",
zap.Error(err))
panic(err)
}
discordBot.RegisterCommand(commands.NewEchoCommand)
discordBot.RegisterCommand(commands.NewGreetCommand)
discordBot.RegisterCommand(commands.NewPingCommand)
discordBot.RegisterCommand(commands.NewWebCommand)
// Start bot
if err := discordBot.Start(); err != nil {
tracing.Logger.Ctx(ctx).
Panic("failed to start bot",
zap.Error(err))
panic(err)
}
fmt.Println("Bot is now running. Press CTRL-C to exit.")
defer discordBot.Stop()
// Wait for interrupt signal
sc := make(chan os.Signal, 1)
signal.Notify(sc,
syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-sc
},
}
func init() {
dcbotCmd.Flags().
String("preshared-key", "poop", "preshared key")
dcbotCmd.Flags().
String("discord-bot-token", "", "discord bot token")
dcbotCmd.Flags().
String("api-endpoint", "http://backend:8080", "api endpoint")
dcbotCmd.Flags().
String("external-url", "http://localhost:8080", "external url")
dcbotCmd.Flags().
Bool("zap-production", true, "Toggle production log format")
dcbotCmd.Flags().
String("uptrace-dsn", "", "Uptrace DSN (disabled by default)")
}

37
cmds/genToken.go Normal file
View File

@@ -0,0 +1,37 @@
package cmds
import (
"fmt"
"net/http"
"github.com/go-resty/resty/v2"
"github.com/spf13/cobra"
)
type genLoginUrlPayload struct {
LoginUrl string `json:"loginUrl"`
}
type loginPayload struct {
Token string `json:"token"`
}
var genTokenCmd = &cobra.Command{
Use: "gen-token",
Run: func(cmd *cobra.Command, args []string) {
client := resty.New()
var payload genLoginUrlPayload
resp, err := client.R().
SetBody(`{"userId": "testuser1"}`).
SetAuthToken("poop").
SetResult(&payload).
Post("http://localhost:8080/auth/gen-login-url")
if err != nil || resp.StatusCode() != http.StatusOK {
panic(err)
}
fmt.Printf("url: %s\n", payload.LoginUrl)
},
}

View File

@@ -21,4 +21,6 @@ func init() {
cobra.EnableTraverseRunHooks = true
RootCmd.AddCommand(serveCmd)
RootCmd.AddCommand(genTokenCmd)
RootCmd.AddCommand(dcbotCmd)
}

View File

@@ -101,6 +101,7 @@ var serveCmd = &cobra.Command{
Use(middlewares.AccessLog).
Use(middlewares.CORSHandler)
backend.OPTIONS("/*any", utils.GetHealthz)
backend.GET("/healthz", utils.GetHealthz)
apiGroup := backend.NewGroup("/api").
@@ -115,8 +116,16 @@ var serveCmd = &cobra.Command{
authGroup := backend.NewGroup("/auth")
authGroup.POST("/login", auths.PostLogin)
authGroup.POST("/gen-login-url",
midHandlers.CheckPresharedKey(auths.PostGenLoginUrl))
botGroup := backend.NewGroup("/bot").
Use(midHandlers.CheckPresharedKey)
botApiGroup := botGroup.NewGroup("/api")
botApiGroup.GET("/aliases", apis.GetAliases)
botApiGroup.GET("/images", apis.GetImages)
botAuthGroup := botGroup.NewGroup("/auth")
botAuthGroup.POST("/gen-login-url", auths.PostGenLoginUrl)
imgGroup := backend.NewGroup("/img")
imgGroup.GET("/:filename", imgs.Get)
@@ -136,12 +145,16 @@ var serveCmd = &cobra.Command{
func init() {
serveCmd.Flags().
String("port", "8080", "Port to listen on")
serveCmd.Flags().
Bool("https", false, "Enable https mode")
serveCmd.Flags().
String("external-url", "http://localhost:8080", "External url for login")
serveCmd.Flags().
String("cors-origin", "", "CORS origin")
serveCmd.Flags().
String("preshared-key", "poop", "Preshared key for Discord Bot")
serveCmd.Flags().
Bool("reset", false, "Reset database")
serveCmd.Flags().
Int64("access-token-timeout", 300, "Timeout of Access Token JWT")

View File

@@ -60,8 +60,7 @@ services:
restart: unless-stopped
backend:
build:
context: .
image: go2025/backend:latest
env_file:
- path: ./.env
required: false
@@ -75,6 +74,16 @@ services:
- uptrace
restart: unless-stopped
dcbot:
image: go2025/backend:latest
command: ["dcbot"]
env_file:
- path: ./.env
required: false
depends_on:
- backend
restart: unless-stopped
volumes:
redis: {}
postgres: {}

View File

@@ -55,7 +55,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.getAliasesOutputAlias"
"$ref": "#/definitions/api.GetAliasesOutputAlias"
}
}
},
@@ -186,7 +186,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.getImagesOutputImage"
"$ref": "#/definitions/api.GetImagesOutputImage"
}
}
},
@@ -208,7 +208,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.postGenLoginUrlInput"
"$ref": "#/definitions/auth.PostGenLoginUrlInput"
}
}
],
@@ -216,7 +216,7 @@ const docTemplate = `{
"200": {
"description": "Payload",
"schema": {
"$ref": "#/definitions/auth.postGenLoginUrlOutput"
"$ref": "#/definitions/auth.PostGenLoginUrlOutput"
}
},
"400": {
@@ -247,7 +247,7 @@ const docTemplate = `{
}
},
"definitions": {
"api.getAliasesOutputAlias": {
"api.GetAliasesOutputAlias": {
"type": "object",
"properties": {
"id": {
@@ -258,7 +258,7 @@ const docTemplate = `{
}
}
},
"api.getImagesOutputImage": {
"api.GetImagesOutputImage": {
"type": "object",
"properties": {
"aliasesIds": {
@@ -309,7 +309,7 @@ const docTemplate = `{
}
}
},
"auth.postGenLoginUrlInput": {
"auth.PostGenLoginUrlInput": {
"type": "object",
"properties": {
"userId": {
@@ -317,7 +317,7 @@ const docTemplate = `{
}
}
},
"auth.postGenLoginUrlOutput": {
"auth.PostGenLoginUrlOutput": {
"type": "object",
"properties": {
"loginUrl": {

View File

@@ -47,7 +47,7 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.getAliasesOutputAlias"
"$ref": "#/definitions/api.GetAliasesOutputAlias"
}
}
},
@@ -178,7 +178,7 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.getImagesOutputImage"
"$ref": "#/definitions/api.GetImagesOutputImage"
}
}
},
@@ -200,7 +200,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.postGenLoginUrlInput"
"$ref": "#/definitions/auth.PostGenLoginUrlInput"
}
}
],
@@ -208,7 +208,7 @@
"200": {
"description": "Payload",
"schema": {
"$ref": "#/definitions/auth.postGenLoginUrlOutput"
"$ref": "#/definitions/auth.PostGenLoginUrlOutput"
}
},
"400": {
@@ -239,7 +239,7 @@
}
},
"definitions": {
"api.getAliasesOutputAlias": {
"api.GetAliasesOutputAlias": {
"type": "object",
"properties": {
"id": {
@@ -250,7 +250,7 @@
}
}
},
"api.getImagesOutputImage": {
"api.GetImagesOutputImage": {
"type": "object",
"properties": {
"aliasesIds": {
@@ -301,7 +301,7 @@
}
}
},
"auth.postGenLoginUrlInput": {
"auth.PostGenLoginUrlInput": {
"type": "object",
"properties": {
"userId": {
@@ -309,7 +309,7 @@
}
}
},
"auth.postGenLoginUrlOutput": {
"auth.PostGenLoginUrlOutput": {
"type": "object",
"properties": {
"loginUrl": {

View File

@@ -1,13 +1,13 @@
basePath: /
definitions:
api.getAliasesOutputAlias:
api.GetAliasesOutputAlias:
properties:
id:
type: integer
name:
type: string
type: object
api.getImagesOutputImage:
api.GetImagesOutputImage:
properties:
aliasesIds:
items:
@@ -40,12 +40,12 @@ definitions:
type: string
type: array
type: object
auth.postGenLoginUrlInput:
auth.PostGenLoginUrlInput:
properties:
userId:
type: string
type: object
auth.postGenLoginUrlOutput:
auth.PostGenLoginUrlOutput:
properties:
loginUrl:
type: string
@@ -88,7 +88,7 @@ paths:
description: OK
schema:
items:
$ref: '#/definitions/api.getAliasesOutputAlias'
$ref: '#/definitions/api.GetAliasesOutputAlias'
type: array
"400":
description: Bad Request
@@ -174,7 +174,7 @@ paths:
description: OK
schema:
items:
$ref: '#/definitions/api.getImagesOutputImage'
$ref: '#/definitions/api.GetImagesOutputImage'
type: array
"400":
description: Bad Request
@@ -188,12 +188,12 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/auth.postGenLoginUrlInput'
$ref: '#/definitions/auth.PostGenLoginUrlInput'
responses:
"200":
description: Payload
schema:
$ref: '#/definitions/auth.postGenLoginUrlOutput'
$ref: '#/definitions/auth.PostGenLoginUrlOutput'
"400":
description: Bad Request
/auth/login:

17
go.mod
View File

@@ -3,6 +3,7 @@ module gitea.konchin.com/go2025/backend
go 1.25.4
require (
github.com/bwmarrin/discordgo v0.29.0
github.com/go-resty/resty/v2 v2.17.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/minio/minio-go/v7 v7.0.97
@@ -21,6 +22,7 @@ require (
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
go.uber.org/zap v1.27.1
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9
)
require (
@@ -38,6 +40,7 @@ require (
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
@@ -80,13 +83,13 @@ require (
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/grpc v1.75.1 // indirect

37
go.sum
View File

@@ -1,5 +1,7 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -44,6 +46,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -186,28 +190,33 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ=

View File

@@ -29,8 +29,12 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl
golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4=
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@@ -7,7 +7,7 @@ import (
"github.com/uptrace/bunrouter"
)
type getAliasesOutputAlias struct {
type GetAliasesOutputAlias struct {
Id int64 `json:"id"`
Name string `json:"name"`
}
@@ -16,7 +16,7 @@ type getAliasesOutputAlias struct {
//
// @summary Get aliases
// @description get alias ids and names
// @success 200 {object} []getAliasesOutputAlias
// @success 200 {object} []GetAliasesOutputAlias
// @failure 400
// @failure 401
// @router /api/aliases [get]
@@ -34,10 +34,10 @@ func (self *Handlers) GetAliases(
}
}
var output []getAliasesOutputAlias
var output []GetAliasesOutputAlias
for _, alias := range aliases {
output = append(output, getAliasesOutputAlias{
output = append(output, GetAliasesOutputAlias{
Id: alias.Id,
Name: alias.Name,
})

View File

@@ -13,7 +13,7 @@ import (
"go.uber.org/zap"
)
type getImagesOutputImage struct {
type GetImagesOutputImage struct {
Id int64 `json:"id"`
Extension string `json:"extension"`
Uploader string `json:"uploadedUserId"`
@@ -25,7 +25,7 @@ type getImagesOutputImage struct {
//
// @param images query []int64 false "Image Ids" attribute(optional)
// @param aliases query []int64 false "Alias Ids" attribute(optional)
// @success 200 {object} []getImagesOutputImage
// @success 200 {object} []GetImagesOutputImage
// @failure 400
// @failure 401
// @router /api/images [get]
@@ -83,13 +83,13 @@ func (self *Handlers) GetImages(
}
}
var output []getImagesOutputImage
var output []GetImagesOutputImage
for _, img := range images {
var aliases []int64
for _, alias := range img.Aliases {
aliases = append(aliases, alias.Id)
}
output = append(output, getImagesOutputImage{
output = append(output, GetImagesOutputImage{
Id: img.Id,
Extension: img.Extension,
Uploader: img.Uploader,

View File

@@ -10,18 +10,18 @@ import (
"github.com/uptrace/bunrouter"
)
type postGenLoginUrlInput struct {
type PostGenLoginUrlInput struct {
UserId string `json:"userId"`
}
type postGenLoginUrlOutput struct {
type PostGenLoginUrlOutput struct {
LoginUrl string `json:"loginUrl"`
}
// PostGenLoginUrl
//
// @param payload body postGenLoginUrlInput true "Payload"
// @success 200 {object} postGenLoginUrlOutput "Payload"
// @param payload body PostGenLoginUrlInput true "Payload"
// @success 200 {object} PostGenLoginUrlOutput "Payload"
// @failure 400
// @router /auth/gen-login-url [post]
func (self *Handlers) PostGenLoginUrl(
@@ -38,7 +38,7 @@ func (self *Handlers) PostGenLoginUrl(
}
}
var input postGenLoginUrlInput
var input PostGenLoginUrlInput
if err := json.Unmarshal(b, &input); err != nil {
return middlewares.HTTPError{
StatusCode: http.StatusBadRequest,
@@ -56,7 +56,7 @@ func (self *Handlers) PostGenLoginUrl(
}
}
return bunrouter.JSON(w, postGenLoginUrlOutput{
return bunrouter.JSON(w, PostGenLoginUrlOutput{
LoginUrl: viper.GetString("external-url") +
"/login?" +
"token=" + token,

View File

@@ -65,8 +65,12 @@ func (self *Handlers) PostLogin(
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: session.RefreshToken,
Path: "/",
Secure: viper.GetBool("https"),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(time.Duration(
viper.GetInt64("REFRESH_TOKEN_TIMEOUT")) * time.Second),
viper.GetInt64("refresh-token-timeout")) * time.Second),
})
return utils.Success(w)

View File

@@ -61,7 +61,7 @@ func (self *BunDatabase) UpdateRefreshToken(
}
err := self.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
err := self.db.NewSelect().
err := tx.NewSelect().
Model(&ret).
WherePK().
Scan(ctx)
@@ -76,7 +76,7 @@ func (self *BunDatabase) UpdateRefreshToken(
return err
}
_, err = self.db.NewUpdate().
_, err = tx.NewUpdate().
Model((*models.Session)(nil)).
Set("refresh_token = ?", ret.RefreshToken).
Where("user_id = ?", userId).
@@ -176,7 +176,7 @@ func (self *BunDatabase) UpdateAliases(
Name: ali,
})
}
_, err := self.db.NewInsert().
_, err := tx.NewInsert().
Model(&aliases).
On("CONFLICT (name) DO NOTHING").
Exec(ctx)
@@ -184,6 +184,22 @@ func (self *BunDatabase) UpdateAliases(
return err
}
err = tx.NewSelect().
Model(&aliases).
Where("name IN (?)", bun.In(aliasNames)).
Scan(ctx)
if err != nil {
return err
}
_, err = tx.NewDelete().
Model((*models.AliasImage)(nil)).
Where("image_id = ?", imageId).
Exec(ctx)
if err != nil {
return err
}
var rels []models.AliasImage
for _, alias := range aliases {
rels = append(rels, models.AliasImage{
@@ -191,7 +207,7 @@ func (self *BunDatabase) UpdateAliases(
ImageId: imageId,
})
}
_, err = self.db.NewInsert().
_, err = tx.NewInsert().
Model(&rels).
On(`CONFLICT ("alias_id", "image_id") DO NOTHING`).
Exec(ctx)
@@ -199,6 +215,17 @@ func (self *BunDatabase) UpdateAliases(
return err
}
_, err = tx.NewDelete().
Model((*models.Alias)(nil)).
Where("NOT EXISTS (?)", tx.NewSelect().
Model((*models.AliasImage)(nil)).
Where("alias.id = alias_image.alias_id").
Limit(1)).
Exec(ctx)
if err != nil {
return err
}
return nil
})
}
@@ -218,20 +245,30 @@ func (self *BunDatabase) DeleteImage(
imageId int64,
) error {
return self.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
_, err := self.db.NewDelete().
_, err := tx.NewDelete().
Model((*models.Image)(nil)).
Where("id = ?", imageId).
Exec(ctx)
if err != nil {
return err
}
_, err = self.db.NewDelete().
_, err = tx.NewDelete().
Model((*models.AliasImage)(nil)).
Where("image_id = ?", imageId).
Exec(ctx)
if err != nil {
return err
}
_, err = tx.NewDelete().
Model((*models.Alias)(nil)).
Where("NOT EXISTS (?)", tx.NewSelect().
Model((*models.AliasImage)(nil)).
Where("alias.id = alias_image.alias_id").
Limit(1)).
Exec(ctx)
if err != nil {
return err
}
return nil
})
}
@@ -241,20 +278,30 @@ func (self *BunDatabase) DeleteAlias(
aliasId int64,
) error {
return self.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
_, err := self.db.NewDelete().
_, err := tx.NewDelete().
Model((*models.Alias)(nil)).
Where("id = ?", aliasId).
Exec(ctx)
if err != nil {
return err
}
_, err = self.db.NewDelete().
_, err = tx.NewDelete().
Model((*models.AliasImage)(nil)).
Where("alias_id = ?", aliasId).
Exec(ctx)
if err != nil {
return err
}
_, err = tx.NewDelete().
Model((*models.Image)(nil)).
Where("NOT EXISTS (?)", tx.NewSelect().
Model((*models.AliasImage)(nil)).
Where("image.id = alias_image.image_id").
Limit(1)).
Exec(ctx)
if err != nil {
return err
}
return nil
})
}

View File

@@ -3,6 +3,7 @@ package middlewares
import (
"context"
"net/http"
"time"
"gitea.konchin.com/go2025/backend/interfaces"
"gitea.konchin.com/go2025/backend/models"
@@ -49,7 +50,9 @@ func refreshAccessToken(
Name: "access_token",
Value: ret,
Path: "/",
Secure: false,
Secure: viper.GetBool("https"),
Expires: time.Now().Add(time.Duration(
viper.GetInt64("access-token-timeout")) * time.Second),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})

View File

@@ -62,7 +62,8 @@ func (self *Handlers) CheckRefreshToken(
Name: "refresh_token",
Value: session.RefreshToken,
Path: "/",
Secure: false,
Secure: viper.GetBool("https"),
Expires: claim.ExpiresAt.Time,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})

View File

@@ -13,7 +13,7 @@ var client *resty.Client
func Test_00_Healthz(t *testing.T) {
for i := 0; i < 10; i++ {
resp, err := client.R().
Get("http://localhost:8080/healthz")
Get("/healthz")
if err == nil && resp.StatusCode() == http.StatusOK {
return
}
@@ -24,4 +24,5 @@ func Test_00_Healthz(t *testing.T) {
func init() {
client = resty.New()
client.SetBaseURL("http://localhost:8080")
}

View File

@@ -18,7 +18,7 @@ func Test_01_Login(t *testing.T) {
t.Run("check preshared key failed", func(t *testing.T) {
resp, err := client.R().
SetBody(`{"userId": "testuser1"}`).
Post("http://localhost:8080/auth/gen-login-url")
Post("/bot/auth/gen-login-url")
if err != nil {
t.Fatal("request failed")
}
@@ -32,7 +32,7 @@ func Test_01_Login(t *testing.T) {
SetBody(`{"userId": "testuser1"}`).
SetAuthToken("poop").
SetResult(&payload).
Post("http://localhost:8080/auth/gen-login-url")
Post("/bot/auth/gen-login-url")
if err != nil || resp.StatusCode() != http.StatusOK {
t.Fatal("failed to get login url")
@@ -46,7 +46,8 @@ func Test_01_Login(t *testing.T) {
resp, err = client.R().
SetBody(loginPayload{Token: loginUrl.Query().Get("token")}).
Post(loginUrl.Scheme + "://" + loginUrl.Host + "/auth/login")
SetAuthToken("poop").
Post("/auth/login")
if err != nil || resp.StatusCode() != http.StatusOK {
t.Fatal("failed to login")
}

View File

@@ -28,7 +28,7 @@ func Test_02_PostImage(t *testing.T) {
resp, err := client.R().
SetBody(rawImage).
SetResult(&image).
Post("http://localhost:8080/api/image")
Post("/api/image")
if err != nil || resp.StatusCode() != http.StatusOK {
t.Logf("%+v", resp)

View File

@@ -3,6 +3,7 @@ package tests
import (
"fmt"
"net/http"
"strconv"
"testing"
)
@@ -11,15 +12,62 @@ type putImageAliasPayload struct {
}
func Test_03_PutImageAliases(t *testing.T) {
t.Run("Put one alias", func(t *testing.T) {
payload := putImageAliasPayload{
Aliases: []string{"huh"},
}
resp, err := client.R().
SetBody(payload).
Put(fmt.Sprintf("http://localhost:8080/api/image/%d/aliases",
image.Id))
Put(fmt.Sprintf("/api/image/%d/aliases", image.Id))
if err != nil || resp.StatusCode() != http.StatusOK {
t.Logf("%+v", resp)
t.Fatal("failed to put image alias")
}
})
t.Run("Put many alias", func(t *testing.T) {
payload := putImageAliasPayload{
Aliases: []string{"huh", "testalias1", "testalias2"},
}
resp, err := client.R().
SetBody(payload).
Put(fmt.Sprintf("/api/image/%d/aliases", image.Id))
if err != nil || resp.StatusCode() != http.StatusOK {
t.Logf("%+v", resp)
t.Fatal("failed to put image alias")
}
})
t.Run("Remove alias", func(t *testing.T) {
payload := putImageAliasPayload{
Aliases: []string{"huh", "testalias1"},
}
resp, err := client.R().
SetBody(payload).
Put(fmt.Sprintf("/api/image/%d/aliases", image.Id))
if err != nil || resp.StatusCode() != http.StatusOK {
t.Logf("%+v", resp)
t.Fatal("failed to put image alias")
}
resp, err = client.R().
SetResult(&aliases).
Get("/api/aliases")
if err != nil || resp.StatusCode() != http.StatusOK {
t.Logf("%+v", resp)
t.Fatal("failed to get aliases")
}
for _, alias := range aliases {
if alias.Name == "testalias2" {
t.Logf("%+v", resp)
resp, _ = client.R().
SetQueryParam("aliases",
strconv.FormatInt(alias.Id, 10)).
Get("/api/images")
t.Logf("%+v", resp)
t.Fatal("alias should be deleted")
}
}
})
}

View File

@@ -15,7 +15,7 @@ var aliases []aliasPayload
func Test_04_GetAliases(t *testing.T) {
resp, err := client.R().
SetResult(&aliases).
Get("http://localhost:8080/api/aliases")
Get("/api/aliases")
if err != nil || resp.StatusCode() != http.StatusOK {
t.Logf("%+v", resp)
t.Fatal("failed to get aliases")

View File

@@ -7,9 +7,11 @@ import (
)
func Test_07_DeleteAlias(t *testing.T) {
deleteId := aliases[0].Id
resp, err := client.R().
Delete(fmt.Sprintf("http://localhost:8080/api/alias/%d",
aliases[0].Id))
deleteId))
if err != nil || resp.StatusCode() != http.StatusOK {
t.Logf("%+v", resp)
t.Fatal("failed to delete alias")
@@ -23,7 +25,10 @@ func Test_07_DeleteAlias(t *testing.T) {
t.Fatal("failed to get aliases")
}
if len(aliases) > 0 {
for _, alias := range aliases {
if alias.Id == deleteId {
t.Logf("%+v", resp)
t.Fatal("alias not deleted")
}
}
}

View File

@@ -4,14 +4,40 @@ import (
"context"
"gitea.konchin.com/go2025/backend/models"
"github.com/spf13/viper"
"github.com/uptrace/bun"
)
func InitDB(ctx context.Context, db *bun.DB) error {
db.RegisterModel(
(*models.AliasImage)(nil),
(*models.Alias)(nil),
(*models.Image)(nil),
(*models.Session)(nil),
)
if viper.GetBool("reset") {
return db.ResetModel(ctx,
(*models.AliasImage)(nil),
(*models.Alias)(nil),
(*models.Image)(nil),
(*models.Session)(nil),
)
} else {
modls := []any{
(*models.AliasImage)(nil),
(*models.Alias)(nil),
(*models.Image)(nil),
(*models.Session)(nil),
}
for _, model := range modls {
_, err := db.NewCreateTable().
Model(model).
IfNotExists().
Exec(ctx)
if err != nil {
return err
}
}
return nil
}
}