Added the ability to upload and use avatars.

This commit is contained in:
Azareal 2016-12-02 15:03:31 +00:00
parent 7a1a468f93
commit 9bafab7a12
15 changed files with 309 additions and 24 deletions

View File

@ -33,7 +33,7 @@ Set the password column of your user account in the database to what you want yo
# Run the program # Run the program
go run errors.go main.go pages.go post.go routes.go topic.go user.go utils.go config.go go run errors.go main.go pages.go reply.go routes.go topic.go user.go utils.go config.go
Alternatively, you could run the run.bat batch file on Windows. Alternatively, you could run the run.bat batch file on Windows.
@ -47,6 +47,10 @@ More moderation features.
Fix the bug where errors are sent off in raw HTML rather than formatted HTML. Fix the bug where errors are sent off in raw HTML rather than formatted HTML.
Fix the custom pages.
Add emails as a requirement for registration and add a simple anti-spam measure.
Add an alert system. Add an alert system.
Add a report feature. Add a report feature.

BIN
ren.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -6,3 +6,6 @@ var dbuser = "root"
var dbpassword = "password" var dbpassword = "password"
var dbname = "grosolo" var dbname = "grosolo"
var dbport = "3306" // You probably won't need to change this var dbport = "3306" // You probably won't need to change this
// Limiters
var max_request_size = 5 * megabyte

View File

@ -10,6 +10,7 @@ CREATE TABLE `users`(
`createdAt` datetime not null, `createdAt` datetime not null,
`lastActiveAt` datetime not null, `lastActiveAt` datetime not null,
`session` varchar(200) DEFAULT '', `session` varchar(200) DEFAULT '',
`avatar` varchar(20) DEFAULT '',
primary key(`uid`) primary key(`uid`)
); );

View File

