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
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.
@ -47,6 +47,10 @@ More moderation features.
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 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 dbname = "grosolo"
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,
`lastActiveAt` datetime not null,
`session` varchar(200) DEFAULT '',
`avatar` varchar(20) DEFAULT '',
primary key(`uid`)
);

View File

@ -13,6 +13,8 @@ const hour int = 60 * 60
const day int = hour * 24
const month int = day * 30
const year int = day * 365
const kilobyte int = 1024
const megabyte int = 1024 * 1024
const saltLength int = 32
const sessionLength int = 80
var db *sql.DB
@ -27,6 +29,7 @@ var update_session_stmt *sql.Stmt
var logout_stmt *sql.Stmt
var set_password_stmt *sql.Stmt
var get_password_stmt *sql.Stmt
var set_avatar_stmt *sql.Stmt
var register_stmt *sql.Stmt
var username_exists_stmt *sql.Stmt
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.")
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 {
log.Fatal(err)
}
@ -113,6 +116,12 @@ func init_database(err error) {
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
// create_account_stmt, err = db.Prepare("INSERT INTO
@ -122,7 +131,7 @@ func init_database(err error) {
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` = ?")
if err != nil {
log.Fatal(err)
@ -140,8 +149,10 @@ func main(){
}
// In a directory to stop it clashing with the other paths
fs := http.FileServer(http.Dir("./public"))
http.Handle("/static/", http.StripPrefix("/static/",fs))
fs_p := http.FileServer(http.Dir("./public"))
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("/topics/create/", route_topic_create)
@ -170,6 +181,8 @@ func main(){
//http.HandleFunc("/user/edit/", route_logout)
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/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/ban/", route_logout)
http.HandleFunc("/", route_topics)

View File

@ -59,6 +59,31 @@ li:not(:last-child)
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
{
width: 99%;
@ -69,29 +94,59 @@ li:not(:last-child)
font-weight: bold;
text-transform: uppercase;
}
.rowitem.passive
{
font-weight: normal;
text-transform: none;
}
.rowitem:not(:last-child)/*:not(:only-child)*/
{
border-bottom: 1px dotted #ccc;
}
.rowitem a
{
text-decoration: none;
color: black;
}
.rowitem a:hover
{
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
{
/*height: 40px;*/

View File

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

View File

@ -4,7 +4,11 @@ import "log"
import "fmt"
import "strconv"
import "bytes"
import "regexp"
import "strings"
import "time"
import "io"
import "os"
import "net/http"
import "html"
import "database/sql"
@ -110,6 +114,8 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){
replyCreatedAt string
replyLastEdit int
replyLastEditBy int
replyAvatar string
replyHasAvatar bool
is_closed bool
sticky bool
@ -160,7 +166,7 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){
// Get the replies..
//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 {
InternalError(err,w,r,user)
return
@ -168,12 +174,22 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){
defer rows.Close()
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 {
InternalError(err,w,r,user)
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++
}
err = rows.Err()
@ -516,6 +532,114 @@ func route_account_own_edit_critical_submit(w http.ResponseWriter, r *http.Reque
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) {
user := SessionCheck(w,r)
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

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" . }}
<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>
<div class="rowblock">
<div class="colblock_right">
<form action="/user/edit/critical/submit/" method="post">
<div class="formrow">
<div class="formitem"><a>Current Password</a></div>

View File

@ -1,8 +1,16 @@
{{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>
<div class="rowblock">
<div class="colblock_right">
<form action="/user/edit/critical/submit/" method="post">
<div class="formrow">
<div class="formitem"><a>Current Password</a></div>

View File

@ -17,21 +17,22 @@
</form>
</div>
<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>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{index .Something "content"}}</textarea>
<br /><br />
<a class="topic_status" style="padding-left: 0px;margin-left: 0px;">{{index .Something "createdByName"}}<a/>
</div>
</div><br />
<div class="rowblock">
{{range $index, $element := .ItemList}}<div class="rowitem passive deletable_block editable_parent">
<div class="rowblock" style="overflow: hidden;">
{{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>
<br /><br />
<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/delete/submit/{{$element.ID}}/"><button class="topic_status delete_item">Delete</button></a>
</div>{{end}}
</div>{{ end }}
</div>
<div class="rowblock">
<form action="/reply/create/" method="post">

View File

@ -15,6 +15,8 @@ type User struct
Is_Super_Admin bool
Session string
Loggedin bool
Avatar string
HasAvatar bool
}
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) {
user := User{0,"",0,false,false,"",false}
user := User{0,"",0,false,false,"",false,"",false}
var err error
var cookie *http.Cookie
@ -61,7 +63,7 @@ func SessionCheck(w http.ResponseWriter, r *http.Request) (User) {
log.Print("Session: " + user.Session)
// 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 {
log.Print("Couldn't find the user session")
return user
@ -70,6 +72,12 @@ func SessionCheck(w http.ResponseWriter, r *http.Request) (User) {
return user
}
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
log.Print("Logged in")
log.Print("ID: " + strconv.Itoa(user.ID))