Skip to content

Commit

Permalink
Merge pull request #2 from faulteh/1-add-a-web-framework
Browse files Browse the repository at this point in the history
1 add a web framework
  • Loading branch information
faulteh authored Dec 27, 2024
2 parents 6becc66 + d3ca244 commit 461cbec
Show file tree
Hide file tree
Showing 25 changed files with 784 additions and 94 deletions.
52 changes: 52 additions & 0 deletions .air-bot.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
bin = "./tmp/bot"
cmd = "go build -o ./tmp/bot ./cmd/bot"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "bot-build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
main_only = false
silent = false
time = false

[misc]
clean_on_exit = false

[proxy]
app_port = 0
enabled = false
proxy_port = 0

[screen]
clear_on_rebuild = false
keep_scroll = true
52 changes: 52 additions & 0 deletions .air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
bin = "./tmp/web"
cmd = "go build -o ./tmp/web ./cmd/web"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "web-build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
main_only = false
silent = false
time = false

[misc]
clean_on_exit = false

[proxy]
app_port = 0
enabled = false
proxy_port = 0

[screen]
clear_on_rebuild = false
keep_scroll = true
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ go.work.sum
# env file
.env

bin/
bin/
tmp/
41 changes: 40 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ FROM golang:1.23-alpine AS builder
# Install dependencies required to run the Makefile
RUN apk add --no-cache make

# Set up Go environment variables
ENV GOPATH=/golang
ENV PATH=$GOPATH/bin:$PATH

# Set the working directory
WORKDIR /app

Expand All @@ -14,7 +18,7 @@ COPY . .
RUN make build

# Use a minimal base image for the final container
FROM alpine:latest
FROM alpine:latest AS deploy

# Set the working directory
WORKDIR /app
Expand All @@ -24,8 +28,12 @@ RUN apk add --no-cache ca-certificates

# Copy the binary from the builder
COPY --from=builder /app/bin/bot /app/bin/bot
COPY --from=builder /app/bin/web /app/bin/web

# Set environment variables for database connection
ENV GOPATH=/golang
ENV PATH=$GOPATH/bin:$PATH

ENV DB_HOST=postgres
ENV DB_PORT=5432
ENV DB_USER=napandgo
Expand All @@ -36,5 +44,36 @@ ENV DB_SSLMODE=disable
# Expose any required ports (optional)
EXPOSE 8080

# Run the container as a non-root user
USER nobody

# Run the bot binary
CMD ["/app/bin/bot"]

# Development image we actually use go so we can use hot reloading with air
FROM builder AS dev

# Set environment variables for database connection
ENV GOPATH=/golang
ENV PATH=$GOPATH/bin:$PATH

ENV DB_HOST=postgres
ENV DB_PORT=5432
ENV DB_USER=napandgo
ENV DB_PASSWORD=napandgo
ENV DB_NAME=napandgo
ENV DB_SSLMODE=disable

# Install air for Go for hot reloading
RUN apk add --no-cache git
RUN go install github.com/air-verse/air@latest
RUN git config --global --add safe.directory /app

# Expose any required ports (optional)
EXPOSE 8080

# Run the container as a non-root user
USER root

# Run the bot binary
CMD ["air", "-c", ".air-bot.toml"]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ bot:

# Build the web interface binary
web:
$(GO_BUILD_CMD) -o bin/web ./cmd/web
CGO_ENABLED=$(CGO_ENABLED) $(GO_BUILD_CMD) -o bin/web ./cmd/web

# Run both bot and web
run: build
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,47 @@ My way to learn Go by creating a Discord bot with a bunch of features
- Replies to a message when it is sent a message in a channel

Features will be added as they get completed

## Usage

The application is designed to be run in a collection of containers with Postgres.

```
docker compose up -d
docker compose logs -f
```

## Create a Discord application and Bot