@ -13,6 +13,8 @@ const hour int = 60 * 60
const day int = hour * 24 const day int = hour * 24
const month int = day * 30 const month int = day * 30
const year int = day * 365 const year int = day * 365
const kilobyte int = 1024
const megabyte int = 1024 * 1024
const saltLength int = 32 const saltLength int = 32
const sessionLength int = 80 const sessionLength int = 80
var db *sql.DB var db *sql.DB
@ -27,6 +29,7 @@ var update_session_stmt *sql.Stmt
var logout_stmt *sql.Stmt var logout_stmt *sql.Stmt
var set_password_stmt *sql.Stmt var set_password_stmt *sql.Stmt
var get_password_stmt *sql.Stmt var get_password_stmt *sql.Stmt
var set_avatar_stmt *sql.Stmt
var register_stmt *sql.Stmt var register_stmt *sql.Stmt
var username_exists_stmt *sql.Stmt var username_exists_stmt *sql.Stmt
var custom_pages map[string]string = make(map[string]string) var custom_pages map[string]string = make(map[string]string)
@ -48,7 +51,7 @@ func init_database(err error) {
} }
log.Print("Preparing get_session statement.") log.Print("Preparing get_session statement.")
get_session_stmt, err = db.Prepare("SELECT `uid`, `name`, `group`, `is_super_admin`, `session` FROM `users` WHERE `uid` = ? AND `session` = ? AND `session` <> ''") get_session_stmt, err = db.Prepare("SELECT `uid`, `name`, `group`, `is_super_admin`, `session`, `avatar` FROM `users` WHERE `uid` = ? AND `session` = ? AND `session` <> ''")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -113,6 +116,12 @@ func init_database(err error) {
log.Fatal(err) log.Fatal(err)
} }
log.Print("Preparing set_avatar statement.")
set_avatar_stmt, err = db.Prepare("UPDATE users SET avatar = ? WHERE uid = ?")
if err != nil {
log.Fatal(err)
}
// Add an admin version of register_stmt with more flexibility // Add an admin version of register_stmt with more flexibility
// create_account_stmt, err = db.Prepare("INSERT INTO // create_account_stmt, err = db.Prepare("INSERT INTO
@ -122,7 +131,7 @@ func init_database(err error) {
log.Fatal(err) log.Fatal(err)
} }
log.Print("Preparing get_session statement.") log.Print("Preparing username_exists statement.")
username_exists_stmt, err = db.Prepare("SELECT `name` FROM `users` WHERE `name` = ?") username_exists_stmt, err = db.Prepare("SELECT `name` FROM `users` WHERE `name` = ?")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -140,8 +149,10 @@ func main(){
} }
// In a directory to stop it clashing with the other paths // In a directory to stop it clashing with the other paths
fs := http.FileServer(http.Dir("./public")) fs_p := http.FileServer(http.Dir("./public"))
http.Handle("/static/", http.StripPrefix("/static/",fs)) http.Handle("/static/", http.StripPrefix("/static/",fs_p))
fs_u := http.FileServer(http.Dir("./uploads"))
http.Handle("/uploads/", http.StripPrefix("/uploads/",fs_u))
http.HandleFunc("/overview/", route_overview) http.HandleFunc("/overview/", route_overview)
http.HandleFunc("/topics/create/", route_topic_create) http.HandleFunc("/topics/create/", route_topic_create)
@ -170,6 +181,8 @@ func main(){
//http.HandleFunc("/user/edit/", route_logout) //http.HandleFunc("/user/edit/", route_logout)
http.HandleFunc("/user/edit/critical/", route_account_own_edit_critical) // Password & Email http.HandleFunc("/user/edit/critical/", route_account_own_edit_critical) // Password & Email
http.HandleFunc("/user/edit/critical/submit/", route_account_own_edit_critical_submit) http.HandleFunc("/user/edit/critical/submit/", route_account_own_edit_critical_submit)
http.HandleFunc("/user/edit/avatar/", route_account_own_edit_avatar) // Password & Email
http.HandleFunc("/user/edit/avatar/submit/", route_account_own_edit_avatar_submit)
//http.HandleFunc("/user/:id/edit/", route_logout) //http.HandleFunc("/user/:id/edit/", route_logout)
//http.HandleFunc("/user/:id/ban/", route_logout) //http.HandleFunc("/user/:id/ban/", route_logout)
http.HandleFunc("/", route_topics) http.HandleFunc("/", route_topics)

View File

@ -59,6 +59,31 @@ li:not(:last-child)
display: none; display: none;
} }
.colblock_left
{
border: 1px solid #ccc;
padding: 0px;
padding-top: 0px;
width: 30%;
float: left;
}
.colblock_right
{
border: 1px solid #ccc;
padding: 0px;
padding-top: 0px;
width: 65%;
overflow: hidden;
}
.colblock_left:empty
{
display: none;
}
.colblock_right:empty
{
display: none;
}
.rowitem .rowitem
{ {
width: 99%; width: 99%;
@ -69,29 +94,59 @@ li:not(:last-child)
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
} }
.rowitem.passive .rowitem.passive
{ {
font-weight: normal; font-weight: normal;
text-transform: none; text-transform: none;
} }
.rowitem:not(:last-child)/*:not(:only-child)*/ .rowitem:not(:last-child)/*:not(:only-child)*/
{ {
border-bottom: 1px dotted #ccc; border-bottom: 1px dotted #ccc;
} }
.rowitem a .rowitem a
{ {
text-decoration: none; text-decoration: none;
color: black; color: black;
} }
.rowitem a:hover .rowitem a:hover
{ {
color: silver; color: silver;
} }
.col_left
{
width: 30%;
float: left;
}
.col_right
{
width: 69%;
overflow: hidden;
}
.colitem
{
padding-left: 8px;
padding-right: 8px;
padding-top: 17px;
padding-bottom: 12px;
font-weight: bold;
text-transform: uppercase;
}
.colitem.passive
{
font-weight: normal;
text-transform: none;
}
.colitem a
{
text-decoration: none;
color: black;
}
.colitem a:hover
{
color: silver;
}
.formrow .formrow
{ {
/*height: 40px;*/ /*height: 40px;*/

View File

@ -10,5 +10,6 @@ type Reply struct
CreatedAt string CreatedAt string
LastEdit int LastEdit int
LastEditBy int LastEditBy int
Avatar string
HasAvatar bool
} }

View File

