瀏覽代碼

workspace and login working

bmallred 9 年之前
父節點
當前提交
49a70b2f81
共有 12 個文件被更改,包括 511 次插入99 次删除
  1. 1 0
      .gitignore
  2. 61 6
      account/account.go
  3. 17 0
      account/account_test.go
  4. 6 0
      handlers/blog.go
  5. 200 1
      handlers/content.go
  6. 15 5
      handlers/logon.go
  7. 13 3
      html/article.html
  8. 4 4
      html/default.html
  9. 174 73
      html/workspace.html
  10. 二進制
      loop
  11. 11 3
      main.go
  12. 9 4
      security/security_test.go

+ 1 - 0
.gitignore

@ -21,3 +21,4 @@ _testmain.go
21 21
22 22
*.exe
23 23
loop
24
loop_users

+ 61 - 6
account/account.go

@ -16,7 +16,7 @@ import (
16 16
17 17
const (
18 18
	UserFile          = "loop_users"
19
	publishDateLayout = "200601021504"
19
	PublishDateLayout = "200601021504"
20 20
)
21 21
22 22
type Account struct {
@ -204,7 +204,7 @@ func (a *Account) Add(content, filename string, file []byte) (string, error) {
204 204
205 205
	// Create new content directory if necessary
206 206
	if content == "" {
207
		content = fmt.Sprintf("%x", security.Hash(userDir+time.Now().Format(publishDateLayout), environment.Salt()))
207
		content = fmt.Sprintf("%x", security.Hash(userDir+time.Now().Format(PublishDateLayout), environment.Salt()))
208 208
	}
209 209
210 210
	// Create new content directory
@ -218,9 +218,9 @@ func (a *Account) Add(content, filename string, file []byte) (string, error) {
218 218
219 219
	// Check if filename already exists
220 220
	filePath := contentPath + s + filename
221
	if _, err := os.Stat(filePath); os.IsExist(err) {
222
		return filePath, err
223
	}
221
	//if _, err := os.Stat(filePath); os.IsExist(err) {
222
	//	return filePath, err
223
	//}
224 224
225 225
	// Write file to content directory
226 226
	fi, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
@ -250,7 +250,7 @@ func (a *Account) Publish(title, content string, date time.Time) error {
250 250
	userDir := strings.ToLower(a.Username)
251 251
	s := string(filepath.Separator)
252 252
	contentPath := baseDir + s + userDir + s + "content" + s + content
253
	publishPath := baseDir + s + userDir + s + "blog" + s + date.Format(publishDateLayout) + "-" + prettyTitle
253
	publishPath := baseDir + s + userDir + s + "blog" + s + date.Format(PublishDateLayout) + "-" + prettyTitle
254 254
255 255
	// Check if the content exists
256 256
	if _, err := os.Stat(contentPath); os.IsNotExist(err) {
@ -270,3 +270,58 @@ func (a *Account) Publish(title, content string, date time.Time) error {
270 270
271 271
	return nil
272 272
}
273
274
func (a *Account) Content() ([]string, error) {
275
	baseDir := os.Getenv("LOOP_DATA")
276
	userDir := strings.ToLower(a.Username)
277
	s := string(filepath.Separator)
278
	contentPath := baseDir + s + userDir + s + "content"
279
280
	d, err := os.Open(contentPath)
281
	if err != nil {
282
		return []string{}, err
283
	}
284
285
	return d.Readdirnames(0)
286
}
287
288
func (a *Account) Read(title string) (string, error) {
289
	baseDir := os.Getenv("LOOP_DATA")
290
	userDir := strings.ToLower(a.Username)
291
	s := string(filepath.Separator)
292
	fp := baseDir + s + userDir + s + "content" + s + title + s + "document.md"
293
294
	// Check to see if the file exists
295
	if _, err := os.Stat(fp); os.IsNotExist(err) {
296
		return "", err
297
	}
298
299
	// Open the file
300
	fi, err := os.Open(fp)
301
	if err != nil {
302
		return "", err
303
	}
304
305
	// Read all of its contents
306
	raw, err := ioutil.ReadAll(fi)
307
	if err != nil {
308
		return "", err
309
	}
310
311
	return string(raw), nil
312
}
313
314
func (a *Account) Remove(title string) error {
315
	baseDir := os.Getenv("LOOP_DATA")
316
	userDir := strings.ToLower(a.Username)
317
	s := string(filepath.Separator)
318
	fp := baseDir + s + userDir + s + "content" + s + title
319
320
	// Check to see if the file exists
321
	if _, err := os.Stat(fp); os.IsNotExist(err) {
322
		return err
323
	}
324
325
	// Delete the directory
326
	return os.RemoveAll(fp)
327
}

+ 17 - 0
account/account_test.go

@ -72,6 +72,23 @@ func TestAdd(t *testing.T) {
72 72
	}
73 73
}
74 74
75
func TestContent(t *testing.T) {
76
	a := Account{
77
		Username:   "guest",
78
		Passphrase: "guest",
79
	}
80
81
	environment.ConfigureEnvironment()
82
	a.Add("", "README.md", []byte{})
83
	c, err := a.Content()
84
	if err != nil {
85
		t.Error(err)
86
	}
87
	if len(c) == 0 {
88
		t.Error("No content found")
89
	}
90
}
91
75 92
func TestPublish(t *testing.T) {
76 93
	a := Account{
77 94
		Username:   "guest",

+ 6 - 0
handlers/blog.go

@ -16,6 +16,12 @@ var (
16 16
	))
17 17
)
18 18
19
type PageBlog struct {
20
	Title     string
21
	Published string
22
	Body      template.HTML
23
}
24
19 25
func BlogHandler(w http.ResponseWriter, r *http.Request) {
20 26
	if err := templateBlog.Execute(w, nil); err != nil {
21 27
		http.Error(w, err.Error(), http.StatusInternalServerError)

+ 200 - 1
handlers/content.go

@ -1,9 +1,19 @@
1 1
package handlers
2 2
3 3
import (
4
	"errors"
4 5
	"html/template"
5 6
	"log"
6 7
	"net/http"
8
	"time"
9
10
	"code.revolvingcow.com/revolvingcow/loop/account"
11
	"code.revolvingcow.com/revolvingcow/loop/environment"
12
	"code.revolvingcow.com/revolvingcow/loop/markdown"
13
	"code.revolvingcow.com/revolvingcow/loop/security"
14
15
	"github.com/gorilla/mux"
16
	"github.com/gorilla/sessions"
7 17
)
8 18
9 19
var (
@ -11,11 +21,200 @@ var (
11 21
		"html/loop.html",
12 22
		"html/workspace.html",
13 23
	))
24
	store = sessions.NewCookieStore([]byte(environment.Salt()))
14 25
)
15 26
27
// WorkspaceModel passes information to the template engine providing context to the web page.
28
type WorkspaceModel struct {
29
	Username    string
30
	Directories []string
31
	Title       string
32
	Content     string
33
}
34
35
// ContentHandler is the default handler used for user content.
16 36
func ContentHandler(w http.ResponseWriter, r *http.Request) {
17 37
	log.Println("Serving template for the content handler")
18
	if err := templateWorkspace.Execute(w, nil); err != nil {
38
39
	session, _ := store.Get(r, "session-account")
40
	account, err := validateCredentials(session)
41
	if err != nil {
42
		http.Error(w, err.Error(), http.StatusUnauthorized)
43
		return
44
	}
45
46
	directories, _ := account.Content()
47
	model := WorkspaceModel{
48
		Username:    account.Username,
49
		Directories: directories,
50
	}
51
52
	if err := templateWorkspace.Execute(w, model); err != nil {
19 53
		http.Error(w, err.Error(), http.StatusInternalServerError)
54
		return
20 55
	}
21 56
}
57
58
// ContentCreateHandler creates new content in the user context.
59
func ContentCreateHandler(w http.ResponseWriter, r *http.Request) {
60
	log.Println("Creating new document")
61
62
	session, _ := store.Get(r, "session-account")
63
	account, err := validateCredentials(session)
64
	if err != nil {
65
		http.Error(w, err.Error(), http.StatusUnauthorized)
66
		return
67
	}
68
69
	vars := mux.Vars(r)
70
	title := vars["title"]
71
	if title != "" {
72
		account.Add(title, "document.md", []byte{})
73
	} else {
74
		http.Redirect(w, r, account.Username, http.StatusSeeOther)
75
	}
76
77
	http.Redirect(w, r, "/"+account.Username+"/edit/"+title, http.StatusSeeOther)
78
}
79
80
// ContentReadHandler reads a document in the user context.
81
func ContentReadHandler(w http.ResponseWriter, r *http.Request) {
82
	log.Println("Reading document")
83
84
	session, _ := store.Get(r, "session-account")
85
	account, err := validateCredentials(session)
86
	if err != nil {
87
		http.Error(w, err.Error(), http.StatusUnauthorized)
88
		return
89
	}
90
91
	vars := mux.Vars(r)
92
	title := vars["title"]
93
	if title == "" {
94
		http.Error(w, "No title given", http.StatusInternalServerError)
95
		return
96
	}
97
98
	contents, err := account.Read(title)
99
	if err != nil {
100
		http.Error(w, err.Error(), http.StatusInternalServerError)
101
		return
102
	}
103
104
	directories, _ := account.Content()
105
	model := WorkspaceModel{
106
		Username:    account.Username,
107
		Directories: directories,
108
		Title:       title,
109
		Content:     contents,
110
	}
111
112
	if err := templateWorkspace.Execute(w, model); err != nil {
113
		http.Error(w, err.Error(), http.StatusInternalServerError)
114
		return
115
	}
116
}
117
118
// ContentPreviewHandler displays a Markdown document in HTML format.
119
func ContentPreviewHandler(w http.ResponseWriter, r *http.Request) {
120
	log.Println("Previewing document")
121
122
	session, _ := store.Get(r, "session-account")
123
	account, err := validateCredentials(session)
124
	if err != nil {
125
		http.Error(w, err.Error(), http.StatusUnauthorized)
126
		return
127
	}
128
129
	vars := mux.Vars(r)
130
	title := vars["title"]
131
	contents := r.FormValue("contents")
132
	if contents == "" {
133
		contents, _ = account.Read(title)
134
	}
135
	md := template.HTML(string(markdown.MarkdownToHtml([]byte(contents))))
136
	model := PageBlog{
137
		Title:     title,
138
		Published: time.Now().Format("2006-01-02 15:04"),
139
		Body:      md,
140
	}
141
142
	if err := templateArticle.Execute(w, model); err != nil {
143
		http.Error(w, err.Error(), http.StatusInternalServerError)
144
		return
145
	}
146
}
147
148
// ContentUpdateHandler updates content in the user context.
149
func ContentUpdateHandler(w http.ResponseWriter, r *http.Request) {
150
	log.Println("Updating document")
151
152
	session, _ := store.Get(r, "session-account")
153
	account, err := validateCredentials(session)
154
	if err != nil {
155
		http.Error(w, err.Error(), http.StatusUnauthorized)
156
		return
157
	}
158
159
	vars := mux.Vars(r)
160
	title := vars["title"]
161
	if title == "" {
162
		http.Error(w, "No title given", http.StatusInternalServerError)
163
		return
164
	}
165
166
	contents := r.FormValue("contents")
167
	if contents == "" {
168
		http.Error(w, "No content given", http.StatusInternalServerError)
169
		return
170
	}
171
172
	account.Add(title, "document.md", []byte(contents))
173
}
174
175
// ContentUploadHandler allows for multiple document uploads in the user context.
176
func ContentUploadHandler(w http.ResponseWriter, r *http.Request) {
177
	log.Println("Uploading attachment")
178
}
179
180
// ContentDeleteHandler deletes content in the user context.
181
func ContentDeleteHandler(w http.ResponseWriter, r *http.Request) {
182
	log.Println("Deleting document")
183
184
	session, _ := store.Get(r, "session-account")
185
	account, err := validateCredentials(session)
186
	if err != nil {
187
		http.Error(w, err.Error(), http.StatusUnauthorized)
188
		return
189
	}
190
191
	vars := mux.Vars(r)
192
	title := vars["title"]
193
	if title == "" {
194
		http.Error(w, "No title given", http.StatusInternalServerError)
195
		return
196
	}
197
198
	account.Remove(title)
199
	http.Redirect(w, r, "/"+account.Username, http.StatusSeeOther)
200
}
201
202
// validateCredentials attempts to validate session information.
203
func validateCredentials(session *sessions.Session) (account.Account, error) {
204
	user := session.Values["username"]
205
	pass := session.Values["passphrase"]
206
	if user == nil || pass == nil {
207
		return account.Account{}, errors.New("No session state")
208
	}
209
210
	account := account.Account{
211
		Username:   user.(string),
212
		Passphrase: security.GeneratePassphrase(user.(string), pass.(string)),
213
	}
214
	valid, err := account.Validate()
215
	if err != nil || !valid {
216
		return account, errors.New("Invalid credentials")
217
	}
218
219
	return account, nil
220
}

+ 15 - 5
handlers/logon.go

@ -1,26 +1,36 @@
1 1
package handlers
2 2
3 3
import (
4
	"log"
4 5
	"net/http"
5 6
6 7
	"code.revolvingcow.com/revolvingcow/loop/account"
8
	"code.revolvingcow.com/revolvingcow/loop/security"
7 9
)
8 10
11
// LogonHandler is responsible for the initial log on procedures.
9 12
func LogonHandler(w http.ResponseWriter, r *http.Request) {
10
	email := r.FormValue("email")
11
	password := r.FormValue("password")
13
	log.Println("Logon handler")
14
15
	login := r.FormValue("login")
16
	passphrase := r.FormValue("passphrase")
12 17
	a := account.Account{
13
		Username:   email,
14
		Passphrase: password,
18
		Username:   login,
19
		Passphrase: security.GeneratePassphrase(login, passphrase),
15 20
	}
16 21
17 22
	err := a.Create()
18 23
	if err != nil {
19
		if err.Error() == "Account already exists" {
24
		if err.Error() != "Account already exists" {
20 25
			http.Error(w, err.Error(), http.StatusInternalServerError)
21 26
			return
22 27
		}
23 28
	}
24 29
30
	session, _ := store.Get(r, "session-account")
31
	session.Values["username"] = login
32
	session.Values["passphrase"] = passphrase
33
	store.Save(r, w, session)
34
25 35
	http.Redirect(w, r, "/"+a.Username, http.StatusSeeOther)
26 36
}

+ 13 - 3
html/article.html

@ -1,12 +1,22 @@
1 1
{{ define "title" }}{{ end }}
2
{{ define "styles" }}{{ end }}
2
{{ define "styles" }}
3
    <style type="text/css">
4
        h3 small { display: block; font-size: 45%; color: #666; }
5
        #markdown ul { padding-left: 2em; }
6
        #markdown ul li { list-style-type: initial; }
7
        #markdown code { padding: 1em; background-color: #eee; }
8
    </style>
9
{{ end }}
3 10
{{ define "content" }}
4 11
    <article>
5 12
        <h3>
6 13
            {{ .Title }}
7
            <small>{{ .Published }}</small>
14
            <small>Published on {{ .Published }}</small>
15
            <hr />
8 16
        </h3>
9
        {{ .Body }}
17
        <div id="markdown">
18
            {{ .Body }}
19
        </div>
10 20
    </article>
11 21
{{ end }}
12 22
{{ define "scripts" }}{{ end }}

+ 4 - 4
html/default.html

@ -150,12 +150,12 @@
150 150
                    <form class="col s12" method="post" action="/logon">
151 151
                        <div class="row">
152 152
                            <div class="input-field col s12">
153
                                <input id="email" type="text" class="validate">
154
                                <label for="email">Email</label>
153
                                <input id="login" name="login" type="text" class="validate">
154
                                <label for="login">Login</label>
155 155
                            </div>
156 156
                            <div class="input-field col s12">
157
                                <input id="password" type="password" class="validate">
158
                                <label for="password">Password</label>
157
                                <input id="passphrase" name="passphrase" type="password" class="validate">
158
                                <label for="passphrase">Passphrase</label>
159 159
                            </div>
160 160
                            <div class="input-field col s12">
161 161
                                <button class="btn waves-effect waves-light" type="submit" name="action">

+ 174 - 73
html/workspace.html

@ -1,119 +1,142 @@
1
{{ define "title" }}Loop Workspace{{ end }}
1
{{ define "title" }}Loop: Workspace{{ end }}
2 2
{{ define "styles" }}
3 3
    <style>
4 4
        pre#editor {
5 5
            height: 100vh;
6 6
        }
7 7
8
        iframe#view {
9
            height: 100vh;
10
            borders: none;
11
        }
12
13
        div#welcome {
14
            height: 100vh;
15
        }
16
17
        div#welcome h5 {
18
            width: 100%;
19
        }
20
8 21
        div#holder {
9 22
            border: 2px dashed #ccc;
10 23
            /*width: 300px;*/
11 24
            min-height: 110px;
12 25
            margin: 20px auto;
13 26
        }
27
28
        .select-wrapper {
29
            z-index: 2;
30
        }
31
14 32
        div#holder.hover {
15 33
            border: 2px dashed #0c0;
16 34
        }
35
17 36
        div#holder p {
18 37
            margin: 10px;
19 38
            font-size: 14px;
20 39
        }
40
21 41
        progress {
22 42
            width: 100%;
23 43
        }
44
24 45
        progress:after {
25 46
            content: '%';
26 47
        }
48
27 49
        .fail {
28 50
            background: #c00;
29 51
            padding: 2px;
30 52
            color: #fff;
31 53
        }
54
32 55
        .hidden {
33 56
            display: none !important;
34 57
        }
58
59
        #editor-actions a.btn-flat {
60
            padding: 0 0.5rem;
61
        }
62
63
        .fixed-action-btn ul button.btn-floating {
64
            opacity: 0;
65
        }
66
67
        .fullscreen {
68
            width: 100%;
69
            height: 100vh;
70
        }
35 71
    </style>
36 72
{{ end }}
37 73
{{ define "content" }}
38
    <div id="toolbar" class="row">
39
        <div class="col s12 center">
40
            <!-- Toolbar of stuffs -->
41
            <button id="fullscreen" class="btn waves-effect waves-light">
42
                <i class="mdi-navigation-fullscreen left hide-on-med-and-down"></i>
43
                <i class="mdi-navigation-fullscreen hide-on-large-only"></i>
44
                <span class="hide-on-med-and-down">Focus Mode</span>
45
            </button>
46
            <button id="save" class="btn waves-effect waves-light">
47
                <i class="mdi-content-save left hide-on-med-and-down"></i>
48
                <i class="mdi-content-save hide-on-large-only"></i>
49
                <span class="hide-on-med-and-down">Save</span>
50
            </button>
51
            <button id="create" class="btn waves-effect waves-light">
52
                <i class="mdi-content-create left hide-on-med-and-down"></i>
53
                <i class="mdi-content-create hide-on-large-only"></i>
54
                <span class="hide-on-med-and-down">Create</span>
55
            </button>
56
            <button id="publish" class="btn waves-effect waves-light">
57
                <i class="mdi-editor-publish left hide-on-med-and-down"></i>
58
                <i class="mdi-editor-publish hide-on-large-only"></i>
59
                <span class="hide-on-med-and-down">Publish</span>
60
            </button>
61
            <a id="upload" class="btn waves-effect waves-light modal-trigger" href="#upload-area">
62
                <i class="mdi-file-file-upload left hide-on-med-and-down"></i>
63
                <i class="mdi-file-file-upload hide-on-large-only"></i>
64
                <span class="hide-on-med-and-down">Upload</span>
65
            </a>
66
        </div>
67
    </div>
68 74
    <div class="row">
69 75
        <div id="left-pane" class="col s4 m4 l2">
70
            <!-- Directory of stuffs -->
71 76
            <div class="row">
72 77
                <div class="col s12">
73 78
                    <label>Workspace</label>
74 79
                    <select>
75 80
                        <option value="" disabled>Select a directory</option>
76
                        <option value="Persona" selected>Personal</option>
77
                        <option value="Blog">Blog</option>
81
                        <option value="Personal" selected>Personal</option>
78 82
                    </select>
79 83
                </div>
84
                <div class="col s12 center">
85
                    <a class="waves-effect waves-light btn modal-trigger" href="#modal-new-directory">New Content</a>
86
                </div>
80 87
            </div>
81 88
            <div class="row">
82
                <div class="col s12">
89
                <div class="col s12" style="overflow-y: auto;">
83 90
                    <ul class="collection">
84
                        <li class="collection-item avatar dismissable">
85
                            <i class="mdi-file-folder circle"></i>
86
                            <span class="title">Title</span>
87
                            <p>(3 files)</p>
88
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
89
                        </li>
90
                        <li class="collection-item avatar dismissable">
91
                            <i class="mdi-file-folder circle"></i>
92
                            <span class="title">Title</span>
93
                            <p></p>
94
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
95
                        </li>
96
                        <li class="collection-item avatar dismissable">
97
                            <i class="mdi-file-folder circle"></i>
98
                            <span class="title">Title</span>
99
                            <p></p>
100
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
101
                        </li>
102
                        <li class="collection-item avatar dismissable">
103
                            <i class="mdi-file-folder circle"></i>
104
                            <span class="title">Title</span>
105
                            <p>(7 files)</p>
106
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
107
                        </li>
91
                        {{ range $d := .Directories }}
92
                            <li class="collection-item avatar dismissable">
93
                                <i class="mdi-file-folder circle"></i>
94
                                <span class="title"><a href="/{{ $.Username }}/edit/{{ $d }}">{{ $d }}</a></span>
95
                                <p><!--(3 files)--></p>
96
                                <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
97
                            </li>
98
                        {{ end }}
108 99
                    </ul>
109 100
                </div>
110 101
            </div>
111 102
        </div>
112 103
        <div id="workarea" class="col s8 m8 l10">
113
            <div class="row">
114
                <pre id="editor" class="col s12 m12 l12"></pre>
115
                <!--<div id="preview" class="col s12 m12 l6"></div>-->
104
            <div class="fixed-action-btn" style="bottom: 45px; right: 24px;">
105
                <a id="create" class="btn-floating btn-large red" title="Edit">
106
                    <i class="large mdi-editor-mode-edit"></i>
107
                </a>
108
                <ul>
109
                    <li><a id="delete" class="btn-floating red" title="Delete"><i class="large mdi-action-delete"></i></a></li>
110
                    <li><a id="save" class="btn-floating amber" title="Save"><i class="large mdi-content-save"></i></a></li>
111
                    <li><a id="publish" class="btn-floating green" title="Publish"><i class="large mdi-editor-publish"></i></a></li>
112
                    <li><a id="upload" class="btn-floating blue modal-trigger" href="#upload-area" title="Attach file"><i class="large mdi-editor-attach-file"></i></a></li>
113
                </ul>
116 114
            </div>
115
            {{ if .Title }}
116
                <div id="zoom" class="row">
117
                    <div id="editor-actions" class="col s12 m12 l12">
118
                        <a id="fullscreen" class="waves-effect waves-teal btn-flat right" title="Fullscreen">
119
                            <i class="mdi-navigation-fullscreen left hide-on-med-and-down"></i>
120
                            <i class="mdi-navigation-fullscreen hide-on-large-only"></i>
121
                            <span class="hide-on-med-and-down">Focus Mode</span>
122
                        </a>
123
                        <a id="preview" class="waves-effect waves-teal btn-flat right" title="Toggle Mode">
124
                            <i class="mdi-action-cached left hide-on-med-and-down"></i>
125
                            <i class="mdi-action-cached hide-on-large-only"></i>
126
                            <span class="hide-on-med-and-down">Preview</span>
127
                        </a>
128
                    </div>
129
                    <div class="col s12 m12 l12"><h4>{{ .Title }}</h4></div>
130
                    <pre id="editor" class="col s12 m12 l12">{{ .Content }}</pre>
131
                    <iframe id="view" class="col s12 m12 l12" style="display: none;" frameBorder="0" seamless="seamless" src="/{{ .Username }}/edit/{{ .Title }}/preview"></iframe>
132
                </div>
133
            {{ else }}
134
                <div class="row">
135
                    <div id="welcome" class="col s12 m12 l12 valign-wrapper">
136
                        <h5 class="valign center">Welcome!</h5>
137
                    </div>
138
                </div>
139
            {{ end }}
117 140
        </div>
118 141
    </div>
119 142
    <div class="row">
@ -121,7 +144,6 @@
121 144
            <div id="upload-area" class="modal bottom-sheet">
122 145
                <div class="modal-content">
123 146
                    <h4>Upload</h4>
124
125 147
                    <div id="holder" class="valign-wrapper">
126 148
                        <p class="valign center" style="width: 100%;">Drag &amp; drop your files here!</p>
127 149
                    </div>
@ -139,6 +161,23 @@
139 161
            </div>
140 162
        </div>
141 163
    </div>
164
    <div id="modal-new-directory" class="modal">
165
        <div class="modal-content">
166
            <h4>New Content</h4>
167
            <form id="new-directory" class="col s12" method="post" action="/{{ .Username }}/edit/">
168
                <div class="row">
169
                    <div class="input-field col s12">
170
                        <input id="title" name="title" type="text" class="validate">
171
                        <label for="title">Document Title</label>
172
                    </div>
173
                </div>
174
            </form>
175
        </div>
176
        <div class="modal-footer">
177
            <a id="new" href="#" class="modal-action modal-close waves-effect waves-green btn-flat">Create</a>
178
            <a href="#" class="modal-action modal-close waves-effect waves-red btn-flat">Cancel</a>
179
        </div>
180
    </div>
142 181
{{ end }}
143 182
{{ define "scripts" }}
144 183
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.1.8/ace.js"></script>
@ -146,9 +185,11 @@
146 185
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.1.8/mode-markdown.js"></script>
147 186
    <script type="text/javascript">
148 187
        (function () {
149
            var editor = ace.edit('editor');
150
            editor.setTheme('ace/theme/chrome');
151
            editor.getSession().setMode('ace/mode/markdown');
188
            if (document.getElementById('editor')) {
189
                var editor = ace.edit('editor');
190
                editor.setTheme('ace/theme/chrome');
191
                editor.getSession().setMode('ace/mode/markdown');
192
            }
152 193
        })();
153 194
154 195
        (function () {
@ -232,17 +273,17 @@
232 273
233 274
        $(function () {
234 275
            function toggleFullScreen() {
235
                var editorElement = document.getElementById('editor');
276
                var zoom = document.getElementById('zoom');
236 277
237
                if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullScreenElement && !document.msFullscreenElement) {
278
                if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
238 279
                    if (document.documentElement.requestFullscreen) {
239
                        editorElement.requestFullscreen();
280
                        zoom.requestFullscreen();
240 281
                    } else if (document.documentElement.msRequestFullscreen) {
241
                        editorElement.msRequestFullscreen();
282
                        zoom.msRequestFullscreen();
242 283
                    } else if (document.documentElement.mozRequestFullScreen) {
243
                        editorElement.mozRequestFullScreen();
284
                        zoom.mozRequestFullScreen();
244 285
                    } else if (document.documentElement.webkitRequestFullscreen) {
245
                        editorElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
286
                        zoom.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
246 287
                    }
247 288
                } else {
248 289
                    if (document.exitFullscreen) {
@ -259,10 +300,70 @@
259 300
260 301
            $('select').material_select();
261 302
            $('.modal-trigger').leanModal();
262
            $('button#fullscreen').click(function (event) {
303
304
            $('#fullscreen').click(function (event) {
263 305
                event.preventDefault();
306
                $('#zoom').toggleClass('fullscreen');
264 307
                toggleFullScreen();
265 308
            });
309
310
            $('#preview').click(function (event) {
311
                event.preventDefault();
312
                var editor = ace.edit('editor');
313
314
                if (!$('#view').is(':visible')) {
315
                    $.ajax({
316
                        url: window.location.href,
317
                        method: 'put',
318
                        data: {
319
                            'contents': editor.getValue()
320
                        }
321
                    })
322
                    .done(function () {
323
                        $('#view').attr('src', $('#view').attr('src'));
324
                        $('#editor, #view').toggle();
325
                    });
326
                } else {
327
                    $('#editor, #view').toggle();
328
                }
329
            });
330
331
            $('a#delete').click(function (event) {
332
                event.preventDefault();
333
                var title = $('input#title').val();
334
335
                $.ajax({
336
                    url: window.location.href,
337
                    method: 'delete'
338
                })
339
                .done(function () {
340
                    window.location.href = '/{{ .Username }}';
341
                });
342
            });
343
344
            $('a#new').click(function (event) {
345
                event.preventDefault();
346
                var title = $('input#title').val();
347
348
                var form = $('form#new-directory');
349
                form.attr('action', form.attr('action') + title);
350
                form.submit();
351
352
                return false;
353
            });
354
355
            $('a#save').click(function (event) {
356
                event.preventDefault();
357
                var editor = ace.edit('editor');
358
359
                $.ajax({
360
                    url: window.location.href,
361
                    method: 'put',
362
                    data: {
363
                        'contents': editor.getValue()
364
                    }
365
                });
366
            });
266 367
        });
267 368
    </script>
268 369
{{ end }}

二進制
loop


+ 11 - 3
main.go

@ -21,13 +21,21 @@ func main() {
21 21
	auth := r.PathPrefix("/logon").Subrouter()
22 22
	auth.Methods("POST").HandlerFunc(handlers.LogonHandler)
23 23
24
	content := r.PathPrefix("/{username}").Subrouter()
24
	doc := r.PathPrefix("/{user}/edit/{title}").Subrouter()
25
	doc.HandleFunc("/preview", handlers.ContentPreviewHandler).Methods("GET")
26
	doc.Methods("GET").HandlerFunc(handlers.ContentReadHandler)
27
	doc.Methods("POST").HandlerFunc(handlers.ContentCreateHandler)
28
	//doc.Methods("POST").HandlerFunc(handlers.ContentUploadHandler)
29
	doc.Methods("PUT").HandlerFunc(handlers.ContentUpdateHandler)
30
	doc.Methods("DELETE").HandlerFunc(handlers.ContentDeleteHandler)
31
32
	content := r.PathPrefix("/{user}").Subrouter()
25 33
	content.Methods("GET").HandlerFunc(handlers.ContentHandler)
26 34
27
	article := r.PathPrefix("/{username}/blog/{article}").Subrouter()
35
	article := r.PathPrefix("/{user}/blog/{article}").Subrouter()
28 36
	article.Methods("GET").HandlerFunc(handlers.BlogArticleHandler)
29 37
30
	blog := r.PathPrefix("/{username}/blog").Subrouter()
38
	blog := r.PathPrefix("/{user}/blog").Subrouter()
31 39
	blog.Methods("GET").HandlerFunc(handlers.BlogHandler)
32 40
33 41
	log.Print("Serving content on ", environment.Address())

+ 9 - 4
security/security_test.go

@ -1,8 +1,6 @@
1 1
package security
2 2
3
import (
4
	"testing"
5
)
3
import "testing"
6 4
7 5
func TestEncoding(t *testing.T) {
8 6
	clear := "encoding test"
@ -40,5 +38,12 @@ func TestHashCredentials(t *testing.T) {
40 38
}
41 39
42 40
func TestGeneratePassphrase(t *testing.T) {
43
	t.Skip("Need a test for generating a passphrase")
41
	username := "bryan"
42
	passphrase := "test"
43
	expected := "o47ZaAu3pd5cK0ZA412Z3rpsi8MDdlUZPkJ9Z51x7QlNUc7S4EGYuT9XIe4HYtzy7vxHah528ibkfd4CjHCSsg=="
44
	generated := GeneratePassphrase(username, passphrase)
45
46
	if expected != generated {
47
		t.FailNow()
48
	}
44 49
}