- Go to the [Discord Developer portal](https://discord.com/developers/applications)
- Click the *New Application* button at the top right
![Discord application page](docs/images/screen-new-app.png)
- Give the application a name
![Discord create app modal](docs/images/screen-create-app.png)
- Click the OAuth menu item and configure the a redirect URI. Don't forget to save the page!
- For development http://localhost:3000/api/oauth
- For production https://YOURSERVER/api/oauth
![Discord OAuth page](docs/images/screen-bot-settings.png)
- Click the *Reset Secret* button and copy the new secret that you are given. This will become DISCORD_CLIENT_SECRET in your environment file
- Copy the Client ID - this will become DISCORD_CLIENT_ID in your environment file.
- Click the Bot menu item, and configure the bot settings (you will need to enable several Intents as noted below)
![Discord Bot page](docs/images/screen-bot-settings2.png)
- Click the *Reset Token* button and copy the new token that you receive. This will become your DISCORD_BOT_TOKEN environment variable.

## Installation

- Create an .env file using the .env-prod-example as a template
- Enter your values for DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET and DISCORD_BOT_TOKEN from the above procedure.

## Development Notes

### Project layout

- cmd/bot -- stuff for the bot
- cmd/web -- stuff for the web UI
- config -- handles configuration (mainly loading by environment variables)
- db -- stuff for the database
- docs -- additional documentation
- models -- database models
- static -- web UI static files
- templates -- web UI templates
127 changes: 127 additions & 0 deletions cmd/web/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Package web runs Gin based webserver for the application.
// auth.go has endpoints to handle authentication and session management
package main

import (
"context"
"encoding/json"
"log"
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"

"github.com/faulteh/nap-and-go/config"
)

// AuthRequired is middleware to check if the user is authenticated
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")
if user == nil {
// User not logged in, redirect to login
c.Redirect(http.StatusFound, "/login")
c.Abort()
return
}
// User is logged in; proceed to the next handler
c.Next()
}
}

// homeHandler redirects to login or servers page based on the session
func homeHandler(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")
if user == nil {
// Redirect to login page
c.Redirect(http.StatusFound, "/login")
return
}
// Redirect to servers page
c.Redirect(http.StatusFound, "/servers/")
}

// loginPageHandler handles the home page rendering login page
func loginPageHandler(c *gin.Context) {
// Render the home page
c.HTML(http.StatusOK, "login.html", nil)
}

// loginRedirectHandler redirects the user to the Discord OAuth2 login page
func loginRedirectHandler(c *gin.Context) {
oauth2Config := config.LoadDiscordConfig().OAuth2Config()
url := oauth2Config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
c.Redirect(http.StatusFound, url)
}

// discordCallbackHandler handles the Discord OAuth2 callback
func discordCallbackHandler(c *gin.Context) {
state := c.Query("state")
if state != "state-token" {
// Redirect back to login page such that the login page
c.Redirect(http.StatusFound, "/login")
return
}

code := c.Query("code")
if code == "" {
// Redirect back to login page such that the login page
c.Redirect(http.StatusFound, "/login")
return
}

// Exchange code for token
oauth2Config := config.LoadDiscordConfig().OAuth2Config()
token, err := oauth2Config.Exchange(context.Background(), code)
if err != nil {
log.Printf("Token exchange failed: %v\n", err)
// Redirect back to login page such that the login page
c.Redirect(http.StatusFound, "/login")
return
}

// Fetch user info
client := oauth2Config.Client(context.Background(), token)
resp, err := client.Get("https://discord.com/api/users/@me")
if err != nil {
log.Printf("Failed to get user info: %v\n", err)
// Redirect back to login page such that the login page
c.Redirect(http.StatusFound, "/login")
return
}
defer resp.Body.Close() //nolint:errcheck

var user map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
log.Printf("Failed to parse user info: %v\n", err)
// Redirect back to login page such that the login page
c.Redirect(http.StatusFound, "/login")
return
}

// Store user data in session
session := sessions.Default(c)
session.Set("user", user)
session.Set("token", token)
if err := session.Save(); err != nil {
log.Printf("Failed to save session: %v\n", err)
// Redirect back to login page such that the login page
c.Redirect(http.StatusFound, "/login")
return
}

// Redirect to the servers page
c.Redirect(http.StatusFound, "/servers/")
}

// logoutHandler handles the logout
func logoutHandler(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
session.Save() //nolint:errcheck
// Redirect back to login page
c.Redirect(http.StatusFound, "/login")
}
15 changes: 15 additions & 0 deletions cmd/web/dashboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Package main provides the web UI
// dashboard.go has the handlers for the dashboard view and configuration of the bot
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

// dashboardHandler handles the dashboard view
func dashboardHandler(c *gin.Context) {
// Render the dashboard view
c.HTML(http.StatusOK, "dashboard.html", nil)
}
Loading

0 comments on commit 461cbec

Please sign in to comment.