initial commit, basic framework setup
This commit is contained in:
commit
27170540d0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
direnv.ps1
|
||||
bin
|
||||
.air.toml
|
11
LICENSE.md
Normal file
11
LICENSE.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
Copyright 2024 Abdulmujeeb Raji
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Discourse the right way
|
||||
**Fog** is a **F**orum mixed with a Bl**og**, intended to encourage fruitful
|
||||
discussion and the spread of knowledge.
|
||||
|
||||
## Posts
|
||||
These are your typical blog posts, written in Markdown. They can include video,
|
||||
images and (one day) custom JS for interactivity.
|
||||
|
||||
Posts can be replies to other posts and will be directly linked underneath the
|
||||
main post. You can reply to multiple posts at once, and you will be linked in
|
||||
the replies of all posts. Neighbouring posts which reply to the same parent post
|
||||
will be linked under the reply post.
|
||||
|
||||
After you make a post, you must wait a customizable amount of time before
|
||||
making another (default is a day). Think before you speak.
|
||||
|
||||
There is no voting system and no comments system. If you have something to say
|
||||
about a post, say it. Views on a post are also not recorded.
|
||||
|
||||
## Channels
|
||||
Channels are a way of sorting posts by their general theme. All posts made to a
|
||||
channel can be sorted by newest (default), oldest and random.
|
||||
|
||||
Moderators can pin posts to the top of channels.
|
97
cmd/api/api.go
Normal file
97
cmd/api/api.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type application struct {
|
||||
config config
|
||||
v1 *v1Handler
|
||||
}
|
||||
|
||||
type config struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
func (h *application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
wWrapper := &wrappedResponseWriter{w, http.StatusOK}
|
||||
w = wWrapper
|
||||
|
||||
// fullpath middleware
|
||||
ctx := context.WithValue(r.Context(), "fullPath", r.URL.Path)
|
||||
|
||||
// logger middleware
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
log.Printf("request at %s, responded with status %d in %dms", ctx.Value("fullPath"), wWrapper.statusCode, time.Since(startTime).Milliseconds())
|
||||
}()
|
||||
|
||||
// recoverer middleware
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
|
||||
jsonBody, _ := json.Marshal(map[string]string{
|
||||
"error": "There was an internal server error",
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write(jsonBody)
|
||||
}
|
||||
}()
|
||||
|
||||
// timeout middleware
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
defer func() {
|
||||
cancel()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
w.WriteHeader(http.StatusGatewayTimeout)
|
||||
}
|
||||
}()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
var head string
|
||||
head, r.URL.Path = shiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "v1":
|
||||
h.v1.ServeHTTP(w, r)
|
||||
default:
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) run() error {
|
||||
srv := &http.Server{
|
||||
Addr: app.config.addr,
|
||||
Handler: app,
|
||||
WriteTimeout: time.Second * 30,
|
||||
ReadTimeout: time.Second * 10,
|
||||
IdleTimeout: time.Minute,
|
||||
}
|
||||
|
||||
log.Printf("server has started at %s", app.config.addr)
|
||||
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
// ShiftPath splits off the first component of p, which will be cleaned of
|
||||
// relative components before processing. head will never contain a slash and
|
||||
// tail will always be a rooted path without trailing slash.
|
||||
func shiftPath(p string) (head, tail string) {
|
||||
p = path.Clean("/" + p)
|
||||
i := strings.Index(p[1:], "/") + 1
|
||||
if i <= 0 {
|
||||
return p[1:], "/"
|
||||
}
|
||||
return p[1:i], p[i:]
|
||||
}
|
10
cmd/api/health.go
Normal file
10
cmd/api/health.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
type healthCheckHandler struct {
|
||||
}
|
||||
|
||||
func (h *healthCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
}
|
24
cmd/api/main.go
Normal file
24
cmd/api/main.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"midnadimple.com/fog/internal/env"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
addr: env.GetString("FOG_ADDR", ":8080"),
|
||||
}
|
||||
|
||||
v1 := &v1Handler{
|
||||
healthCheck: &healthCheckHandler{},
|
||||
}
|
||||
|
||||
app := &application{
|
||||
config: cfg,
|
||||
v1: v1,
|
||||
}
|
||||
|
||||
log.Fatal(app.run())
|
||||
}
|
19
cmd/api/v1.go
Normal file
19
cmd/api/v1.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
type v1Handler struct {
|
||||
healthCheck *healthCheckHandler
|
||||
}
|
||||
|
||||
func (h *v1Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = shiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
case "health":
|
||||
h.healthCheck.ServeHTTP(w, r)
|
||||
default:
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
}
|
13
cmd/api/wrappers.go
Normal file
13
cmd/api/wrappers.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
type wrappedResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *wrappedResponseWriter) WriteHeader(code int) {
|
||||
w.statusCode = code
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
31
internal/env/env.go
vendored
Normal file
31
internal/env/env.go
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetString(key, fallback string) string {
|
||||
val, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func GetInt(key string, fallback int) int {
|
||||
val, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
|
||||
valInt, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
log.Printf("Failed to convert environment variable with key %s and value %s to int. Using fallback %d. (err: %s)", key, val, fallback, err)
|
||||
return fallback
|
||||
}
|
||||
|
||||
return valInt
|
||||
}
|
1
web/README.md
Normal file
1
web/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Frontend TODO
|
3
web/go.mod
Normal file
3
web/go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module midnadimple.com/fog/web
|
||||
|
||||
go 1.23.4
|
Loading…
Reference in a new issue