├── dev.sh ├── screenshot1.png ├── screenshot2.png ├── public ├── img │ ├── me.jpg │ ├── mint.png │ └── sky.jpeg ├── materialize │ └── font │ │ ├── roboto │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Light.woff │ │ ├── Roboto-Light.woff2 │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Thin.woff │ │ ├── Roboto-Thin.woff2 │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-Regular.woff │ │ └── Roboto-Regular.woff2 │ │ └── material-design-icons │ │ ├── Material-Design-Icons.eot │ │ ├── Material-Design-Icons.ttf │ │ ├── Material-Design-Icons.woff │ │ ├── Material-Design-Icons.woff2 │ │ └── LICENSE.txt ├── js │ ├── aurora.js │ ├── t.min.js │ ├── profile.js │ ├── golem.js │ └── msg.js └── css │ └── aurora.css ├── doc.go ├── templates ├── 403.html ├── 404.html ├── home.html ├── snippets │ ├── profile_pic_upload.html │ ├── people.html │ ├── pitch.html │ ├── home.html │ ├── uploads.html │ ├── profile_update_form.html │ └── profile_info.html ├── base │ ├── footer.html │ └── head.html ├── profile │ ├── update.html │ └── home.html └── auth │ ├── login.html │ └── register.html ├── config ├── build.json └── app.json ├── cmd └── aurora │ └── aurora.go ├── profile.go ├── utils_test.go ├── models_test.go ├── LICENCE ├── auth.go ├── ZANZIBAR ├── profile_test.go ├── docs └── getting-started.md ├── README.md ├── models.go ├── .gitignore ├── auth_test.go ├── session_test.go ├── session.go ├── forms_test.go ├── utils.go ├── msg_test.go ├── forms.go ├── bin └── build.go ├── uploads.go ├── uploads_test.go ├── msg.go ├── i18n.go ├── remix.go └── remix_test.go /dev.sh: -------------------------------------------------------------------------------- 1 | go run bin/build.go -v 2 | 3 | ./builds/0.0.1/aurora 4 | -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/screenshot2.png -------------------------------------------------------------------------------- /public/img/me.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/img/me.jpg -------------------------------------------------------------------------------- /public/img/mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/img/mint.png -------------------------------------------------------------------------------- /public/img/sky.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/img/sky.jpeg -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package aurora is a minimalistic social network library. 3 | */ 4 | package aurora 5 | -------------------------------------------------------------------------------- /templates/403.html: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |

forbidden

3 | {{template "base/footer" .}} 4 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |

shit not found

