init repo

This commit is contained in:
rmanach
2025-01-02 18:59:37 +01:00
commit 6576f45eac
15 changed files with 1068 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
package login
import (
"bytes"
"errors"
"fmt"
"librapi/handlers/login/templates"
"librapi/services"
"net/http"
"sync"
"github.com/rs/zerolog/log"
)
var (
adminUsername = sync.OnceValue[string](func() string {
return "test"
})
adminPassword = sync.OnceValue[string](func() string {
return "test"
})
)
var (
ErrInvalidUsername = errors.New("username must not be empty")
ErrInvalidPassword = errors.New("password must not be empty")
ErrInvalidCredentials = errors.New("bad credentials")
)
type LoginField struct {
Name string
Value string
Err string
}
type LoginForm struct {
Username LoginField
Password LoginField
Error error
Method string
}
func NewLoginForm() LoginForm {
return LoginForm{
Username: LoginField{
Name: "username",
},
Password: LoginField{
Name: "password",
},
Method: http.MethodPost,
}
}
func (lf *LoginForm) HasErrors() bool {
return lf.Username.Err != "" || lf.Password.Err != ""
}
func (lf *LoginForm) IsSuccess() bool {
return lf.Method == http.MethodPost && lf.Error != nil && !lf.HasErrors()
}
func (lf *LoginForm) ValidCredentials() bool {
return lf.Username.Value == adminUsername() && lf.Password.Value == adminPassword()
}
func Handler(s *services.SessionStore) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getLogin(w, r, s)
case http.MethodPost:
postLogin(w, r, s)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
}
func extractLoginForm(r *http.Request) LoginForm {
lf := NewLoginForm()
username := r.FormValue(lf.Username.Name)
if username == "" {
lf.Username.Err = ErrInvalidUsername.Error()
}
lf.Username.Value = username
password := r.FormValue(lf.Password.Name)
if password == "" {
lf.Password.Err = ErrInvalidPassword.Error()
}
lf.Password.Value = password
return lf
}
func postLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) {
loginForm := templates.GetLoginForm()
if loginForm == nil {
log.Error().Msg("unable to load login form")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
lf := extractLoginForm(r)
if lf.HasErrors() {
buf := bytes.NewBufferString("")
if err := loginForm.Execute(buf, &lf); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
}
w.WriteHeader(400)
fmt.Fprint(w, buf.String())
return
}
if ok := lf.ValidCredentials(); !ok {
lf.Error = ErrInvalidCredentials
buf := bytes.NewBufferString("")
if err := loginForm.Execute(buf, &lf); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
w.WriteHeader(401)
fmt.Fprint(w, buf.String())
return
}
session, err := s.NewSession()
if err != nil {
log.Err(err).Msg("unable to create a new session")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
}
cookie := session.GenerateCookie()
http.SetCookie(w, cookie)
loginSuccess := templates.GetLoginSuccess()
if loginSuccess == nil {
log.Error().Msg("unable to load login success")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
fmt.Fprint(w, loginSuccess.Tree.Root.String())
}
func getLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) {
loginForm := templates.GetLoginForm()
if loginForm == nil {
log.Error().Msg("unable to load login form")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
if s.IsLogged(r) {
loginSuccess := templates.GetLoginSuccess()
if loginSuccess == nil {
log.Error().Msg("unable to load login success")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
fmt.Fprint(w, loginSuccess.Tree.Root.String())
return
}
buf := bytes.NewBufferString("")
if err := loginForm.Execute(buf, &LoginForm{}); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
if _, err := fmt.Fprint(w, buf); err != nil {
log.Err(err).Msg("unable to write to response")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
}
}
+66
View File
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<style>
.main-container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
}
.form-item {
align-self: flex-start;
margin: 10px;
width: 300px;
}
.form-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.error {
color: red;
}
</style>
</head>
<body>
<h1>Login</h1>
<form action="/login" method="post" enctype="multipart/form-data">
<div class="main-container">
<div class="form-item">
<div class="form-container">
<label>Username:</label>
<input type="text" name="username" value="{{.Username.Value}}" />
</div>
{{ if .Username.Err }}
<div class="error">{{.Username.Err}}</div>
{{ end }}
</div>
<div class="form-item">
<div class="form-container">
<label>Password:</label>
<input type="password" name="password" value="{{.Password.Value}}" />
</div>
{{ if .Password.Err }}
<div class="error">{{.Password.Err}}</div>
{{ end }}
</div>
<div class="form-item">
<div class="form-container">
<button id="submit" type="submit">Login</button>
</div>
</div>
</div>
</form>
{{ if ne (errStr .Error) "" }}
<div class="error">{{.Error | errStr}}</div>
{{ end }}
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<body>
<h1>Login</h1>
<div>You're logged</div>
<h2>Available urls</h2>
<ul>
<li><a href="/upload">Upload a book</a></li>
</ul>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
package templates
import (
_ "embed"
"html/template"
"sync"
"github.com/rs/zerolog/log"
)
//go:embed form.html.tpl
var form string
//go:embed success.html.tpl
var success string
var funcMap = template.FuncMap{
"errStr": func(err error) string {
if err != nil {
return err.Error()
}
return ""
},
}
var GetLoginForm = sync.OnceValue[*template.Template](func() *template.Template {
tmpl, err := template.New("loginForm").Funcs(funcMap).Parse(form)
if err != nil {
log.Err(err).Msg("unable to parse login form")
return nil
}
return tmpl
})
var GetLoginSuccess = sync.OnceValue[*template.Template](func() *template.Template {
tmpl, err := template.New("loginSuccess").Parse(success)
if err != nil {
log.Err(err).Msg("unable to parse login success")
return nil
}
return tmpl
})
+258
View File
@@ -0,0 +1,258 @@
package upload
import (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"librapi/handlers/upload/templates"
"librapi/services"
)
const MaxFileSize = 200 // in MB
var (
ErrInvalidName = errors.New("book name must not be empty")
ErrInvalidEditor = errors.New("book editor must not be empty")
ErrInvalidYear = errors.New("invalid year, unable to parse")
ErrInvalidAuthors = errors.New("must at least contains one author")
ErrFileMaxSizeReached = errors.New("max file size reached, must be <= 200MB")
ErrFileOpen = errors.New("unable to open file from form")
ErrUnauthorized = errors.New("unvalid authorization key")
)
type StrList = []string
type BookFile struct {
file multipart.File
Header *multipart.FileHeader
}
func (bf *BookFile) GetFilename() string {
return bf.Header.Filename
}
func (bf *BookFile) CheckSize() error {
if bf.Header.Size > (MaxFileSize << 20) {
return ErrFileMaxSizeReached
}
return nil
}
type BookFieldType interface {
int | string | StrList | BookFile
}
type BookField[T BookFieldType] struct {
Name string
Value T
Err string
}
type BookForm struct {
Name BookField[string]
Editor BookField[string]
Authors BookField[StrList]
Year BookField[int]
Keywords BookField[StrList]
File BookField[BookFile]
Error string
Method string
}
func NewBookForm() BookForm {
return BookForm{
Name: BookField[string]{
Name: "name",
},
Editor: BookField[string]{
Name: "editor",
},
Authors: BookField[StrList]{
Name: "authors",
},
Year: BookField[int]{
Name: "year",
},
Keywords: BookField[StrList]{
Name: "keywords",
},
File: BookField[BookFile]{
Name: "file",
},
Method: http.MethodPost,
}
}
func (bf *BookForm) HasErrors() bool {
return bf.Name.Err != "" || bf.Authors.Err != "" || bf.Editor.Err != "" || bf.Year.Err != "" || bf.Keywords.Err != "" || bf.File.Err != ""
}
func (bf *BookForm) IsSuccess() bool {
return bf.Method == http.MethodPost && bf.Error == "" && !bf.HasErrors()
}
func Handler(s *services.SessionStore) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getUploadFile(w, r)
case http.MethodPost:
postUploadFile(w, r, s)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
}
func extractBookForm(r *http.Request) BookForm {
bf := NewBookForm()
name := r.FormValue(bf.Name.Name)
if name == "" {
bf.Name.Err = ErrInvalidName.Error()
}
bf.Name.Value = name
editor := r.FormValue(bf.Editor.Name)
if editor == "" {
bf.Editor.Err = ErrInvalidEditor.Error()
}
bf.Editor.Value = editor
if a := r.FormValue(bf.Authors.Name); len(a) != 0 {
bf.Authors.Value = strings.Split(",", a)
} else {
bf.Authors.Err = ErrInvalidAuthors.Error()
}
year, errParse := strconv.Atoi(r.FormValue(bf.Year.Name))
if errParse != nil {
log.Err(errParse).Msg("unable to parse date")
bf.Year.Err = ErrInvalidYear.Error()
}
bf.Year.Value = year
if kw := r.FormValue(bf.Keywords.Name); len(kw) != 0 {
bf.Keywords.Value = strings.Split(",", kw)
}
file, fileh, err := r.FormFile(bf.File.Name)
if err != nil {
log.Err(err).Msg("unable to get file from form")
bf.File.Err = ErrFileOpen.Error()
return bf
}
bf.File.Value = BookFile{
file: file,
Header: fileh,
}
if err := bf.File.Value.CheckSize(); err != nil {
bf.File.Err = err.Error()
}
return bf
}
func postUploadFile(w http.ResponseWriter, r *http.Request, s *services.SessionStore) {
uploadForm := templates.GetUploadForm()
if uploadForm == nil {
log.Error().Msg("unable to load upload form")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
if !s.IsLogged(r) {
buf := bytes.NewBufferString("")
if err := uploadForm.Execute(buf, &BookForm{Error: services.ErrUnauthorized.Error()}); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
}
w.WriteHeader(401)
fmt.Fprint(w, buf.String())
return
}
buf := bytes.NewBufferString("")
bf := extractBookForm(r)
if err := uploadForm.Execute(buf, &bf); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
}
if bf.HasErrors() {
w.WriteHeader(400)
fmt.Fprint(w, buf.String())
return
}
filename := bf.File.Value.GetFilename()
log.Info().Str("filename", filename).Msg("file is uploading...")
dst, err := os.Create(filename)
if err != nil {
if err := uploadForm.Execute(buf, &BookForm{Error: "unexpected error occured while creating file"}); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
}
w.WriteHeader(500)
fmt.Fprint(w, buf.String())
return
}
defer dst.Close()
if _, err := io.Copy(dst, bf.File.Value.file); err != nil {
if err := uploadForm.Execute(buf, &BookForm{Error: "unexpected error occured while uploading file"}); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
w.WriteHeader(500)
fmt.Fprint(w, buf.String())
return
}
buf.Reset()
if err := uploadForm.Execute(buf, &BookForm{Method: http.MethodPost}); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
fmt.Fprint(w, buf.String())
}
func getUploadFile(w http.ResponseWriter, r *http.Request) {
uploadForm := templates.GetUploadForm()
if uploadForm == nil {
log.Error().Msg("unable to load upload form")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
buf := bytes.NewBufferString("")
if err := uploadForm.Execute(buf, &BookForm{}); err != nil {
log.Err(err).Msg("unable to generate template")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
return
}
if _, err := fmt.Fprint(w, buf); err != nil {
log.Err(err).Msg("unable to write to response")
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
}
}
+106
View File
@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<style>
.main-container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
}
.form-item {
align-self: flex-start;
margin: 10px;
width: 300px;
}
.form-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.error {
color: red;
}
</style>
</head>
<body>
<h1>Upload a book</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<div class="main-container">
<div class="form-item">
<div class="form-container">
<label>Book name:</label>
<input type="text" name="name" value="{{.Name.Value}}" />
</div>
{{ if .Name.Err }}
<div class="error">{{.Name.Err}}</div>
{{ end }}
</div>
<div class="form-item">
<div class="form-container">
<label>Editor:</label>
<input type="text" name="editor" value="{{.Editor.Value}}" />
</div>
{{ if .Editor.Err }}
<div class="error">{{.Editor.Err}}</div>
{{ end }}
</div>
<div class="form-item">
<div class="form-container">
<label>Authors:</label>
<input type="text" name="authors" value="{{.Authors.Value | join}}" />
</div>
{{ if .Authors.Err }}
<div class="error">{{.Authors.Err}}</div>
{{ end }}
</div>
<div class="form-item">
<div class="form-container">
<label>Year:</label>
<input type="number" name="year" value="{{.Year.Value | year}}" />
</div>
{{ if .Year.Err }}
<div class="error">{{.Year.Err}}</div>
{{ end }}
</div>
<div class="form-item">
<div class="form-container">
<label>Keywords:</label>
<input type="text" name="keywords" value="{{.Keywords.Value | join}}" />
</div>
{{ if .Keywords.Err }}
<div class="error">{{.Keywords.Err}}</div>
{{ end }}
</div>
<div class="form-item">
<div class="form-container">
<input type="file" name="file" files="{{.File.Value.Header | filename}}" accept=".pdf" />
<button id="submit" type="submit">Upload</button>
</div>
</div>
</div>
</form>
{{ if .File.Err }}
<div class="error">{{.File.Err}}</div>
{{ end }}
{{ if ne .Error "" }}
<div class="error">{{.Error}}</div>
{{ end }}
</body>
<script type="text/javascript">
var submit = document.getElementById("submit");
submit.addEventListener("submit", function () {
submit.disabled = true;
});
{ { if .IsSuccess } }
alert("file uploaded successfully");
{ { end } }
</script>
</html>
+46
View File
@@ -0,0 +1,46 @@
package templates
import (
_ "embed"
"html/template"
"mime/multipart"
"strconv"
"strings"
"sync"
"github.com/rs/zerolog/log"
)
var funcMap = template.FuncMap{
"year": func(s int) string {
if s == 0 {
return ""
}
return strconv.Itoa(s)
},
"join": func(s []string) string {
if len(s) == 0 {
return ""
} else {
return strings.Join(s, ",")
}
},
"filename": func(h *multipart.FileHeader) string {
if h != nil {
return h.Filename
}
return ""
},
}
//go:embed form.html.tpl
var form string
var GetUploadForm = sync.OnceValue[*template.Template](func() *template.Template {
tmpl, err := template.New("uploadForm").Funcs(funcMap).Parse(form)
if err != nil {
log.Err(err).Msg("unable to parse upload form")
return nil
}
return tmpl
})