Browse Source

Finish token creation pages

Morgan Bazalgette 1 year ago
parent
commit
32864e68be
Signed by: Morgan Bazalgette <the@howl.moe> GPG Key ID: 40D328300D245DA5
9 changed files with 284 additions and 40 deletions
  1. 2
    0
      data/locales/templates.pot
  2. 122
    0
      dev.go
  3. 8
    0
      funcmap.go
  4. 4
    0
      main.go
  5. 0
    3
      services/cieca/csrf.go
  6. 11
    11
      static/dist.min.js
  7. 48
    18
      static/ripple.js
  8. 55
    0
      templates/dev/edit_token.html
  9. 34
    8
      templates/dev/tokens.html

+ 2
- 0
data/locales/templates.pot View File

@@ -1079,3 +1079,5 @@ msgstr ""
1079 1079
 msgid "You look new here. Allow us to introduce you to what Ripple is."
1080 1080
 msgstr ""
1081 1081
 
1082
+msgid "The Ripple API is the system through which developers can create applications to interact with Ripple. <b>If you're asked to fill out an API token from this page, be wary and only actually create the token if you really trust the owner of the application.</b>"
1083
+msgstr ""

+ 122
- 0
dev.go View File

@@ -0,0 +1,122 @@
1
+package main
2
+
3
+import (
4
+	"crypto/md5"
5
+	"database/sql"
6
+	"fmt"
7
+	"time"
8
+
9
+	"github.com/gin-gonic/gin"
10
+	"zxq.co/ripple/rippleapi/common"
11
+)
12
+
13
+func createAPIToken(c *gin.Context) {
14
+	ctx := getContext(c)
15
+	if ctx.User.ID == 0 {
16
+		resp403(c)
17
+		return
18
+	}
19
+
20
+	sess := getSession(c)
21
+	defer func() {
22
+		sess.Save()
23
+		c.Redirect(302, "/dev/tokens")
24
+	}()
25
+
26
+	if ok, _ := CSRF.Validate(ctx.User.ID, c.PostForm("csrf")); !ok {
27
+		addMessage(c, errorMessage{T(c, "Your session has expired. Please try redoing what you were trying to do.")})
28
+		return
29
+	}
30
+
31
+	privileges := common.Privileges(common.Int(c.PostForm("privileges"))).CanOnly(ctx.User.Privileges)
32
+	description := c.PostForm("description")
33
+
34
+	var (
35
+		tokenStr string
36
+		tokenMD5 string
37
+	)
38
+
39
+	for {
40
+		tokenStr = common.RandomString(32)
41
+		tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr)))
42
+
43
+		var id int
44
+		err := db.QueryRow("SELECT id FROM tokens WHERE token = ? LIMIT 1", tokenMD5).Scan(&id)
45
+		if err == sql.ErrNoRows {
46
+			break
47
+		}
48
+		if err != nil {
49
+			c.Error(err)
50
+			resp500(c)
51
+			return
52
+		}
53
+	}
54
+
55
+	_, err := db.Exec("INSERT INTO tokens(user, privileges, description, token, private, last_updated) VALUES (?, ?, ?, ?, '0', ?)",
56
+		ctx.User.ID, privileges, description, tokenMD5, time.Now().Unix())
57
+	if err != nil {
58
+		c.Error(err)
59
+		resp500(c)
60
+		return
61
+	}
62
+
63
+	addMessage(c, successMessage{
64
+		fmt.Sprintf("Your token has been created successfully! Your token is: <code>%s</code>.<br>Keep it safe, don't show it around, and store it now! We won't show it to you again.", tokenStr),
65
+	})
66
+}
67
+
68
+func deleteAPIToken(c *gin.Context) {
69
+	ctx := getContext(c)
70
+	if ctx.User.ID == 0 {
71
+		resp403(c)
72
+		return
73
+	}
74
+
75
+	sess := getSession(c)
76
+	defer func() {
77
+		sess.Save()
78
+		c.Redirect(302, "/dev/tokens")
79
+	}()
80
+
81
+	if ok, _ := CSRF.Validate(ctx.User.ID, c.PostForm("csrf")); !ok {
82
+		addMessage(c, errorMessage{T(c, "Your session has expired. Please try redoing what you were trying to do.")})
83
+		return
84
+	}
85
+
86
+	db.Exec("DELETE FROM tokens WHERE id = ? AND user = ? AND private = 0", c.PostForm("id"), ctx.User.ID)
87
+	addMessage(c, successMessage{"That token has been deleted successfully."})
88
+}
89
+
90
+func editAPIToken(c *gin.Context) {
91
+	ctx := getContext(c)
92
+	if ctx.User.ID == 0 {
93
+		resp403(c)
94
+		return
95
+	}
96
+
97
+	sess := getSession(c)
98
+	defer func() {
99
+		sess.Save()
100
+		c.Redirect(302, "/dev/tokens/edit?id="+c.PostForm("id"))
101
+	}()
102
+
103
+	if ok, _ := CSRF.Validate(ctx.User.ID, c.PostForm("csrf")); !ok {
104
+		addMessage(c, errorMessage{T(c, "Your session has expired. Please try redoing what you were trying to do.")})
105
+		return
106
+	}
107
+
108
+	privileges := common.Privileges(common.Int(c.PostForm("privileges"))).CanOnly(ctx.User.Privileges)
109
+	description := c.PostForm("description")
110
+
111
+	_, err := db.Exec("UPDATE tokens SET privileges = ?, description = ? WHERE user = ? AND id = ? AND private = 0",
112
+		privileges, description, ctx.User.ID, c.PostForm("id"))
113
+	if err != nil {
114
+		c.Error(err)
115
+		resp500(c)
116
+		return
117
+	}
118
+
119
+	addMessage(c, successMessage{
120
+		"Your token has been edited successfully!",
121
+	})
122
+}

