initial commit, basic framework setup

This commit is contained in:
Abdulmujeeb Raji 2024-12-30 06:29:34 +00:00
commit 27170540d0
Signed by: midnadimple
GPG key ID: EB02C582F8C3962B
12 changed files with 239 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
direnv.ps1
bin
.air.toml

11
LICENSE.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module midnadimple.com/fog
go 1.23.4

31
internal/env/env.go vendored Normal file
View 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
View file

@ -0,0 +1 @@
# Frontend TODO

3
web/go.mod Normal file
View file

@ -0,0 +1,3 @@
module midnadimple.com/fog/web
go 1.23.4