3 | {{template "base/footer" .}} 4 | -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Bold.woff -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Light.woff -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Light.woff2 -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Medium.woff -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Thin.woff -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Thin.woff2 -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Medium.woff2 -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Regular.woff -------------------------------------------------------------------------------- /public/materialize/font/roboto/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/roboto/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /public/materialize/font/material-design-icons/Material-Design-Icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/material-design-icons/Material-Design-Icons.eot -------------------------------------------------------------------------------- /public/materialize/font/material-design-icons/Material-Design-Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/material-design-icons/Material-Design-Icons.ttf -------------------------------------------------------------------------------- /public/materialize/font/material-design-icons/Material-Design-Icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/material-design-icons/Material-Design-Icons.woff -------------------------------------------------------------------------------- /public/materialize/font/material-design-icons/Material-Design-Icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/aurora/HEAD/public/materialize/font/material-design-icons/Material-Design-Icons.woff2 -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 | {{if .InSession}} 3 | {{template "snippets/home" .}} 4 | {{else}} 5 | {{template "snippets/pitch" .}} 6 | {{end}} 7 | {{template "base/footer" .}} 8 | -------------------------------------------------------------------------------- /config/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"aurora", 3 | "version":"0.0.1", 4 | "public":"public", 5 | "templates":"templates", 6 | "dest":"builds", 7 | "src":"cmd/aurora", 8 | "config":"config", 9 | "database_dir":"db" 10 | } 11 | -------------------------------------------------------------------------------- /public/js/aurora.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by gernest on 5/4/15. 3 | */ 4 | 5 | $(document).ready(function(){ 6 | // paralax 7 | $('.parallax').parallax(); 8 | $('.collapsible').collapsible({ 9 | accordion : false 10 | }); 11 | $('.materialboxed').materialbox(); 12 | $(".button-collapse").sideNav(); 13 | }); -------------------------------------------------------------------------------- /templates/snippets/profile_pic_upload.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/base/footer.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /cmd/aurora/aurora.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/gernest/aurora" 10 | ) 11 | 12 | func main() { 13 | d, err := ioutil.ReadFile("config/app.json") 14 | if err != nil { 15 | panic(err) 16 | } 17 | cfg := &aurora.RemixConfig{} 18 | err = json.Unmarshal(d, cfg) 19 | if err != nil { 20 | panic(err) 21 | } 22 | rx := aurora.NewRemix(cfg) 23 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./public")))) 24 | http.Handle("/", rx.Routes()) 25 | log.Println("starting server ar port 8080...") 26 | log.Fatal(http.ListenAndServe(":8080", nil)) 27 | } 28 | -------------------------------------------------------------------------------- /public/js/t.min.js: -------------------------------------------------------------------------------- 1 | (function(){function c(a){this.t=a}function l(a,b){for(var e=b.split(".");e.length;){if(!(e[0]in a))return!1;a=a[e.shift()]}return a}function d(a,b){return a.replace(h,function(e,a,i,f,c,h,k,m){var f=l(b,f),j="",g;if(!f)return"!"==i?d(c,b):k?d(m,b):"";if(!i)return d(h,b);if("@"==i){e=b._key;a=b._val;for(g in f)f.hasOwnProperty(g)&&(b._key=g,b._val=f[g],j+=d(c,b));b._key=e;b._val=a;return j}}).replace(k,function(a,c,d){return(a=l(b,d))||0===a?"%"==c?(new Option(a)).innerHTML.replace(/"/g,"""): 2 | a:""})}var h=/\{\{(([@!]?)(.+?))\}\}(([\s\S]+?)(\{\{:\1\}\}([\s\S]+?))?)\{\{\/\1\}\}/g,k=/\{\{([=%])(.+?)\}\}/g;c.prototype.render=function(a){return d(this.t,a)};window.t=c})(); -------------------------------------------------------------------------------- /profile.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import "github.com/gernest/nutz" 4 | 5 | // CreateProfile creates a new profile using Profile.ID as the jey 6 | func CreateProfile(db nutz.Storage, p *Profile, bucket string, nest ...string) error { 7 | return createIfNotexist(db, p, bucket, p.ID) 8 | } 9 | 10 | // GetProfile retrives a profile with a given id 11 | func GetProfile(db nutz.Storage, bucket, id string, nest ...string) (*Profile, error) { 12 | p := &Profile{} 13 | err := getAndUnmarshall(db, bucket, id, p) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return p, err 18 | } 19 | 20 | // UpdateProfile updates a given profile 21 | func UpdateProfile(db nutz.Storage, p *Profile, bucket string, nest ...string) error { 22 | return marshalAndUpdate(db, p, bucket, p.ID) 23 | } 24 | -------------------------------------------------------------------------------- /config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"aurora", 3 | "url":"", 4 | "cdn_mode":false, 5 | "run_mode":"dev", 6 | "dev_mode":true, 7 | "title":"Aurora: The minimalistic social network", 8 | "description":"A simple social networking app", 9 | "database_dir":"db", 10 | "accounts_bucket":"accounts", 11 | "accounts_database":"db/accounts.bdb", 12 | "database_extension":".bdb", 13 | "profiles_bucket":"profiles", 14 | "sessions_name":"_aurora", 15 | "sessions_database":"db/sessions.bdb", 16 | "sessions_bucket":"sessions", 17 | "sessions_max_age":604800, 18 | "session_path":"/", 19 | "login_redirect":"/", 20 | "profile_pic_field":"profile", 21 | "photos_field":"photos", 22 | "messages_bucket":"messages", 23 | "templates_extensions":[".html",".tpl",".tmpl"], 24 | "templates_dir":"templates" 25 | } 26 | -------------------------------------------------------------------------------- /templates/snippets/people.html: -------------------------------------------------------------------------------- 1 | {{ $gobal:=.}} 2 | {{ with .people }} 3 | {{range .}} 4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ .FirstName }} 11 | {{ .LastName }} 12 |
13 |
14 | 20 |
21 |
22 |
23 |
24 | {{end}} 25 | {{end}} -------------------------------------------------------------------------------- /templates/profile/update.html: -------------------------------------------------------------------------------- 1 | {{if .error}} 2 |
3 |
4 |
5 | {{.error}} 6 |
7 |
8 |
9 | {{end}} 10 | {{if .success}} 11 |
12 |
13 |
14 | Profile was updated successful 15 |

Hey, please you should know that by filling all the fields 16 | provided you help us make good suggestions on people you want 17 | to meet. So dont be to clever and skip some for your own sanity 18 |

19 |
20 | 24 |
25 |
26 | {{end}} -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestFlash(t *testing.T) { 9 | var ( 10 | flash = NewFlash() 11 | success = "success" 12 | notice = "note" 13 | err = "error" 14 | ) 15 | 16 | defer clenUp(t) 17 | flash.Success(success) 18 | flash.Notice(notice) 19 | flash.Error(err) 20 | d := flash.Data 21 | if d["FlashNotice"].(string) != notice { 22 | t.Errorf("Expected %s got %s", notice, d["FlashNotice"]) 23 | } 24 | if d["FlashSuccess"].(string) != success { 25 | t.Errorf("Expected %s got %s", notice, d["FlashSuccess"]) 26 | } 27 | if d["FlashError"].(string) != err { 28 | t.Errorf("Expected %s got %s", err, d["FlashError"]) 29 | } 30 | } 31 | 32 | // deletes test database files 33 | func clenUp(t *testing.T) { 34 | ts, _, rx := testServer(t) 35 | defer ts.Close() 36 | err := os.RemoveAll(rx.cfg.DBDir) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /models_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestProfile_MyBirthDay(t *testing.T) { 10 | n := time.Now() 11 | p := &Profile{ 12 | BirthDate: n, 13 | } 14 | if p.MyBirthDay() != n.Format(birthDateFormat) { 15 | t.Errorf("expected %s got %s", n.Format(birthDateFormat), p.MyBirthDay()) 16 | } 17 | } 18 | 19 | func TestProfile_Sex(t *testing.T) { 20 | p := &Profile{} 21 | 22 | if p.Sex() != "" { 23 | t.Errorf("expected a empty string got %s", p.Sex()) 24 | } 25 | p.Gender = male 26 | if strings.ToLower(p.Sex()) != "mwanaume" { 27 | t.Errorf("expected mwanaume got %s", strings.ToLower(p.Sex())) 28 | } 29 | p.Gender = female 30 | if strings.ToLower(p.Sex()) != "mwanamke" { 31 | t.Errorf("expected mwanamke got %s", strings.ToLower(p.Sex())) 32 | } 33 | p.Gender = zombie 34 | if strings.ToLower(p.Sex()) != "undead" { 35 | t.Errorf("expected undead got %s", strings.ToLower(p.Sex())) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 geofrey ernest 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gernest/nutz" 7 | ) 8 | 9 | // CreateAccount creates a new account, where id will be the value returned by 10 | // invoking Email() method. 11 | func CreateAccount(db nutz.Storage, a Account, bucket string) error { 12 | return createIfNotexist(db, a, bucket, a.Email()) 13 | } 14 | 15 | // GetUser retrives a user. 16 | func GetUser(db nutz.Storage, bucket, email string, nest ...string) (*User, error) { 17 | usr := &User{} 18 | err := getAndUnmarshall(db, bucket, email, usr) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return usr, nil 23 | } 24 | 25 | // GetAllUsers returns a slice of all users. 26 | func GetAllUsers(db nutz.Storage, bucket string, nest ...string) ([]string, error) { 27 | var usrs []string 28 | d := db.GetAll(bucket, nest...) 29 | if d.Error != nil { 30 | return nil, d.Error 31 | } 32 | for _, v := range d.DataList { 33 | us := &User{} 34 | err := json.Unmarshal(v, us) 35 | if err != nil { 36 | // log thus 37 | } 38 | if err == nil { 39 | usrs = append(usrs, us.UUID) 40 | } 41 | } 42 | return usrs, nil 43 | } 44 | -------------------------------------------------------------------------------- /templates/snippets/pitch.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
7 |

aurora

8 |
9 |
10 | angalia 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /templates/snippets/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 17 | tafuta 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{ template "snippets/people" .}} 28 |
29 |
30 |
31 |
32 |
-------------------------------------------------------------------------------- /templates/profile/home.html: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |
4 |
5 |
6 |
7 | {{ with .profile.Picture }} 8 | 9 | {{else}} 10 | 11 | {{end}} 12 | {{if .myProfile}} 13 | Badili picha 14 |
15 | {{end}} 16 |
17 |
18 |
19 | {{template "snippets/profile_info" .}} 20 |
21 |
22 | {{if .myProfile}} 23 |
24 |
25 | {{template "snippets/profile_update_form" .}} 26 |
27 |
28 |
29 | {{end}} 30 |
31 |
32 | {{if .myProfile}} 33 | 34 | 35 | {{end}} 36 | {{template "base/footer" .}} 37 | -------------------------------------------------------------------------------- /public/js/profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by gernest on 5/22/15. 3 | */ 4 | 5 | $(document).ready(function(){ 6 | var tmpl=$('#profile-pic-upload'); 7 | var dz=new Dropzone('#my-pic',{ 8 | url: "/uploads", // Set the url 9 | autoQueue: true, 10 | paramName: "profile", 11 | previewTemplate: tmpl.html(), 12 | clickable: "#profile-pic", 13 | addRemoveLinks:true, 14 | thumbnailWidth:120, 15 | thumbnailHeigh:120, 16 | maxFiles:1, 17 | acceptedFiles:"image/*", 18 | previewsContainer: "#pic-preview" 19 | }); 20 | dz.on('complete',function(file){ 21 | dz.removeFile(file); 22 | }); 23 | dz.on('success',function(file,data){ 24 | src='/imgs?'+data.query 25 | $('#profile-picture').attr('src',src); 26 | console.log(data); 27 | }); 28 | var gz =new Dropzone('#gallery-upload',{ 29 | url: "/uploads", // Set the url 30 | autoQueue: true, 31 | paramName: "photos", 32 | previewTemplate: tmpl.html(), 33 | clickable: "#pandisha-kibao", 34 | previewsContainer: ".preview-container" 35 | }); 36 | gz.on('complete',function(file){ 37 | gz.removeAllFiles(true); 38 | }); 39 | gz.on('success',function(file,data){ 40 | console.log(data); 41 | }); 42 | gz.on("error",function(file,msg){ 43 | console.log(msg); 44 | }); 45 | // birth date field 46 | $('#birth-date').pickadate({ 47 | selectYears:true, 48 | selectMonths:true 49 | }); 50 | 51 | // select 52 | $('select').material_select(); 53 | }); -------------------------------------------------------------------------------- /ZANZIBAR: -------------------------------------------------------------------------------- 1 | {{/* builds aurora */}} 2 | 3 | {{/* configurations */}} 4 | {{$name := "aurora"}} 5 | {{$version := "0.0.1"}} 6 | {{$public := "public"}} 7 | {{$templates := "templates"}} 8 | {{$destination:= "builds"}} 9 | {{$config := "config"}} 10 | {{$database := "db"}} 11 | {{$cmd :="cmd/aurora/aurora.go"}} 12 | {{$buildPath := printf "%s/%s" $destination $version}} 13 | {{/* end configuration */}} 14 | 15 | {{printf "building %s it might take a while please wait .." $name}} 16 | {{/* setup */}} 17 | {{/* get all dependencies */}} 18 | {{run "go" "get" "-t"}} 19 | 20 | {{/* remove any previous builds */}} 21 | {{clean $destination}} 22 | {{mkdir $buildPath 0700}} 23 | {{/* end setup */}} 24 | 25 | {{/* test */}} 26 | {{run "go" "test"}} 27 | {{/* end test */}} 28 | 29 | {{/* create binary */}} 30 | {{$bin:=printf "%s/%s" $buildPath $name}} 31 | 32 | {{run "go" "build" "-o" $bin $cmd}} 33 | {{/* end binary*/}} 34 | 35 | {{/* assemble */}} 36 | {{/* prepare database */}} 37 | {{$dbDir:=printf "%s/%s" $buildPath $database}} 38 | {{mkdir $dbDir 0700}} 39 | 40 | {{/* copy configurations */}} 41 | {{$cfg:=printf "%s/%s" $buildPath $config}} 42 | {{file $config}} 43 | {{copy . $cfg|ignore}} 44 | {{end}} 45 | 46 | {{/* copy public files */}} 47 | {{$pub:=printf "%s/%s" $buildPath $public}} 48 | {{file $public}} 49 | {{copy . $pub}} 50 | {{end}} 51 | 52 | {{/* copy templates */}} 53 | {{$tmpl:=printf "%s/%s" $buildPath $templates}} 54 | {{file $templates}} 55 | {{copy . $tmpl}} 56 | {{end}} 57 | {{/* end assemble */}} 58 | {{printf "[SUCCESS] built %s version %s" $name $version}} 59 | -------------------------------------------------------------------------------- /profile_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import "testing" 4 | 5 | var ( 6 | pids = []string{ 7 | "db0668ac-7eba-40dd-56ee-0b1c0b9b415d", 8 | "e6917dfe-b4f6-49b8-5628-83dd2a430e9a", 9 | "bc5288cf-4120-4f3c-5957-b19e093a12f4", 10 | } 11 | ) 12 | 13 | func TestCreateProfile(t *testing.T) { 14 | pBucket := "profiles" 15 | for _, id := range pids { 16 | p := &Profile{ID: id} 17 | err := CreateProfile(db, p, pBucket) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | } 22 | err := CreateProfile(db, &Profile{ID: pids[0]}, pBucket) 23 | if err == nil { 24 | t.Error("Expected an error") 25 | } 26 | } 27 | func TestGetProfile(t *testing.T) { 28 | pBucket := "profiles" 29 | for _, id := range pids { 30 | p, err := GetProfile(db, pBucket, id) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | if p.ID != id { 35 | t.Errorf("Expected %s got %s", id, p.ID) 36 | } 37 | } 38 | p, err := GetProfile(db, pBucket, "bogus") 39 | if err == nil { 40 | t.Error("Expected an error") 41 | } 42 | if p != nil { 43 | t.Errorf("Expected nil, got %v", p) 44 | } 45 | } 46 | func TestUpdateProfile(t *testing.T) { 47 | var ( 48 | city = "mwanza" 49 | country = "Tanzania" 50 | pBucket = "profiles" 51 | ) 52 | 53 | // Make sure the database used is removed. 54 | for _, id := range pids { 55 | p, err := GetProfile(db, pBucket, id) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | p.City = city 60 | p.Country = country 61 | err = UpdateProfile(db, p, pBucket) 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | } 66 | p := &Profile{ID: "bogus", Country: country, City: city} 67 | err := UpdateProfile(db, p, pBucket) 68 | if err == nil { 69 | t.Error("Expected an error got nil instead") 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | 12 | 14 |
15 | {{if .errors.Email}} 16 |
17 |

{{.errors.Email}}

18 |
19 | {{end}} 20 |
21 |
22 |
23 | 25 | 27 |
28 | {{if .errors.Password}} 29 |
30 |

{{.errors.Password}}

31 |
32 | {{end}} 33 |
34 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | {{template "base/footer" .}} 45 | -------------------------------------------------------------------------------- /templates/snippets/uploads.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 9 |
10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |

18 | 19 |
20 |
21 |

22 |
23 |
24 |
25 |
26 |
27 | 31 | 35 | 39 |
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /public/css/aurora.css: -------------------------------------------------------------------------------- 1 | 2 | .pavement { 3 | padding: 14px 0; 4 | margin-top: 5px; 5 | line-height: 36px; 6 | background-color: #5c5757; 7 | color: #e6e6e6; } 8 | 9 | .block{ 10 | width: 100%; 11 | } 12 | .parallax-container { 13 | height:250px; 14 | } 15 | 16 | .chat-box{ 17 | position:fixed; 18 | right:20px; 19 | bottom:0px; 20 | width:250px; 21 | } 22 | .msgs{ 23 | background:white; 24 | max-height:200px; 25 | overflow: scroll; 26 | } 27 | 28 | .chat-head,.msg_head{ 29 | background:#f39c12; 30 | color:white; 31 | padding:15px; 32 | font-weight:bold; 33 | cursor:pointer; 34 | border-radius:5px 5px 0px 0px; 35 | } 36 | 37 | .msg-box{ 38 | position:fixed; 39 | bottom:-5px; 40 | width:250px; 41 | background:white; 42 | border-radius:5px 5px 0px 0px; 43 | } 44 | 45 | .msg-head{ 46 | background:#3498db; 47 | } 48 | 49 | .msg-body{ 50 | background:white; 51 | height:200px; 52 | font-size:12px; 53 | padding:15px; 54 | overflow:auto; 55 | overflow-x: hidden; 56 | } 57 | .msg-input{ 58 | width:100%; 59 | border: 1px solid white; 60 | border-top:1px solid #DDDDDD; 61 | -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ 62 | -moz-box-sizing: border-box; /* Firefox, other Gecko */ 63 | box-sizing: border-box; 64 | } 65 | 66 | .close{ 67 | float:right; 68 | cursor:pointer; 69 | } 70 | .minimize{ 71 | float:right; 72 | cursor:pointer; 73 | padding-right:5px; 74 | 75 | } 76 | 77 | .user{ 78 | position:relative; 79 | padding:10px 30px; 80 | } 81 | .user:hover{ 82 | background:#f8f8f8; 83 | cursor:pointer; 84 | 85 | } 86 | .user:before{ 87 | content:''; 88 | position:absolute; 89 | background:#2ecc71; 90 | height:10px; 91 | width:10px; 92 | left:10px; 93 | top:15px; 94 | border-radius:6px; 95 | } 96 | 97 | .msg-a{ 98 | min-height:10px; 99 | margin-bottom:5px; 100 | border-radius:5px; 101 | } 102 | 103 | .msg-b{ 104 | min-height:10px; 105 | margin-bottom:5px; 106 | border-radius:5px; 107 | } 108 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started with aurora 2 | 3 | ### Overiview 4 | Aurora is a simple yet useful attempt to create a minimalistic social network with Go and 5 | bolt database. 6 | #### Project structure 7 | The project is divided into two parts, a library and an app. The directories in this project 8 | are as follows. 9 | 10 | * bin: 11 | This is wehere the binaries should be, unfortunate you will only find the build script file 12 | `build.go` in this directory. 13 | * builds: 14 | This is where the built project is stored. Aurora needs configuration files and templates to run 15 | so, a built binary together with all the dependency files are copied here. Inside this directory 16 | the build versions are used to identify builds. 17 | * config: 18 | configurations are found here 19 | * cmd: 20 | This is where the aurora commandline application is. 21 | * docs: 22 | Project documentation 23 | * public: 24 | All javascript,css,fonts and images are stored here. 25 | * templates: 26 | templates used by aurora. 27 | 28 | 29 | ## Installation. 30 | You will have to build this project in order to install. Make sure you have a working 31 | golang environment, and a GOPATH. 32 | 33 | get the project 34 | 35 | go get github.com/gernest/aurora 36 | 37 | 38 | cd into the installed library 39 | 40 | cd $GOPATH/github.com/gernest/aurora 41 | 42 | Run the build script(NOTE: I have used the script for linux only, so help is needed to 43 | provide scripts for other platforms.) I am waiting for go 1.5 to provide cross platforms 44 | builds. This will take a while to complete as the script runs the test suite before building. 45 | 46 | go run bin/build.go 47 | 48 | If you see nothing then the build was success. You should see a directory in the builds directory 49 | with the version number e.g `0.0.1`. You can copy this folder anywhere you want or even rename it. 50 | You can start aurora inside this directory like this 51 | 52 | ,/aurora 53 | 54 | A simple single command to help start aurora , you can do like this. 55 | 56 | cd buils/0.0.1&&./aurora # assuming the build is version 0.0.1 57 | 58 | 59 | After running the above command a server is started at port `8080` on localhost. So you 60 | need to point your browser to `localhost:8080` to view the site. 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aurora [![Build Status](https://drone.io/github.com/gernest/aurora/status.png)](https://drone.io/github.com/gernest/aurora/latest)[![Coverage Status](https://coveralls.io/repos/gernest/aurora/badge.svg?branch=master)](https://coveralls.io/r/gernest/aurora?branch=master) [![GoDoc](https://godoc.org/github.com/gernest/aurora?status.svg)](https://godoc.org/github.com/gernest/aurora) 2 | 3 | ### What is aurora? 4 | Aurora is a lightweight social network application written in [Go programming language](http://golang.org/), 5 | and using [bolt database](https://github.com/boltdb/bolt) as its main storage source. 6 | 7 | This is not for production use, I started this project as a way to learn more about the Go 8 | programming language, and also to experiment with testing web services in Go. 9 | 10 | ## Guide 11 | 1. [Getting started](docs/getting-started.md) 12 | - [x] Overview 13 | - [x] Installation 14 | 15 | 16 | ### Alternative way to build 17 | I was digging in the go standard library, and thought I should experiment on the `text/template`. I extended it and made a toy yet working build tool(a.k.a template based build tool). The source code is found here [zanzibar](https://github.com/gernest/zanzibar). 18 | 19 | You can build aurora using zanzibar tool. Install it first. 20 | 21 | go get github.com/gernest/zanzibar 22 | 23 | clone aurora 24 | 25 | git clone https://github.com/gernest/aurora 26 | 27 | cd into aurora and run zanzibar 28 | 29 | cd aurora&&zanzibar 30 | 31 | you should find your build in builds directory. 32 | 33 | Roadmap 34 | ------- 35 | 36 | ## Features 37 | - [x] chat 38 | - [x] profile management 39 | - [x] photo upload 40 | - [x] ??? any ideas? 41 | 42 | 43 | Scrrenshots 44 | ___________ 45 | 46 | ![screenshot1](screenshot1.png) 47 | 48 | ![screenshot2](screenshot2.png) 49 | 50 | ## Contributing 51 | This is a playground, all kinds of contibutions are welcome. Since it is for learning and 52 | experimenting, feel free to think of new ideas. 53 | 54 | The important thing is to test, whatever you add make sure it has tests. Also be clear on what 55 | exactly ypur PR does. 56 | 57 | ## License 58 | 59 | This project is under the MIT License. See the [LICENSE](https://github.com/gernest/aurora/blob/master/LICENCE) file for the full license text. 60 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import "time" 4 | 5 | const ( 6 | male = iota + 1 // 1 7 | female // 2 8 | zombie // 3 9 | ) 10 | 11 | // Account is an interface for a user account 12 | type Account interface { 13 | Email() string 14 | Password() string 15 | } 16 | 17 | // User contains details about a user 18 | type User struct { 19 | UUID string `json:"uuid" gforms:"-"` 20 | FirstName string `json:"first_name" gforms:"first_name"` 21 | LastName string `json:"last_name" gforms:"last_name"` 22 | EmailAddress string `json:"email" gforms:"email_address"` 23 | Pass string `json:"password" gforms:"pass"` 24 | ConfirmPass string `json:"-" gforms:"confirm_pass"` 25 | CreatedAt time.Time `json:"created_at" gforms:"-"` 26 | UpdatedAt time.Time `json:"updated_at" gforms:"-"` 27 | } 28 | 29 | // Email user email address 30 | func (u *User) Email() string { 31 | return u.EmailAddress 32 | } 33 | 34 | // Password user password 35 | func (u *User) Password() string { 36 | return u.Pass 37 | } 38 | 39 | // Profile contains additional information about the user 40 | type Profile struct { 41 | ID string `json:"id" gforms:"-"` 42 | FirstName string `json:"first_name" gforms:"first_name"` 43 | LastName string `json:"last_name" gforms:"last_name"` 44 | Picture *Photo `json:"picture" gforms:"-"` 45 | Age int `json:"age" gforms:"age"` 46 | IsUpdate bool `json:"is_update" gforms:"-"` 47 | BirthDate time.Time `json:"birth_date" gforms:"birth_date"` 48 | Gender int `json:"gender" gforms:"gender"` 49 | Photos []*Photo `json:"photos" gforms:"-"` 50 | City string `json:"city" gforms:"city"` 51 | Country string `json:"country" gforms:"country"` 52 | Street string `json:"street" gforms:"street"` 53 | CreatedAt time.Time `json:"created_at" gforms:"-"` 54 | UpdatedAt time.Time `json:"update_at" gforms:"-"` 55 | } 56 | 57 | func (p *Profile) MyBirthDay() string { 58 | if p.BirthDate.IsZero() { 59 | return time.Now().Format(birthDateFormat) 60 | } 61 | return p.BirthDate.Format(birthDateFormat) 62 | } 63 | 64 | func (p *Profile) Sex() string { 65 | switch p.Gender { 66 | case male: 67 | return "Mwanaume" 68 | case female: 69 | return "Mwanamke" 70 | case zombie: 71 | return "undead" 72 | } 73 | return "" 74 | } 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | 29 | ### Node template 30 | # Logs 31 | logs 32 | *.log 33 | 34 | # Runtime data 35 | pids 36 | *.pid 37 | *.seed 38 | 39 | # Directory for instrumented libs generated by jscoverage/JSCover 40 | lib-cov 41 | 42 | # Coverage directory used by tools like istanbul 43 | coverage 44 | 45 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 46 | .grunt 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (http://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directory 55 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 56 | node_modules 57 | 58 | 59 | ### JetBrains template 60 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 61 | 62 | *.iml 63 | 64 | ## Directory-based project format: 65 | .idea/ 66 | # if you remove the above rule, at least ignore the following: 67 | 68 | # User-specific stuff: 69 | # .idea/workspace.xml 70 | # .idea/tasks.xml 71 | # .idea/dictionaries 72 | 73 | # Sensitive or high-churn files: 74 | # .idea/dataSources.ids 75 | # .idea/dataSources.xml 76 | # .idea/sqlDataSources.xml 77 | # .idea/dynamic.xml 78 | # .idea/uiDesigner.xml 79 | 80 | # Gradle: 81 | # .idea/gradle.xml 82 | # .idea/libraries 83 | 84 | # Mongo Explorer plugin: 85 | # .idea/mongoSettings.xml 86 | 87 | ## File-based project format: 88 | *.ipr 89 | *.iws 90 | 91 | ## Plugin-specific files: 92 | 93 | # IntelliJ 94 | /out/ 95 | 96 | # mpeltonen/sbt-idea plugin 97 | .idea_modules/ 98 | 99 | # JIRA plugin 100 | atlassian-ide-plugin.xml 101 | 102 | # Crashlytics plugin (for Android Studio and IntelliJ) 103 | com_crashlytics_export_strings.xml 104 | crashlytics.properties 105 | crashlytics-build.properties 106 | 107 | 108 | 109 | builds 110 | db 111 | Godeps/_workspace -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gernest/nutz" 7 | ) 8 | 9 | var db = nutz.NewStorage("_test.ddb", 0600, nil) 10 | 11 | func TestCreateAccount(t *testing.T) { 12 | bucket := "test_create_account" 13 | dataset := []struct { 14 | uuid, email string 15 | }{ 16 | {"db0668ac-7eba-40dd-56ee-0b1c0b9b415d", "gernest@aurora.com"}, 17 | {"e6917dfe-b4f6-49b8-5628-83dd2a430e9a", "gernest@aurora.tz"}, 18 | {"bc5288cf-4120-4f3c-5957-b19e093a12f4", "gernest@aurora.io"}, 19 | } 20 | for _, u := range dataset { 21 | usr := &User{ 22 | EmailAddress: u.email, 23 | UUID: u.uuid, 24 | } 25 | if err := CreateAccount(db, usr, bucket); err != nil { 26 | t.Error(err) 27 | } 28 | } 29 | } 30 | 31 | func TestGetUser(t *testing.T) { 32 | bucket := "test_get" 33 | dataset := []struct { 34 | uuid, email string 35 | }{ 36 | {"db0668ac-7eba-40dd-56ee-0b1c0b9b415d", "gernest@aurora.com"}, 37 | {"e6917dfe-b4f6-49b8-5628-83dd2a430e9a", "gernest@aurora.tz"}, 38 | {"bc5288cf-4120-4f3c-5957-b19e093a12f4", "gernest@aurora.io"}, 39 | } 40 | for _, u := range dataset { 41 | usr := &User{ 42 | EmailAddress: u.email, 43 | UUID: u.uuid, 44 | } 45 | if err := CreateAccount(db, usr, bucket); err != nil { 46 | t.Error(err) 47 | } 48 | } 49 | for _, u := range dataset { 50 | user, err := GetUser(db, bucket, u.email) 51 | if err != nil { 52 | t.Errorf("geeting user %v", err) 53 | } 54 | if user.UUID != u.uuid { 55 | t.Errorf("expected %s got %s", u.uuid, user.UUID) 56 | } 57 | if user.EmailAddress != u.email { 58 | t.Errorf("expected %s got %s", u.email, user.EmailAddress) 59 | } 60 | } 61 | } 62 | func TestGetAll(t *testing.T) { 63 | var origin string 64 | bucket := "get_all" 65 | for _ = range []int{1, 2, 3, 4} { 66 | usr := &User{ 67 | UUID: getUUID(), 68 | } 69 | usr.EmailAddress = usr.UUID 70 | err := CreateAccount(db, usr, bucket) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | origin = origin + "," + usr.UUID 75 | } 76 | curr, err := GetAllUsers(db, bucket) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | for _, v := range curr { 81 | if !contains(origin, v) { 82 | t.Errorf("expected %s to be in %s", v, origin) 83 | } 84 | } 85 | zz, err := GetAllUsers(db, "lora") 86 | if err == nil { 87 | t.Error("expected an error") 88 | } 89 | if zz != nil { 90 | t.Errorf("expected nil got %v", zz) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gorilla/securecookie" 9 | "github.com/gorilla/sessions" 10 | ) 11 | 12 | func TestSession_New(t *testing.T) { 13 | store, req := sessSetup(t) 14 | testNewSess(store, req, t) 15 | } 16 | 17 | func TestSession_Save(t *testing.T) { 18 | store, req := sessSetup(t) 19 | testNewSess(store, req, t) 20 | testSaveSess(store, req, t, "user", "gernest") 21 | } 22 | 23 | func TestSess_Get(t *testing.T) { 24 | var ( 25 | maxAge = 30 26 | sPath = "/" 27 | cName = "youngWarlock" 28 | secret = []byte("my-secret") 29 | ) 30 | opts := &sessions.Options{MaxAge: maxAge, Path: sPath} 31 | store, req := sessSetup(t) 32 | s := testSaveSess(store, req, t, "user", "gernest") 33 | c, err := securecookie.EncodeMulti(s.Name(), s.ID, securecookie.CodecsFromPairs(secret)...) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | newCookie := sessions.NewCookie(s.Name(), c, opts) 38 | req.AddCookie(newCookie) 39 | s, err = store.New(req, cName) 40 | if err != nil { 41 | t.Error(err) 42 | } 43 | if s.IsNew { 44 | t.Errorf("Expected false, actual %v", s.IsNew) 45 | } 46 | ss, err := store.Get(req, cName) 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | if ss.IsNew { 51 | t.Errorf("Expected false, actual %v", ss.IsNew) 52 | } 53 | if ss.Values["user"] != "gernest" { 54 | t.Errorf("Expected gernest, actual %s", ss.Values["user"]) 55 | } 56 | } 57 | func TestSess_Delete(t *testing.T) { 58 | store, req := sessSetup(t) 59 | s := testSaveSess(store, req, t, "user", "gernest") 60 | defer db.DeleteDatabase() 61 | w := httptest.NewRecorder() 62 | err := store.Delete(req, w, s) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | } 67 | 68 | 69 | func sessSetup(t *testing.T) (*Session, *http.Request) { 70 | var ( 71 | maxAge = 30 72 | sPath = "/" 73 | sBucket = "sessions" 74 | secret = []byte("my-secret") 75 | testURL = "http://www.example.com" 76 | ) 77 | opts := &sessions.Options{MaxAge: maxAge, Path: sPath} 78 | store := NewSessStore(db, sBucket, 10, opts, secret) 79 | req, err := http.NewRequest("GET", testURL, nil) 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | return store, req 84 | } 85 | 86 | func testNewSess(ss *Session, req *http.Request, t *testing.T) *sessions.Session { 87 | var cName = "youngWarlock" 88 | 89 | s, err := ss.New(req, cName) 90 | if err == nil { 91 | if !s.IsNew { 92 | t.Errorf("Expected true actual %v", s.IsNew) 93 | } 94 | t.Errorf("Expected \"http: named cookie not present\" actual nil") 95 | } 96 | return s 97 | } 98 | func testSaveSess(ss *Session, req *http.Request, t *testing.T, key, val string) *sessions.Session { 99 | s := testNewSess(ss, req, t) 100 | s.Values[key] = val 101 | w := httptest.NewRecorder() 102 | err := s.Save(req, w) 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | return s 107 | } 108 | -------------------------------------------------------------------------------- /templates/snippets/profile_update_form.html: -------------------------------------------------------------------------------- 1 | {{$g:=.}} 2 | {{with .profile}} 3 |
4 |

badili habari zangu

5 |
6 |
7 | {{if $g.error}} 8 |
9 |

{{$g.error}}

10 |
11 | {{end}} 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 | 36 |
37 |
38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 | 59 |
60 |
61 |
62 | 69 |
70 |
71 |
72 |
73 |
74 | {{template "snippets/profile_pic_upload" .}} 75 | {{end}} -------------------------------------------------------------------------------- /public/js/golem.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2013 Niklas Voss 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | (function(global) { 20 | 21 | if (global["WebSocket"]) { 22 | var seperator = " ", 23 | DefaultJSONProtocol = { 24 | unpack: function(data) { 25 | var name = data.split(seperator)[0]; 26 | return [name, data.substring(name.length+1, data.length)]; 27 | }, 28 | unmarshal: function(data) { 29 | return JSON.parse(data); 30 | }, 31 | marshalAndPack: function(name, data) { 32 | return name + seperator + JSON.stringify(data); 33 | } 34 | }; 35 | 36 | function Connection(addr, debug) { 37 | 38 | this.ws = new WebSocket(addr); 39 | 40 | this.callbacks = {}; 41 | 42 | this.debug = debug 43 | 44 | this.ws.onclose = this.onClose.bind(this); 45 | this.ws.onopen = this.onOpen.bind(this); 46 | this.ws.onmessage = this.onMessage.bind(this); 47 | } 48 | 49 | Connection.prototype = { 50 | constructor: Connection, 51 | protocol: DefaultJSONProtocol, 52 | setProtocol: function(protocol) { 53 | this.protocol = protocol; 54 | }, 55 | enableBinary: function() { 56 | this.ws.binaryType = 'arraybuffer'; 57 | }, 58 | onClose: function(evt) { 59 | if (this.debug) { 60 | console.log("golem: Connection closed!"); 61 | } 62 | if (this.callbacks["close"]) this.callbacks["close"](evt); 63 | }, 64 | onMessage: function(evt) { 65 | var data = this.protocol.unpack(evt.data); 66 | if (this.debug) { 67 | console.log("golem: Received "+data[0]+"-Event."); 68 | } 69 | if (this.callbacks[data[0]]) { 70 | var obj = this.protocol.unmarshal(data[1]); 71 | this.callbacks[data[0]](obj); 72 | } 73 | }, 74 | onOpen: function(evt) { 75 | if (this.debug) { 76 | console.log("golem: Connection established!"); 77 | } 78 | if (this.callbacks["open"]) this.callbacks["open"](evt); 79 | }, 80 | on: function(name, callback) { 81 | this.callbacks[name] = callback; 82 | }, 83 | emit: function(name, data) { 84 | this.ws.send(this.protocol.marshalAndPack(name, data)); 85 | } 86 | 87 | } 88 | 89 | global.golem = { 90 | Connection: Connection 91 | }; 92 | 93 | } else { 94 | 95 | console.warn("golem: WebSockets not supported!"); 96 | 97 | } 98 | })(this) -------------------------------------------------------------------------------- /templates/snippets/profile_info.html: -------------------------------------------------------------------------------- 1 | {{$u:=.user}} 2 | {{ with .profile }} 3 |
4 |
5 |
6 |
    7 |
  • 8 |
    9 | {{ .FirstName}} 10 | {{.LastName }} 11 |
    12 |
  • 13 |
  • 14 | umri 15 |
    {{.Age}}
    16 |
  • 17 |
  • 18 | jinsia 19 |
    {{.Sex}}
    20 |
  • 21 |
  • 22 | Jiji 23 |
    {{.City}}
    24 |
  • 25 |
  • 26 | Nchi 27 |
    {{.Country}}
    28 |
  • 29 |
  • 30 | Mtaa 31 |
    {{.Street}}
    32 |
  • 33 |
34 | 35 |
    36 |
  • 37 |
    angalia picha zangu
    38 |
    39 | {{with .Photos}} 40 |
    41 | {{range .}} 42 | 43 | {{end}} 44 |
    45 | {{else}} 46 |

    hakuna picha yoyote

    47 | {{end}} 48 |
    49 |
  • 50 |
  • 51 |
    nitumie ujumbe
    52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 | 60 |
    61 |
    62 | 63 | 64 |
    65 |
    66 |
    67 |
    68 | 69 |
    70 |
    71 | 72 |
    73 | 74 |
    75 |
    76 |
  • 77 |
78 |
79 |
80 |
81 | {{ end }} 82 | -------------------------------------------------------------------------------- /templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 | 12 | 14 |
15 |
16 | 18 | 20 |
21 | {{if .errors.FirstName}} 22 |
23 |

{{.errors.FirstName}}

24 |
25 | {{end}} 26 | {{if .errors.LastName}} 27 |
28 |

{{.errors.LastName}}

29 |
30 | {{end}} 31 |
32 |
33 |
34 | 36 | 38 |
39 | {{if .errors.Password}} 40 |
41 |

{{.errors.Password}}

42 |
43 | {{end}} 44 |
45 |
46 |
47 | 49 | 51 |
52 | {{if .errors.ConfirmPassword}} 53 |
54 |

{{.errors.ConfirmPassword}}

55 |
56 | {{end}} 57 |
58 |
59 |
60 | 62 | 64 |
65 | {{if .errors.Email}} 66 |
67 |

{{.errors.Email}}

68 |
69 | {{end}} 70 |
71 | 75 |
76 |
77 | 78 |
79 |
80 | {{template "base/footer" .}} 81 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gernest/nutz" 12 | "github.com/gorilla/securecookie" 13 | "github.com/gorilla/sessions" 14 | ) 15 | 16 | // Session implemets gorilla session store interface 17 | type Session struct { 18 | store nutz.Storage 19 | bucket string 20 | options *sessions.Options 21 | codecs []securecookie.Codec 22 | duration int // Time before the session expires 23 | } 24 | 25 | type sessionValue struct { 26 | Data string `json:"data"` 27 | Expires time.Time `json:"expires"` 28 | } 29 | 30 | // NewSessStore creates a new session store 31 | func NewSessStore(db nutz.Storage, bucket string, duration int, opts *sessions.Options, secrets ...[]byte) *Session { 32 | return &Session{ 33 | store: db, 34 | bucket: bucket, 35 | options: opts, 36 | codecs: securecookie.CodecsFromPairs(secrets...), 37 | duration: duration, 38 | } 39 | } 40 | 41 | // Get retrieves a session 42 | func (s *Session) Get(r *http.Request, name string) (*sessions.Session, error) { 43 | return sessions.GetRegistry(r).Get(s, name) 44 | } 45 | 46 | // New always returns a session, if the sessio is not found, a new one is created. 47 | func (s *Session) New(r *http.Request, name string) (*sessions.Session, error) { 48 | session := sessions.NewSession(s, name) 49 | session.Options = s.options 50 | session.IsNew = true 51 | 52 | cookie, err := r.Cookie(name) 53 | if err != nil { 54 | return session, err 55 | } 56 | err = securecookie.DecodeMulti(name, cookie.Value, &session.ID, s.codecs...) 57 | if err != nil { 58 | return session, err 59 | } 60 | err = s.load(session) 61 | if err != nil { 62 | return session, err 63 | } 64 | session.IsNew = false 65 | return session, err 66 | } 67 | 68 | // Save persist a session 69 | func (s *Session) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { 70 | sessID := base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)) 71 | if session.ID == "" { 72 | session.ID = strings.TrimRight(sessID, "=") 73 | } 74 | if err := s.save(session); err != nil { 75 | return err 76 | } 77 | e, err := securecookie.EncodeMulti(session.Name(), session.ID, s.codecs...) 78 | if err != nil { 79 | return err 80 | } 81 | http.SetCookie(w, sessions.NewCookie(session.Name(), e, session.Options)) 82 | return nil 83 | } 84 | 85 | // Delete remove a session from database, and expires the cliet cookie 86 | func (s *Session) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { 87 | options := *session.Options 88 | options.MaxAge = -1 89 | http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options)) 90 | for k := range session.Values { 91 | delete(session.Values, k) 92 | } 93 | ss := s.store.Delete(s.bucket, session.ID) 94 | return ss.Error 95 | } 96 | 97 | func (s *Session) save(session *sessions.Session) error { 98 | encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, s.codecs...) 99 | if err != nil { 100 | return err 101 | } 102 | v, err := json.Marshal(sessionValue{ 103 | Data: encoded, 104 | Expires: s.getExpires(session.Options.MaxAge), 105 | }) 106 | ss := s.store.Create(s.bucket, session.ID, v) 107 | return ss.Error 108 | } 109 | 110 | func (s *Session) load(session *sessions.Session) error { 111 | v := &sessionValue{} 112 | ss := s.store.Get(s.bucket, session.ID) 113 | err := json.Unmarshal(ss.Data, v) 114 | if err != nil { 115 | return err 116 | } 117 | if v.Expires.Sub(time.Now()) < 0 { 118 | return errors.New("aurora: session expired") 119 | } 120 | err = securecookie.DecodeMulti(session.Name(), v.Data, &session.Values, s.codecs...) 121 | if err != nil { 122 | return err 123 | } 124 | return nil 125 | } 126 | 127 | func (s *Session) getExpires(maxAge int) time.Time { 128 | if maxAge <= 0 { 129 | return time.Now().Add(time.Second * time.Duration(s.duration)) 130 | } 131 | return time.Now().Add(time.Second * time.Duration(maxAge)) 132 | } 133 | -------------------------------------------------------------------------------- /forms_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/bluele/gforms" 11 | ) 12 | 13 | func TestIsName(t *testing.T) { 14 | t.Parallel() 15 | var ( 16 | req1, req2 *http.Request 17 | err error 18 | form *gforms.FormInstance 19 | vars url.Values 20 | ) 21 | vars = url.Values{ 22 | "name": {"gernest"}, 23 | } 24 | Form := gforms.DefineForm(gforms.NewFields( 25 | gforms.NewTextField( 26 | "name", 27 | gforms.Validators{ 28 | IsName(), 29 | }, 30 | ), 31 | )) 32 | 33 | // Should pass when the name field is alpanumeric 34 | req1, err = http.NewRequest("POST", "/", strings.NewReader(vars.Encode())) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | req1.Header.Add("Content-Type", "application/x-www-form-urlencoded") 39 | form = Form(req1) 40 | if !form.IsValid() { 41 | t.Errorf("validating form %v", form.Errors()) 42 | } 43 | 44 | // Should fail when the name field aint alphanumeric 45 | vars = url.Values{"name": {"g-ernest"}} 46 | req2, err = http.NewRequest("POST", "/", strings.NewReader(vars.Encode())) 47 | if err != nil { 48 | t.Errorf("creating new request %v", err) 49 | } 50 | req2.Header.Add("Content-Type", "application/x-www-form-urlencoded") 51 | form = Form(req2) 52 | if form.IsValid() { 53 | t.Error("expected validation errors") 54 | } 55 | if form.Errors().Get("name")[0] != MsgName { 56 | t.Errorf("Expected %s got %s", MsgName, form.Errors().Get("name")[0]) 57 | } 58 | } 59 | 60 | func TestComposeRegisterForm(t *testing.T) { 61 | t.Parallel() 62 | Form := ComposeRegisterForm() 63 | vars := url.Values{ 64 | "first_name": {"gernest"}, 65 | "last_name": {"aurora"}, 66 | "email_address": {"gernest@aurora.com"}, 67 | "pass": {"mypassword"}, 68 | "confirm_pass": {"mypassword"}, 69 | } 70 | req1, err := http.NewRequest("POST", "/", strings.NewReader(vars.Encode())) 71 | if err != nil { 72 | t.Errorf("creating new request %v", err) 73 | } 74 | req1.Header.Add("Content-Type", "application/x-www-form-urlencoded") 75 | form1 := Form(req1) 76 | if !form1.IsValid() { 77 | t.Errorf("validating form %v", form1.Errors()) 78 | } 79 | usr := form1.GetModel().(User) 80 | if usr.EmailAddress != vars.Get("email_address") { 81 | t.Errorf("retrieving model form: expecting %s got %s", vars.Get("email_address"), usr.EmailAddress) 82 | } 83 | 84 | vars.Set("first_name", "---") 85 | req2, err := http.NewRequest("POST", "/", strings.NewReader(vars.Encode())) 86 | if err != nil { 87 | t.Errorf("creating new request %v", err) 88 | } 89 | req2.Header.Add("Content-Type", "application/x-www-form-urlencoded") 90 | form2 := Form(req2) 91 | if form2.IsValid() { 92 | t.Error("expected validation error") 93 | } 94 | } 95 | 96 | func TestBirthDateValidator(t *testing.T) { 97 | t.Parallel() 98 | var ( 99 | req1, req2 *http.Request 100 | vars url.Values 101 | err error 102 | form *gforms.FormInstance 103 | now = time.Now() 104 | yearsAgo = func(yrs int) time.Time { 105 | n := time.Now() 106 | nowAFter := n.AddDate(18, 1, 1) 107 | dur := nowAFter.Sub(n) 108 | return n.Add(-dur) 109 | 110 | } 111 | ) 112 | Form := gforms.DefineForm(gforms.NewFields( 113 | gforms.NewDateTimeField( 114 | "date", 115 | time.RFC822, 116 | gforms.Validators{ 117 | BirthDateValidator{Limit: 18, Message: MsgMinAge}, 118 | }, 119 | ), 120 | )) 121 | 122 | vars = url.Values{"date": {now.Format(time.RFC822)}} 123 | req1, err = http.NewRequest("POST", "/", strings.NewReader(vars.Encode())) 124 | if err != nil { 125 | t.Error(err) 126 | } 127 | req1.Header.Add("Content-Type", "application/x-www-form-urlencoded") 128 | form = Form(req1) 129 | if form.IsValid() { 130 | t.Error("Expected some errors") 131 | } 132 | 133 | vars = url.Values{"date": {yearsAgo(18).Format(time.RFC822)}} 134 | req2, err = http.NewRequest("POST", "/", strings.NewReader(vars.Encode())) 135 | if err != nil { 136 | t.Error(err) 137 | } 138 | req2.Header.Add("Content-Type", "application/x-www-form-urlencoded") 139 | form = Form(req2) 140 | if !form.IsValid() { 141 | t.Error(form.Errors()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/nu7hatch/gouuid" 11 | "golang.org/x/crypto/bcrypt" 12 | 13 | "github.com/gernest/nutz" 14 | "github.com/gorilla/sessions" 15 | ) 16 | 17 | // serialize the given object obj into json format and saves it into the dtabase 18 | func marshalAndCreate(db nutz.Storage, obj interface{}, buck, key string, nest ...string) error { 19 | data, err := json.Marshal(obj) 20 | if err != nil { 21 | return err 22 | } 23 | if len(nest) > 0 { 24 | c := db.Create(buck, key, data, nest...) 25 | return c.Error 26 | 27 | } 28 | c := db.Create(buck, key, data) 29 | return c.Error 30 | } 31 | 32 | // serialize the given object to json and saves it into the database 33 | func marshalAndUpdate(db nutz.Storage, obj interface{}, buck, key string, nest ...string) error { 34 | data, err := json.Marshal(obj) 35 | if err != nil { 36 | return err 37 | } 38 | c := db.Update(buck, key, data, nest...) 39 | return c.Error 40 | } 41 | 42 | // serialize and saves th object to the database, but checks first if the key already exist. 43 | // When there is already a record with a given key an error is returned. 44 | func createIfNotexist(db nutz.Storage, obj interface{}, buck, key string, nest ...string) error { 45 | if g := db.Get(buck, key, nest...); g.Error != nil { 46 | return marshalAndCreate(db, obj, buck, key, nest...) 47 | } 48 | return errors.New("aurora: already exist") 49 | } 50 | 51 | // Encrypts a given string using bcrypt library. It returns the hashed password as a string, 52 | // or any error 53 | func hashPassword(pass string) (string, error) { 54 | np, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) 55 | return string(np), err 56 | } 57 | 58 | // Flash is a helper for storing and retrieving flash messages 59 | // TODO : Move this to another file, it just don't look like it belongs here 60 | type Flash struct { 61 | Data map[string]interface{} 62 | } 63 | 64 | // NewFlash creates a new flash 65 | func NewFlash() *Flash { 66 | return &Flash{Data: make(map[string]interface{})} 67 | } 68 | 69 | // Success adds a success message 70 | func (f *Flash) Success(msg string) { 71 | f.Data["FlashSuccess"] = msg 72 | } 73 | 74 | // Notice adds a notice flash message 75 | func (f *Flash) Notice(msg string) { 76 | f.Data["FlashNotice"] = msg 77 | } 78 | 79 | // Error adds an error message 80 | func (f *Flash) Error(msg string) { 81 | f.Data["FlashError"] = msg 82 | } 83 | 84 | // Save saves the flsah to the given session 85 | func (f *Flash) Save(s *sessions.Session) { 86 | data, err := json.Marshal(f) 87 | if err == nil { 88 | s.AddFlash(data) 89 | } 90 | } 91 | 92 | // Get retrieves any flash messages in the session 93 | func (f *Flash) Get(s *sessions.Session) *Flash { 94 | if flashes := s.Flashes(); flashes != nil { 95 | data := flashes[0] 96 | if err := json.Unmarshal(data.([]byte), f); err != nil { 97 | log.Println(err) 98 | return nil 99 | } 100 | return f 101 | } 102 | return nil 103 | } 104 | 105 | // Retrives data from the dataase, and marshalls the result to the given obj. Thhis 106 | // uses json decoding. 107 | func getAndUnmarshall(db nutz.Storage, bucket, key string, obj interface{}, nest ...string) error { 108 | g := db.Get(bucket, key, nest...) 109 | if g.Error != nil { 110 | return g.Error 111 | } 112 | err := json.Unmarshal(g.Data, obj) 113 | if err != nil { 114 | return err 115 | } 116 | return nil 117 | } 118 | 119 | // Verfiess if the given has mathes the password. The hash must be a bcrypt encoded has. 120 | // it uses bcrypt to compare the two passwords 121 | func verifyPass(hash, pass string) error { 122 | return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) 123 | } 124 | 125 | // returns a new UUIDv4 string 126 | func getUUID() string { 127 | id, err := uuid.NewV4() 128 | if err != nil { 129 | // TODO :log 130 | } 131 | return id.String() 132 | } 133 | 134 | func getProfileDatabase(dbDir, profileID, dbExt string) string { 135 | return filepath.Join(dbDir, profileID+dbExt) 136 | } 137 | 138 | func setAge(born time.Time) int { 139 | n := time.Now() 140 | return n.Year() - born.Year() 141 | } 142 | -------------------------------------------------------------------------------- /public/js/msg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by gernest on 5/14/15. 3 | */ 4 | $(document).ready(function(){ 5 | var addr='ws://'+window.location.host+'/msg'; 6 | var conn= new golem.Connection(addr,true); 7 | var fm=$('form#msg'); 8 | var send=$('button.msg-send'); 9 | var txt =$('textarea#msg-text'); 10 | var pg=$('div.progress'); 11 | var sendEvt='send'; 12 | var alertSendSuccess = "sendSuccess"; 13 | var alertSendFailed = "sendFailled"; 14 | var alertInbox = "messageInbox"; 15 | var alertRead = "messageRead"; 16 | var chatBox=$('#i-chat'); 17 | var idPrefix='aurora'; 18 | var msgBox=''+ 19 | '
  • '+ 20 | '
    1{{=sender_name}}
    '+ 21 | '
    '+ 22 | '
    '+ 23 | '
    '+ 24 | '

    {{=text}}

    '+ 25 | '
    '+ 26 | '
    '+ 27 | '
    '+ 28 | '
    '+ 29 | '
    '+ 30 | '
    '+ 31 | '
    '+ 32 | '
    '+ 33 | '
    '+ 34 | '
    '+ 35 | ''+ 36 | ''+ 37 | '
    '+ 38 | '
    '+ 39 | ''+ 40 | '
    '+ 41 | ''+ 42 | '
    '+ 43 | '
    '+ 44 | '
    '+ 45 | '
  • '; 46 | var singleMsg=''+ 47 | '
    '+ 48 | '

    {{=text}}

    '+ 49 | '
    '; 50 | 51 | var msgTmpl= new t(msgBox); 52 | var singleMsgTmpl=new t(singleMsg); 53 | var addInbox=function(obj){ 54 | sid='#'+idPrefix+obj.sender_id; 55 | var base=chatBox.find(sid); 56 | if(base.length>0){ 57 | bn=base.find('.msgs'); 58 | bn.append(singleMsgTmpl.render(obj)); 59 | bn.scrollTop(bn[0].scrollHeight); 60 | n=base.find('i.notice'); 61 | n.text(Number(n.text())+1); 62 | }else{ 63 | chatBox.append(msgTmpl.render(obj)) 64 | .one('click',function(e){ 65 | $(this).collapsible(); 66 | b =$(this); 67 | $(this).find('i.notice').text(''); 68 | $(this).find('button.i-msg-send') 69 | .on('click',function(e){ 70 | e.preventDefault(); 71 | var mfm=$(this).parents('form#i-msg'); 72 | var mtxt=mfm.find('textarea#i-msg-text'); 73 | var msg={ 74 | "recepient_id":$(this).attr('msg-s'), 75 | "sender_id":$(this).attr('msg-r'), 76 | "text":mtxt.val() 77 | }; 78 | conn.emit(sendEvt,msg); 79 | var msgB=''+ 80 | '
    '+ 81 | '

    '+mtxt.val()+'

    '+ 82 | '
    '; 83 | bn=b.find('.msgs'); 84 | bn.append(msgB); 85 | bn.scrollTop(bn[0].scrollHeight); 86 | mtxt.val(''); 87 | }); 88 | }); 89 | } 90 | 91 | }; 92 | 93 | send.click(function(e){ 94 | e.preventDefault(); 95 | pg.toggleClass('hide'); 96 | var msg={ 97 | 'recepient_id':send.attr('msg-r'), 98 | 'sender_id':send.attr('msg-s'), 99 | 'text':txt.val() 100 | }; 101 | conn.emit(sendEvt,msg); 102 | pg.toggleClass('hide'); 103 | txt.val(""); 104 | }); 105 | 106 | conn.on(alertSendSuccess,function(data){ 107 | Materialize.toast("ujumbe umefanikiwa kutumwa",900); 108 | }); 109 | 110 | conn.on(alertSendFailed,function(data){ 111 | console.log(data); 112 | }); 113 | 114 | conn.on(alertInbox,function(data){ 115 | Materialize.toast("kuna ujumbe wako",999); 116 | addInbox(data); 117 | }); 118 | }); -------------------------------------------------------------------------------- /templates/base/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{if .CdnMode}} 10 | 11 | 13 | 14 | 15 | 16 | {{else}} 17 | 19 | 21 | 22 | 23 | 24 | {{end}} 25 | {{if .Title}} 26 | {{.Title}} 27 | {{else}} 28 | {{.AppTitle}} 29 | {{end}} 30 | 31 | 32 |
    33 | 106 |
    107 | {{if .InSession}} 108 |
    109 |
    110 |
    111 | {{if .flash.Success}} 112 |

    {{.flash.Success}}

    113 | {{end}} 114 |
    115 |
    116 |
    117 |
    118 | 120 |
    121 | {{else}} 122 |
    123 |
    124 |
    125 | {{end}} -------------------------------------------------------------------------------- /msg_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | ) 16 | 17 | func TestMessenger(t *testing.T) { 18 | var ( 19 | email = "wesuckssoomuch@aurora.com" 20 | pass = "mamamia" 21 | loginPath = "/auth/login" 22 | userID = "37c37153-089e-4c19-466e-2f467ac07c1e" 23 | ) 24 | 25 | ts, client, rx := testServer(t) 26 | defer ts.Close() 27 | if rx != nil { 28 | } 29 | if client != nil { 30 | } 31 | origin, err := url.Parse(ts.URL) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | 36 | // Websocket route url. 37 | wsURL := fmt.Sprintf("ws://%s/msg", origin.Host) 38 | u, err := url.Parse(wsURL) 39 | if err != nil { 40 | t.Errorf("parsing wesocket url %v", err) 41 | } 42 | 43 | // There is no session yet, when 44 | aConn, err := net.Dial("tcp", origin.Host) 45 | if err != nil { 46 | t.Errorf("establishing a connection %v", err) 47 | } 48 | 49 | ws1, _, err := websocket.NewClient(aConn, u, make(http.Header), 1024, 1024) 50 | if err == nil { 51 | t.Error("expected an error got nil instead") 52 | } 53 | if ws1 != nil { 54 | t.Error("Expected nil") 55 | ws1.Close() 56 | } 57 | 58 | // Create a user and profile. Login and start a new session. This should make 59 | // the handshake succeed. 60 | usr := &User{UUID: userID, EmailAddress: email} 61 | 62 | // NOTE: the user password is stored as a hash, so we have to generate the hash to 63 | // minic the user account. 64 | ps, err := hashPassword(pass) 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | usr.Pass = ps 69 | err = CreateAccount(setDB(rx.db, rx.cfg.AccountsDB), usr, rx.cfg.AccountsBucket) 70 | if err != nil { 71 | t.Errorf("creating a new account %v", err) 72 | } 73 | 74 | // Create a new profile, based on the user we have created above. 75 | pdbStr := getProfileDatabase(rx.cfg.DBDir, usr.UUID, rx.cfg.DBExtension) 76 | pdb := setDB(rx.db, pdbStr) 77 | p := &Profile{ID: usr.UUID} 78 | err = CreateProfile(pdb, p, rx.cfg.ProfilesBucket) 79 | if err != nil { 80 | t.Errorf("creating profile: %v", err) 81 | } 82 | 83 | // login and create a session for the user we have just created. 84 | varsLogin := url.Values{"email": {email}, "password": {pass}} 85 | res9, err := client.PostForm(fmt.Sprintf("%s%s", ts.URL, loginPath), varsLogin) 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | err = checkResponse(res9, http.StatusOK, "search") 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | 94 | // Get the cookie data from the client, so that we can send the data in the header for 95 | // the following websocket request. 96 | h := make(http.Header) 97 | for _, cookie := range client.Jar.Cookies(origin) { 98 | h.Set("Cookie", cookie.String()) 99 | } 100 | 101 | // A bad request should fail, tmsg is the message to be send for a chat. 102 | tmsg := &MSG{SenderID: userID, Text: "hellp gernest", RecipientID: usr.UUID} 103 | d, err := marshalAndPach("send", tmsg) 104 | if err != nil { 105 | t.Errorf("marshaling and packing %v", err) 106 | } 107 | nConn, err := net.Dial("tcp", origin.Host) 108 | if err != nil { 109 | t.Errorf("establishing a connection %v", err) 110 | } 111 | ws3, _, err := websocket.NewClient(nConn, u, h, 1024, 1024) 112 | if err != nil { 113 | t.Errorf("extablishing websocket connection %v", err) 114 | } 115 | defer ws3.Close() 116 | 117 | // Set a one second deadline to the connection 118 | setDeadline(t, ws3) 119 | 120 | err = ws3.WriteMessage(websocket.TextMessage, d) 121 | if err != nil { 122 | t.Errorf("writing message %v", err) 123 | } 124 | _, rs, err := ws3.ReadMessage() 125 | if err != nil { 126 | t.Errorf("reading message %v", err) 127 | } 128 | evt, dmsg, err := unpackMSG(rs) 129 | if err != nil { 130 | t.Errorf("unpacking read message %v \n %s", err, string(rs)) 131 | 132 | } 133 | if evt != alertSendSuccess { 134 | t.Errorf("Expected %s got %s", alertSendSuccess, evt) 135 | } 136 | if !contains(string(dmsg), tmsg.Text) { 137 | t.Errorf("Expected %s to contain %s", string(rs), tmsg.Text) 138 | } 139 | } 140 | 141 | // Marshalls and pack the message( dPtr ) into a protocl that is used to transfer 142 | // messages. 143 | func marshalAndPach(name string, dPtr interface{}) ([]byte, error) { 144 | var protocolSeperator = " " 145 | if data, err := json.Marshal(dPtr); err == nil { 146 | result := []byte(name + protocolSeperator) 147 | return append(result, data...), nil 148 | } else { 149 | return nil, err 150 | } 151 | } 152 | 153 | // Decodes received message from the chat server. 154 | func unpackMSG(data []byte) (string, []byte, error) { 155 | protocolSeperator := " " 156 | result := strings.SplitN(string(data), protocolSeperator, 2) 157 | if len(result) != 2 { 158 | return "", nil, errors.New("Unable to extract event name from data.") 159 | } 160 | return result[0], []byte(result[1]), nil 161 | } 162 | 163 | // sets deadline to a websocket connection 164 | func setDeadline(t *testing.T, ws *websocket.Conn) { 165 | err := ws.SetWriteDeadline(time.Now().Add(time.Second)) 166 | if err != nil { 167 | t.Errorf("set write deadline %v", err) 168 | } 169 | err = ws.SetReadDeadline(time.Now().Add(time.Second)) 170 | if err != nil { 171 | t.Errorf("set read deadline %v", err) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /forms.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "time" 8 | 9 | valid "github.com/asaskevich/govalidator" 10 | "github.com/bluele/gforms" 11 | ) 12 | 13 | const ( 14 | ageLimit int = 18 15 | birthDateFormat string = "2 January, 2006" 16 | ) 17 | 18 | var ( 19 | // MsgRequired is the error message for required validation. 20 | MsgRequired = "hili eneo halitakiwi kuachwa wazi" 21 | 22 | // MsgName is the error message displayed for a name validation 23 | MsgName = "hili eneo linatakiwa liwe mchanganyiko wa herufi na namba" 24 | 25 | //MsgEmail is the error message displayed for email validation 26 | MsgEmail = "email sio sahihi. mfano gernest@aurora.com" 27 | 28 | // MsgMinLength is the error message for a minimum length validation 29 | MsgMinLength = "namba ya siri inatakiwa kuanzia herufi 6 na kuendelea" 30 | 31 | // MsgEqual is the error message for equality validation 32 | MsgEqual = "%s inatakiwa iwe sawa na %s" 33 | 34 | // MsgMinAge the minimum age linit 35 | MsgMinAge = "umri unatakiwa uwe zaidi ya miaka %d" 36 | ) 37 | 38 | // This is an interface which is helpful for implementing a custom validator. 39 | // I adopted this so that I can use the functions defined in the govalidator library. 40 | type validateFunc func(string) bool 41 | 42 | // CustomValidator a custom validator for gforms 43 | type CustomValidator struct { 44 | Vf validateFunc 45 | Message string 46 | gforms.Validator 47 | } 48 | 49 | // Validate validates fields 50 | func (vl CustomValidator) Validate(fi *gforms.FieldInstance, fo *gforms.FormInstance) error { 51 | v := fi.V 52 | if v.IsNil || v.Kind != reflect.String || v.Value == "" { 53 | return nil 54 | } 55 | if !vl.Vf(v.RawStr) { 56 | return errors.New(vl.Message) 57 | } 58 | return nil 59 | 60 | } 61 | 62 | // ComposeRegisterForm builds a registration form for validation(with gforms) 63 | func ComposeRegisterForm() gforms.ModelForm { 64 | return gforms.DefineModelForm(User{}, gforms.NewFields( 65 | gforms.NewTextField( 66 | "first_name", 67 | gforms.Validators{ 68 | gforms.Required(MsgRequired), 69 | IsName(), 70 | }, 71 | ), 72 | gforms.NewTextField( 73 | "last_name", 74 | gforms.Validators{ 75 | gforms.Required(MsgRequired), 76 | IsName(), 77 | }, 78 | ), 79 | gforms.NewTextField( 80 | "email_address", 81 | gforms.Validators{ 82 | gforms.Required(MsgRequired), 83 | gforms.EmailValidator(MsgEmail), 84 | }, 85 | ), 86 | gforms.NewTextField( 87 | "pass", 88 | gforms.Validators{ 89 | gforms.Required(MsgRequired), 90 | IsName(), 91 | gforms.MinLengthValidator(6, MsgMinLength), 92 | }, 93 | ), 94 | gforms.NewTextField( 95 | "confirm_pass", 96 | gforms.Validators{ 97 | gforms.Required(MsgRequired), 98 | IsName(), 99 | gforms.MinLengthValidator(6, MsgMinLength), 100 | EqualValidator{to: "pass", Message: MsgEqual}, 101 | }, 102 | ), 103 | )) 104 | } 105 | 106 | // IsName retruns a name validator 107 | func IsName() CustomValidator { 108 | return CustomValidator{Vf: valid.IsAlphanumeric, Message: MsgName} 109 | } 110 | 111 | // EqualValidator checks if the two fields are equal. The to attribute is the name of 112 | // the field whose value must be equal to the current field 113 | type EqualValidator struct { 114 | gforms.Validator 115 | to string 116 | Message string 117 | } 118 | 119 | // Validate checks if the given field is egual to the field in the to attribute 120 | func (vl EqualValidator) Validate(fi *gforms.FieldInstance, fo *gforms.FormInstance) error { 121 | v := fi.V 122 | if v.IsNil || v.Kind != reflect.String || v.Value == "" { 123 | return nil 124 | } 125 | fi2, ok := fo.GetField(vl.to) 126 | if ok { 127 | v2 := fi2.GetV() 128 | 129 | if v.Value != v2.Value { 130 | return fmt.Errorf(vl.Message, fi.GetName(), fi2.GetName()) 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | // BirthDateValidator validates the birth date, handy to keep minors offsite 137 | type BirthDateValidator struct { 138 | Limit int 139 | Message string 140 | gforms.Validator 141 | } 142 | 143 | // Validate checks if the given field instance esceeds the Limit attribute 144 | func (vl BirthDateValidator) Validate(fi *gforms.FieldInstance, fo *gforms.FormInstance) error { 145 | v := fi.V 146 | now := time.Now() 147 | 148 | if v.IsNil { 149 | return nil 150 | } 151 | if v.Kind == reflect.String { 152 | iv := v.Value.(string) 153 | born, err := time.Parse(birthDateFormat, iv) 154 | if err != nil { 155 | return err 156 | } 157 | if now.Year()-born.Year() < vl.Limit { 158 | return fmt.Errorf(vl.Message, vl.Limit) 159 | } 160 | return nil 161 | } 162 | iv := v.Value.(time.Time) 163 | if now.Year()-iv.Year() < vl.Limit { 164 | return fmt.Errorf(vl.Message, vl.Limit) 165 | } 166 | return nil 167 | } 168 | 169 | // holds login form data 170 | type loginForm struct { 171 | Email string `gforms:"email"` 172 | Password string `gforms:"password"` 173 | } 174 | 175 | // ComposeLoginForm builds a login form for validation( with gforms) 176 | func ComposeLoginForm() gforms.ModelForm { 177 | return gforms.DefineModelForm(loginForm{}, gforms.NewFields( 178 | gforms.NewTextField( 179 | "email", 180 | gforms.Validators{ 181 | gforms.Required(MsgRequired), 182 | gforms.EmailValidator(MsgEmail), 183 | }, 184 | ), 185 | gforms.NewTextField( 186 | "password", 187 | gforms.Validators{ 188 | gforms.Required(MsgRequired), 189 | gforms.MinLengthValidator(6, MsgMinLength), 190 | }, 191 | ), 192 | )) 193 | } 194 | 195 | // ComposeProfileForm builds a profile form for validation (using gform) 196 | func ComposeProfileForm() gforms.ModelForm { 197 | return gforms.DefineModelForm(Profile{}, gforms.NewFields( 198 | gforms.NewTextField( 199 | "first_name", 200 | gforms.Validators{ 201 | IsName(), 202 | }, 203 | ), 204 | gforms.NewTextField( 205 | "last_name", 206 | gforms.Validators{ 207 | IsName(), 208 | }, 209 | ), 210 | gforms.NewDateTimeField( 211 | "birth_date", 212 | birthDateFormat, 213 | gforms.Validators{ 214 | BirthDateValidator{Limit: ageLimit, Message: MsgMinAge}, 215 | }, 216 | ), 217 | )) 218 | } 219 | -------------------------------------------------------------------------------- /bin/build.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "path/filepath" 14 | ) 15 | 16 | type AuroraCLI struct { 17 | cfg *BuildConfig 18 | verbose bool 19 | buildDir string 20 | configFile string 21 | o *log.Logger 22 | } 23 | 24 | type BuildConfig struct { 25 | AppName string `json:"name"` 26 | Version string `json:"version"` 27 | Public string `json:"public"` 28 | Templates string `json:"templates"` 29 | Dest string `json:"dest"` 30 | Watch bool `json:"wtach"` 31 | Src string `json:"src"` 32 | WorkingDir string `json:"-"` 33 | ConfigDir string `json:"config"` 34 | DBDIr string `json:"database_dir"` 35 | } 36 | 37 | func NewCLI() *AuroraCLI { 38 | return &AuroraCLI{verbose: false, o: log.New(os.Stdout, "", log.Ltime)} 39 | } 40 | 41 | func (a *AuroraCLI) Setup() { 42 | a.log("===SETUP") 43 | out, err := exec.Command("go", "get", "-v").Output() 44 | a.logErr(err) 45 | if len(out) > 0 { 46 | a.log(fmt.Sprintf("%s \n", out)) 47 | } 48 | bd := path.Join(a.cfg.WorkingDir, path.Join(a.cfg.Dest, a.cfg.Version)) 49 | a.logErr(os.MkdirAll(bd, 0700)) 50 | a.buildDir = bd 51 | a.clean() 52 | a.log("---DONE") 53 | } 54 | func (a *AuroraCLI) RunTests() { 55 | a.log("===TESTING") 56 | if a.verbose { 57 | out, err := exec.Command("go", "test", "-v").Output() 58 | a.logErr(err) 59 | if len(out) > 0 { 60 | a.log(fmt.Sprintf("%s", out)) 61 | } 62 | } else { 63 | out, err := exec.Command("go", "test").Output() 64 | a.logErr(err) 65 | if len(out) > 0 { 66 | a.log(fmt.Sprintf("%s", out)) 67 | } 68 | } 69 | a.log("---DONE") 70 | } 71 | func (a *AuroraCLI) CreateBinary() { 72 | a.log("===CREATING BINARY") 73 | o := filepath.Join(a.cfg.Dest, filepath.Join(a.cfg.Version, a.cfg.AppName)) 74 | src := filepath.Join(a.cfg.Src, a.cfg.AppName+".go") 75 | out, err := exec.Command("go", "build", "-o", o, "-v", src).Output() 76 | a.logErr(err) 77 | if len(out) > 0 { 78 | a.log(fmt.Sprintf("%s", out)) 79 | } 80 | a.log("---DONE") 81 | } 82 | func (a *AuroraCLI) Assemble() { 83 | a.log("===ASSEMBLING") 84 | // copy public folder 85 | a.logErr(a.copyDir(a.cfg.Public, path.Join(a.buildDir, a.cfg.Public))) 86 | 87 | // copy templates 88 | a.logErr(a.copyDir(a.cfg.Templates, path.Join(a.buildDir, a.cfg.Templates))) 89 | 90 | // copy application configurations 91 | appCfg := path.Join(a.buildDir, a.cfg.ConfigDir) 92 | a.logErr(a.copyDir(a.cfg.ConfigDir, appCfg)) 93 | a.log("---DONE") 94 | 95 | } 96 | func (a *AuroraCLI) SetupDatabase() { 97 | a.log("===SETUP DATABASE") 98 | 99 | // create database directory 100 | if a.cfg.DBDIr == "" { 101 | a.logErr(os.MkdirAll(path.Join(a.buildDir, "db"), 0700)) 102 | } else if path.IsAbs(a.cfg.DBDIr) { 103 | a.logErr(os.MkdirAll(a.cfg.DBDIr, 0700)) 104 | } else { 105 | a.logErr(os.MkdirAll(path.Join(a.buildDir, a.cfg.DBDIr), 0700)) 106 | } 107 | a.log("---DONE") 108 | } 109 | func (a *AuroraCLI) Build() { 110 | // load configuration file 111 | a.logErr(a.loadConfig()) 112 | 113 | // setup build env 114 | a.Setup() 115 | 116 | // run tests 117 | a.RunTests() 118 | 119 | // create binary 120 | a.CreateBinary() 121 | 122 | // assemble evrything into the build directory 123 | a.Assemble() 124 | 125 | // setup database 126 | a.SetupDatabase() 127 | 128 | a.log("[SUCCESS] build complete.") 129 | } 130 | func (a *AuroraCLI) loadConfig() error { 131 | a.log("===CONFIGURING BUILD") 132 | cfg := new(BuildConfig) 133 | pwd, err := os.Getwd() 134 | if err != nil { 135 | return err 136 | } 137 | cfgFile := filepath.Join(pwd, a.configFile) 138 | d, err := ioutil.ReadFile(cfgFile) 139 | if err != nil { 140 | return err 141 | } 142 | err = json.Unmarshal(d, cfg) 143 | if err != nil { 144 | return err 145 | } 146 | cfg.WorkingDir = pwd 147 | a.cfg = cfg 148 | a.log("---DONE") 149 | return nil 150 | } 151 | 152 | func (a *AuroraCLI) log(msg interface{}) { 153 | if a.verbose { 154 | a.o.Output(2, fmt.Sprintln(msg)) 155 | } 156 | } 157 | func (a *AuroraCLI) logErr(err error) { 158 | if err != nil { 159 | a.o.Output(2, fmt.Sprintln(err)) 160 | os.Exit(1) 161 | } 162 | } 163 | 164 | func (a *AuroraCLI) copyDir(src, dest string) (err error) { 165 | 166 | // get properties of source dir 167 | sourceinfo, err := os.Stat(src) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | // create dest dir 173 | 174 | err = os.MkdirAll(dest, sourceinfo.Mode()) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | directory, _ := os.Open(src) 180 | 181 | objects, err := directory.Readdir(-1) 182 | 183 | for _, obj := range objects { 184 | 185 | sourcefilepointer := src + "/" + obj.Name() 186 | 187 | destinationfilepointer := dest + "/" + obj.Name() 188 | 189 | if obj.IsDir() { 190 | // create sub-directories - recursively 191 | err = a.copyDir(sourcefilepointer, destinationfilepointer) 192 | if err != nil { 193 | break 194 | } 195 | } else { 196 | // perform copy 197 | err = a.copyFile(sourcefilepointer, destinationfilepointer) 198 | if err != nil { 199 | break 200 | } 201 | } 202 | 203 | } 204 | return 205 | } 206 | 207 | func (a *AuroraCLI) copyFile(source string, dest string) (err error) { 208 | sourcefile, err := os.Open(source) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | defer sourcefile.Close() 214 | 215 | destfile, err := os.Create(dest) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | defer destfile.Close() 221 | 222 | _, err = io.Copy(destfile, sourcefile) 223 | if err == nil { 224 | sourceinfo, err := os.Stat(source) 225 | if err != nil { 226 | err = os.Chmod(dest, sourceinfo.Mode()) 227 | } 228 | 229 | } 230 | 231 | return 232 | } 233 | func (a *AuroraCLI) clean() { 234 | a.log("===CLEANING") 235 | a.logErr(os.RemoveAll(a.buildDir)) 236 | a.log("---DONE") 237 | } 238 | func main() { 239 | v := flag.Bool("v", false, "logs build messages on stdout") 240 | c := flag.String("c", "config/build.json", "specifies wich configuration file to use") 241 | flag.Parse() 242 | a := NewCLI() 243 | if *v { 244 | a.verbose = true 245 | } 246 | a.configFile = *c 247 | a.Build() 248 | } 249 | -------------------------------------------------------------------------------- /uploads.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "image/jpeg" 8 | "image/png" 9 | "mime/multipart" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/gernest/nutz" 14 | ) 15 | 16 | // FileUpload represents the uploaded file 17 | type FileUpload struct { 18 | Body *multipart.File 19 | Ext string 20 | } 21 | 22 | // Photo is the metadata of an uploaded image file 23 | type Photo struct { 24 | ID string `json:"id"` 25 | 26 | // Type is the photo's file extension e.g jpeg or png 27 | Type string `json:"type"` 28 | 29 | //Size is the size of the photo. 30 | Size int `json:"size"` 31 | 32 | //UploadedBy is the ID of the user who uploaded the photo 33 | UploadedBy string `json:"uploaded_by"` 34 | 35 | UploadedAt time.Time `json:"uploaded_at"` 36 | 37 | // UpdatedAt is the time the photo was updated. I keep this filed so as 38 | // to provide, last modified time when serving the photo. 39 | UpdatedAt time.Time `json:"updated_at"` 40 | } 41 | 42 | // GetFileUpload retrieves uploaded file from a request.This function, returns only 43 | // the first file that matches, thus retrieving a single file only. 44 | // the fieldName parameter is the name of the field which holds the file data. 45 | func GetFileUpload(r *http.Request, fieldName string) (*FileUpload, error) { 46 | file, _, err := r.FormFile(fieldName) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return getUploadFile(file) 51 | } 52 | 53 | // a slice to hold a couple of errors 54 | type listErr []error 55 | 56 | func (l listErr) Error() string { 57 | var rst string 58 | for i, e := range l { 59 | if i == 0 { 60 | if e != nil { 61 | rst = e.Error() 62 | } 63 | continue 64 | } 65 | if e != nil { 66 | rst = rst + ", " + e.Error() 67 | } 68 | } 69 | return rst 70 | } 71 | 72 | // GetMultipleFileUpload retrieves multiple files uploaded on a single request. 73 | // The fieldName parameter is the form field containing the files 74 | func GetMultipleFileUpload(r *http.Request, fieldName string) ([]*FileUpload, error) { 75 | const defaultMaxMemory = 32 << 20 //32MB 76 | 77 | err := r.ParseMultipartForm(defaultMaxMemory) 78 | if err != nil { 79 | return nil, err 80 | } 81 | if up := r.MultipartForm.File[fieldName]; len(up) > 0 { 82 | var rst []*FileUpload 83 | var ferr listErr 84 | for _, v := range up { 85 | f, err := v.Open() 86 | if err != nil { 87 | ferr = append(ferr, err) 88 | continue 89 | } 90 | file, err := getUploadFile(f) 91 | if err != nil { 92 | ferr = append(ferr, err) 93 | continue 94 | } 95 | rst = append(rst, file) 96 | } 97 | if len(ferr) > 0 { 98 | return rst, ferr 99 | } 100 | return rst, nil 101 | } 102 | return nil, http.ErrMissingFile 103 | } 104 | 105 | // SaveUploadFile saves the uploaded photos to the profile database. In aurora, every user 106 | // has his/her own personal database. 107 | // 108 | // The db argument should be the user's database. The uploaded file is storesd in two versions 109 | // meta, and data. The meta, is the metadata about the uploaded file, in our case a Photo 110 | // object. The photo object is marshalled and stored in a metaBucket. 111 | // 112 | // The data part is the actual encoded file, its stored in the dataBucket. 113 | func SaveUploadFile(db nutz.Storage, file *FileUpload, p *Profile) (*Photo, error) { 114 | var ( 115 | // The bucket in which all photos will reside. 116 | photoBucket = "photos" 117 | 118 | // The bucket which stores metadata about the photos. This bucket iscreated 119 | // inside the photoBucket. 120 | metaBucket = "meta" 121 | 122 | // The bucket in which actual data that is in []byte is stored. its also created 123 | // inside the photoBucket 124 | dataBucket = "data" 125 | 126 | // NOTE: To keep the structure of recording data sane, I have used nested buckets. 127 | // So, the structure of the photo storage buckets is roughly like this. 128 | // 129 | // photoBucket 130 | // |---metaBucket 131 | // |---daaBucket 132 | ) 133 | pic := &Photo{ 134 | ID: getUUID(), 135 | Type: file.Ext, 136 | UploadedBy: p.ID, 137 | UploadedAt: time.Now(), 138 | UpdatedAt: time.Now(), 139 | } 140 | data, err := encodePhoto(file) 141 | if err != nil { 142 | return nil, err 143 | } 144 | pic.Size = len(data) 145 | err = marshalAndCreate(db, pic, photoBucket, pic.ID, metaBucket) 146 | if err != nil { 147 | return nil, err 148 | } 149 | s := db.Create(photoBucket, pic.ID, data, dataBucket) 150 | if s.Error != nil { 151 | return nil, s.Error 152 | } 153 | return pic, nil 154 | } 155 | 156 | // extracts file extension. 157 | func getFileExt(file multipart.File) (string, error) { 158 | buf := make([]byte, 512) 159 | _, err := file.Read(buf) 160 | defer file.Seek(0, 0) 161 | if err != nil { 162 | return "", err 163 | } 164 | f := http.DetectContentType(buf) 165 | switch f { 166 | case "image/jpeg", "image/jpg": 167 | return "jpg", nil 168 | case "image/png": 169 | return "png", nil 170 | default: 171 | return "", fmt.Errorf("aurora: file %s not supported", f) 172 | } 173 | } 174 | 175 | // Returns *FileUpload from the given multipart data. There is nothing fancy here, only that 176 | // We need to get the file extension. 177 | func getUploadFile(file multipart.File) (*FileUpload, error) { 178 | ext, err := getFileExt(file) 179 | if err != nil { 180 | return nil, err 181 | } 182 | return &FileUpload{&file, ext}, nil 183 | } 184 | 185 | // encodes a given photo, and returns a []byte of the photo. It currently supports 186 | // png, and jpeg formats. The encoded data is the one which will be stored in the database. 187 | func encodePhoto(file *FileUpload) ([]byte, error) { 188 | ext := file.Ext 189 | switch ext { 190 | case "jpg", "jpeg": 191 | img, err := jpeg.Decode(*file.Body) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | // this is supposed to increase the quality of the image. But I'm not sure 197 | // yet if it is necessary or we should just put nil, which will result into 198 | // using default values. 199 | opts := jpeg.Options{Quality: 98} 200 | 201 | buf := new(bytes.Buffer) 202 | jpeg.Encode(buf, img, &opts) 203 | return buf.Bytes(), nil 204 | case "png", "PNG": 205 | img, err := png.Decode(*file.Body) 206 | if err != nil { 207 | return nil, err 208 | } 209 | buf := new(bytes.Buffer) 210 | png.Encode(buf, img) 211 | return buf.Bytes(), nil 212 | } 213 | return nil, errors.New("aurora: file not supported") 214 | } 215 | -------------------------------------------------------------------------------- /uploads_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "mime/multipart" 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/gernest/nutz" 13 | ) 14 | 15 | func TestGetFileUpload(t *testing.T) { 16 | jpegFile := "me.jpg" 17 | fieldName := "profile" 18 | pngFile := "mint.png" 19 | 20 | req, err := requestWithFile(jpegFile) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | f, err := GetFileUpload(req, fieldName) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | checkExtension(f, "jpg", t) 29 | 30 | f, err = GetFileUpload(req, "nothere") 31 | if err == nil { 32 | t.Error("Expected an error, got nil instead") 33 | } 34 | if f != nil { 35 | t.Errorf("Expected nil, got %v", f) 36 | } 37 | 38 | req1, err := requestWithFile(pngFile) 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | f, err = GetFileUpload(req1, fieldName) 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | checkExtension(f, "png", t) 47 | } 48 | 49 | func TestGetMultipleFileUpload(t *testing.T) { 50 | fileName := "me.jpg" 51 | req := requestMuliFile(fileName, t) 52 | files, err := GetMultipleFileUpload(req, "photos") 53 | if err != nil { 54 | list := err.(listErr) 55 | if len(list) != 2 { 56 | t.Errorf("Expected two errors got %d", len(list)) 57 | } 58 | if len(files) != 3 { 59 | t.Errorf("Expected 3 files got %d", len(files)) 60 | } 61 | if len(files) == 3 { 62 | xt := "jpg" 63 | for _, v := range files { 64 | checkExtension(v, xt, t) 65 | } 66 | } 67 | } 68 | if len(files) != 3 { 69 | t.Errorf("Expected 3 files got %d", len(files)) 70 | } 71 | if len(files) == 3 { 72 | xt := "jpg" 73 | for _, v := range files { 74 | checkExtension(v, xt, t) 75 | } 76 | } 77 | 78 | files, err = GetMultipleFileUpload(req, "nothere") 79 | if err == nil { 80 | t.Error("Expected an error, got nil instead") 81 | } 82 | 83 | req1, err := requestMultiWithoutErr() 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | files, err = GetMultipleFileUpload(req1, "photos") 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | if len(files) != 3 { 92 | t.Errorf("Expected 3 files got %d", len(files)) 93 | } 94 | } 95 | func TestSaveUploadFile(t *testing.T) { 96 | pBucket := "profiles" 97 | id := "db0668ac-7eba-40dd-56ee-0b1c0b9b415p" 98 | uploadsDB := "fixture/uploads.bdb" 99 | 100 | pdb := nutz.NewStorage(uploadsDB, 0600, nil) 101 | defer pdb.DeleteDatabase() 102 | 103 | //jpg 104 | req, err := requestWithFile("me.jpg") 105 | if err != nil { 106 | t.Error(err) 107 | } 108 | f, err := GetFileUpload(req, "profile") 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | checkExtension(f, "jpg", t) 113 | 114 | err = CreateProfile(pdb, &Profile{ID: id}, pBucket) 115 | if err != nil { 116 | t.Error(err) 117 | } 118 | p, err := GetProfile(pdb, pBucket, id) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | pic, err := SaveUploadFile(pdb, f, p) 123 | if err != nil { 124 | t.Error(err) 125 | } 126 | if f.Ext != pic.Type { 127 | t.Errorf(" checking file type: expected %s got %s", f.Ext, pic.Type) 128 | } 129 | 130 | // png 131 | req1, err := requestWithFile("mint.png") 132 | if err != nil { 133 | t.Error(err) 134 | } 135 | f, err = GetFileUpload(req1, "profile") 136 | if err != nil { 137 | t.Error(err) 138 | } 139 | checkExtension(f, "png", t) 140 | pic, err = SaveUploadFile(pdb, f, p) 141 | if err != nil { 142 | t.Error(err) 143 | } 144 | if f.Ext != pic.Type { 145 | t.Errorf("checking file type: expected %s got %s", f.Ext, pic.Type) 146 | } 147 | 148 | } 149 | 150 | func checkExtension(f *FileUpload, ext string, t *testing.T) { 151 | rext, err := getFileExt(*f.Body) 152 | if err != nil { 153 | t.Error(err) 154 | } 155 | if rext != ext { 156 | t.Errorf(" checking file extension: expected %s got %s", ext, rext) 157 | } 158 | } 159 | 160 | func requestWithFile(fileName string) (*http.Request, error) { 161 | buf := &bytes.Buffer{} 162 | w := multipart.NewWriter(buf) 163 | public := "public/img/" 164 | 165 | defer w.Close() 166 | f, err := ioutil.ReadFile(fmt.Sprintf("%s%s", public, fileName)) 167 | if err != nil { 168 | return nil, err 169 | } 170 | ww, err := w.CreateFormFile("profile", "me.jpg") 171 | if err != nil { 172 | return nil, err 173 | } 174 | ww.Write(f) 175 | req, err := http.NewRequest("POST", "http://bogus.com", buf) 176 | req.Header.Set("Content-Type", w.FormDataContentType()) 177 | return req, nil 178 | } 179 | 180 | func requestMuliFile(fileName string, t *testing.T) *http.Request { 181 | var ( 182 | kind = "multi" 183 | testURL = "http://bogus.com" 184 | cType = "Content-Type" 185 | ) 186 | content, contentType := testUpData(fileName, kind, t) 187 | req, err := http.NewRequest("POST", testURL, content) 188 | if err != nil { 189 | t.Error(err) 190 | } 191 | req.Header.Set(cType, contentType) 192 | return req 193 | } 194 | 195 | func requestMultiWithoutErr() (*http.Request, error) { 196 | var ( 197 | buf = &bytes.Buffer{} 198 | w = multipart.NewWriter(buf) 199 | fileName = "public/img/me.jpg" 200 | testURL = "http://bogus.com" 201 | fieldName = "photos" 202 | cType = "Content-Type" 203 | f []byte 204 | err error 205 | req *http.Request 206 | ) 207 | defer w.Close() 208 | 209 | f, err = ioutil.ReadFile(fileName) 210 | if err != nil { 211 | return nil, err 212 | } 213 | first, err := w.CreateFormFile(fieldName, "home.jpg") 214 | if err != nil { 215 | return nil, err 216 | } 217 | first.Write(f) 218 | second, err := w.CreateFormFile(fieldName, "baby.jpg") 219 | if err != nil { 220 | return nil, err 221 | } 222 | second.Write(f) 223 | third, err := w.CreateFormFile(fieldName, "wanker.jpg") 224 | if err != nil { 225 | return nil, err 226 | } 227 | third.Write(f) 228 | req, err = http.NewRequest("POST", testURL, buf) 229 | req.Header.Set(cType, w.FormDataContentType()) 230 | return req, nil 231 | } 232 | 233 | func TestListErr(t *testing.T) { 234 | var err listErr 235 | hello := errors.New("hello") 236 | world := errors.New("wordl") 237 | err = append(err, hello, world) 238 | if err.Error() != hello.Error()+", "+world.Error() { 239 | t.Errorf("lisErr: expected %s, %s got %s", hello.Error(), world.Error(), err.Error()) 240 | } 241 | } 242 | 243 | func testUpData(fileName, kind string, t *testing.T) (*bytes.Buffer, string) { 244 | var ( 245 | buf = &bytes.Buffer{} 246 | w = multipart.NewWriter(buf) 247 | public = "public/img/" 248 | kindMulti = "multi" 249 | kindSingle = "single" 250 | multiFieldName = "photos" 251 | singleFieldName = "profile" 252 | f []byte 253 | err error 254 | ) 255 | 256 | defer w.Close() 257 | switch kind { 258 | case kindMulti: 259 | f, err = ioutil.ReadFile(fmt.Sprintf("%s%s", public, fileName)) 260 | if err != nil { 261 | t.Error(err) 262 | } 263 | first, err := w.CreateFormFile(multiFieldName, "home.jpg") 264 | if err != nil { 265 | t.Error(err) 266 | } 267 | first.Write(f) 268 | second, err := w.CreateFormFile(multiFieldName, "baby.jpg") 269 | if err != nil { 270 | t.Error(err) 271 | } 272 | second.Write(f) 273 | third, err := w.CreateFormFile(multiFieldName, "wanker.jpg") 274 | if err != nil { 275 | t.Error(err) 276 | } 277 | third.Write(f) 278 | fourth, err := w.CreateFormFile(multiFieldName, "wankerer.jpg") 279 | if err != nil { 280 | t.Error(err) 281 | } 282 | fourth.Write([]byte("shit")) 283 | 284 | fifth, err := w.CreateFormFile(multiFieldName, "wankeroma.jpg") 285 | if err != nil { 286 | t.Error(err) 287 | } 288 | fifth.Write([]byte("shit")) 289 | case kindSingle: 290 | f, err = ioutil.ReadFile(fmt.Sprintf("%s%s", public, fileName)) 291 | if err != nil { 292 | t.Error(err) 293 | } 294 | ww, err := w.CreateFormFile(singleFieldName, "me.jpg") 295 | if err != nil { 296 | t.Error(err) 297 | } 298 | ww.Write(f) 299 | } 300 | return buf, w.FormDataContentType() 301 | } 302 | -------------------------------------------------------------------------------- /msg.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/muesli/cache2go" 9 | 10 | "github.com/gernest/golem" 11 | ) 12 | 13 | const ( 14 | mainRoom = "aurora" 15 | 16 | // events 17 | sendEvt = "send" 18 | receiveEvt = "receive" 19 | sendFailedEvt = "sendFailed" 20 | ignoreEvt = "ignore" 21 | infoEvt = "info" 22 | readEvt = "read" 23 | 24 | // message buckets 25 | outboxBucket = "outbox" 26 | inboxBucket = "inbox" 27 | draftBucket = "drafts" 28 | readBucket = "read" 29 | 30 | // message allerts 31 | alertSendSuccess = "sendSuccess" 32 | alertSendFailed = "sendFailled" 33 | alertInbox = "messageInbox" 34 | alertRead = "messageRead" 35 | 36 | // cache 37 | onlineCache = "online" 38 | ) 39 | 40 | // MSG this is the base message exchanged between users 41 | type MSG struct { 42 | ID string `json:"id"` 43 | SenderID string `json:"sender_id"` 44 | RecipientID string `json:"recepient_id"` 45 | Text string `json:"text"` 46 | SentAt time.Time `json:"sent_at"` 47 | ReceivedAt time.Time `json:"received_at"` 48 | Status int `json:"status"` 49 | SenderName string `json:"sender_name"` 50 | } 51 | 52 | // InfoMSG this is for sharing information across the messenger nodes 53 | type InfoMSG struct { 54 | Title string `json:"title"` 55 | Body string `json:"body"` 56 | Sender string `json:"sender"` 57 | } 58 | 59 | // Messenger the messanger from the gods 60 | type Messenger struct { 61 | rx *Remix 62 | rm *golem.RoomManager 63 | route *golem.Router 64 | online *cache2go.CacheTable 65 | } 66 | 67 | // NewMessenger creates a new messenger 68 | func NewMessenger(rx *Remix) *Messenger { 69 | return &Messenger{ 70 | rx: rx, 71 | rm: golem.NewRoomManager(), 72 | route: golem.NewRouter(), 73 | online: cache2go.Cache(onlineCache), 74 | } 75 | } 76 | 77 | func (m *Messenger) validateSession(w http.ResponseWriter, r *http.Request) bool { 78 | if ss, ok := m.rx.isInSession(r); ok && !ss.IsNew { 79 | return true 80 | } 81 | return false 82 | } 83 | 84 | // add user to online user's list 85 | func (m *Messenger) onConnect(conn *golem.Connection, r *http.Request) { 86 | if ss, ok := m.rx.isInSession(r); ok && !ss.IsNew { 87 | _, p, err := m.rx.getCurrentUserAndProfile(ss) 88 | if err == nil { 89 | conn.UserID = p.ID 90 | conn.SetSendCallBack(m.callMeBack) 91 | m.rm.Join(mainRoom, conn) 92 | m.rm.Join(p.ID, conn) 93 | m.online.Add(p.ID, 0, p) 94 | } 95 | } 96 | } 97 | 98 | func (m *Messenger) callMeBack(conn *golem.Connection, msg *golem.Message) *golem.Message { 99 | p := m.currentUser(conn) 100 | switch msg.GetEvent() { 101 | case sendEvt: 102 | switch data := msg.GetData().(type) { 103 | case *MSG: 104 | if p != nil { 105 | if p.ID == data.SenderID { 106 | data.SenderName = fmt.Sprintf("%s %s", p.FirstName, p.LastName) 107 | data.SentAt = time.Now() 108 | err := m.saveMsg(outboxBucket, p.ID, data) 109 | if err != nil { 110 | data.Status = http.StatusInternalServerError 111 | return setMSG(alertSendFailed, data, msg) 112 | } 113 | if m.isOnline(data.RecipientID) { 114 | m.rm.Emit(data.RecipientID, receiveEvt, data) 115 | data.Status = http.StatusOK 116 | return setMSG(alertSendSuccess, data, msg) 117 | } 118 | err = m.saveMsg(inboxBucket, data.RecipientID, data) 119 | if err != nil { 120 | data.Status = http.StatusInternalServerError 121 | return setMSG(alertSendFailed, data, msg) 122 | } 123 | data.Status = http.StatusOK 124 | return setMSG(alertSendSuccess, data, msg) 125 | } 126 | 127 | } 128 | } 129 | case receiveEvt: 130 | switch data := msg.GetData().(type) { 131 | case *MSG: 132 | if p != nil { 133 | if p.ID == data.RecipientID { 134 | data.ReceivedAt = time.Now() 135 | err := m.saveMsg(inboxBucket, p.ID, data) 136 | if err != nil { 137 | msg.SetEvent(ignoreEvt) 138 | if m.isOnline(data.SenderID) { 139 | m.rm.Emit(data.SenderID, sendFailedEvt, data) 140 | return msg 141 | } 142 | err = m.moveTo(draftBucket, outboxBucket, data.SenderID, data.ID) 143 | if err != nil { 144 | // TODO: log this? 145 | } 146 | return msg 147 | } 148 | data.Status = http.StatusOK 149 | return setMSG(alertInbox, data, msg) 150 | } 151 | } 152 | } 153 | case sendFailedEvt: 154 | switch data := msg.GetData().(type) { 155 | case *MSG: 156 | if p != nil && data.SenderID == p.ID { 157 | err := m.moveTo(draftBucket, outboxBucket, p.ID, data.ID) 158 | if err != nil { 159 | // TODO: log this? 160 | } 161 | return setMSG(alertSendFailed, nil, msg) 162 | } 163 | 164 | } 165 | case readEvt: 166 | switch data := msg.GetData().(type) { 167 | case *MSG: 168 | if p != nil && data.RecipientID == p.ID { 169 | err := m.moveTo(readBucket, inboxBucket, p.ID, data.ID) 170 | if err != nil { 171 | // TODO: log this? 172 | } 173 | return setMSG(alertRead, nil, msg) 174 | } 175 | 176 | } 177 | 178 | } 179 | return msg 180 | } 181 | 182 | // persist a message 183 | func (m *Messenger) saveMsg(bucket string, profileID string, msg *MSG) error { 184 | 185 | if msg.ID == "" { 186 | msg.ID = getUUID() 187 | } 188 | pdb := getProfileDatabase(m.rx.cfg.DBDir, profileID, m.rx.cfg.DBExtension) 189 | mdb := setDB(m.rx.db, pdb) 190 | return marshalAndCreate(mdb, msg, bucket, msg.ID, m.rx.cfg.MessagesBucket) 191 | } 192 | 193 | // moves message data from one bucket to another. 194 | func (m *Messenger) moveTo(dest, src, profileID, msgID string) error { 195 | pdb := getProfileDatabase(m.rx.cfg.DBDir, profileID, m.rx.cfg.DBExtension) 196 | mdb := setDB(m.rx.db, pdb) 197 | d := mdb.Get(src, msgID, m.rx.cfg.MessagesBucket) 198 | if d.Error != nil { 199 | return d.Error 200 | } 201 | s := mdb.Create(dest, msgID, d.Data, m.rx.cfg.MessagesBucket) 202 | if s.Error != nil { 203 | return s.Error 204 | } 205 | return mdb.Delete(src, msgID, m.rx.cfg.MessagesBucket).Error 206 | } 207 | 208 | // gets the user's profile of a given websocket connection. 209 | func (m *Messenger) currentUser(conn *golem.Connection) *Profile { 210 | pdb := getProfileDatabase(m.rx.cfg.DBDir, conn.UserID, m.rx.cfg.DBExtension) 211 | mdb := setDB(m.rx.db, pdb) 212 | p, err := GetProfile(mdb, m.rx.cfg.ProfilesBucket, conn.UserID) 213 | if err != nil { 214 | // log this 215 | return nil 216 | } 217 | return p 218 | } 219 | 220 | // sets the event and data attributes of a given MSG. 221 | func setMSG(evt string, data interface{}, msg *golem.Message) *golem.Message { 222 | if evt != "" { 223 | msg.SetEvent(evt) 224 | } 225 | if data != nil { 226 | msg.SetData(data) 227 | } 228 | return msg 229 | } 230 | 231 | // sends an info message 232 | func (m *Messenger) info(conn *golem.Connection, msg *InfoMSG) { 233 | m.rm.Emit(mainRoom, infoEvt, msg) 234 | } 235 | 236 | // sends a message 237 | func (m *Messenger) send(conn *golem.Connection, msg *MSG) { 238 | m.rm.Emit(msg.SenderID, sendEvt, msg) 239 | } 240 | 241 | // reading a message. 242 | func (m *Messenger) read(conn *golem.Connection, msg *MSG) { 243 | m.rm.Emit(msg.RecipientID, readEvt, msg) 244 | } 245 | 246 | // when the connection is closed, it makes sure the cache is updated and all the channels 247 | // the given connection was subscribed to are unsubscribed. 248 | func (m *Messenger) onClose(conn *golem.Connection) { 249 | m.online.Delete(conn.UserID) 250 | m.rm.Leave(conn.UserID, conn) 251 | m.rm.Leave(mainRoom, conn) 252 | } 253 | 254 | // checks if the user with a given key is still online. 255 | // it uses the siple cache2go to store online users in memory. 256 | func (m *Messenger) isOnline(key string) bool { 257 | return m.online.Exists(key) 258 | } 259 | 260 | // Handler handles websocket connections for messaging 261 | func (m *Messenger) Handler() func(http.ResponseWriter, *http.Request) { 262 | m.route.OnHandshake(m.validateSession) 263 | m.route.OnConnect(m.onConnect) 264 | m.route.OnClose(m.onClose) 265 | m.route.On("info", m.info) 266 | m.route.On("read", m.read) 267 | m.route.On("send", m.send) 268 | return m.route.Handler() 269 | } 270 | -------------------------------------------------------------------------------- /i18n.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | //ISO3166List based on https://www.iso.org/obp/ui/#search/code/ Code Type "Officially Assigned Codes" 4 | var ISO3166List = []ISO3166Entry{ 5 | {"Afghanistan", "Afghanistan (l')", "AF", "AFG", "004"}, 6 | {"Albania", "Albanie (l')", "AL", "ALB", "008"}, 7 | {"Antarctica", "Antarctique (l')", "AQ", "ATA", "010"}, 8 | {"Algeria", "Algérie (l')", "DZ", "DZA", "012"}, 9 | {"American Samoa", "Samoa américaines (les)", "AS", "ASM", "016"}, 10 | {"Andorra", "Andorre (l')", "AD", "AND", "020"}, 11 | {"Angola", "Angola (l')", "AO", "AGO", "024"}, 12 | {"Antigua and Barbuda", "Antigua-et-Barbuda", "AG", "ATG", "028"}, 13 | {"Azerbaijan", "Azerbaïdjan (l')", "AZ", "AZE", "031"}, 14 | {"Argentina", "Argentine (l')", "AR", "ARG", "032"}, 15 | {"Australia", "Australie (l')", "AU", "AUS", "036"}, 16 | {"Austria", "Autriche (l')", "AT", "AUT", "040"}, 17 | {"Bahamas (the)", "Bahamas (les)", "BS", "BHS", "044"}, 18 | {"Bahrain", "Bahreïn", "BH", "BHR", "048"}, 19 | {"Bangladesh", "Bangladesh (le)", "BD", "BGD", "050"}, 20 | {"Armenia", "Arménie (l')", "AM", "ARM", "051"}, 21 | {"Barbados", "Barbade (la)", "BB", "BRB", "052"}, 22 | {"Belgium", "Belgique (la)", "BE", "BEL", "056"}, 23 | {"Bermuda", "Bermudes (les)", "BM", "BMU", "060"}, 24 | {"Bhutan", "Bhoutan (le)", "BT", "BTN", "064"}, 25 | {"Bolivia (Plurinational State of)", "Bolivie (État plurinational de)", "BO", "BOL", "068"}, 26 | {"Bosnia and Herzegovina", "Bosnie-Herzégovine (la)", "BA", "BIH", "070"}, 27 | {"Botswana", "Botswana (le)", "BW", "BWA", "072"}, 28 | {"Bouvet Island", "Bouvet (l'Île)", "BV", "BVT", "074"}, 29 | {"Brazil", "Brésil (le)", "BR", "BRA", "076"}, 30 | {"Belize", "Belize (le)", "BZ", "BLZ", "084"}, 31 | {"British Indian Ocean Territory (the)", "Indien (le Territoire britannique de l'océan)", "IO", "IOT", "086"}, 32 | {"Solomon Islands", "Salomon (Îles)", "SB", "SLB", "090"}, 33 | {"Virgin Islands (British)", "Vierges britanniques (les Îles)", "VG", "VGB", "092"}, 34 | {"Brunei Darussalam", "Brunéi Darussalam (le)", "BN", "BRN", "096"}, 35 | {"Bulgaria", "Bulgarie (la)", "BG", "BGR", "100"}, 36 | {"Myanmar", "Myanmar (le)", "MM", "MMR", "104"}, 37 | {"Burundi", "Burundi (le)", "BI", "BDI", "108"}, 38 | {"Belarus", "Bélarus (le)", "BY", "BLR", "112"}, 39 | {"Cambodia", "Cambodge (le)", "KH", "KHM", "116"}, 40 | {"Cameroon", "Cameroun (le)", "CM", "CMR", "120"}, 41 | {"Canada", "Canada (le)", "CA", "CAN", "124"}, 42 | {"Cabo Verde", "Cabo Verde", "CV", "CPV", "132"}, 43 | {"Cayman Islands (the)", "Caïmans (les Îles)", "KY", "CYM", "136"}, 44 | {"Central African Republic (the)", "République centrafricaine (la)", "CF", "CAF", "140"}, 45 | {"Sri Lanka", "Sri Lanka", "LK", "LKA", "144"}, 46 | {"Chad", "Tchad (le)", "TD", "TCD", "148"}, 47 | {"Chile", "Chili (le)", "CL", "CHL", "152"}, 48 | {"China", "Chine (la)", "CN", "CHN", "156"}, 49 | {"Taiwan (Province of China)", "Taïwan (Province de Chine)", "TW", "TWN", "158"}, 50 | {"Christmas Island", "Christmas (l'Île)", "CX", "CXR", "162"}, 51 | {"Cocos (Keeling) Islands (the)", "Cocos (les Îles)/ Keeling (les Îles)", "CC", "CCK", "166"}, 52 | {"Colombia", "Colombie (la)", "CO", "COL", "170"}, 53 | {"Comoros (the)", "Comores (les)", "KM", "COM", "174"}, 54 | {"Mayotte", "Mayotte", "YT", "MYT", "175"}, 55 | {"Congo (the)", "Congo (le)", "CG", "COG", "178"}, 56 | {"Congo (the Democratic Republic of the)", "Congo (la République démocratique du)", "CD", "COD", "180"}, 57 | {"Cook Islands (the)", "Cook (les Îles)", "CK", "COK", "184"}, 58 | {"Costa Rica", "Costa Rica (le)", "CR", "CRI", "188"}, 59 | {"Croatia", "Croatie (la)", "HR", "HRV", "191"}, 60 | {"Cuba", "Cuba", "CU", "CUB", "192"}, 61 | {"Cyprus", "Chypre", "CY", "CYP", "196"}, 62 | {"Czech Republic (the)", "tchèque (la République)", "CZ", "CZE", "203"}, 63 | {"Benin", "Bénin (le)", "BJ", "BEN", "204"}, 64 | {"Denmark", "Danemark (le)", "DK", "DNK", "208"}, 65 | {"Dominica", "Dominique (la)", "DM", "DMA", "212"}, 66 | {"Dominican Republic (the)", "dominicaine (la République)", "DO", "DOM", "214"}, 67 | {"Ecuador", "Équateur (l')", "EC", "ECU", "218"}, 68 | {"El Salvador", "El Salvador", "SV", "SLV", "222"}, 69 | {"Equatorial Guinea", "Guinée équatoriale (la)", "GQ", "GNQ", "226"}, 70 | {"Ethiopia", "Éthiopie (l')", "ET", "ETH", "231"}, 71 | {"Eritrea", "Érythrée (l')", "ER", "ERI", "232"}, 72 | {"Estonia", "Estonie (l')", "EE", "EST", "233"}, 73 | {"Faroe Islands (the)", "Féroé (les Îles)", "FO", "FRO", "234"}, 74 | {"Falkland Islands (the) [Malvinas]", "Falkland (les Îles)/Malouines (les Îles)", "FK", "FLK", "238"}, 75 | {"South Georgia and the South Sandwich Islands", "Géorgie du Sud-et-les Îles Sandwich du Sud (la)", "GS", "SGS", "239"}, 76 | {"Fiji", "Fidji (les)", "FJ", "FJI", "242"}, 77 | {"Finland", "Finlande (la)", "FI", "FIN", "246"}, 78 | {"Åland Islands", "Åland(les Îles)", "AX", "ALA", "248"}, 79 | {"France", "France (la)", "FR", "FRA", "250"}, 80 | {"French Guiana", "Guyane française (la )", "GF", "GUF", "254"}, 81 | {"French Polynesia", "Polynésie française (la)", "PF", "PYF", "258"}, 82 | {"French Southern Territories (the)", "Terres australes françaises (les)", "TF", "ATF", "260"}, 83 | {"Djibouti", "Djibouti", "DJ", "DJI", "262"}, 84 | {"Gabon", "Gabon (le)", "GA", "GAB", "266"}, 85 | {"Georgia", "Géorgie (la)", "GE", "GEO", "268"}, 86 | {"Gambia (the)", "Gambie (la)", "GM", "GMB", "270"}, 87 | {"Palestine, State of", "Palestine, État de", "PS", "PSE", "275"}, 88 | {"Germany", "Allemagne (l')", "DE", "DEU", "276"}, 89 | {"Ghana", "Ghana (le)", "GH", "GHA", "288"}, 90 | {"Gibraltar", "Gibraltar", "GI", "GIB", "292"}, 91 | {"Kiribati", "Kiribati", "KI", "KIR", "296"}, 92 | {"Greece", "Grèce (la)", "GR", "GRC", "300"}, 93 | {"Greenland", "Groenland (le)", "GL", "GRL", "304"}, 94 | {"Grenada", "Grenade (la)", "GD", "GRD", "308"}, 95 | {"Guadeloupe", "Guadeloupe (la)", "GP", "GLP", "312"}, 96 | {"Guam", "Guam", "GU", "GUM", "316"}, 97 | {"Guatemala", "Guatemala (le)", "GT", "GTM", "320"}, 98 | {"Guinea", "Guinée (la)", "GN", "GIN", "324"}, 99 | {"Guyana", "Guyana (le)", "GY", "GUY", "328"}, 100 | {"Haiti", "Haïti", "HT", "HTI", "332"}, 101 | {"Heard Island and McDonald Islands", "Heard-et-Îles MacDonald (l'Île)", "HM", "HMD", "334"}, 102 | {"Holy See (the)", "Saint-Siège (le)", "VA", "VAT", "336"}, 103 | {"Honduras", "Honduras (le)", "HN", "HND", "340"}, 104 | {"Hong Kong", "Hong Kong", "HK", "HKG", "344"}, 105 | {"Hungary", "Hongrie (la)", "HU", "HUN", "348"}, 106 | {"Iceland", "Islande (l')", "IS", "ISL", "352"}, 107 | {"India", "Inde (l')", "IN", "IND", "356"}, 108 | {"Indonesia", "Indonésie (l')", "ID", "IDN", "360"}, 109 | {"Iran (Islamic Republic of)", "Iran (République Islamique d')", "IR", "IRN", "364"}, 110 | {"Iraq", "Iraq (l')", "IQ", "IRQ", "368"}, 111 | {"Ireland", "Irlande (l')", "IE", "IRL", "372"}, 112 | {"Israel", "Israël", "IL", "ISR", "376"}, 113 | {"Italy", "Italie (l')", "IT", "ITA", "380"}, 114 | {"Côte d'Ivoire", "Côte d'Ivoire (la)", "CI", "CIV", "384"}, 115 | {"Jamaica", "Jamaïque (la)", "JM", "JAM", "388"}, 116 | {"Japan", "Japon (le)", "JP", "JPN", "392"}, 117 | {"Kazakhstan", "Kazakhstan (le)", "KZ", "KAZ", "398"}, 118 | {"Jordan", "Jordanie (la)", "JO", "JOR", "400"}, 119 | {"Kenya", "Kenya (le)", "KE", "KEN", "404"}, 120 | {"Korea (the Democratic People's Republic of)", "Corée (la République populaire démocratique de)", "KP", "PRK", "408"}, 121 | {"Korea (the Republic of)", "Corée (la République de)", "KR", "KOR", "410"}, 122 | {"Kuwait", "Koweït (le)", "KW", "KWT", "414"}, 123 | {"Kyrgyzstan", "Kirghizistan (le)", "KG", "KGZ", "417"}, 124 | {"Lao People's Democratic Republic (the)", "Lao, République démocratique populaire", "LA", "LAO", "418"}, 125 | {"Lebanon", "Liban (le)", "LB", "LBN", "422"}, 126 | {"Lesotho", "Lesotho (le)", "LS", "LSO", "426"}, 127 | {"Latvia", "Lettonie (la)", "LV", "LVA", "428"}, 128 | {"Liberia", "Libéria (le)", "LR", "LBR", "430"}, 129 | {"Libya", "Libye (la)", "LY", "LBY", "434"}, 130 | {"Liechtenstein", "Liechtenstein (le)", "LI", "LIE", "438"}, 131 | {"Lithuania", "Lituanie (la)", "LT", "LTU", "440"}, 132 | {"Luxembourg", "Luxembourg (le)", "LU", "LUX", "442"}, 133 | {"Macao", "Macao", "MO", "MAC", "446"}, 134 | {"Madagascar", "Madagascar", "MG", "MDG", "450"}, 135 | {"Malawi", "Malawi (le)", "MW", "MWI", "454"}, 136 | {"Malaysia", "Malaisie (la)", "MY", "MYS", "458"}, 137 | {"Maldives", "Maldives (les)", "MV", "MDV", "462"}, 138 | {"Mali", "Mali (le)", "ML", "MLI", "466"}, 139 | {"Malta", "Malte", "MT", "MLT", "470"}, 140 | {"Martinique", "Martinique (la)", "MQ", "MTQ", "474"}, 141 | {"Mauritania", "Mauritanie (la)", "MR", "MRT", "478"}, 142 | {"Mauritius", "Maurice", "MU", "MUS", "480"}, 143 | {"Mexico", "Mexique (le)", "MX", "MEX", "484"}, 144 | {"Monaco", "Monaco", "MC", "MCO", "492"}, 145 | {"Mongolia", "Mongolie (la)", "MN", "MNG", "496"}, 146 | {"Moldova (the Republic of)", "Moldova , République de", "MD", "MDA", "498"}, 147 | {"Montenegro", "Monténégro (le)", "ME", "MNE", "499"}, 148 | {"Montserrat", "Montserrat", "MS", "MSR", "500"}, 149 | {"Morocco", "Maroc (le)", "MA", "MAR", "504"}, 150 | {"Mozambique", "Mozambique (le)", "MZ", "MOZ", "508"}, 151 | {"Oman", "Oman", "OM", "OMN", "512"}, 152 | {"Namibia", "Namibie (la)", "NA", "NAM", "516"}, 153 | {"Nauru", "Nauru", "NR", "NRU", "520"}, 154 | {"Nepal", "Népal (le)", "NP", "NPL", "524"}, 155 | {"Netherlands (the)", "Pays-Bas (les)", "NL", "NLD", "528"}, 156 | {"Curaçao", "Curaçao", "CW", "CUW", "531"}, 157 | {"Aruba", "Aruba", "AW", "ABW", "533"}, 158 | {"Sint Maarten (Dutch part)", "Saint-Martin (partie néerlandaise)", "SX", "SXM", "534"}, 159 | {"Bonaire, Sint Eustatius and Saba", "Bonaire, Saint-Eustache et Saba", "BQ", "BES", "535"}, 160 | {"New Caledonia", "Nouvelle-Calédonie (la)", "NC", "NCL", "540"}, 161 | {"Vanuatu", "Vanuatu (le)", "VU", "VUT", "548"}, 162 | {"New Zealand", "Nouvelle-Zélande (la)", "NZ", "NZL", "554"}, 163 | {"Nicaragua", "Nicaragua (le)", "NI", "NIC", "558"}, 164 | {"Niger (the)", "Niger (le)", "NE", "NER", "562"}, 165 | {"Nigeria", "Nigéria (le)", "NG", "NGA", "566"}, 166 | {"Niue", "Niue", "NU", "NIU", "570"}, 167 | {"Norfolk Island", "Norfolk (l'Île)", "NF", "NFK", "574"}, 168 | {"Norway", "Norvège (la)", "NO", "NOR", "578"}, 169 | {"Northern Mariana Islands (the)", "Mariannes du Nord (les Îles)", "MP", "MNP", "580"}, 170 | {"United States Minor Outlying Islands (the)", "Îles mineures éloignées des États-Unis (les)", "UM", "UMI", "581"}, 171 | {"Micronesia (Federated States of)", "Micronésie (États fédérés de)", "FM", "FSM", "583"}, 172 | {"Marshall Islands (the)", "Marshall (Îles)", "MH", "MHL", "584"}, 173 | {"Palau", "Palaos (les)", "PW", "PLW", "585"}, 174 | {"Pakistan", "Pakistan (le)", "PK", "PAK", "586"}, 175 | {"Panama", "Panama (le)", "PA", "PAN", "591"}, 176 | {"Papua New Guinea", "Papouasie-Nouvelle-Guinée (la)", "PG", "PNG", "598"}, 177 | {"Paraguay", "Paraguay (le)", "PY", "PRY", "600"}, 178 | {"Peru", "Pérou (le)", "PE", "PER", "604"}, 179 | {"Philippines (the)", "Philippines (les)", "PH", "PHL", "608"}, 180 | {"Pitcairn", "Pitcairn", "PN", "PCN", "612"}, 181 | {"Poland", "Pologne (la)", "PL", "POL", "616"}, 182 | {"Portugal", "Portugal (le)", "PT", "PRT", "620"}, 183 | {"Guinea-Bissau", "Guinée-Bissau (la)", "GW", "GNB", "624"}, 184 | {"Timor-Leste", "Timor-Leste (le)", "TL", "TLS", "626"}, 185 | {"Puerto Rico", "Porto Rico", "PR", "PRI", "630"}, 186 | {"Qatar", "Qatar (le)", "QA", "QAT", "634"}, 187 | {"Réunion", "Réunion (La)", "RE", "REU", "638"}, 188 | {"Romania", "Roumanie (la)", "RO", "ROU", "642"}, 189 | {"Russian Federation (the)", "Russie (la Fédération de)", "RU", "RUS", "643"}, 190 | {"Rwanda", "Rwanda (le)", "RW", "RWA", "646"}, 191 | {"Saint Barthélemy", "Saint-Barthélemy", "BL", "BLM", "652"}, 192 | {"Saint Helena, Ascension and Tristan da Cunha", "Sainte-Hélène, Ascension et Tristan da Cunha", "SH", "SHN", "654"}, 193 | {"Saint Kitts and Nevis", "Saint-Kitts-et-Nevis", "KN", "KNA", "659"}, 194 | {"Anguilla", "Anguilla", "AI", "AIA", "660"}, 195 | {"Saint Lucia", "Sainte-Lucie", "LC", "LCA", "662"}, 196 | {"Saint Martin (French part)", "Saint-Martin (partie française)", "MF", "MAF", "663"}, 197 | {"Saint Pierre and Miquelon", "Saint-Pierre-et-Miquelon", "PM", "SPM", "666"}, 198 | {"Saint Vincent and the Grenadines", "Saint-Vincent-et-les Grenadines", "VC", "VCT", "670"}, 199 | {"San Marino", "Saint-Marin", "SM", "SMR", "674"}, 200 | {"Sao Tome and Principe", "Sao Tomé-et-Principe", "ST", "STP", "678"}, 201 | {"Saudi Arabia", "Arabie saoudite (l')", "SA", "SAU", "682"}, 202 | {"Senegal", "Sénégal (le)", "SN", "SEN", "686"}, 203 | {"Serbia", "Serbie (la)", "RS", "SRB", "688"}, 204 | {"Seychelles", "Seychelles (les)", "SC", "SYC", "690"}, 205 | {"Sierra Leone", "Sierra Leone (la)", "SL", "SLE", "694"}, 206 | {"Singapore", "Singapour", "SG", "SGP", "702"}, 207 | {"Slovakia", "Slovaquie (la)", "SK", "SVK", "703"}, 208 | {"Viet Nam", "Viet Nam (le)", "VN", "VNM", "704"}, 209 | {"Slovenia", "Slovénie (la)", "SI", "SVN", "705"}, 210 | {"Somalia", "Somalie (la)", "SO", "SOM", "706"}, 211 | {"South Africa", "Afrique du Sud (l')", "ZA", "ZAF", "710"}, 212 | {"Zimbabwe", "Zimbabwe (le)", "ZW", "ZWE", "716"}, 213 | {"Spain", "Espagne (l')", "ES", "ESP", "724"}, 214 | {"South Sudan", "Soudan du Sud (le)", "SS", "SSD", "728"}, 215 | {"Sudan (the)", "Soudan (le)", "SD", "SDN", "729"}, 216 | {"Western Sahara*", "Sahara occidental (le)*", "EH", "ESH", "732"}, 217 | {"Suriname", "Suriname (le)", "SR", "SUR", "740"}, 218 | {"Svalbard and Jan Mayen", "Svalbard et l'Île Jan Mayen (le)", "SJ", "SJM", "744"}, 219 | {"Swaziland", "Swaziland (le)", "SZ", "SWZ", "748"}, 220 | {"Sweden", "Suède (la)", "SE", "SWE", "752"}, 221 | {"Switzerland", "Suisse (la)", "CH", "CHE", "756"}, 222 | {"Syrian Arab Republic", "République arabe syrienne (la)", "SY", "SYR", "760"}, 223 | {"Tajikistan", "Tadjikistan (le)", "TJ", "TJK", "762"}, 224 | {"Thailand", "Thaïlande (la)", "TH", "THA", "764"}, 225 | {"Togo", "Togo (le)", "TG", "TGO", "768"}, 226 | {"Tokelau", "Tokelau (les)", "TK", "TKL", "772"}, 227 | {"Tonga", "Tonga (les)", "TO", "TON", "776"}, 228 | {"Trinidad and Tobago", "Trinité-et-Tobago (la)", "TT", "TTO", "780"}, 229 | {"United Arab Emirates (the)", "Émirats arabes unis (les)", "AE", "ARE", "784"}, 230 | {"Tunisia", "Tunisie (la)", "TN", "TUN", "788"}, 231 | {"Turkey", "Turquie (la)", "TR", "TUR", "792"}, 232 | {"Turkmenistan", "Turkménistan (le)", "TM", "TKM", "795"}, 233 | {"Turks and Caicos Islands (the)", "Turks-et-Caïcos (les Îles)", "TC", "TCA", "796"}, 234 | {"Tuvalu", "Tuvalu (les)", "TV", "TUV", "798"}, 235 | {"Uganda", "Ouganda (l')", "UG", "UGA", "800"}, 236 | {"Ukraine", "Ukraine (l')", "UA", "UKR", "804"}, 237 | {"Macedonia (the former Yugoslav Republic of)", "Macédoine (l'ex‑République yougoslave de)", "MK", "MKD", "807"}, 238 | {"Egypt", "Égypte (l')", "EG", "EGY", "818"}, 239 | {"United Kingdom of Great Britain and Northern Ireland (the)", "Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le)", "GB", "GBR", "826"}, 240 | {"Guernsey", "Guernesey", "GG", "GGY", "831"}, 241 | {"Jersey", "Jersey", "JE", "JEY", "832"}, 242 | {"Isle of Man", "Île de Man", "IM", "IMN", "833"}, 243 | {"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"}, 244 | {"United States of America (the)", "États-Unis d'Amérique (les)", "US", "USA", "840"}, 245 | {"Virgin Islands (U.S.)", "Vierges des États-Unis (les Îles)", "VI", "VIR", "850"}, 246 | {"Burkina Faso", "Burkina Faso (le)", "BF", "BFA", "854"}, 247 | {"Uruguay", "Uruguay (l')", "UY", "URY", "858"}, 248 | {"Uzbekistan", "Ouzbékistan (l')", "UZ", "UZB", "860"}, 249 | {"Venezuela (Bolivarian Republic of)", "Venezuela (République bolivarienne du)", "VE", "VEN", "862"}, 250 | {"Wallis and Futuna", "Wallis-et-Futuna", "WF", "WLF", "876"}, 251 | {"Samoa", "Samoa (le)", "WS", "WSM", "882"}, 252 | {"Yemen", "Yémen (le)", "YE", "YEM", "887"}, 253 | {"Zambia", "Zambie (la)", "ZM", "ZMB", "894"}, 254 | } 255 | 256 | // SupportedCountries these are the coutries supported by aurora 257 | var SupportedCountries = []ISO3166Entry{ 258 | {"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"}, 259 | } 260 | 261 | // ISO3166Entry stores country codes 262 | type ISO3166Entry struct { 263 | EnglishShortName string `json:"eglish_short_name"` 264 | FrenchShortName string `json:"french_short_name"` 265 | Alpha2Code string `json:"alpha2code"` 266 | Alpha3Code string `json:"alpha3code"` 267 | Numeric string `json:"numeric"` 268 | } 269 | -------------------------------------------------------------------------------- /remix.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gernest/nutz" 13 | "github.com/gernest/render" 14 | "github.com/gorilla/mux" 15 | "github.com/gorilla/sessions" 16 | ) 17 | 18 | var ( 19 | errNotFound = errors.New("samahani kitu ulichoulizia hatujakipata") 20 | errInternalServer = errors.New("du! naona imezingua, jaribu tena badae") 21 | errForbidden = errors.New("du! hauna ruhususa ya kufika kwenye hii kurasa") 22 | errBadForm = errors.New("du! inaonekana fomu haujaijaza vizuri, tafadhali rudia tena") 23 | ) 24 | 25 | // Remix all the fun is here 26 | type Remix struct { 27 | db nutz.Storage 28 | sess *Session 29 | rendr *render.Render 30 | cfg *RemixConfig 31 | msg *Messenger 32 | } 33 | 34 | // RemixConfig contain configuration values for Remix 35 | type RemixConfig struct { 36 | AppName string `json:"name"` 37 | AppURL string `json:"url"` 38 | CdnMode bool `json:"cdn_mode"` 39 | RunMode string `json:"run_mode"` 40 | AppTitle string `json:"title"` 41 | AppDescription string `json:"description"` 42 | 43 | // path to the directory where databases will be stored 44 | DBDir string `json:"database_dir"` 45 | 46 | AccountsBucket string `json:"accounts_bucket"` 47 | AccountsDB string `json:"accounts_database"` 48 | DBExtension string `json:"database_extension"` 49 | ProfilesBucket string `json:"profiles_bucket"` 50 | 51 | SessionName string `json:"sessions_name"` 52 | SessionsDB string `json:"sessions_database"` 53 | SessionsBucket string `json:"sessions_bucket"` 54 | SessMaxAge int `json:"sessions_max_age"` 55 | SessionPath string `json:"session_path"` 56 | 57 | // The path to point to when login is success 58 | LoginRedirect string `json:"login_redirect"` 59 | 60 | ProfilePicField string `json:"profile_pic_field"` 61 | PhotosField string `json:"photos_field"` 62 | 63 | MessagesBucket string `json:"messages_bucket"` 64 | 65 | TemplatesExtensions []string `json:"templates_extensions"` 66 | TemplatesDir string `json:"templates_dir"` 67 | DevMode bool `json:"dev_mode"` 68 | } 69 | 70 | type jsonUploads struct { 71 | Error string `json:"errors"` 72 | ProfilePic *Photo `json:"profile_photo"` 73 | Photos []*Photo `json:"photos"` 74 | } 75 | type jsonErr struct { 76 | Text string `json:"test"` 77 | } 78 | 79 | // NewRemix iitialize a *Remix instance using the given cfg 80 | func NewRemix(cfg *RemixConfig) *Remix { 81 | secret := []byte("my-top-secret") 82 | rOpts := render.Options{ 83 | Directory: cfg.TemplatesDir, 84 | Extensions: cfg.TemplatesExtensions, 85 | IsDevelopment: cfg.DevMode, 86 | DefaultData: setConfigData(cfg), 87 | } 88 | sOpts := &sessions.Options{ 89 | MaxAge: cfg.SessMaxAge, 90 | Path: cfg.SessionPath, 91 | } 92 | db := nutz.NewStorage(cfg.SessionsDB, 0600, nil) 93 | store := NewSessStore(db, cfg.SessionsBucket, 10, sOpts, secret) 94 | rx := &Remix{ 95 | db: db, 96 | sess: store, 97 | rendr: render.New(rOpts), 98 | cfg: cfg, 99 | } 100 | rx.msg = NewMessenger(rx) 101 | return rx 102 | } 103 | 104 | // Home is where the homepage is 105 | func (rx *Remix) Home(w http.ResponseWriter, r *http.Request) { 106 | data := rx.setSessionData(r) 107 | if ss, ok := rx.isInSession(r); ok { 108 | people, err := rx.getAllProfiles() 109 | if err != nil { 110 | // log this? 111 | } 112 | _, cp, err := rx.getCurrentUserAndProfile(ss) 113 | if err != nil { 114 | // log this? 115 | } 116 | data.Add("user", cp) 117 | data.Add("people", people) 118 | } 119 | rx.rendr.HTML(w, http.StatusOK, "home", data) 120 | return 121 | } 122 | 123 | // Register creates a new user account 124 | func (rx *Remix) Register(w http.ResponseWriter, r *http.Request) { 125 | var ( 126 | data = render.NewTemplateData() 127 | loginPath = "/auth/login" 128 | registerPath = "auth/register" 129 | ok bool 130 | ss *sessions.Session 131 | ) 132 | if ss, ok = rx.isInSession(r); ok { 133 | http.Redirect(w, r, loginPath, http.StatusFound) 134 | return 135 | } 136 | 137 | if r.Method == "GET" { 138 | rx.rendr.HTML(w, http.StatusOK, registerPath, data) 139 | return 140 | } 141 | if r.Method == "POST" { 142 | form := ComposeRegisterForm()(r) 143 | if !form.IsValid() { 144 | data.Add("errors", form.Errors()) 145 | rx.rendr.HTML(w, http.StatusOK, registerPath, data) 146 | return 147 | } 148 | user := form.GetModel().(User) 149 | hash, err := hashPassword(user.Pass) 150 | if err != nil { 151 | rx.rendr.HTML(w, http.StatusInternalServerError, "500", data) 152 | return 153 | } 154 | 155 | user.Pass = hash 156 | user.UUID = getUUID() 157 | err = CreateAccount(setDB(rx.db, rx.cfg.AccountsDB), &user, rx.cfg.AccountsBucket) 158 | if err != nil { 159 | rx.rendr.HTML(w, http.StatusInternalServerError, "500", data) 160 | return 161 | } 162 | flash := NewFlash() 163 | flash.Success("akaunti imefanikiwa kutengenezwa") 164 | flash.Save(ss) 165 | ss.Values["user"] = user.EmailAddress 166 | ss.Values["isAuthorized"] = true 167 | err = ss.Save(r, w) 168 | if err != nil { 169 | rx.rendr.HTML(w, http.StatusInternalServerError, "500", data) 170 | return 171 | } 172 | 173 | // create a new profile 174 | pdb := getProfileDatabase(rx.cfg.DBDir, user.UUID, rx.cfg.DBExtension) 175 | profile := &Profile{ 176 | ID: user.UUID, 177 | FirstName: strings.ToTitle(user.FirstName), 178 | LastName: strings.ToTitle(user.LastName), 179 | } 180 | err = CreateProfile(setDB(rx.db, pdb), profile, rx.cfg.ProfilesBucket) 181 | if err != nil { 182 | // log this 183 | } 184 | http.Redirect(w, r, rx.cfg.LoginRedirect, http.StatusFound) 185 | return 186 | } 187 | 188 | } 189 | 190 | // Login creates new session for a user 191 | func (rx *Remix) Login(w http.ResponseWriter, r *http.Request) { 192 | var ( 193 | data = render.NewTemplateData() 194 | flash = NewFlash() 195 | loginPath = "auth/login" 196 | ok bool 197 | ss *sessions.Session 198 | ) 199 | 200 | if ss, ok = rx.isInSession(r); ok { 201 | http.Redirect(w, r, rx.cfg.LoginRedirect, http.StatusFound) 202 | return 203 | } 204 | if r.Method == "GET" { 205 | fd := flash.Get(ss) 206 | if fd != nil { 207 | data.Add("flash", fd.Data) 208 | } 209 | rx.rendr.HTML(w, http.StatusOK, loginPath, data) 210 | return 211 | } 212 | if r.Method == "POST" { 213 | form := ComposeLoginForm()(r) 214 | if !form.IsValid() { 215 | data.Add("errors", form.Errors()) 216 | rx.rendr.HTML(w, http.StatusOK, loginPath, data) 217 | return 218 | } 219 | 220 | lform := form.GetModel().(loginForm) 221 | user, err := GetUser(setDB(rx.db, rx.cfg.AccountsDB), rx.cfg.AccountsBucket, lform.Email) 222 | if err != nil { 223 | data.Add("error", "email au namba ya siri sio sahihi, tafadhali jaribu tena") 224 | rx.rendr.HTML(w, http.StatusOK, loginPath, data) 225 | return 226 | } 227 | if err = verifyPass(user.Password(), lform.Password); err != nil { 228 | data.Add("error", "email au namba ya siri sio sahihi, tafadhali jaribu tena") 229 | rx.rendr.HTML(w, http.StatusOK, loginPath, data) 230 | return 231 | } 232 | ss, err = rx.sess.New(r, rx.cfg.SessionName) 233 | if err != nil { 234 | //log this 235 | } 236 | ss.Values["user"] = user.EmailAddress 237 | ss.Values["isAuthorized"] = true 238 | err = ss.Save(r, w) 239 | if err != nil { 240 | rx.rendr.HTML(w, http.StatusInternalServerError, "500", data) 241 | return 242 | } 243 | http.Redirect(w, r, rx.cfg.LoginRedirect, http.StatusFound) 244 | return 245 | } 246 | } 247 | 248 | // ServeImages serves images uploaded by users 249 | func (rx *Remix) ServeImages(w http.ResponseWriter, r *http.Request) { 250 | var ( 251 | vars = r.URL.Query() 252 | pic = &Photo{} 253 | imageID = vars.Get("iid") 254 | profileID = vars.Get("pid") 255 | photoBucket = "photos" 256 | metaBucket = "meta" 257 | dataBucket = "data" 258 | ) 259 | 260 | pdb := getProfileDatabase(rx.cfg.DBDir, profileID, rx.cfg.DBExtension) 261 | db := setDB(rx.db, pdb) 262 | 263 | err := getAndUnmarshall(db, photoBucket, imageID, pic, metaBucket) 264 | if err != nil { 265 | http.NotFound(w, r) 266 | return 267 | } 268 | raw := db.Get(photoBucket, imageID, dataBucket) 269 | if raw.Error != nil { 270 | http.NotFound(w, r) 271 | return 272 | } 273 | picName := fmt.Sprintf("%s.%s", pic.ID, pic.Type) 274 | http.ServeContent(w, r, picName, pic.UpdatedAt, bytes.NewReader(raw.Data)) 275 | } 276 | 277 | // Uploads uploads files 278 | func (rx *Remix) Uploads(w http.ResponseWriter, r *http.Request) { 279 | var ( 280 | ok bool 281 | ss *sessions.Session 282 | rst []*Photo 283 | errs listErr 284 | ) 285 | if ss, ok = rx.isInSession(r); !ok { 286 | jr := &jsonUploads{Error: errForbidden.Error()} 287 | rx.rendr.JSON(w, http.StatusForbidden, jr) 288 | return 289 | } 290 | if r.Method == "POST" { 291 | _, profile, err := rx.getCurrentUserAndProfile(ss) 292 | if err != nil { 293 | jr := &jsonUploads{Error: err.Error()} 294 | rx.rendr.JSON(w, http.StatusInternalServerError, jr) 295 | return 296 | } 297 | 298 | pdbStr := getProfileDatabase(rx.cfg.DBDir, profile.ID, rx.cfg.DBExtension) 299 | pdb := setDB(rx.db, pdbStr) 300 | 301 | f, serr := GetFileUpload(r, rx.cfg.ProfilePicField) 302 | if serr == nil { 303 | pic, err := SaveUploadFile(pdb, f, profile) 304 | if err != nil { 305 | jr := &jsonUploads{Error: err.Error()} 306 | rx.rendr.JSON(w, http.StatusInternalServerError, jr) 307 | return 308 | } 309 | profile.Picture = pic 310 | err = UpdateProfile(pdb, profile, rx.cfg.ProfilesBucket) 311 | if err != nil { 312 | jr := &jsonUploads{Error: err.Error()} 313 | rx.rendr.JSON(w, http.StatusInternalServerError, jr) 314 | return 315 | } 316 | rx.rendr.JSON(w, http.StatusOK, pic) 317 | return 318 | } 319 | 320 | files, ferr := GetMultipleFileUpload(r, rx.cfg.PhotosField) 321 | if ferr != nil && len(files) > 0 || err == nil && len(files) > 0 { 322 | for _, v := range files { 323 | pic, err := SaveUploadFile(pdb, v, profile) 324 | if err != nil { 325 | errs = append(errs, err) 326 | continue 327 | } 328 | rst = append(rst, pic) 329 | } 330 | if ferr != nil { 331 | errs = append(errs, ferr) 332 | } 333 | if len(rst) == 0 && len(errs) > 0 { 334 | jr := &jsonUploads{Error: errs.Error()} 335 | rx.rendr.JSON(w, http.StatusInternalServerError, jr) 336 | return 337 | } 338 | profile.Photos = append(profile.Photos, rst...) 339 | err = UpdateProfile(pdb, profile, rx.cfg.ProfilesBucket) 340 | if err != nil { 341 | jr := &jsonUploads{Error: err.Error()} 342 | rx.rendr.JSON(w, http.StatusInternalServerError, jr) 343 | return 344 | } 345 | jr := &jsonUploads{Error: errs.Error(), Photos: rst} 346 | rx.rendr.JSON(w, http.StatusOK, jr) 347 | return 348 | } 349 | if serr != nil { 350 | jr := &jsonUploads{Error: serr.Error()} 351 | rx.rendr.JSON(w, http.StatusInternalServerError, jr) 352 | return 353 | } 354 | } 355 | } 356 | 357 | // Logout deletes current session 358 | func (rx *Remix) Logout(w http.ResponseWriter, r *http.Request) { 359 | if ss, ok := rx.isInSession(r); ok && ss != nil { 360 | err := rx.sess.Delete(r, w, ss) 361 | if err != nil { 362 | // log this 363 | } 364 | http.Redirect(w, r, rx.cfg.LoginRedirect, http.StatusFound) 365 | return 366 | } 367 | } 368 | 369 | // Profile viewing and updating profile 370 | func (rx *Remix) Profile(w http.ResponseWriter, r *http.Request) { 371 | var ( 372 | vars = r.URL.Query() 373 | data = rx.setSessionData(r) 374 | id = vars.Get("id") 375 | view = vars.Get("view") 376 | all = vars.Get("all") 377 | update = vars.Get("u") 378 | profileHome = "profile/home" 379 | loginPath = "/auth/login" 380 | ok bool 381 | flash *Flash 382 | ss *sessions.Session 383 | ) 384 | pdb := getProfileDatabase(rx.cfg.DBDir, id, rx.cfg.DBExtension) 385 | if r.Method == "GET" { 386 | if id != "" && view == "true" && all != "true" { 387 | p, err := GetProfile(setDB(rx.db, pdb), rx.cfg.ProfilesBucket, id) 388 | if err != nil { 389 | if rx.isAjax(r) { 390 | if err != nil { 391 | // TODO: log this err 392 | rx.rendr.JSON(w, http.StatusNotFound, &jsonErr{errNotFound.Error()}) 393 | return 394 | } 395 | rx.rendr.JSON(w, http.StatusOK, p) 396 | return 397 | } 398 | data.Add("error", errNotFound) 399 | rx.rendr.HTML(w, http.StatusNotFound, "404", data) 400 | return 401 | 402 | } 403 | if ss, ok := rx.isInSession(r); ok { 404 | _, cp, err := rx.getCurrentUserAndProfile(ss) 405 | if err != nil { 406 | // log this? 407 | } 408 | data.Add("user", cp) 409 | if cp.ID == p.ID { 410 | data.Add("myProfile", true) 411 | } 412 | } 413 | data.Add("profile", p) 414 | rx.rendr.HTML(w, http.StatusOK, profileHome, data) 415 | return 416 | } 417 | if all == "true" && view == "true" { 418 | p, err := rx.getAllProfiles() 419 | if rx.isAjax(r) { 420 | if err != nil { 421 | // TODO: log this err 422 | rx.rendr.JSON(w, http.StatusNotFound, &jsonErr{errNotFound.Error()}) 423 | return 424 | } 425 | if p != nil { 426 | rx.rendr.JSON(w, http.StatusOK, p) 427 | return 428 | } 429 | 430 | } 431 | if err != nil { 432 | data.Add("error", errNotFound) 433 | rx.rendr.HTML(w, http.StatusNotFound, "404", data) 434 | return 435 | } 436 | data.Add("profiles", p) 437 | rx.rendr.HTML(w, http.StatusOK, profileHome, data) 438 | return 439 | } 440 | } 441 | if r.Method == "POST" { 442 | if update == "true" { 443 | if ss, ok = rx.isInSession(r); ok { 444 | form := ComposeProfileForm()(r) 445 | _, p, err := rx.getCurrentUserAndProfile(ss) 446 | if err != nil { 447 | if rx.isAjax(r) { 448 | rx.rendr.JSON(w, http.StatusInternalServerError, &jsonErr{errInternalServer.Error()}) 449 | return 450 | } 451 | data.Add("error", errInternalServer.Error()) 452 | rx.rendr.HTML(w, http.StatusInternalServerError, "500", data) 453 | return 454 | } 455 | if p.ID != id { 456 | if rx.isAjax(r) { 457 | rx.rendr.JSON(w, http.StatusForbidden, &jsonErr{errForbidden.Error()}) 458 | return 459 | } 460 | data.Add("error", errForbidden.Error()) 461 | rx.rendr.HTML(w, http.StatusInternalServerError, "403", data) 462 | return 463 | } 464 | if !form.IsValid() { 465 | if rx.isAjax(r) { 466 | rx.rendr.JSON(w, http.StatusOK, &jsonErr{errBadForm.Error()}) 467 | return 468 | } 469 | data.Add("error", form.Errors()) 470 | data.Add("profile", p) 471 | data.Add("myProfile", true) 472 | rx.rendr.HTML(w, http.StatusOK, profileHome, data) 473 | return 474 | } 475 | prof := form.GetModel().(Profile) 476 | p = makeProfUptodate(p, prof) 477 | err = UpdateProfile(setDB(rx.db, pdb), p, rx.cfg.ProfilesBucket) 478 | if err != nil { 479 | if rx.isAjax(r) { 480 | rx.rendr.JSON(w, http.StatusInternalServerError, &jsonErr{errInternalServer.Error()}) 481 | return 482 | } 483 | data.Add("error", errInternalServer.Error()) 484 | rx.rendr.HTML(w, http.StatusInternalServerError, "500", data) 485 | return 486 | } 487 | if rx.isAjax(r) { 488 | rx.rendr.JSON(w, http.StatusOK, p) 489 | return 490 | } 491 | tmpVars := url.Values{ 492 | "id": {p.ID}, 493 | "view": {"true"}, 494 | "all": {"false"}, 495 | } 496 | tmpURL := fmt.Sprintf("/profile?%s", tmpVars.Encode()) 497 | http.Redirect(w, r, tmpURL, http.StatusFound) 498 | return 499 | } 500 | if rx.isAjax(r) { 501 | rx.rendr.JSON(w, http.StatusForbidden, &jsonErr{errForbidden.Error()}) 502 | return 503 | } 504 | flash = NewFlash() 505 | flash.Error("unatakiwa uingie kwanza kabla ya kupata ruhusa ya kutumia hii kurasa") 506 | flash.Save(ss) 507 | ss.Save(r, w) 508 | http.Redirect(w, r, loginPath, http.StatusFound) 509 | return 510 | } 511 | } 512 | } 513 | 514 | func (rx *Remix) getAllProfiles() ([]*Profile, error) { 515 | var rst []*Profile 516 | usrs, err := GetAllUsers(setDB(rx.db, rx.cfg.AccountsDB), rx.cfg.AccountsBucket) 517 | if err != nil { 518 | return nil, err 519 | } 520 | for _, v := range usrs { 521 | pdb := getProfileDatabase(rx.cfg.DBDir, v, rx.cfg.DBExtension) 522 | p, err := GetProfile(setDB(rx.db, pdb), rx.cfg.ProfilesBucket, v) 523 | if err != nil { 524 | // log this 525 | } 526 | if p != nil { 527 | rst = append(rst, p) 528 | } 529 | } 530 | if len(rst) == 0 { 531 | return nil, errNotFound 532 | } 533 | return rst, nil 534 | } 535 | 536 | // Routes returs a mux of all registered routes 537 | func (rx *Remix) Routes() *mux.Router { 538 | var ( 539 | homePath = "/" 540 | registerPath = "/auth/register" 541 | loginPath = "/auth/login" 542 | logoutPath = "/auth/logout" 543 | imagesPath = "/imgs" 544 | uploadsPath = "/uploads" 545 | profilePath = "/profile" 546 | messengerPath = "/msg" 547 | ) 548 | h := mux.NewRouter() 549 | h.HandleFunc(homePath, rx.Home) 550 | h.HandleFunc(registerPath, rx.Register).Methods("GET", "POST") 551 | h.HandleFunc(loginPath, rx.Login).Methods("GET", "POST") 552 | h.HandleFunc(logoutPath, rx.Logout) 553 | h.HandleFunc(imagesPath, rx.ServeImages).Methods("GET") 554 | h.HandleFunc(uploadsPath, rx.Uploads) 555 | h.HandleFunc(profilePath, rx.Profile) 556 | h.HandleFunc(messengerPath, rx.msg.Handler()) 557 | return h 558 | } 559 | 560 | func (rx *Remix) isInSession(r *http.Request) (*sessions.Session, bool) { 561 | var ( 562 | ss *sessions.Session 563 | err error 564 | ) 565 | if ss, err = rx.sess.Get(r, rx.cfg.SessionName); err == nil { 566 | if v, ok := ss.Values["isAuthorized"]; ok && v == true { 567 | return ss, true 568 | } 569 | } 570 | return ss, false 571 | } 572 | 573 | func (rx *Remix) setSessionData(r *http.Request) render.TemplateData { 574 | var ( 575 | data = render.NewTemplateData() 576 | flash = NewFlash() 577 | ) 578 | if ss, ok := rx.isInSession(r); ok { 579 | fd := flash.Get(ss) 580 | if fd != nil { 581 | data.Add("flash", fd.Data) 582 | } 583 | data.Add("InSession", true) 584 | user, p, err := rx.getCurrentUserAndProfile(ss) 585 | if err != nil { 586 | return data 587 | } 588 | data.Add("CurrentUser", user) 589 | data.Add("Profile", p) 590 | return data 591 | } 592 | return data 593 | } 594 | 595 | func (rx *Remix) getCurrentUserAndProfile(ss *sessions.Session) (*User, *Profile, error) { 596 | if e, ok := ss.Values["user"]; ok { 597 | email := e.(string) 598 | user, err := GetUser(setDB(rx.db, rx.cfg.AccountsDB), rx.cfg.AccountsBucket, email) 599 | if err != nil { 600 | return nil, nil, err 601 | } 602 | pdb := getProfileDatabase(rx.cfg.DBDir, user.UUID, rx.cfg.DBExtension) 603 | p, err := GetProfile(setDB(rx.db, pdb), rx.cfg.ProfilesBucket, user.UUID) 604 | if err != nil { 605 | return nil, nil, err 606 | } 607 | return user, p, nil 608 | } 609 | return nil, nil, errors.New("aurora: session values not set") 610 | } 611 | 612 | // switches databases 613 | func setDB(db nutz.Storage, dbname string) nutz.Storage { 614 | d := db 615 | d.DBName = dbname 616 | return d 617 | } 618 | 619 | // Sets basic configuration values which has use to the templates 620 | func setConfigData(c *RemixConfig) render.TemplateData { 621 | data := render.NewTemplateData() 622 | data.Add("AppName", c.AppName) 623 | data.Add("AppTitle", c.AppTitle) 624 | data.Add("AppDescription", c.AppDescription) 625 | data.Add("CdnMode", c.CdnMode) 626 | data.Add("AppURL", c.AppURL) 627 | data.Add("RunMode", c.RunMode) 628 | return data 629 | 630 | } 631 | 632 | // checks if the request is ajax 633 | func (rx *Remix) isAjax(r *http.Request) bool { 634 | return r.Header.Get("X-Requested-With") == "XMLHttpRequest" 635 | } 636 | 637 | func makeProfUptodate(des *Profile, src Profile) *Profile { 638 | if src.FirstName != "" { 639 | des.FirstName = strings.ToTitle(src.FirstName) 640 | } 641 | if src.LastName != "" { 642 | des.LastName = strings.ToTitle(src.LastName) 643 | } 644 | t := time.Time{} 645 | if src.BirthDate != t { 646 | des.Age = setAge(src.BirthDate) 647 | des.BirthDate = src.BirthDate 648 | } 649 | if src.City != "" { 650 | des.City = src.City 651 | } 652 | if src.Street != "" { 653 | des.Street = src.Street 654 | } 655 | if src.Country != "" { 656 | des.Country = src.Country 657 | } 658 | if src.Gender > 0 { 659 | des.Gender = src.Gender 660 | } 661 | des.UpdatedAt = time.Now() 662 | return des 663 | } 664 | -------------------------------------------------------------------------------- /public/materialize/font/material-design-icons/LICENSE.txt: -------------------------------------------------------------------------------- 1 | https://github.com/google/material-design-icons/blob/master/LICENSE 2 | https://github.com/FezVrasta/bootstrap-material-design/blob/master/fonts/LICENSE.txt 3 | 4 | Attribution-ShareAlike 4.0 International 5 | 6 | ======================================================================= 7 | 8 | Creative Commons Corporation ("Creative Commons") is not a law firm and 9 | does not provide legal services or legal advice. Distribution of 10 | Creative Commons public licenses does not create a lawyer-client or 11 | other relationship. Creative Commons makes its licenses and related 12 | information available on an "as-is" basis. Creative Commons gives no 13 | warranties regarding its licenses, any material licensed under their 14 | terms and conditions, or any related information. Creative Commons 15 | disclaims all liability for damages resulting from their use to the 16 | fullest extent possible. 17 | 18 | Using Creative Commons Public Licenses 19 | 20 | Creative Commons public licenses provide a standard set of terms and 21 | conditions that creators and other rights holders may use to share 22 | original works of authorship and other material subject to copyright 23 | and certain other rights specified in the public license below. The 24 | following considerations are for informational purposes only, are not 25 | exhaustive, and do not form part of our licenses. 26 | 27 | Considerations for licensors: Our public licenses are 28 | intended for use by those authorized to give the public 29 | permission to use material in ways otherwise restricted by 30 | copyright and certain other rights. Our licenses are 31 | irrevocable. Licensors should read and understand the terms 32 | and conditions of the license they choose before applying it. 33 | Licensors should also secure all rights necessary before 34 | applying our licenses so that the public can reuse the 35 | material as expected. Licensors should clearly mark any 36 | material not subject to the license. This includes other CC- 37 | licensed material, or material used under an exception or 38 | limitation to copyright. More considerations for licensors: 39 | wiki.creativecommons.org/Considerations_for_licensors 40 | 41 | Considerations for the public: By using one of our public 42 | licenses, a licensor grants the public permission to use the 43 | licensed material under specified terms and conditions. If 44 | the licensor's permission is not necessary for any reason--for 45 | example, because of any applicable exception or limitation to 46 | copyright--then that use is not regulated by the license. Our 47 | licenses grant only permissions under copyright and certain 48 | other rights that a licensor has authority to grant. Use of 49 | the licensed material may still be restricted for other 50 | reasons, including because others have copyright or other 51 | rights in the material. A licensor may make special requests, 52 | such as asking that all changes be marked or described. 53 | Although not required by our licenses, you are encouraged to 54 | respect those requests where reasonable. More_considerations 55 | for the public: 56 | wiki.creativecommons.org/Considerations_for_licensees 57 | 58 | ======================================================================= 59 | 60 | Creative Commons Attribution-ShareAlike 4.0 International Public 61 | License 62 | 63 | By exercising the Licensed Rights (defined below), You accept and agree 64 | to be bound by the terms and conditions of this Creative Commons 65 | Attribution-ShareAlike 4.0 International Public License ("Public 66 | License"). To the extent this Public License may be interpreted as a 67 | contract, You are granted the Licensed Rights in consideration of Your 68 | acceptance of these terms and conditions, and the Licensor grants You 69 | such rights in consideration of benefits the Licensor receives from 70 | making the Licensed Material available under these terms and 71 | conditions. 72 | 73 | 74 | Section 1 -- Definitions. 75 | 76 | a. Adapted Material means material subject to Copyright and Similar 77 | Rights that is derived from or based upon the Licensed Material 78 | and in which the Licensed Material is translated, altered, 79 | arranged, transformed, or otherwise modified in a manner requiring 80 | permission under the Copyright and Similar Rights held by the 81 | Licensor. For purposes of this Public License, where the Licensed 82 | Material is a musical work, performance, or sound recording, 83 | Adapted Material is always produced where the Licensed Material is 84 | synched in timed relation with a moving image. 85 | 86 | b. Adapter's License means the license You apply to Your Copyright 87 | and Similar Rights in Your contributions to Adapted Material in 88 | accordance with the terms and conditions of this Public License. 89 | 90 | c. BY-SA Compatible License means a license listed at 91 | creativecommons.org/compatiblelicenses, approved by Creative 92 | Commons as essentially the equivalent of this Public License. 93 | 94 | d. Copyright and Similar Rights means copyright and/or similar rights 95 | closely related to copyright including, without limitation, 96 | performance, broadcast, sound recording, and Sui Generis Database 97 | Rights, without regard to how the rights are labeled or 98 | categorized. For purposes of this Public License, the rights 99 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 100 | Rights. 101 | 102 | e. Effective Technological Measures means those measures that, in the 103 | absence of proper authority, may not be circumvented under laws 104 | fulfilling obligations under Article 11 of the WIPO Copyright 105 | Treaty adopted on December 20, 1996, and/or similar international 106 | agreements. 107 | 108 | f. Exceptions and Limitations means fair use, fair dealing, and/or 109 | any other exception or limitation to Copyright and Similar Rights 110 | that applies to Your use of the Licensed Material. 111 | 112 | g. License Elements means the license attributes listed in the name 113 | of a Creative Commons Public License. The License Elements of this 114 | Public License are Attribution and ShareAlike. 115 | 116 | h. Licensed Material means the artistic or literary work, database, 117 | or other material to which the Licensor applied this Public 118 | License. 119 | 120 | i. Licensed Rights means the rights granted to You subject to the 121 | terms and conditions of this Public License, which are limited to 122 | all Copyright and Similar Rights that apply to Your use of the 123 | Licensed Material and that the Licensor has authority to license. 124 | 125 | j. Licensor means the individual(s) or entity(ies) granting rights 126 | under this Public License. 127 | 128 | k. Share means to provide material to the public by any means or 129 | process that requires permission under the Licensed Rights, such 130 | as reproduction, public display, public performance, distribution, 131 | dissemination, communication, or importation, and to make material 132 | available to the public including in ways that members of the 133 | public may access the material from a place and at a time 134 | individually chosen by them. 135 | 136 | l. Sui Generis Database Rights means rights other than copyright 137 | resulting from Directive 96/9/EC of the European Parliament and of 138 | the Council of 11 March 1996 on the legal protection of databases, 139 | as amended and/or succeeded, as well as other essentially 140 | equivalent rights anywhere in the world. 141 | 142 | m. You means the individual or entity exercising the Licensed Rights 143 | under this Public License. Your has a corresponding meaning. 144 | 145 | 146 | Section 2 -- Scope. 147 | 148 | a. License grant. 149 | 150 | 1. Subject to the terms and conditions of this Public License, 151 | the Licensor hereby grants You a worldwide, royalty-free, 152 | non-sublicensable, non-exclusive, irrevocable license to 153 | exercise the Licensed Rights in the Licensed Material to: 154 | 155 | a. reproduce and Share the Licensed Material, in whole or 156 | in part; and 157 | 158 | b. produce, reproduce, and Share Adapted Material. 159 | 160 | 2. Exceptions and Limitations. For the avoidance of doubt, where 161 | Exceptions and Limitations apply to Your use, this Public 162 | License does not apply, and You do not need to comply with 163 | its terms and conditions. 164 | 165 | 3. Term. The term of this Public License is specified in Section 166 | 6(a). 167 | 168 | 4. Media and formats; technical modifications allowed. The 169 | Licensor authorizes You to exercise the Licensed Rights in 170 | all media and formats whether now known or hereafter created, 171 | and to make technical modifications necessary to do so. The 172 | Licensor waives and/or agrees not to assert any right or 173 | authority to forbid You from making technical modifications 174 | necessary to exercise the Licensed Rights, including 175 | technical modifications necessary to circumvent Effective 176 | Technological Measures. For purposes of this Public License, 177 | simply making modifications authorized by this Section 2(a) 178 | (4) never produces Adapted Material. 179 | 180 | 5. Downstream recipients. 181 | 182 | a. Offer from the Licensor -- Licensed Material. Every 183 | recipient of the Licensed Material automatically 184 | receives an offer from the Licensor to exercise the 185 | Licensed Rights under the terms and conditions of this 186 | Public License. 187 | 188 | b. Additional offer from the Licensor -- Adapted Material. 189 | Every recipient of Adapted Material from You 190 | automatically receives an offer from the Licensor to 191 | exercise the Licensed Rights in the Adapted Material 192 | under the conditions of the Adapter's License You apply. 193 | 194 | c. No downstream restrictions. You may not offer or impose 195 | any additional or different terms or conditions on, or 196 | apply any Effective Technological Measures to, the 197 | Licensed Material if doing so restricts exercise of the 198 | Licensed Rights by any recipient of the Licensed 199 | Material. 200 | 201 | 6. No endorsement. Nothing in this Public License constitutes or 202 | may be construed as permission to assert or imply that You 203 | are, or that Your use of the Licensed Material is, connected 204 | with, or sponsored, endorsed, or granted official status by, 205 | the Licensor or others designated to receive attribution as 206 | provided in Section 3(a)(1)(A)(i). 207 | 208 | b. Other rights. 209 | 210 | 1. Moral rights, such as the right of integrity, are not 211 | licensed under this Public License, nor are publicity, 212 | privacy, and/or other similar personality rights; however, to 213 | the extent possible, the Licensor waives and/or agrees not to 214 | assert any such rights held by the Licensor to the limited 215 | extent necessary to allow You to exercise the Licensed 216 | Rights, but not otherwise. 217 | 218 | 2. Patent and trademark rights are not licensed under this 219 | Public License. 220 | 221 | 3. To the extent possible, the Licensor waives any right to 222 | collect royalties from You for the exercise of the Licensed 223 | Rights, whether directly or through a collecting society 224 | under any voluntary or waivable statutory or compulsory 225 | licensing scheme. In all other cases the Licensor expressly 226 | reserves any right to collect such royalties. 227 | 228 | 229 | Section 3 -- License Conditions. 230 | 231 | Your exercise of the Licensed Rights is expressly made subject to the 232 | following conditions. 233 | 234 | a. Attribution. 235 | 236 | 1. If You Share the Licensed Material (including in modified 237 | form), You must: 238 | 239 | a. retain the following if it is supplied by the Licensor 240 | with the Licensed Material: 241 | 242 | i. identification of the creator(s) of the Licensed 243 | Material and any others designated to receive 244 | attribution, in any reasonable manner requested by 245 | the Licensor (including by pseudonym if 246 | designated); 247 | 248 | ii. a copyright notice; 249 | 250 | iii. a notice that refers to this Public License; 251 | 252 | iv. a notice that refers to the disclaimer of 253 | warranties; 254 | 255 | v. a URI or hyperlink to the Licensed Material to the 256 | extent reasonably practicable; 257 | 258 | b. indicate if You modified the Licensed Material and 259 | retain an indication of any previous modifications; and 260 | 261 | c. indicate the Licensed Material is licensed under this 262 | Public License, and include the text of, or the URI or 263 | hyperlink to, this Public License. 264 | 265 | 2. You may satisfy the conditions in Section 3(a)(1) in any 266 | reasonable manner based on the medium, means, and context in 267 | which You Share the Licensed Material. For example, it may be 268 | reasonable to satisfy the conditions by providing a URI or 269 | hyperlink to a resource that includes the required 270 | information. 271 | 272 | 3. If requested by the Licensor, You must remove any of the 273 | information required by Section 3(a)(1)(A) to the extent 274 | reasonably practicable. 275 | 276 | b. ShareAlike. 277 | 278 | In addition to the conditions in Section 3(a), if You Share 279 | Adapted Material You produce, the following conditions also apply. 280 | 281 | 1. The Adapter's License You apply must be a Creative Commons 282 | license with the same License Elements, this version or 283 | later, or a BY-SA Compatible License. 284 | 285 | 2. You must include the text of, or the URI or hyperlink to, the 286 | Adapter's License You apply. You may satisfy this condition 287 | in any reasonable manner based on the medium, means, and 288 | context in which You Share Adapted Material. 289 | 290 | 3. You may not offer or impose any additional or different terms 291 | or conditions on, or apply any Effective Technological 292 | Measures to, Adapted Material that restrict exercise of the 293 | rights granted under the Adapter's License You apply. 294 | 295 | 296 | Section 4 -- Sui Generis Database Rights. 297 | 298 | Where the Licensed Rights include Sui Generis Database Rights that 299 | apply to Your use of the Licensed Material: 300 | 301 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 302 | to extract, reuse, reproduce, and Share all or a substantial 303 | portion of the contents of the database; 304 | 305 | b. if You include all or a substantial portion of the database 306 | contents in a database in which You have Sui Generis Database 307 | Rights, then the database in which You have Sui Generis Database 308 | Rights (but not its individual contents) is Adapted Material, 309 | 310 | including for purposes of Section 3(b); and 311 | c. You must comply with the conditions in Section 3(a) if You Share 312 | all or a substantial portion of the contents of the database. 313 | 314 | For the avoidance of doubt, this Section 4 supplements and does not 315 | replace Your obligations under this Public License where the Licensed 316 | Rights include other Copyright and Similar Rights. 317 | 318 | 319 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 320 | 321 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 322 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 323 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 324 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 325 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 326 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 327 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 328 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 329 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 330 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 331 | 332 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 333 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 334 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 335 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 336 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 337 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 338 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 339 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 340 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 341 | 342 | c. The disclaimer of warranties and limitation of liability provided 343 | above shall be interpreted in a manner that, to the extent 344 | possible, most closely approximates an absolute disclaimer and 345 | waiver of all liability. 346 | 347 | 348 | Section 6 -- Term and Termination. 349 | 350 | a. This Public License applies for the term of the Copyright and 351 | Similar Rights licensed here. However, if You fail to comply with 352 | this Public License, then Your rights under this Public License 353 | terminate automatically. 354 | 355 | b. Where Your right to use the Licensed Material has terminated under 356 | Section 6(a), it reinstates: 357 | 358 | 1. automatically as of the date the violation is cured, provided 359 | it is cured within 30 days of Your discovery of the 360 | violation; or 361 | 362 | 2. upon express reinstatement by the Licensor. 363 | 364 | For the avoidance of doubt, this Section 6(b) does not affect any 365 | right the Licensor may have to seek remedies for Your violations 366 | of this Public License. 367 | 368 | c. For the avoidance of doubt, the Licensor may also offer the 369 | Licensed Material under separate terms or conditions or stop 370 | distributing the Licensed Material at any time; however, doing so 371 | will not terminate this Public License. 372 | 373 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 374 | License. 375 | 376 | 377 | Section 7 -- Other Terms and Conditions. 378 | 379 | a. The Licensor shall not be bound by any additional or different 380 | terms or conditions communicated by You unless expressly agreed. 381 | 382 | b. Any arrangements, understandings, or agreements regarding the 383 | Licensed Material not stated herein are separate from and 384 | independent of the terms and conditions of this Public License. 385 | 386 | 387 | Section 8 -- Interpretation. 388 | 389 | a. For the avoidance of doubt, this Public License does not, and 390 | shall not be interpreted to, reduce, limit, restrict, or impose 391 | conditions on any use of the Licensed Material that could lawfully 392 | be made without permission under this Public License. 393 | 394 | b. To the extent possible, if any provision of this Public License is 395 | deemed unenforceable, it shall be automatically reformed to the 396 | minimum extent necessary to make it enforceable. If the provision 397 | cannot be reformed, it shall be severed from this Public License 398 | without affecting the enforceability of the remaining terms and 399 | conditions. 400 | 401 | c. No term or condition of this Public License will be waived and no 402 | failure to comply consented to unless expressly agreed to by the 403 | Licensor. 404 | 405 | d. Nothing in this Public License constitutes or may be interpreted 406 | as a limitation upon, or waiver of, any privileges and immunities 407 | that apply to the Licensor or You, including from the legal 408 | processes of any jurisdiction or authority. 409 | 410 | 411 | ======================================================================= 412 | 413 | Creative Commons is not a party to its public licenses. 414 | Notwithstanding, Creative Commons may elect to apply one of its public 415 | licenses to material it publishes and in those instances will be 416 | considered the "Licensor." Except for the limited purpose of indicating 417 | that material is shared under a Creative Commons public license or as 418 | otherwise permitted by the Creative Commons policies published at 419 | creativecommons.org/policies, Creative Commons does not authorize the 420 | use of the trademark "Creative Commons" or any other trademark or logo 421 | of Creative Commons without its prior written consent including, 422 | without limitation, in connection with any unauthorized modifications 423 | to any of its public licenses or any other arrangements, 424 | understandings, or agreements concerning use of licensed material. For 425 | the avoidance of doubt, this paragraph does not form part of the public 426 | licenses. 427 | 428 | Creative Commons may be contacted at creativecommons.org. 429 | -------------------------------------------------------------------------------- /remix_test.go: -------------------------------------------------------------------------------- 1 | package aurora 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/cookiejar" 9 | "net/http/httptest" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestRemix_Home(t *testing.T) { 17 | var ( 18 | err error 19 | res *http.Response 20 | ) 21 | ts, client, _ := testServer(t) 22 | defer ts.Close() 23 | 24 | res, err = client.Get(ts.URL) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | err = checkResponse(res, http.StatusOK, "pitch") 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | } 33 | 34 | func TestRemix_Register(t *testing.T) { 35 | var ( 36 | registratinPath = "/auth/register" 37 | pass = "mamamia" 38 | err error 39 | res1, res2, res3, res4, res5 *http.Response 40 | vars url.Values 41 | ) 42 | 43 | ts, client, rx := testServer(t) 44 | defer ts.Close() 45 | 46 | registerURL := fmt.Sprintf("%s%s", ts.URL, registratinPath) 47 | 48 | // get the form 49 | res1, err = client.Get(registerURL) 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | err = checkResponse(res1, http.StatusOK) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | 58 | // Failing validation 59 | vars = url.Values{ 60 | "first_name": {"gernest"}, 61 | "lastname": {"aurora"}, 62 | "email_address": {"gernest@aurora.com"}, 63 | "pass": {"ringadongdilo"}, 64 | "confirm_pass": {"ringadondilo"}, 65 | } 66 | res2, err = client.PostForm(registerURL, vars) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | err = checkResponse(res2, http.StatusOK) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | 75 | // a valid form 76 | vars = url.Values{ 77 | "first_name": {"gernest"}, 78 | "last_name": {"aurora"}, 79 | "email_address": {"gernest@aurora.com"}, 80 | "pass": {pass}, 81 | "confirm_pass": {pass}, 82 | } 83 | 84 | // case there is an issue with db 85 | rx.cfg.AccountsBucket = "" 86 | res3, err = client.PostForm(registerURL, vars) 87 | if err != nil { 88 | t.Error(err) 89 | } 90 | err = checkResponse(res3, http.StatusInternalServerError) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | rx.cfg.AccountsBucket = "accounts" //Restore our config 95 | 96 | // case everything is ok 97 | res4, err = client.PostForm(registerURL, vars) 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | 102 | err = checkResponse(res4, http.StatusOK, "search") 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | 107 | // case session is not new it should redirects to login path 108 | res5, err = client.PostForm(registerURL, vars) 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | err = checkResponse(res5, http.StatusOK, "/auth/logout") 113 | if err != nil { 114 | t.Error(err) 115 | } 116 | 117 | // making sure our password was encrypted 118 | user, err := GetUser(setDB(rx.db, rx.cfg.AccountsDB), rx.cfg.AccountsBucket, "gernest@aurora.com") 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | err = verifyPass(user.Pass, pass) 123 | if err != nil { 124 | t.Error(err) 125 | } 126 | } 127 | 128 | func TestRemix_Login(t *testing.T) { 129 | var ( 130 | email = "gernest@aurora.com" 131 | loginPath = "/auth/login" 132 | pass = "mamamia" 133 | err error 134 | res, res1, res2, res3, res4 *http.Response 135 | vars url.Values 136 | ) 137 | 138 | ts, client, _ := testServer(t) 139 | defer ts.Close() 140 | loginURL := fmt.Sprintf("%s%s", ts.URL, loginPath) 141 | 142 | // get the login form 143 | res, err = client.Get(loginURL) 144 | if err != nil { 145 | t.Error(err) 146 | } 147 | err = checkResponse(res, http.StatusOK) 148 | if err != nil { 149 | t.Error(err) 150 | } 151 | 152 | // invalid form 153 | vars = url.Values{ 154 | "email": {"bogus"}, 155 | "password": {"myass"}, 156 | } 157 | res1, err = client.PostForm(loginURL, vars) 158 | if err != nil { 159 | t.Error(err) 160 | } 161 | err = checkResponse(res1, http.StatusOK, "login-form") 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | 166 | // case no such user but valid form 167 | vars = url.Values{ 168 | "email": {"gernesti@aurora.com"}, 169 | "password": {"heydollringadongdillo"}, 170 | } 171 | res2, err = client.PostForm(loginURL, vars) 172 | if err != nil { 173 | t.Error(err) 174 | } 175 | err = checkResponse(res2, http.StatusOK, "login-form") 176 | if err != nil { 177 | t.Error(err) 178 | } 179 | 180 | // wrong password 181 | vars = url.Values{ 182 | "email": {"gernest@aurora.com"}, 183 | "password": {"heydollringadongdilloo"}, 184 | } 185 | res3, err = client.PostForm(loginURL, vars) 186 | if err != nil { 187 | t.Error(err) 188 | } 189 | err = checkResponse(res3, http.StatusOK, "login-form") 190 | if err != nil { 191 | t.Error(err) 192 | } 193 | 194 | // case everything is ok, it should redirect to the path specified in Remix.cfg 195 | vars = url.Values{ 196 | "email": {email}, 197 | "password": {pass}, 198 | } 199 | res4, err = client.PostForm(loginURL, vars) 200 | if err != nil { 201 | t.Error(err) 202 | } 203 | err = checkResponse(res4, http.StatusOK, "search") 204 | if err != nil { 205 | t.Error(err) 206 | } 207 | 208 | } 209 | 210 | func TestRemix_Uploads(t *testing.T) { 211 | var ( 212 | w = &bytes.Buffer{} 213 | uploadPath = "/uploads" 214 | loginPath = "/auth/login" 215 | pass = "mamamia" 216 | contentType string 217 | err error 218 | res, res0, res1, res2, res3, res4 *http.Response 219 | vars url.Values 220 | content *bytes.Buffer 221 | ) 222 | ts, client, rx := testServer(t) 223 | defer ts.Close() 224 | vars = url.Values{ 225 | "email": {"gernest@aurora.com"}, 226 | "password": {pass}, 227 | } 228 | loginURL := fmt.Sprintf("%s%s", ts.URL, loginPath) 229 | upURL := fmt.Sprintf("%s%s", ts.URL, uploadPath) 230 | 231 | content, contentType = testUpData("me.jpg", "single", t) 232 | res0, err = client.Post(upURL, contentType, content) 233 | if err != nil { 234 | t.Error(err) 235 | } 236 | err = checkResponse(res0, http.StatusForbidden, errForbidden.Error()) 237 | if err != nil { 238 | t.Error(err) 239 | } 240 | 241 | res, err = client.PostForm(loginURL, vars) 242 | if err != nil { 243 | t.Error(err) 244 | } 245 | err = checkResponse(res, http.StatusOK) 246 | if err != nil { 247 | t.Error(err) 248 | } 249 | 250 | content, contentType = testUpData("me.jpg", "single", t) 251 | res1, err = client.Post(upURL, contentType, content) 252 | if err != nil { 253 | t.Error(err) 254 | } 255 | err = checkResponse(res1, http.StatusOK, "jpg") 256 | if err != nil { 257 | t.Error(err) 258 | } 259 | 260 | content, contentType = testUpData("me.jpg", "multi", t) 261 | res2, err = client.Post(upURL, contentType, content) 262 | defer res2.Body.Close() 263 | if err != nil { 264 | t.Error(err) 265 | } 266 | if res2.StatusCode != http.StatusOK { 267 | t.Errorf("Ecpected %d got %d", http.StatusOK, res2.StatusCode) 268 | } 269 | w.Reset() 270 | io.Copy(w, res2.Body) 271 | if !contains(w.String(), "jpg") { 272 | t.Errorf("Expected to save jpg file got %s", w.String()) 273 | } 274 | 275 | bAb := rx.cfg.AccountsBucket 276 | rx.cfg.AccountsBucket = "" 277 | 278 | content, contentType = testUpData("me.jpg", "single", t) 279 | res3, err = client.Post(upURL, contentType, content) 280 | if err != nil { 281 | t.Error(err) 282 | } 283 | defer res3.Body.Close() 284 | if res3.StatusCode != http.StatusInternalServerError { 285 | t.Errorf("Ecpected %d got %d", http.StatusInternalServerError, res3.StatusCode) 286 | } 287 | w.Reset() 288 | io.Copy(w, res3.Body) 289 | if !contains(w.String(), "bucket") { 290 | t.Errorf("Expected to save jpg file got %s", w.String()) 291 | } 292 | rx.cfg.AccountsBucket = bAb 293 | 294 | bAb = rx.cfg.ProfilePicField 295 | rx.cfg.ProfilePicField = "" 296 | 297 | content, contentType = testUpData("me.jpg", "single", t) 298 | res4, err = client.Post(upURL, contentType, content) 299 | defer res4.Body.Close() 300 | if err != nil { 301 | t.Error(err) 302 | } 303 | 304 | if res4.StatusCode != http.StatusInternalServerError { 305 | t.Errorf("Ecpected %d got %d", http.StatusInternalServerError, res4.StatusCode) 306 | } 307 | w.Reset() 308 | io.Copy(w, res4.Body) 309 | if !contains(w.String(), " no such file") { 310 | t.Errorf("Expected %s to contain no such file", w.String()) 311 | } 312 | rx.cfg.ProfilePicField = bAb 313 | } 314 | 315 | func TestRemixt_ServeImages(t *testing.T) { 316 | var ( 317 | email = "gernest@aurora.com" 318 | imagesPath = "/imgs" 319 | res *http.Response 320 | err error 321 | user *User 322 | p *Profile 323 | ) 324 | ts, client, rx := testServer(t) 325 | defer ts.Close() 326 | 327 | user, err = GetUser(setDB(rx.db, rx.cfg.AccountsDB), rx.cfg.AccountsBucket, email) 328 | if err != nil { 329 | t.Error(err) 330 | } 331 | pdb := getProfileDatabase(rx.cfg.DBDir, user.UUID, rx.cfg.DBExtension) 332 | p, err = GetProfile(setDB(rx.db, pdb), rx.cfg.ProfilesBucket, user.UUID) 333 | if err != nil { 334 | t.Error(err) 335 | } 336 | if len(p.Photos) != 3 { 337 | t.Errorf("Expected 3 got %d", len(p.Photos)) 338 | } 339 | 340 | query := url.Values{ 341 | "iid": {p.Picture.ID}, 342 | "pid": {p.ID}, 343 | } 344 | imgURL := fmt.Sprintf("%s%s?%s", ts.URL, imagesPath, query.Encode()) 345 | res, err = client.Get(imgURL) 346 | defer res.Body.Close() 347 | if err != nil { 348 | t.Error(err) 349 | } 350 | if res.StatusCode != http.StatusOK { 351 | t.Errorf("Expected %d got %d", http.StatusOK, res.StatusCode) 352 | } 353 | 354 | // failure case 355 | vars := url.Values{ 356 | "iid": {"bogus"}, 357 | "pid": {p.Picture.UploadedBy}, 358 | } 359 | res1, err := client.Get(fmt.Sprintf("%s%s?%s", ts.URL, imagesPath, vars.Encode())) 360 | defer res1.Body.Close() 361 | if err != nil { 362 | t.Error(err) 363 | } 364 | if res1.StatusCode != http.StatusNotFound { 365 | t.Errorf("Expected %d got %d", http.StatusNotFound, res1.StatusCode) 366 | } 367 | } 368 | 369 | func TestRemix_Logout(t *testing.T) { 370 | var ( 371 | loginPath = "/auth/login" 372 | logoutPath = "/auth/logout" 373 | email = "gernest@aurora.com" 374 | pass = "mamamia" 375 | err error 376 | res, res1 *http.Response 377 | vars url.Values 378 | ) 379 | 380 | ts, client, _ := testServer(t) 381 | defer ts.Close() 382 | vars = url.Values{ 383 | "email": {email}, 384 | "password": {pass}, 385 | } 386 | 387 | loginURL := fmt.Sprintf("%s%s", ts.URL, loginPath) 388 | outURL := fmt.Sprintf("%s%s", ts.URL, logoutPath) 389 | 390 | res, err = client.PostForm(loginURL, vars) 391 | if err != nil { 392 | t.Error(err) 393 | } 394 | err = checkResponse(res, http.StatusOK, "search") 395 | if err != nil { 396 | t.Error(err) 397 | } 398 | 399 | res1, err = client.Get(outURL) 400 | if err != nil { 401 | t.Error(err) 402 | } 403 | err = checkResponse(res1, http.StatusOK, "search") 404 | if err == nil { 405 | t.Error("Expected an error") 406 | } 407 | } 408 | 409 | func TestRemix_Profile(t *testing.T) { 410 | var ( 411 | profilePath = "/profile" 412 | loginPath = "/auth/login" 413 | pass = "mamamia" 414 | birthDate = "2 January, 1989" 415 | err error 416 | ) 417 | 418 | emails := []string{ 419 | "gernest@aurora.mza", 420 | "gernest@aurora.tz", 421 | "gernest@aurora.tx", 422 | } 423 | 424 | ts, client, rx := testServer(t) 425 | defer ts.Close() 426 | 427 | // create user accounts and profiles, using the id's in pids global variables 428 | // and emails in the emsils slice. The id, email pairs correspond to the two 429 | // slice's index 430 | for k, v := range pids { 431 | usr := &User{EmailAddress: emails[k], UUID: v} 432 | ps, err := hashPassword(pass) 433 | if err != nil { 434 | t.Error(err) 435 | } 436 | usr.Pass = ps 437 | err = CreateAccount(setDB(rx.db, rx.cfg.AccountsDB), usr, rx.cfg.AccountsBucket) 438 | if err != nil { 439 | t.Error(err) 440 | } 441 | pdbStr := getProfileDatabase(rx.cfg.DBDir, usr.UUID, rx.cfg.DBExtension) 442 | pdb := setDB(rx.db, pdbStr) 443 | p := &Profile{ID: usr.UUID} 444 | err = CreateProfile(pdb, p, rx.cfg.ProfilesBucket) 445 | if err != nil { 446 | t.Error(err) 447 | } 448 | 449 | } 450 | 451 | for _, v := range pids { 452 | 453 | // A correct profile url query, this is for viewing a single profile only 454 | vars := url.Values{ 455 | "id": {v}, 456 | "view": {"true"}, 457 | "all": {"false"}, 458 | } 459 | 460 | // A wrong profile url query, notice that the id field is not a correct 461 | // uuid string and also there aint no such profiles in the database. 462 | // This also is for viewing a single profile 463 | vars2 := url.Values{ 464 | "id": {v + "shit"}, 465 | "view": {"true"}, 466 | "all": {"false"}, 467 | } 468 | 469 | // case a valid profile query, and the request is standard http. 470 | res, err := client.Get(fmt.Sprintf("%s/profile?%s", ts.URL, vars.Encode())) 471 | if err != nil { 472 | t.Error(err) 473 | } 474 | err = checkResponse(res, http.StatusOK) 475 | if err != nil { 476 | t.Error(err) 477 | } 478 | 479 | // case wrong profile url query, to be precise, the id is wrong that is it is not 480 | // a valid uuid and no any profile matches. The request is standard http. 481 | res0, err := client.Get(fmt.Sprintf("%s/profile?%s", ts.URL, vars2.Encode())) 482 | if err != nil { 483 | t.Error(err) 484 | } 485 | err = checkResponse(res0, http.StatusNotFound, "shit not found") 486 | if err != nil { 487 | t.Error(err) 488 | } 489 | 490 | // case a valid profile query, and the request is standard ajax. 491 | res1, err := httpGetAjax(client, fmt.Sprintf("%s/profile?%s", ts.URL, vars.Encode())) 492 | if err != nil { 493 | t.Error(err) 494 | } 495 | err = checkResponse(res1, http.StatusOK, v) 496 | if err != nil { 497 | t.Error(err) 498 | } 499 | 500 | // case wrong profile url query, to be precise, the id is wrong that is it is not 501 | // a valid uuid and no any profile matches. The request is standard ajax. 502 | res2, err := httpGetAjax(client, fmt.Sprintf("%s/profile?%s", ts.URL, vars2.Encode())) 503 | if err != nil { 504 | t.Error(err) 505 | } 506 | err = checkResponse(res2, http.StatusNotFound, errNotFound.Error()) 507 | if err != nil { 508 | t.Error(err) 509 | } 510 | } 511 | 512 | // A correct profile url query for viewing all profiles 513 | vars := url.Values{"view": {"true"}, "all": {"true"}} 514 | 515 | getAllURL := fmt.Sprintf("%s/profile?%s", ts.URL, vars.Encode()) 516 | 517 | // case viewing all profiles via standard http 518 | res3, err := client.Get(getAllURL) 519 | if err != nil { 520 | t.Error(err) 521 | } 522 | err = checkResponse(res3, http.StatusOK) 523 | if err != nil { 524 | t.Error(err) 525 | } 526 | 527 | // case viewing all profiles via ajax 528 | res4, err := httpGetAjax(client, getAllURL) 529 | if err != nil { 530 | t.Error(err) 531 | } 532 | err = checkResponse(res4, http.StatusOK, pids[0]) 533 | if err != nil { 534 | t.Error(err) 535 | } 536 | 537 | // Inorder to test what if the hadler woks fine when an internal server error 538 | // pccur. We set the accounts bucket to "", note that this hsould be restored 539 | // after this test finish inorder for other tests to work properly. 540 | // 541 | // All handlers reiles on the rx.cfg object heavily. 542 | bAcc := rx.cfg.AccountsBucket 543 | rx.cfg.AccountsBucket = "" 544 | 545 | // case an ajax request 546 | res5, err := httpGetAjax(client, getAllURL) 547 | if err != nil { 548 | t.Error(err) 549 | } 550 | err = checkResponse(res5, http.StatusNotFound, errNotFound.Error()) 551 | if err != nil { 552 | t.Error(err) 553 | } 554 | 555 | // case a standard http request 556 | res6, err := client.Get(getAllURL) 557 | if err != nil { 558 | t.Error(err) 559 | } 560 | err = checkResponse(res6, http.StatusNotFound, "shit not found") 561 | if err != nil { 562 | t.Error(err) 563 | } 564 | 565 | // Restore the accounts bucket config value 566 | rx.cfg.AccountsBucket = bAcc 567 | 568 | profileForm := url.Values{"city": {"mwanza"}, "country": {"Tanzania"}} 569 | 570 | // update query for a singele user with id pids[0] 571 | vars = url.Values{"u": {"true"}, "id": {pids[0]}} 572 | 573 | // case posting a valid form but the user is not logged in, the request is a standard http one. 574 | res7, err := client.PostForm(fmt.Sprintf("%s%s?%s", ts.URL, profilePath, vars.Encode()), profileForm) 575 | if err != nil { 576 | t.Error(err) 577 | } 578 | err = checkResponse(res7, http.StatusOK, "login-form") 579 | if err != nil { 580 | t.Error(err) 581 | } 582 | 583 | // case posting a valid form but the user is not logged in, the request is ajax. 584 | res8, err := httpPostAjax(client, fmt.Sprintf("%s/profile?%s", ts.URL, vars.Encode()), strings.NewReader(profileForm.Encode())) 585 | if err != nil { 586 | t.Error(err) 587 | } 588 | err = checkResponse(res8, http.StatusForbidden, errForbidden.Error()) 589 | if err != nil { 590 | t.Error(err) 591 | } 592 | 593 | // login and create a session for user with pids[0] 594 | varsLogin := url.Values{"email": {emails[0]}, "password": {pass}} 595 | res9, err := client.PostForm(fmt.Sprintf("%s%s", ts.URL, loginPath), varsLogin) 596 | if err != nil { 597 | t.Error(err) 598 | } 599 | err = checkResponse(res9, http.StatusOK, "search") 600 | if err != nil { 601 | t.Error(err) 602 | } 603 | 604 | vars = url.Values{"u": {"true"}, "id": {pids[1]}} 605 | 606 | // case posting a valid form and the user is logged in, the request is a standard http one. 607 | // The loggedIn user ID is defferent from the id provided by the url. 608 | res10, err := client.PostForm(fmt.Sprintf("%s%s?%s", ts.URL, profilePath, vars.Encode()), profileForm) 609 | if err != nil { 610 | t.Error(err) 611 | } 612 | err = checkResponse(res10, http.StatusInternalServerError, "forbidden") 613 | if err != nil { 614 | t.Error(err) 615 | } 616 | 617 | varsTrue := url.Values{ 618 | "u": {"true"}, 619 | "id": {pids[0]}, 620 | } 621 | 622 | // The profile url which has the id query match logged user id 623 | loggedUsrURL := fmt.Sprintf("%s%s?%s", ts.URL, profilePath, varsTrue.Encode()) 624 | 625 | // case posting an invalid form but the user is logged in, the request is a standard http one. 626 | profileForm2 := url.Values{ 627 | "city": {"mwanza"}, 628 | "country": {"Tanzania"}, 629 | "age": {"12"}, 630 | "birth_date": {birthDateFormat}, 631 | } 632 | res11, err := client.PostForm(loggedUsrURL, profileForm2) 633 | if err != nil { 634 | t.Error(err) 635 | } 636 | err = checkResponse(res11, http.StatusOK, "umri unatakiwa uwe zaidi ya miaka 18") 637 | if err != nil { 638 | t.Error(err) 639 | } 640 | // case posting a valid form, the user is logged in and the request is standard http 641 | profileForm3 := url.Values{ 642 | "first_name": {"geofrey"}, 643 | "last_name": {"ernest"}, 644 | "gender": {"1"}, 645 | "street": {"kilimahewa"}, 646 | "city": {"mwanza"}, 647 | "country": {"Tanzania"}, 648 | "age": {"19"}, 649 | "birth_date": {birthDate}, 650 | } 651 | res12, err := client.PostForm(loggedUsrURL, profileForm3) 652 | if err != nil { 653 | t.Error(err) 654 | } 655 | err = checkResponse(res12, http.StatusOK, birthDate) 656 | if err != nil { 657 | t.Errorf("checking response %v", err) 658 | } 659 | } 660 | 661 | // Creates a test druve server for using the Remix handlers., it also returns a ready 662 | // to use client, that supports sessions. 663 | func testServer(t *testing.T) (*httptest.Server, *http.Client, *Remix) { 664 | cfg := &RemixConfig{ 665 | AccountsBucket: "accounts", 666 | SessionName: "aurora", 667 | LoginRedirect: "/", 668 | DBDir: "fixture", 669 | DBExtension: ".bdb", 670 | AccountsDB: "fixture/accounts.bdb", 671 | ProfilesBucket: "profiles", 672 | SessionsDB: "fixture/sessions.bdb", 673 | SessionsBucket: "sessions", 674 | ProfilePicField: "profile", 675 | PhotosField: "photos", 676 | TemplatesDir: "templates", 677 | TemplatesExtensions: []string{".tmpl", ".html", ".tpl"}, 678 | SessMaxAge: 30, 679 | SessionPath: "/", 680 | MessagesBucket: "messages", 681 | } 682 | rx := NewRemix(cfg) 683 | jar, err := cookiejar.New(nil) 684 | if err != nil { 685 | t.Error(err) 686 | } 687 | 688 | // Create the database directory if it does not exist 689 | err = os.MkdirAll(rx.cfg.DBDir, 0700) 690 | if err != nil { 691 | t.Error(err) 692 | } 693 | client := &http.Client{Jar: jar} 694 | ts := httptest.NewServer(rx.Routes()) 695 | return ts, client, rx 696 | } 697 | 698 | // checkts if the given str contains substring subStr 699 | func contains(str, substr string) bool { 700 | return strings.Contains(str, substr) 701 | } 702 | 703 | func httpGet(client *http.Client, url string) (*http.Response, error) { 704 | h := make(http.Header) 705 | return httpCall(client, "GET", url, h, nil) 706 | } 707 | 708 | func httpGetAjax(client *http.Client, url string) (*http.Response, error) { 709 | h := make(http.Header) 710 | h.Set("X-Requested-With", "XMLHttpRequest") 711 | return httpCall(client, "GET", url, h, nil) 712 | } 713 | 714 | func httpPostAjax(client *http.Client, url string, body io.Reader) (*http.Response, error) { 715 | h := make(http.Header) 716 | h.Set("X-Requested-With", "XMLHttpRequest") 717 | return httpCall(client, "POST", url, h, body) 718 | } 719 | 720 | func httpCall(client *http.Client, method, url string, header http.Header, body io.Reader) (*http.Response, error) { 721 | req, err := http.NewRequest(method, url, body) 722 | if err != nil { 723 | return nil, err 724 | } 725 | for k, vs := range header { 726 | req.Header[k] = vs 727 | } 728 | return client.Do(req) 729 | } 730 | func checkResponse(res *http.Response, status int, contain ...string) error { 731 | defer res.Body.Close() 732 | var err listErr 733 | w := &bytes.Buffer{} 734 | io.Copy(w, res.Body) 735 | if res.StatusCode != status { 736 | err = append(err, fmt.Errorf("Expected %d got %d \n", status, res.StatusCode)) 737 | } 738 | 739 | if len(contain) > 0 { 740 | if !contains(w.String(), contain[0]) { 741 | err = append(err, fmt.Errorf("Expected %s to contain %s", w.String(), contain[0])) 742 | } 743 | } 744 | if len(err) > 0 { 745 | return err 746 | } 747 | return nil 748 | } 749 | --------------------------------------------------------------------------------