Photoblog: Refactoring
This commit is contained in:
parent
8ec59e4826
commit
b8f03b87c9
@ -15,6 +15,14 @@ import (
|
|||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
USER = "username"
|
||||||
|
PWD = "password"
|
||||||
|
SESS_USER = "username"
|
||||||
|
SESSION = "photoblog.session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Application
|
||||||
type AuthCookie struct {
|
type AuthCookie struct {
|
||||||
Templates *template.Template
|
Templates *template.Template
|
||||||
Store *sessions.CookieStore
|
Store *sessions.CookieStore
|
||||||
@ -22,37 +30,23 @@ type AuthCookie struct {
|
|||||||
PasswordSecret string
|
PasswordSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *AuthCookie) LoginPage(res http.ResponseWriter, req *http.Request) {
|
// Verify Username
|
||||||
formErr := make(map[string]error)
|
|
||||||
switch req.Method {
|
|
||||||
case "POST":
|
|
||||||
username := req.FormValue("username")
|
|
||||||
formErr["username"] = app.VerifyUsername(username)
|
|
||||||
formErr["password"] = app.VerifyPassword(username, req.FormValue("password"), res, req)
|
|
||||||
if formErr["username"] == nil && formErr["password"] == nil {
|
|
||||||
app.SaveUsername(username, res, req)
|
|
||||||
RedirectHome(res, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case "GET":
|
|
||||||
app.Templates.ExecuteTemplate(res, "login.html", formErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (app *AuthCookie) LogoutPage(res http.ResponseWriter, req *http.Request) {
|
|
||||||
app.SaveUsername("", res, req)
|
|
||||||
RedirectHome(res, req)
|
|
||||||
}
|
|
||||||
func (app *AuthCookie) VerifyUsername(username string) error {
|
func (app *AuthCookie) VerifyUsername(username string) error {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return errors.New("Empty username")
|
return errors.New("Empty username")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify password for user, check with file
|
||||||
func (app *AuthCookie) VerifyPassword(username, password string, res http.ResponseWriter, req *http.Request) error {
|
func (app *AuthCookie) VerifyPassword(username, password string, res http.ResponseWriter, req *http.Request) error {
|
||||||
if password == "" {
|
if password == "" {
|
||||||
return errors.New("Empty password")
|
return errors.New("Empty password")
|
||||||
}
|
}
|
||||||
|
if username == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// open password file
|
||||||
passfile, err := os.Open(filepath.Join(app.DataDir.Name(), username, ".password"))
|
passfile, err := os.Open(filepath.Join(app.DataDir.Name(), username, ".password"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Cannot open password file", err)
|
log.Println("Cannot open password file", err)
|
||||||
@ -60,14 +54,13 @@ func (app *AuthCookie) VerifyPassword(username, password string, res http.Respon
|
|||||||
}
|
}
|
||||||
defer passfile.Close()
|
defer passfile.Close()
|
||||||
|
|
||||||
if username == "" {
|
// read password file
|
||||||
return nil
|
|
||||||
}
|
|
||||||
expected, err := ioutil.ReadAll(passfile)
|
expected, err := ioutil.ReadAll(passfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Cannot read password file", err)
|
log.Println("Cannot read password file", err)
|
||||||
return errors.New("Authentification failed")
|
return errors.New("Authentification failed")
|
||||||
}
|
}
|
||||||
|
// hash password and compare
|
||||||
expectedStr := string(expected)
|
expectedStr := string(expected)
|
||||||
var expectedMAC []byte
|
var expectedMAC []byte
|
||||||
fmt.Sscanf(expectedStr, "%x", &expectedMAC)
|
fmt.Sscanf(expectedStr, "%x", &expectedMAC)
|
||||||
@ -78,29 +71,67 @@ func (app *AuthCookie) VerifyPassword(username, password string, res http.Respon
|
|||||||
log.Printf("Unmatched password for %s: %x - %x\n", username, passwordMAC, expectedMAC)
|
log.Printf("Unmatched password for %s: %x - %x\n", username, passwordMAC, expectedMAC)
|
||||||
return errors.New("Authentification failed")
|
return errors.New("Authentification failed")
|
||||||
}
|
}
|
||||||
log.Printf("Authentification successful")
|
log.Printf("Authentification successful (" + username + ")")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (app *AuthCookie) CurrentSession(res http.ResponseWriter, req *http.Request) *sessions.Session {
|
|
||||||
session, _ := app.Store.Get(req, "session")
|
// Save username in session
|
||||||
return session
|
|
||||||
}
|
|
||||||
func (app *AuthCookie) SaveUsername(username string, res http.ResponseWriter, req *http.Request) {
|
func (app *AuthCookie) SaveUsername(username string, res http.ResponseWriter, req *http.Request) {
|
||||||
session := app.CurrentSession(res, req)
|
session := app.CurrentSession(res, req)
|
||||||
session.Values["username"] = username
|
session.Values[SESS_USER] = username
|
||||||
session.Save(req, res)
|
session.Save(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test if a user is logged in
|
||||||
func (app *AuthCookie) IsLoggedIn(res http.ResponseWriter, req *http.Request) bool {
|
func (app *AuthCookie) IsLoggedIn(res http.ResponseWriter, req *http.Request) bool {
|
||||||
session := app.CurrentSession(res, req)
|
session := app.CurrentSession(res, req)
|
||||||
return session != nil && session.Values["username"] != ""
|
return session != nil && session.Values[SESS_USER] != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Current username (from session cookie)
|
||||||
func (app *AuthCookie) Username(res http.ResponseWriter, req *http.Request) string {
|
func (app *AuthCookie) Username(res http.ResponseWriter, req *http.Request) string {
|
||||||
session := app.CurrentSession(res, req)
|
session := app.CurrentSession(res, req)
|
||||||
if session == nil {
|
if session == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return session.Values["username"].(string)
|
val := session.Values[SESS_USER]
|
||||||
|
if val == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
}
|
}
|
||||||
func RedirectHome(res http.ResponseWriter, req *http.Request) {
|
|
||||||
|
// Current session
|
||||||
|
func (app *AuthCookie) CurrentSession(res http.ResponseWriter, req *http.Request) *sessions.Session {
|
||||||
|
session, _ := app.Store.Get(req, SESSION)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to home page
|
||||||
|
func (app *AuthCookie) RedirectHome(res http.ResponseWriter, req *http.Request) {
|
||||||
http.Redirect(res, req, "/", http.StatusSeeOther)
|
http.Redirect(res, req, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ROUTES //
|
||||||
|
|
||||||
|
// login: form
|
||||||
|
func (app *AuthCookie) LoginPage(res http.ResponseWriter, req *http.Request) {
|
||||||
|
formErr := make(map[string]error)
|
||||||
|
if req.Method == http.MethodPost {
|
||||||
|
username := req.FormValue(USER)
|
||||||
|
formErr[USER] = app.VerifyUsername(username)
|
||||||
|
formErr[PWD] = app.VerifyPassword(username, req.FormValue(PWD), res, req)
|
||||||
|
if formErr[USER] == nil && formErr[PWD] == nil {
|
||||||
|
app.SaveUsername(username, res, req)
|
||||||
|
app.RedirectHome(res, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.Templates.ExecuteTemplate(res, "login.html", formErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// logout: delete session and redirect to home
|
||||||
|
func (app *AuthCookie) LogoutPage(res http.ResponseWriter, req *http.Request) {
|
||||||
|
app.SaveUsername("", res, req)
|
||||||
|
app.RedirectHome(res, req)
|
||||||
|
}
|
||||||
|
@ -28,19 +28,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer data.Close()
|
defer data.Close()
|
||||||
|
|
||||||
|
// FIXME config file
|
||||||
app := photo.PhotoBlog{
|
app := photo.PhotoBlog{
|
||||||
admin.AuthCookie{Templates: tpl,
|
admin.AuthCookie{
|
||||||
|
Templates: tpl,
|
||||||
Store: sessions.NewCookieStore([]byte("flQ6QzM/c3Jtdl9ycDx6OXRIfFgK")),
|
Store: sessions.NewCookieStore([]byte("flQ6QzM/c3Jtdl9ycDx6OXRIfFgK")),
|
||||||
DataDir: data,
|
DataDir: data,
|
||||||
PasswordSecret: "d2xnNSwoREQhfSxBVDQ0bF0yb2AK",
|
PasswordSecret: "d2xnNSwoREQhfSxBVDQ0bF0yb2AK",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fileServer := http.FileServer(http.Dir(data.Name()))
|
|
||||||
|
|
||||||
http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
if strings.HasPrefix(req.RequestURI, "/data") {
|
if strings.HasPrefix(req.RequestURI, "/data") {
|
||||||
http.StripPrefix("/data/", fileServer).ServeHTTP(res, req)
|
http.StripPrefix("/data/", http.FileServer(http.Dir(data.Name()))).ServeHTTP(res, req)
|
||||||
} else {
|
} else {
|
||||||
app.HomePage(res, req)
|
app.HomePage(res, req)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package photo
|
package photo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -18,22 +19,59 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
PHOTOEXT = [3]string{".jpg", ".jpeg", ".png"}
|
PHOTOEXT = [3]string{".jpg", ".jpeg", ".png"}
|
||||||
|
PHOTOMIME = [2]string{"image/png", "image/jpeg"}
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppData struct {
|
// Application
|
||||||
|
type PhotoBlog struct {
|
||||||
|
admin.AuthCookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic model
|
||||||
|
type PhotoModel struct {
|
||||||
Username string
|
Username string
|
||||||
Photos []string
|
|
||||||
Err error
|
Err error
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constructor PhotoModel
|
||||||
|
func NewPhotoModel(username string) *PhotoModel {
|
||||||
|
return &PhotoModel{
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home page model
|
||||||
|
type PhotoHomeModel struct {
|
||||||
|
PhotoModel
|
||||||
|
Photos []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor PhotoHomeModel
|
||||||
|
func NewPhotoHomeModel(username string, photos []string) *PhotoHomeModel {
|
||||||
|
return &PhotoHomeModel{
|
||||||
|
PhotoModel: *NewPhotoModel(username),
|
||||||
|
Photos: photos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File and date of last modification
|
||||||
type TimedFile struct {
|
type TimedFile struct {
|
||||||
Mod time.Time
|
Mod time.Time
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
type timeMap []TimedFile
|
// Constructor TimedFile
|
||||||
|
func NewTimedFile(mod time.Time, path string) *TimedFile {
|
||||||
|
return &TimedFile{
|
||||||
|
mod,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimedFile sort
|
||||||
|
type timeMap []*TimedFile
|
||||||
|
|
||||||
func (m timeMap) Len() int {
|
func (m timeMap) Len() int {
|
||||||
return len(m)
|
return len(m)
|
||||||
@ -45,14 +83,7 @@ func (m timeMap) Swap(i, j int) {
|
|||||||
m[i], m[j] = m[j], m[i]
|
m[i], m[j] = m[j], m[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhotoBlog struct {
|
// test if file is a photo (by extension)
|
||||||
admin.AuthCookie
|
|
||||||
}
|
|
||||||
|
|
||||||
func RedirectHome(res http.ResponseWriter, req *http.Request) {
|
|
||||||
// FIXME duplicate admin
|
|
||||||
http.Redirect(res, req, "/", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
func IsPhoto(path string) bool {
|
func IsPhoto(path string) bool {
|
||||||
for _, ext := range PHOTOEXT {
|
for _, ext := range PHOTOEXT {
|
||||||
if strings.HasSuffix(path, ext) {
|
if strings.HasSuffix(path, ext) {
|
||||||
@ -62,6 +93,17 @@ func IsPhoto(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test if mime type is a photo
|
||||||
|
func IsPhotoMime(mime string) bool {
|
||||||
|
for _, accepted := range PHOTOMIME {
|
||||||
|
if mime == accepted {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get Latest files
|
||||||
func Latest(photos timeMap) []string {
|
func Latest(photos timeMap) []string {
|
||||||
latest := make([]string, 0, DISPLAY_SIZE)
|
latest := make([]string, 0, DISPLAY_SIZE)
|
||||||
sort.Sort(photos)
|
sort.Sort(photos)
|
||||||
@ -74,60 +116,57 @@ func Latest(photos timeMap) []string {
|
|||||||
return latest
|
return latest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *PhotoBlog) HomePage(res http.ResponseWriter, req *http.Request) {
|
// Add new uploaded photo
|
||||||
data := AppData{
|
func (app *PhotoBlog) AddPhoto(res http.ResponseWriter, req *http.Request) (string, error) {
|
||||||
Username: app.Username(res, req),
|
|
||||||
}
|
|
||||||
photos := make([]TimedFile, 0)
|
|
||||||
filepath.Walk(app.DataDir.Name(), func(path string, info os.FileInfo, err error) error {
|
|
||||||
if !info.IsDir() && IsPhoto(path) {
|
|
||||||
photos = append(photos, TimedFile{
|
|
||||||
info.ModTime(),
|
|
||||||
path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
data.Photos = Latest(photos)
|
|
||||||
app.Templates.ExecuteTemplate(res, "home.html", data)
|
|
||||||
}
|
|
||||||
func (app *PhotoBlog) UploadPage(res http.ResponseWriter, req *http.Request) {
|
|
||||||
if !app.IsLoggedIn(res, req) {
|
|
||||||
RedirectHome(res, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data := AppData{
|
|
||||||
Username: app.Username(res, req),
|
|
||||||
}
|
|
||||||
switch req.Method {
|
|
||||||
case "POST":
|
|
||||||
message, err := app.NewPhoto(res, req)
|
|
||||||
if err != nil {
|
|
||||||
data.Err = err
|
|
||||||
}
|
|
||||||
data.Message = message
|
|
||||||
fallthrough
|
|
||||||
case "GET":
|
|
||||||
app.Templates.ExecuteTemplate(res, "upload.html", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (app *PhotoBlog) NewPhoto(res http.ResponseWriter, req *http.Request) (string, error) {
|
|
||||||
file, header, err := req.FormFile("file")
|
file, header, err := req.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// FIXME check type
|
mime := header.Header.Get("Content-Type")
|
||||||
tmpFile, err := os.Create(filepath.Join(app.DataDir.Name(), app.Username(res, req), header.Filename))
|
if !IsPhotoMime(mime) {
|
||||||
|
return "", errors.New("This file type is not accepted (" + mime + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := os.Create(filepath.Join(app.DataDir.Name(), app.Username(res, req), header.Filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer tmpFile.Close()
|
defer target.Close()
|
||||||
|
|
||||||
sz, err := io.Copy(tmpFile, file)
|
sz, err := io.Copy(target, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("File uploaded (%d bytes)", sz), nil
|
return fmt.Sprintf("File uploaded (%d bytes)", sz), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ROUTES //
|
||||||
|
|
||||||
|
// Home: list latest photos
|
||||||
|
func (app *PhotoBlog) HomePage(res http.ResponseWriter, req *http.Request) {
|
||||||
|
photos := make([]*TimedFile, 0)
|
||||||
|
filepath.Walk(app.DataDir.Name(), func(path string, info os.FileInfo, err error) error {
|
||||||
|
if !info.IsDir() && IsPhoto(path) {
|
||||||
|
photos = append(photos, NewTimedFile(info.ModTime(), path))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
app.Templates.ExecuteTemplate(res, "home.html", NewPhotoHomeModel(app.Username(res, req), Latest(photos)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload: form upload photo
|
||||||
|
func (app *PhotoBlog) UploadPage(res http.ResponseWriter, req *http.Request) {
|
||||||
|
if !app.IsLoggedIn(res, req) {
|
||||||
|
app.RedirectHome(res, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := NewPhotoModel(app.Username(res, req))
|
||||||
|
if req.Method == http.MethodPost {
|
||||||
|
message, err := app.AddPhoto(res, req)
|
||||||
|
data.Err = err
|
||||||
|
data.Message = message
|
||||||
|
}
|
||||||
|
app.Templates.ExecuteTemplate(res, "upload.html", data)
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<ul>
|
<ul>
|
||||||
<li><p>Home</p></li>
|
<li><a href="/">Home</a></li>
|
||||||
{{ if .Username }}
|
{{ if .Username }}
|
||||||
<li><p>Hello {{ .Username }}</p></li>
|
<li><p>Hello {{ .Username }}</p></li>
|
||||||
<li style="float: right"><a href="/logout">Logout</a></li>
|
<li style="float: right"><a href="/logout">Logout</a></li>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<input type="password" name="password" placeholder="password...">
|
<input type="password" name="password" placeholder="password...">
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<input type="submit">
|
<input type="submit" value="Login">
|
||||||
<a href="/">Back</a>
|
<a href="/">Back</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user