commit 27170540d0d450ab6da8ae6fcb7a7af38070ac76 Author: Abdulmujeeb Raji Date: Mon Dec 30 06:29:34 2024 +0000 initial commit, basic framework setup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81d8f55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +direnv.ps1 +bin +.air.toml \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..57b7efc --- /dev/null +++ b/LICENSE.md @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb4a102 --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/cmd/api/api.go b/cmd/api/api.go new file mode 100644 index 0000000..1ae5c4d --- /dev/null +++ b/cmd/api/api.go @@ -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:] +} diff --git a/cmd/api/health.go b/cmd/api/health.go new file mode 100644 index 0000000..46602a4 --- /dev/null +++ b/cmd/api/health.go @@ -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")) +} diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..fc41f65 --- /dev/null +++ b/cmd/api/main.go @@ -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()) +} diff --git a/cmd/api/v1.go b/cmd/api/v1.go new file mode 100644 index 0000000..c252a2b --- /dev/null +++ b/cmd/api/v1.go @@ -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) + } +} diff --git a/cmd/api/wrappers.go b/cmd/api/wrappers.go new file mode 100644 index 0000000..685400a --- /dev/null +++ b/cmd/api/wrappers.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e8deb9f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module midnadimple.com/fog + +go 1.23.4 diff --git a/internal/env/env.go b/internal/env/env.go new file mode 100644 index 0000000..ff6d7df --- /dev/null +++ b/internal/env/env.go @@ -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 +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..99bd17e --- /dev/null +++ b/web/README.md @@ -0,0 +1 @@ +# Frontend TODO \ No newline at end of file diff --git a/web/go.mod b/web/go.mod new file mode 100644 index 0000000..57346a6 --- /dev/null +++ b/web/go.mod @@ -0,0 +1,3 @@ +module midnadimple.com/fog/web + +go 1.23.4