@ -4,7 +4,11 @@ import "log"
import "fmt" import "fmt"
import "strconv" import "strconv"
import "bytes" import "bytes"
import "regexp"
import "strings"
import "time" import "time"
import "io"
import "os"
import "net/http" import "net/http"
import "html" import "html"
import "database/sql" import "database/sql"
@ -110,6 +114,8 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){
replyCreatedAt string replyCreatedAt string
replyLastEdit int replyLastEdit int
replyLastEditBy int replyLastEditBy int
replyAvatar string
replyHasAvatar bool
is_closed bool is_closed bool
sticky bool sticky bool
@ -160,7 +166,7 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){
// Get the replies.. // Get the replies..
//rows, err := db.Query("select rid, content, createdBy, createdAt from replies where tid = ?", tid) //rows, err := db.Query("select rid, content, createdBy, createdAt from replies where tid = ?", tid)
rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.name from replies left join users ON replies.createdBy = users.uid where tid = ?", tid) rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name from replies left join users ON replies.createdBy = users.uid where tid = ?", tid)
if err != nil { if err != nil {
InternalError(err,w,r,user) InternalError(err,w,r,user)
return return
@ -168,12 +174,22 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
err := rows.Scan(&rid, &replyContent, &replyCreatedBy, &replyCreatedAt, &replyLastEdit, &replyLastEditBy, &replyCreatedByName) err := rows.Scan(&rid, &replyContent, &replyCreatedBy, &replyCreatedAt, &replyLastEdit, &replyLastEditBy, &replyAvatar, &replyCreatedByName)
if err != nil { if err != nil {
InternalError(err,w,r,user) InternalError(err,w,r,user)
return return
} }
replyList[currentID] = Reply{rid,tid,replyContent,replyCreatedBy,replyCreatedByName,replyCreatedAt,replyLastEdit,replyLastEditBy}
if replyAvatar != "" {
replyHasAvatar = true
if replyAvatar[0] == '.' {
replyAvatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + replyAvatar
}
} else {
replyHasAvatar = false
}
replyList[currentID] = Reply{rid,tid,replyContent,replyCreatedBy,replyCreatedByName,replyCreatedAt,replyLastEdit,replyLastEditBy,replyAvatar,replyHasAvatar}
currentID++ currentID++
} }
err = rows.Err() err = rows.Err()
@ -515,7 +531,115 @@ func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Reque
pi := Page{"Edit Password","account-own-edit-success",user,tList,0} pi := Page{"Edit Password","account-own-edit-success",user,tList,0}
templates.ExecuteTemplate(w,"account-own-edit-success.html", pi) templates.ExecuteTemplate(w,"account-own-edit-success.html", pi)
} }
func route_account_own_edit_avatar(w http.ResponseWriter, r *http.Request) {
user := SessionCheck(w,r)
if !user.Loggedin {
errmsg := "You need to login to edit your own account."
pi := Page{"Error","error",user,tList,errmsg}
var b bytes.Buffer
templates.ExecuteTemplate(&b,"error.html", pi)
errpage := b.String()
http.Error(w,errpage,500)
return
}
pi := Page{"Edit Avatar","account-own-edit-avatar",user,tList,0}
templates.ExecuteTemplate(w,"account-own-edit-avatar.html", pi)
}
func route_account_own_edit_avatar_submit(w http.ResponseWriter, r *http.Request) {
if r.ContentLength > int64(max_request_size) {
http.Error(w, "request too large", http.StatusExpectationFailed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, int64(max_request_size))
user := SessionCheck(w,r)
if !user.Loggedin {
errmsg := "You need to login to edit your own account."
pi := Page{"Error","error",user,tList,errmsg}
var b bytes.Buffer
templates.ExecuteTemplate(&b,"error.html", pi)
errpage := b.String()
http.Error(w,errpage,500)
return
}
err := r.ParseMultipartForm(int64(max_request_size))
if err != nil {
LocalError("Upload failed", w, r, user)
return
}
var filename string = ""
var ext string
for _, fheaders := range r.MultipartForm.File {
for _, hdr := range fheaders {
infile, err := hdr.Open();
if err != nil {
LocalError("Upload failed", w, r, user)
return
}
defer infile.Close()
// We don't want multiple files
if filename != "" {
if filename != hdr.Filename {
os.Remove("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext)
LocalError("You may only upload one avatar", w, r, user)
return
}
} else {
filename = hdr.Filename
}
if ext == "" {
extarr := strings.Split(hdr.Filename,".")
if len(extarr) < 2 {
LocalError("Bad file", w, r, user)
return
}
ext = extarr[len(extarr) - 1]
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
LocalError("Bad file extension", w, r, user)
return
}
ext = reg.ReplaceAllString(ext,"")
ext = strings.ToLower(ext)
}
outfile, err := os.Create("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext);
if err != nil {
LocalError("Upload failed [File Creation Failed]", w, r, user)
return
}
defer outfile.Close()
_, err = io.Copy(outfile, infile);
if err != nil {
LocalError("Upload failed [Copy Failed]", w, r, user)
return
}
}
}
_, err = set_avatar_stmt.Exec("." + ext, strconv.Itoa(user.ID))
if err != nil {
InternalError(err,w,r,user)
return
}
user.HasAvatar = true
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext
pi := Page{"Edit Avatar","account-own-edit-avatar-success",user,tList,0}
templates.ExecuteTemplate(w,"account-own-edit-avatar-success.html", pi)
}
func route_logout(w http.ResponseWriter, r *http.Request) { func route_logout(w http.ResponseWriter, r *http.Request) {
user := SessionCheck(w,r) user := SessionCheck(w,r)
if !user.Loggedin { if !user.Loggedin {

View File

@ -1,2 +1,2 @@
go run errors.go main.go pages.go post.go routes.go topic.go user.go utils.go config.go go run errors.go main.go pages.go reply.go routes.go topic.go user.go utils.go config.go
pause pause

View File

@ -0,0 +1,30 @@
{{template "header.html" . }}
<div class="alert_success">Your data was successfully updated</div>
<div class="colblock_left">
<div class="rowitem"><a>My Account</a></div>
<div class="rowitem passive"><a href="/user/edit/critical/">Edit Password</a></div>
<div class="rowitem passive"><a href="/user/edit/avatar/">Edit Avatar</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
</div>
<div class="colblock_right">
<div class="rowitem"><a>Edit Avatar</a></div>
</div>
{{ if .CurrentUser.HasAvatar }}
<div class="colblock_right">
<div class="rowitem"><img src="{{.CurrentUser.Avatar}}" height="128px" max-width="128px" /></div>
</div>
{{end}}
<div class="colblock_right">
<form action="/user/edit/avatar/submit/" method="post" enctype="multipart/form-data">
<div class="formrow">
<div class="formitem"><a>Upload Avatar</a></div>
<div class="formitem"><input name="account-avatar" type="file" /></div>
</div>
<div class="formrow">
<div class="formitem"><button name="account-button" class="formbutton">Update</div></div>
</div>
</form>
</div>
{{template "footer.html" . }}

View File

@ -0,0 +1,29 @@
{{template "header.html" . }}
<div class="colblock_left">
<div class="rowitem"><a>My Account</a></div>
<div class="rowitem passive"><a href="/user/edit/critical/">Edit Password</a></div>
<div class="rowitem passive"><a href="/user/edit/avatar/">Edit Avatar</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
</div>
<div class="colblock_right">
<div class="rowitem"><a>Edit Avatar</a></div>
</div>
{{ if .CurrentUser.HasAvatar }}
<div class="colblock_right">
<div class="rowitem"><img src="{{.CurrentUser.Avatar}}" height="128px" max-width="128px" /></div>
</div>
{{end}}
<div class="colblock_right">
<form action="/user/edit/avatar/submit/" method="post" enctype="multipart/form-data">
<div class="formrow">
<div class="formitem"><a>Upload Avatar</a></div>
<div class="formitem"><input name="account-avatar" type="file" /></div>
</div>
<div class="formrow">
<div class="formitem"><button name="account-button" class="formbutton">Update</div></div>
</div>
</form>
</div>
{{template "footer.html" . }}

View File

@ -1,9 +1,17 @@
{{template "header.html" . }} {{template "header.html" . }}
<div class="alert_success">Your data was successfully updated</div> <div class="alert_success">Your data was successfully updated</div>
<div class="rowblock"> <div class="colblock_left">
<div class="rowitem"><a>My Account</a></div>
<div class="rowitem passive"><a href="/user/edit/critical/">Edit Password</a></div>
<div class="rowitem passive"><a href="/user/edit/avatar/">Edit Avatar</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
</div>
<div class="colblock_right">
<div class="rowitem"><a>Edit Password</a></div> <div class="rowitem"><a>Edit Password</a></div>
</div> </div>
<div class="rowblock"> <div class="colblock_right">
<form action="/user/edit/critical/submit/" method="post"> <form action="/user/edit/critical/submit/" method="post">
<div class="formrow"> <div class="formrow">
<div class="formitem"><a>Current Password</a></div> <div class="formitem"><a>Current Password</a></div>

View File

@ -1,8 +1,16 @@
{{template "header.html" . }} {{template "header.html" . }}
<div class="rowblock"> <div class="colblock_left">
<div class="rowitem"><a>My Account</a></div>
<div class="rowitem passive"><a href="/user/edit/critical/">Edit Password</a></div>
<div class="rowitem passive"><a href="/user/edit/avatar/">Edit Avatar</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
</div>
<div class="colblock_right">
<div class="rowitem"><a>Edit Password</a></div> <div class="rowitem"><a>Edit Password</a></div>
</div> </div>
<div class="rowblock"> <div class="colblock_right">
<form action="/user/edit/critical/submit/" method="post"> <form action="/user/edit/critical/submit/" method="post">
<div class="formrow"> <div class="formrow">
<div class="formitem"><a>Current Password</a></div> <div class="formitem"><a>Current Password</a></div>

View File

@ -17,21 +17,22 @@
</form> </form>
</div> </div>
<div class="rowblock"> <div class="rowblock">
<div class="rowitem passive editable_parent" style="border-bottom: none;"> <div class="rowitem passive editable_parent" style="border-bottom: none;{{ if .CurrentUser.HasAvatar }}background-image: url({{ .CurrentUser.Avatar }});background-position: left;background-repeat: no-repeat;background-size: 128px;padding-left: 136px;{{end}}">
<span class="hide_on_edit topic_content">{{index .Something "content"}}</span> <span class="hide_on_edit topic_content">{{index .Something "content"}}</span>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{index .Something "content"}}</textarea> <textarea name="topic_content" class="show_on_edit topic_content_input">{{index .Something "content"}}</textarea>
<br /><br /> <br /><br />
<a class="topic_status" style="padding-left: 0px;margin-left: 0px;">{{index .Something "createdByName"}}<a/> <a class="topic_status" style="padding-left: 0px;margin-left: 0px;">{{index .Something "createdByName"}}<a/>
</div> </div>
</div><br /> </div><br />
<div class="rowblock"> <div class="rowblock" style="overflow: hidden;">
{{range $index, $element := .ItemList}}<div class="rowitem passive deletable_block editable_parent"> {{range $index, $element := .ItemList}}
<div class="rowitem passive deletable_block editable_parent" style="{{ if $element.HasAvatar }}background-image: url({{ $element.Avatar }});background-position: left;background-repeat: no-repeat;background-size: 128px;padding-left: 136px;{{end}}">
<span class="editable_block">{{$element.Content}}</span> <span class="editable_block">{{$element.Content}}</span>
<br /><br /> <br /><br />
<a class="topic_status" style="padding-left: 0px;margin-left: 0px;">{{$element.CreatedByName}}<a/> <a class="topic_status" style="padding-left: 0px;margin-left: 0px;">{{$element.CreatedByName}}<a/>
<a href="/reply/edit/submit/{{$element.ID}}/"><button class="topic_status edit_item">Edit</button></a> <a href="/reply/edit/submit/{{$element.ID}}/"><button class="topic_status edit_item">Edit</button></a>
<a href="/reply/delete/submit/{{$element.ID}}/"><button class="topic_status delete_item">Delete</button></a> <a href="/reply/delete/submit/{{$element.ID}}/"><button class="topic_status delete_item">Delete</button></a>
</div>{{end}} </div>{{ end }}
</div> </div>
<div class="rowblock"> <div class="rowblock">
<form action="/reply/create/" method="post"> <form action="/reply/create/" method="post">

View File

@ -15,6 +15,8 @@ type User struct
Is_Super_Admin bool Is_Super_Admin bool
Session string Session string
Loggedin bool Loggedin bool
Avatar string
HasAvatar bool
} }
func SetPassword(uid int, password string) (error) { func SetPassword(uid int, password string) (error) {
@ -37,7 +39,7 @@ func SetPassword(uid int, password string) (error) {
} }
func SessionCheck(w http.ResponseWriter, r *http.Request) (User) { func SessionCheck(w http.ResponseWriter, r *http.Request) (User) {
user := User{0,"",0,false,false,"",false} user := User{0,"",0,false,false,"",false,"",false}
var err error var err error
var cookie *http.Cookie var cookie *http.Cookie
@ -61,7 +63,7 @@ func SessionCheck(w http.ResponseWriter, r *http.Request) (User) {
log.Print("Session: " + user.Session) log.Print("Session: " + user.Session)
// Is this session valid..? // Is this session valid..?
err = get_session_stmt.QueryRow(user.ID,user.Session).Scan(&user.ID, &user.Name, &user.Group, &user.Is_Super_Admin, &user.Session) err = get_session_stmt.QueryRow(user.ID,user.Session).Scan(&user.ID, &user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Avatar)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
log.Print("Couldn't find the user session") log.Print("Couldn't find the user session")
return user return user
@ -70,6 +72,12 @@ func SessionCheck(w http.ResponseWriter, r *http.Request) (User) {
return user return user
} }
user.Is_Admin = user.Is_Super_Admin user.Is_Admin = user.Is_Super_Admin
if user.Avatar != "" {
user.HasAvatar = true
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
}
user.Loggedin = true user.Loggedin = true
log.Print("Logged in") log.Print("Logged in")
log.Print("ID: " + strconv.Itoa(user.ID)) log.Print("ID: " + strconv.Itoa(user.ID))