+ 8
- 0
funcmap.go View File

@@ -131,6 +131,11 @@ var funcMap = template.FuncMap{
131 131
 		t = t.Add(time.Hour * 24)
132 132
 		return _time(t.Format(time.RFC3339), t)
133 133
 	},
134
+	// nativeTime creates a native Go time.Time from a RFC3339 timestamp.
135
+	"nativeTime": func(s string) time.Time {
136
+		t, _ := time.Parse(time.RFC3339, s)
137
+		return t
138
+	},
134 139
 	// band is a bitwise AND.
135 140
 	"band": func(i1 int, i ...int) int {
136 141
 		for _, el := range i {
@@ -482,6 +487,9 @@ var funcMap = template.FuncMap{
482 487
 		}
483 488
 		return doc.GetFile(slug, language)
484 489
 	},
490
+	"privilegesToString": func(privs float64) string {
491
+		return common.Privileges(privs).String()
492
+	},
485 493
 }
486 494
 
487 495
 var localeLanguages = []string{"de", "pl", "it", "es", "ru"} //, "ko"}

+ 4
- 0
main.go View File

@@ -291,6 +291,10 @@ func generateEngine() *gin.Engine {
291 291
 	r.GET("/settings/discord/finish", discordFinish)
292 292
 	r.POST("/settings/profbackground/:type", profBackground)
293 293
 
294
+	r.POST("/dev/tokens/create", createAPIToken)
295
+	r.POST("/dev/tokens/delete", deleteAPIToken)
296
+	r.POST("/dev/tokens/edit", editAPIToken)
297
+
294 298
 	r.GET("/donate/rates", btcconversions.GetRates)
295 299
 
296 300
 	r.Any("/blog/*url", blogRedirect)

+ 0
- 3
services/cieca/csrf.go View File

@@ -36,8 +36,5 @@ func (c *ciecaCSRF) Generate(u int) (string, error) {
36 36
 
37 37
 func (c *ciecaCSRF) Validate(u int, token string) (bool, error) {
38 38
 	_, e := c.GetWithExist(strconv.Itoa(u) + token)
39
-	if e {
40
-		c.Delete(strconv.Itoa(u) + token)
41
-	}
42 39
 	return e, nil
43 40
 }

+ 11
- 11
static/dist.min.js
File diff suppressed because it is too large
View File


+ 48
- 18
static/ripple.js View File

@@ -1,17 +1,17 @@
1 1
 /*!
2 2
  * ripple.js
3 3
  * Copyright (C) 2016-2017 Morgan Bazalgette and Giuseppe Guerra
4
- * 
4
+ *
5 5
  * This program is free software: you can redistribute it and/or modify
6 6
  * it under the terms of the GNU Affero General Public License as
7 7
  * published by the Free Software Foundation, either version 3 of the
8 8
  * License, or (at your option) any later version.
9
- * 
9
+ *
10 10
  * This program is distributed in the hope that it will be useful,
11 11
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 13
  * GNU Affero General Public License for more details.
14
- * 
14
+ *
15 15
  * You should have received a copy of the GNU Affero General Public License
16 16
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 17
  */
@@ -28,7 +28,7 @@ var singlePageSnippets = {
28 28
           switch (resp) {
29 29
           case "0":
30 30
             $("#telegram-code").closest(".field").addClass("success");
31
-            redir = redir ? redir : "/"; 
31
+            redir = redir ? redir : "/";
32 32
             window.location.href = redir;
33 33
             break;
34 34
           case "1":
@@ -44,11 +44,11 @@ var singlePageSnippets = {
44 44
 
45 45
   "/leaderboard": function() {
46 46
     page = page === 0 ? 1 : page;
47
-    
47
+
48 48
     function loadLeaderboard() {
49 49
       var wl = window.location;
50
-      window.history.replaceState('', document.title, 
51
-        wl.pathname + 
50
+      window.history.replaceState('', document.title,
51
+        wl.pathname +
52 52
         "?mode=" + favouriteMode +
53 53
         "&p=" + page +
54 54
         (country != "" ? "&country=" + encodeURI(country) : "") +
@@ -71,7 +71,7 @@ var singlePageSnippets = {
71 71
           tb.append(
72 72
             $("<tr />").append(
73 73
               $("<td />").text("#" + ((page-1) * 50 + (++i))),
74
-              $("<td />").html("<a href='/u/" + v.id + "' title='View profile'><i class='" + 
74
+              $("<td />").html("<a href='/u/" + v.id + "' title='View profile'><i class='" +
75 75
                 v.country.toLowerCase() + " flag'></i>" + escapeHTML(v.username) + "</a>"),
76 76
               $("<td />").html(scoreOrPP(v.chosen_mode.ranked_score, v.chosen_mode.pp)),
77 77
               $("<td />").text(v.chosen_mode.accuracy.toFixed(2) + "%"),
@@ -162,7 +162,7 @@ var singlePageSnippets = {
162 162
       if ($(this).is(":checked"))
163 163
         $("#custom-badge-fields").slideDown();
164 164
       else
165
-        $("#custom-badge-fields").slideUp();        
165
+        $("#custom-badge-fields").slideUp();
166 166
     });
167 167
     $("form").submit(function(e) {
168 168
       e.preventDefault();
@@ -252,7 +252,7 @@ var singlePageSnippets = {
252 252
       });
253 253
     });
254 254
   },
255
-  
255
+
256 256
   "/settings/avatar": function() {
257 257
     $("#file").change(function(e) {
258 258
       var f = e.target.files;
@@ -275,7 +275,7 @@ var singlePageSnippets = {
275 275
       if (data.submitted_by_user == 0)
276 276
         $("#by-you").attr("hidden", "hidden");
277 277
       else
278
-        $("#by-you").removeAttr("hidden");      
278
+        $("#by-you").removeAttr("hidden");
279 279
 
280 280
       $("#submitted-by-user").text(data.submitted_by_user);
281 281
       $("#max-per-user").text(data.max_per_user);
@@ -311,7 +311,7 @@ var singlePageSnippets = {
311 311
         postData.set_id = +reData[2];
312 312
       else
313 313
         postData.id = +reData[2];
314
-      var t = $(this);     
314
+      var t = $(this);
315 315
       api("beatmaps/rank_requests", postData, function(data) {
316 316
         t.removeClass("loading");
317 317
         showMessage("success", "Beatmap rank request has been submitted.");
@@ -346,6 +346,12 @@ var singlePageSnippets = {
346 346
       };
347 347
       $("#image-background").empty().append(i);
348 348
     });
349
+  },
350
+
351
+  "/dev/tokens": function() {
352
+    $("#privileges-number").on("input", function() {
353
+      $("#privileges-text").text(privilegesToString($(this).val()));
354
+    });
349 355
   }
350 356
 };
351 357
 
@@ -372,7 +378,7 @@ $(document).ready(function(){
372 378
       twemoji.parse(v);
373 379
     });
374 380
   }
375
-  
381
+
376 382
   // ripple stuff
377 383
   var f = singlePageSnippets[window.location.pathname];
378 384
   if (typeof f === 'function')
@@ -407,7 +413,7 @@ $(document).ready(function(){
407 413
       window.location.pathname = "/u/" + $(this).val();
408 414
     }
409 415
   });
410
-  
416
+
411 417
   // setup timeago
412 418
   $.timeago.settings.allowFuture = true;
413 419
   $("time.timeago").timeago();
@@ -453,7 +459,7 @@ function api(endpoint, data, success, failure, post) {
453 459
     post = failure;
454 460
     failure = undefined;
455 461
   }
456
-  
462
+
457 463
   var errorMessage = "An error occurred while contacting the Ripple API. Please report this to a Ripple developer.";
458 464
 
459 465
   $.ajax({
@@ -528,7 +534,7 @@ function setupSimplepag(callback) {
528 534
 }
529 535
 function disableSimplepagButtons(right) {
530 536
   var el = $(".simplepag");
531
-  
537
+
532 538
   if (page <= 1)
533 539
     el.find(".left.floated .item").addClass("disabled");
534 540
   else
@@ -590,7 +596,7 @@ function formToObject(form) {
590 596
         break;
591 597
       default:
592 598
         value = el.val();
593
-        break;        
599
+        break;
594 600
       }
595 601
       break;
596 602
     }
@@ -635,6 +641,30 @@ i18next.on("loaded", function() {
635 641
 
636 642
 function T(s, settings) {
637 643
   if (typeof settings !== "undefined" && typeof settings.count !== "undefined" && $.inArray(hanayoConf.language, langWhitelist) === -1 && settings.count !== 1)
638
-      s = keyPlurals[s];
644
+    s = keyPlurals[s];
639 645
   return i18next.t(s, settings);
640 646
 }
647
+
648
+var apiPrivileges = [
649
+	"ReadConfidential",
650
+	"Write",
651
+	"ManageBadges",
652
+	"BetaKeys",
653
+	"ManageSettings",
654
+	"ViewUserAdvanced",
655
+	"ManageUser",
656
+	"ManageRoles",
657
+	"ManageAPIKeys",
658
+	"Blog",
659
+	"APIMeta",
660
+	"Beatmap"
661
+];
662
+
663
+function privilegesToString(privs) {
664
+  var privList = [];
665
+  apiPrivileges.forEach(function(value, index) {
666
+    if ((privs & (1 << (index + 1))) != 0)
667
+      privList.push(value);
668
+  });
669
+  return privList.join(", ");
670
+}

+ 55
- 0
templates/dev/edit_token.html View File

@@ -0,0 +1,55 @@
1
+{{/*###
2
+Handler=/dev/tokens/edit
3
+TitleBar=Edit API token
4
+KyutGrill=dev.jpg
5
+MinPrivileges=2
6
+Include=menu.html
7
+HugeHeadingRight=true
8
+*/}}
9
+{{ define "tpl" }}
10
+{{ $ := . }}
11
+<div class="ui container">
12
+	<div class="ui stackable grid">
13
+		{{ template "devSidebar" . }}
14
+		<div class="twelve wide column">
15
+			<div class="ui segment">
16
+				{{ with .Get "tokens?id=%d" (.Gin.Query "id" | atoint) }}
17
+					{{ if .tokens }}
18
+						{{ with index .tokens 0 }}
19
+							<form class="ui form" method="POST" action="/dev/tokens/edit" id="edit-form">
20
+								<div class="field">
21
+									<label>Last used</label>
22
+									<input type="text" value="{{ .last_updated }}" disabled>
23
+								</div>
24
+								<div class="field">
25
+									<label>Privileges</label>
26
+									<input name="privileges" type="number" min="0" value="{{ printf "%.0f" .privileges }}">
27
+								</div>
28
+								<div class="field">
29
+									<label>Description</label>
30
+									<input name="description" type="text" value="{{ .description }}">
31
+								</div>
32
+								<input type="hidden" name="id" value="{{ $.Gin.Query "id" | atoint }}">
33
+								{{ csrfGenerate $.Context.User.ID }}
34
+								{{ ieForm $.Gin }}
35
+							</form>
36
+							<form action="/dev/tokens/delete" method="POST" id="delete-form">
37
+								{{ csrfGenerate $.Context.User.ID }}
38
+								<input type="hidden" name="id" value="{{ $.Gin.Query "id" | atoint }}">
39
+								{{ ieForm $.Gin }}
40
+							</form>
41
+							<div class="ui divider"></div>
42
+							<div style="text-align: right">
43
+								<button type="submit" class="ui blue button" form="edit-form">Save</button>
44
+								<button type="submit" class="ui red button" form="delete-form">Delete</button>
45
+							</div>
46
+						{{ end }}
47
+					{{ else }}
48
+						That token could not be found!
49
+					{{ end }}
50
+				{{ end }}
51
+			</div>
52
+		</div>
53
+	</div>
54
+</div>
55
+{{ end }}

+ 34
- 8
templates/dev/tokens.html View File

@@ -14,10 +14,11 @@ HugeHeadingRight=true
14 14
 		<div class="twelve wide column">
15 15
 			<div class="ui segment">
16 16
 				From this page you can create, modify and delete your personal <a href="https://docs.ripple.moe/docs/api/appendix#authorization">API tokens</a>.<br>
17
-				The Ripple API is the system through which developers can create applications to interact with Ripple. If you're asked to fill out an API token from this page, be wary and only actually create the token if you really trust the owner of the application.
17
+				{{ .T "The Ripple API is the system through which developers can create applications to interact with Ripple. <b>If you're asked to fill out an API token from this page, be wary and only actually create the token if you really trust the owner of the application.</b>" | html }}
18 18
 				<div class="ui divider"></div>
19 19
 				{{ $csrf := csrfGenerate .Context.User.ID }}
20
-				{{ with .Get "tokens" }}
20
+				{{ $page := or (atoint (.Gin.Query "p")) 1 }}
21
+				{{ with .Get "tokens?p=%d&l=50" $page }}
21 22
 					{{ with .tokens }}
22 23
 						<table class="ui fixed table">
23 24
 							<thead>
@@ -31,19 +32,21 @@ HugeHeadingRight=true
31 32
 							<tbody>
32 33
 								{{ range . }}
33 34
 									<tr>
34
-										<td>{{ .privileges }}</td>
35
+										{{/* We are using printf with %.0f so that
36
+										it gets printed as if it was a normal int */}}
37
+										<td><span title="{{ privilegesToString .privileges }}">{{ printf "%.0f" .privileges }}</span></td>
35 38
 										<td>{{ with .description }}{{ . }}{{ else }}<i>(None)</i>{{ end }}</td>
36
-										<td>{{ time .last_updated }}</td>
39
+										<td>{{ if ne (nativeTime .last_updated).Year 1970 }}{{ time .last_updated }}{{ else }}Never{{ end }}</td>
37 40
 										<td>
38 41
 											<form action="/dev/tokens/delete" method="POST">
39 42
 												<input type="hidden" name="id" value="{{ .id }}">
40 43
 												{{ $csrf }}
41 44
 												{{ ieForm $.Gin }}
42 45
 												<div class="ui buttons">
43
-													<a class="ui blue icon button" href="/dev/tokens/edit?id={{ .id }}">
46
+													<a class="ui blue icon button" href="/dev/tokens/edit?id={{ .id }}" title="Edit">
44 47
 														<i class="edit icon"></i>
45 48
 													</a>
46
-													<button class="ui red icon button" type="submit">
49
+													<button class="ui red icon button" type="submit" title="Delete">
47 50
 														<i class="remove icon"></i>
48 51
 													</button>
49 52
 												</div>
@@ -52,6 +55,28 @@ HugeHeadingRight=true
52 55
 									</tr>
53 56
 								{{ end }}
54 57
 							</tbody>
58
+							{{ $left := gt $page 1 }}
59
+							{{ $right := eq (len .) 50 }}
60
+							{{ if or $left $right }}
61
+								<tfoot>
62
+									<tr>
63
+										<th colspan="4">
64
+											<div class="ui right floated pagination menu">
65
+												{{ if $left }}
66
+													<a class="icon item" href="/dev/tokens?p={{ minus (float $page) 1 }}">
67
+														<i class="left chevron icon"></i>
68
+													</a>
69
+												{{ end }}
70
+												{{ if $right }}
71
+													<a class="icon item" href="/dev/tokens?p={{ plus (float $page) 1 }}">
72
+														<i class="right chevron icon"></i>
73
+													</a>
74
+												{{ end }}
75
+											</div>
76
+										</th>
77
+									</tr>
78
+								</tfoot>
79
+							{{ end }}
55 80
 						</table>
56 81
 						<div class="ui divider"></div>
57 82
 					{{ end }}
@@ -60,11 +85,12 @@ HugeHeadingRight=true
60 85
 				<form action="/dev/tokens/create" method="POST" class="ui form">
61 86
 					<div class="field">
62 87
 						<label>Privileges</label>
63
-						<input type="number" value="0">
88
+						<input type="number" name="privileges" value="0" id="privileges-number" min="0">
89
+						<div id="privileges-text"></div>
64 90
 					</div>
65 91
 					<div class="field">
66 92
 						<label>Description</label>
67
-						<input type="text" value="" placeholder="For what are you making this token?">
93
+						<input type="text" name="description" value="" placeholder="For what are you making this token?">
68 94
 					</div>
69 95
 					<button class="ui blue button" type="submit">Create</button>
70 96
 					{{ $csrf }}

Loading…
Cancel
Save