Auth, first part of editing

This commit is contained in:
Tony Garnock-Jones 2014-11-07 19:08:21 -05:00
parent 3d0b80860d
commit 0893583334
6 changed files with 694 additions and 201 deletions

View File

@ -5,9 +5,10 @@
bootstrap-project-link
bootstrap-navigation
bootstrap-active-navigation
bootstrap-navbar-extension-fn
bootstrap-navbar-extension
bootstrap-page-stylesheets
bootstrap-page-scripts
bootstrap-cookies
bootstrap-response
bootstrap-radio
@ -22,9 +23,10 @@
(define bootstrap-project-link (make-parameter "/"))
(define bootstrap-navigation (make-parameter '(("Home" "/"))))
(define bootstrap-active-navigation (make-parameter #f))
(define bootstrap-navbar-extension-fn (make-parameter (lambda () '())))
(define bootstrap-navbar-extension (make-parameter '()))
(define bootstrap-page-stylesheets (make-parameter '()))
(define bootstrap-page-scripts (make-parameter '()))
(define bootstrap-cookies (make-parameter '()))
(define (bootstrap-response title
#:title-element [title-element `(h1 ,title)]
@ -35,6 +37,7 @@
(response/xexpr
#:code code
#:message message
#:cookies (bootstrap-cookies)
#:preamble #"<!DOCTYPE html>\n"
`(html
(head (meta ((charset "utf-8")))
@ -45,9 +48,7 @@
(link ((rel "stylesheet") (href "/style.css") (type "text/css")))
,@(for/list ((sheet (bootstrap-page-stylesheets)))
`(link ((rel "stylesheet") (href ,sheet) (type "text/css"))))
(script ((type "text/javascript") (src "/site.js")))
,@(for/list ((header-script (bootstrap-page-scripts)))
`(script ((type "text/javascript") (src ,header-script)))))
(script ((type "text/javascript") (src "/site.js"))))
(body
(nav ((class "navbar navbar-inverse navbar-fixed-top") (role "navigation"))
(div ((class "container"))
@ -61,7 +62,7 @@
`(li ,@(maybe-splice (equal? text (bootstrap-active-navigation))
`((class "active")))
(a ((href ,url)) ,text))))
,@((bootstrap-navbar-extension-fn))
,@(bootstrap-navbar-extension)
)))
(div ((class "container"))
,title-element
@ -69,7 +70,9 @@
(script ((type "text/javascript")
(src "https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js")))
(script ((type "text/javascript") (src "/bootstrap/js/bootstrap.min.js")))))))
(script ((type "text/javascript") (src "/bootstrap/js/bootstrap.min.js")))
,@(for/list ((script (bootstrap-page-scripts)))
`(script ((type "text/javascript") (src ,script))))))))
;; String String XExpr ... -> XExpr
;; Constructs Bootstrap boilerplate for a radio button.

View File

@ -8,9 +8,12 @@
(require racket/string)
(require net/uri-codec)
(require web-server/servlet)
(require web-server/http/id-cookie)
(require web-server/http/cookie-parse)
(require "bootstrap.rkt")
(require "html-utils.rkt")
(require "packages.rkt")
(require "sessions.rkt")
(define nav-index "Package Index")
(define nav-docs "Documentation")
@ -30,24 +33,6 @@
" Download")
"http://download.racket-lang.org/")))
(bootstrap-navbar-extension-fn
(lambda ()
`(
(form ((class "navbar-form navbar-right")
(role "form"))
(div ((class "form-group"))
(input ((type "text")
(placeholder "Email")
(class "form-control"))))
(div ((class "form-group"))
(input ((type "password")
(placeholder "Password")
(class "form-control"))))
(button ((type "submit")
(class "btn btn-success"))
"Sign in"))
)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-values (request-handler named-url)
@ -55,6 +40,7 @@
[("") main-page]
[("search") search-page]
[("package" (string-arg)) package-page]
[("package" (string-arg) "edit") edit-package-page]
))
(module+ main
@ -63,6 +49,247 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-syntax-rule (authentication-wrap #:request request body ...)
(authentication-wrap* request (lambda () body ...)))
(define COOKIE "pltsession")
(define current-session (make-parameter #f))
(define (current-email)
(define s (current-session))
(and s (session-email s)))
(define (authentication-wrap* request body)
(define original-session-cookie
(findf (lambda (c) (equal? (client-cookie-name c) COOKIE))
(request-cookies request)))
(define original-session-key
(and original-session-cookie (client-cookie-value original-session-cookie)))
(log-info "Session key from cookie: ~a" original-session-key)
(let redo ((session-key original-session-key
))
(define session (lookup-session/touch! session-key))
(log-info "session: ~a" session)
(send/suspend/dispatch
(lambda (embed-url)
(parameterize ((bootstrap-navbar-extension
(cond
[(not session)
`((a ((class "btn btn-default navbar-btn navbar-right")
(href ,(embed-url (lambda (req) (redo (register-page))))))
"Register")
(a ((class "btn btn-success navbar-btn navbar-right")
(href ,(embed-url (lambda (req) (redo (login-page))))))
"Sign in"))]
[else
`((ul ((class "nav navbar-nav navbar-right"))
(li ((class "dropdown"))
(a ((class "dropdown-toggle")
(data-toggle "dropdown"))
,(session-email session)
" "
(span ((class "caret"))))
(ul ((class "dropdown-menu") (role "menu"))
(li "foo")
(li "bar")))))]))
(current-session session)
(bootstrap-cookies
(if session
(list (make-cookie COOKIE session-key
;; TODO #:secure? #t
))
(list (make-cookie COOKIE ""
#:expires "Thu, 01 Jan 1970 00:00:00 GMT")))))
(body))))))
(define (authenticate-with-server! email password code)
(define auth-url
(string->url
(format "https://pkgd.racket-lang.org/jsonp/authenticate?~a"
(alist->form-urlencoded (list (cons 'callback "x")
(cons 'email email)
(cons 'passwd password)
(cons 'code code))))))
(define-values (body-port response-headers) (get-pure-port/headers auth-url))
(match-define (pregexp #px"^x\\((.*)\\);$" (list _ json)) (port->string body-port))
(log-info "JSON: ~a" json)
json)
(define (login-page [error-message #f])
(send/suspend/dispatch
(lambda (embed-url)
(bootstrap-response "Login"
`(form ((class "form-horizontal")
(method "post")
(action ,(embed-url process-login-credentials))
(role "form"))
(div ((class "form-group"))
(label ((class "col-sm-offset-2 col-sm-2 control-label")
(for "email")) "Email address:")
(div ((class "col-sm-5"))
(input ((class "form-control")
(type "email")
(name "email")
(value "")
(id "email")))))
(div ((class "form-group"))
(label ((class "col-sm-offset-2 col-sm-2 control-label")
(for "password")) "Password:")
(div ((class "col-sm-5"))
(input ((class "form-control")
(type "password")
(name "password")
(value "")
(id "password")))))
(div ((class "form-group"))
(div ((class "col-sm-offset-4 col-sm-5"))
(a ((href ,(embed-url (lambda (req) (register-page)))))
"Need to reset your password?")
(div ((class "form-group"))
(div ((class "col-sm-offset-4 col-sm-5"))
(button ((type "submit")
(class "btn btn-primary"))
"Log in"))))
))))))
(define (process-login-credentials request)
(define-form-bindings request (email password))
(match (authenticate-with-server! email password "")
["wrong-code"
(login-page "Something went awry; please try again.")]
[(or "emailed" #f)
(summarise-code-emailing "Incorrect password, or nonexistent user." email)]
[else
(create-session! email password)]))
(define (register-page #:email [email ""]
#:code [code ""]
#:error-message [error-message #f])
(send/suspend/dispatch
(lambda (embed-url)
(bootstrap-response "Register/Reset Account"
#:title-element ""
`(div
(h1 "Got a registration or reset code?")
(p "Great! Enter it below, with your chosen password, to log in.")
(form ((class "form-horizontal")
(method "post")
(action ,(embed-url apply-account-code))
(role "form"))
(div ((class "form-group"))
(label ((class "col-sm-offset-2 col-sm-2 control-label")
(for "email")) "Email address:")
(div ((class "col-sm-5"))
(input ((class "form-control")
(type "email")
(name "email")
(value ,email)
(id "email")))))
(div ((class "form-group"))
(label ((class "col-sm-offset-2 col-sm-2 control-label")
(for "code")) "Code:")
(div ((class "col-sm-5"))
(input ((class "form-control")
(type "text")
(name "code")
(value ,code)
(id "code")))))
(div ((class "form-group"))
(label ((class "col-sm-offset-2 col-sm-2 control-label")
(for "password")) "Password:")
(div ((class "col-sm-5"))
(input ((class "form-control")
(type "password")
(name "password")
(value "")
(id "password")))))
(div ((class "form-group"))
(label ((class "col-sm-offset-2 col-sm-2 control-label")
(for "password")) "Confirm password:")
(div ((class "col-sm-5"))
(input ((class "form-control")
(type "password")
(name "confirm_password")
(value "")
(id "confirm_password")))))
,@(maybe-splice
error-message
`(div ((class "form-group"))
(div ((class "col-sm-offset-4 col-sm-5"))
(div ((class "alert alert-danger"))
(p ,error-message)))))
(div ((class "form-group"))
(div ((class "col-sm-offset-4 col-sm-5"))
(button ((type "submit")
(class "btn btn-primary"))
"Continue")))))
`(div
(h1 "Need a code?")
(p "Enter your email address below, and we'll send you one.")
(form ((class "form-horizontal")
(method "post")
(action ,(embed-url notify-of-emailing))
(role "form"))
(div ((class "form-group"))
(label ((class "col-sm-offset-2 col-sm-2 control-label")
(for "email")) "Email address:")
(div ((class "col-sm-5"))
(input ((class "form-control")
(type "email")
(name "email_for_code")
(value "")
(id "email_for_code")))))
(div ((class "form-group"))
(div ((class "col-sm-offset-4 col-sm-5"))
(button ((type "submit")
(class "btn btn-primary"))
"Email me a code")))))))))
(define (apply-account-code request)
(define-form-bindings request (email code password confirm_password))
(define (retry msg)
(register-page #:email email
#:code code
#:error-message msg))
(cond
[(equal? (string-trim email) "")
(retry "Please enter your email address.")]
[(equal? (string-trim code) "")
(retry "Please enter the code you received in your email.")]
[(not (equal? password confirm_password))
(retry "Please make sure the two password fields match.")]
[(equal? (string-trim password) "")
(retry "Please enter a password.")]
[else
(match (authenticate-with-server! email password code)
["wrong-code"
(retry "The code you entered was incorrect. Please try again.")]
[(or "emailed" #f)
(retry "Something went awry; you have been emailed another code. Please check your email.")]
[else
;; The email and password combo we have been given is good to go.
;; Set a cookie and consider ourselves logged in.
(create-session! email password)])]))
(define (notify-of-emailing request)
(define-form-bindings request (email_for_code))
(authenticate-with-server! email_for_code "" "") ;; TODO check result?
(summarise-code-emailing "Account registration/reset code emailed" email_for_code))
(define (summarise-code-emailing reason email)
(send/suspend/dispatch
(lambda (embed-url)
(bootstrap-response reason
`(p
"We've emailed an account registration/reset code to "
(code ,email) ". Please check your email and then click "
"the button to continue:")
`(a ((class "btn btn-primary")
(href ,(embed-url (lambda (req) (register-page)))))
"Enter your code")))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (package-link package-name)
(define package-name-str (~a package-name))
`(a ((href ,(named-url package-page package-name-str))) ,package-name-str))
@ -162,188 +389,346 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (main-page request)
(bootstrap-response "Racket Package Index"
#:title-element ""
`(div ((class "jumbotron"))
(h1 "Racket Package Index")
(p "These are the packages available via the "
(a ((href "docs.racket-lang.org/pkg/getting-started.html"))
"Racket package system") ".")
(p "Simply run " (kbd "raco pkg install " (var "package-name"))
" to install a package.")
(form ((role "form")
(action ,(named-url search-page)))
(div ((class "form-group"))
(input ((class "form-control")
(type "text")
(placeholder "Search packages")
(name "q")
(value "")
(id "q"))))
))
(package-summary-table (package-search "" '((main-distribution #f))))))
(authentication-wrap
#:request request
(bootstrap-response "Racket Package Index"
#:title-element ""
`(div ((class "jumbotron"))
(h1 "Racket Package Index")
(p "These are the packages available via the "
(a ((href "docs.racket-lang.org/pkg/getting-started.html"))
"Racket package system") ".")
(p "Simply run " (kbd "raco pkg install " (var "package-name"))
" to install a package.")
(form ((role "form")
(action ,(named-url search-page)))
(div ((class "form-group"))
(input ((class "form-control")
(type "text")
(placeholder "Search packages")
(name "q")
(value "")
(id "q"))))
))
(package-summary-table (package-search "" '((main-distribution #f)))))))
(define (package-page request package-name-str)
(define package-name (string->symbol package-name-str))
(define pkg (package-detail package-name))
(define default-version (hash-ref (or (@ pkg versions) (hash)) 'default (lambda () #f)))
(if (not pkg)
(bootstrap-response #:code 404
#:message #"No such package"
"Package not found"
`(div "The package " (code ,package-name-str) " does not exist."))
(bootstrap-response (~a package-name)
#:title-element ""
`(div ((class "jumbotron"))
(h1 ,(~a package-name))
(p ,(@ pkg description))
(div ,@(let ((docs (or (@ pkg build docs) '())))
(match docs
[(list)
`()]
[(list doc)
(define-values (n u) (doc-destruct doc))
(list (buildhost-link
#:attributes `((class "btn btn-success btn-lg"))
u
"Documentation"))]
[_
`((button ((class "btn btn-success btn-lg dropdown-toggle")
(data-toggle "dropdown"))
"Documentation "
(span ((class "caret"))))
(ul ((class "dropdown-menu")
(role "menu"))
,@(for/list ((doc docs)) `(li ,(doc-link doc)))))]))
(authentication-wrap
#:request request
(define package-name (string->symbol package-name-str))
(define pkg (package-detail package-name))
(define default-version (hash-ref (or (@ pkg versions) (hash)) 'default (lambda () #f)))
(if (not pkg)
(bootstrap-response #:code 404
#:message #"No such package"
"Package not found"
`(div "The package " (code ,package-name-str) " does not exist."))
(bootstrap-response (~a package-name)
#:title-element ""
`(div ((class "jumbotron"))
(h1 ,(~a package-name))
(p ,(@ pkg description))
(div ,@(let ((docs (or (@ pkg build docs) '())))
(match docs
[(list)
`()]
[(list doc)
(define-values (n u) (doc-destruct doc))
(list (buildhost-link
#:attributes `((class "btn btn-success btn-lg"))
u
"Documentation"))]
[_
`((button ((class "btn btn-success btn-lg dropdown-toggle")
(data-toggle "dropdown"))
"Documentation "
(span ((class "caret"))))
(ul ((class "dropdown-menu")
(role "menu"))
,@(for/list ((doc docs)) `(li ,(doc-link doc)))))]))
;; Heuristic guess as to whether we should present a "browse"
;; link or a "download" link.
" "
,(if (equal? (@ default-version source)
(@ default-version source_url))
`(a ((class "btn btn-default btn-lg")
(href ,(@ default-version source_url)))
(span ((class "glyphicon glyphicon-download")))
" Snapshot")
`(a ((class "btn btn-default btn-lg")
(href ,(@ default-version source_url)))
(span ((class "glyphicon glyphicon-link")))
" Code"))
))
;; Heuristic guess as to whether we should present a "browse"
;; link or a "download" link.
" "
,(if (equal? (@ default-version source)
(@ default-version source_url))
`(a ((class "btn btn-default btn-lg")
(href ,(@ default-version source_url)))
(span ((class "glyphicon glyphicon-download")))
" Snapshot")
`(a ((class "btn btn-default btn-lg")
(href ,(@ default-version source_url)))
(span ((class "glyphicon glyphicon-link")))
" Code"))
(if (@ pkg checksum-error)
`(div ((class "alert alert-danger")
(role "alert"))
(span ((class "label label-danger"))
"Checksum error")
" The package checksum does not match"
" the package source code.")
"")
,@(maybe-splice
(member (current-email) (or (@ pkg authors) '()))
" "
`(a ((class "btn btn-info btn-lg")
(href ,(named-url edit-package-page package-name-str)))
(span ((class "glyphicon glyphicon-edit")))
" Edit this package"))
))
`(table ((class "package-details"))
(tr (th "Authors")
(td ,(authors-list (@ pkg authors))))
(tr (th "Documentation")
(td ,(doc-links (@ pkg build docs))))
(tr (th "Tags")
(td ,(tag-links (@ pkg tags))))
(tr (th "Last updated")
(td ,(utc->string (@ pkg last-updated))))
(tr (th "Ring")
(td ,(~a (@ pkg ring))))
(tr (th "Conflicts")
(td ,(package-links (@ pkg conflicts))))
(tr (th "Dependencies")
(td ,(package-links (@ pkg dependencies))))
(tr (th "Most recent build results")
(td (ul ((class "build-results"))
,@(maybe-splice
(@ pkg build success-log)
`(li "Compiled successfully: "
,(buildhost-link (@ pkg build success-log) "transcript")))
,@(maybe-splice
(@ pkg build failure-log)
`(li "Compiled unsuccessfully: "
,(buildhost-link (@ pkg build failure-log) "transcript")))
,@(maybe-splice
(@ pkg build conflicts-log)
`(li "Conflicts: "
,(buildhost-link (@ pkg build conflicts-log) "details")))
,@(maybe-splice
(@ pkg build dep-failure-log)
`(li "Dependency problems: "
,(buildhost-link (@ pkg build dep-failure-log) "details")))
)))
(tr (th "Modules")
(td (ul ((class "module-list"))
,@(for/list ((mod (@ pkg modules)))
(match-define (list kind path) mod)
`(li ((class ,kind)) ,path)))))
,@(let* ((vs (or (@ pkg versions) (hash)))
(empty-checksum "9f098dddde7f217879070816090c1e8e74d49432")
(vs (for/hash (((k v) (in-hash vs))
#:when (not (equal? (@ v checksum)
empty-checksum)))
(values k v))))
(maybe-splice
(not (hash-empty? vs))
`(tr (th "Versions")
(td (table ((class "package-versions"))
(tr (th "Version")
(th "Source")
(th "Checksum"))
,@(for/list
(((version-sym v) (in-hash vs)))
`(tr
(td ,(~a version-sym))
(td (a ((href ,(@ v source_url)))
,(@ v source)))
(td ,(@ v checksum)))))))))
(tr (th "Last checked")
(td ,(utc->string (@ pkg last-checked))))
(tr (th "Last edited")
(td ,(utc->string (@ pkg last-edit))))
))))
(if (@ pkg checksum-error)
`(div ((class "alert alert-danger")
(role "alert"))
(span ((class "label label-danger"))
"Checksum error")
" The package checksum does not match"
" the package source code.")
"")
`(table ((class "package-details"))
(tr (th "Authors")
(td ,(authors-list (@ pkg authors))))
(tr (th "Documentation")
(td ,(doc-links (@ pkg build docs))))
(tr (th "Tags")
(td ,(tag-links (@ pkg tags))))
(tr (th "Last updated")
(td ,(utc->string (@ pkg last-updated))))
(tr (th "Ring")
(td ,(~a (@ pkg ring))))
(tr (th "Conflicts")
(td ,(package-links (@ pkg conflicts))))
(tr (th "Dependencies")
(td ,(package-links (@ pkg dependencies))))
(tr (th "Most recent build results")
(td (ul ((class "build-results"))
,@(maybe-splice
(@ pkg build success-log)
`(li "Compiled successfully: "
,(buildhost-link (@ pkg build success-log) "transcript")))
,@(maybe-splice
(@ pkg build failure-log)
`(li "Compiled unsuccessfully: "
,(buildhost-link (@ pkg build failure-log) "transcript")))
,@(maybe-splice
(@ pkg build conflicts-log)
`(li "Conflicts: "
,(buildhost-link (@ pkg build conflicts-log) "details")))
,@(maybe-splice
(@ pkg build dep-failure-log)
`(li "Dependency problems: "
,(buildhost-link (@ pkg build dep-failure-log) "details")))
)))
(tr (th "Modules")
(td (ul ((class "module-list"))
,@(for/list ((mod (@ pkg modules)))
(match-define (list kind path) mod)
`(li ((class ,kind)) ,path)))))
,@(let* ((vs (or (@ pkg versions) (hash)))
(empty-checksum "9f098dddde7f217879070816090c1e8e74d49432")
(vs (for/hash (((k v) (in-hash vs))
#:when (not (equal? (@ v checksum)
empty-checksum)))
(values k v))))
(maybe-splice
(not (hash-empty? vs))
`(tr (th "Versions")
(td (table ((class "package-versions"))
(tr (th "Version")
(th "Source")
(th "Checksum"))
,@(for/list
(((version-sym v) (in-hash vs)))
`(tr
(td ,(~a version-sym))
(td (a ((href ,(@ v source_url)))
,(@ v source)))
(td ,(@ v checksum)))))))))
(tr (th "Last checked")
(td ,(utc->string (@ pkg last-checked))))
(tr (th "Last edited")
(td ,(utc->string (@ pkg last-edit))))
)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(struct draft-package (old-name name description authors tags versions) #:transparent)
(define (edit-package-page request package-name-str)
(authentication-wrap
#:request request
(define package-name (string->symbol package-name-str))
(define pkg (package-detail package-name))
(cond
[(and pkg (not (member (current-email) (or (@ pkg authors) '()))))
;; Not ours. Show it instead.
(package-page request package-name-str)]
[(not pkg)
;; Doesn't exist.
(package-form (draft-package ""
package-name
""
'()
'()
'(("default" ""))))]
[else
(package-form (draft-package package-name
package-name
(@ pkg description)
(@ pkg authors)
(@ pkg tags)
(for/list (((ver info) (in-hash (@ pkg versions))))
(list (symbol->string ver) (@ info source)))))])))
(define (package-source-option source-type value label)
`(option ((value ,value)
,@(maybe-splice (equal? source-type value) '(selected "selected")))
,label))
(define (package-form draft)
(send/suspend/dispatch
(lambda (embed-url)
(define (build-versions-table)
`(table
(tr (th "Version")
(th "Source"))
,@(for/list ((v (draft-package-versions draft)))
(match-define (list version source) v)
(define (control-name c) (format "version__~a__~a" version c))
(define (textfield name label value [placeholder ""])
`(div ((class "form-group"))
(label ((for ,(control-name name))) ,label)
(input ((class "form-control")
(type "text")
(name ,(control-name name))
(id ,(control-name name))
(placeholder ,placeholder)
(value ,value)))))
(define-values (source-type simple-url g-host g-user g-project g-branch)
(match source
[(pregexp #px"github://github\\.com/([^/]+)/([^/]+)(/([^/]+)/?)?"
(list _ u p _ b))
(values "github" "" "github.com" u p (if (equal? b "master") "" (or b #f)))]
[(pregexp #px"git://([^/]+)/([^/]+)/([^/]+)(/([^/]+)/?)?"
(list _ h u p _ b))
(values "git" "" h u p (if (equal? b "master") "" (or b "")))]
[_
(values "simple" source "" "" "" "")]))
`(tr
(td ,version)
(td (div ((class "form-group"))
(label ((for ,(control-name "type"))) "Source type")
" "
(select ((class "package-version-source-type")
(data-packageversion ,version)
(name ,(control-name "type")))
,(package-source-option source-type
"github"
"Github Repository")
,(package-source-option source-type
"git"
"Git Repository")
,(package-source-option source-type
"simple"
"Simple URL")))
,(textfield "simple_url" "Source URL" simple-url)
,(textfield "g_host" "Git Repository Host" g-host)
,(textfield "g_user" "Git Repository User" g-user)
,(textfield "g_project" "Git Repository Project" g-project)
,(textfield "g_branch" "Git Repository Branch" g-branch "master")
,@(maybe-splice
(not (equal? version "default"))
`(button ((type "submit")
(name ,(control-name "delete")))
(span ((class "glyphicon glyphicon-delete")))
" Delete version")))))))
(parameterize ((bootstrap-page-scripts '("/editpackage.js")))
(bootstrap-response (format "Editing package ~a" (draft-package-old-name draft))
`(form ((method "post")
(action "TODO")
(role "form"))
(div ((class "container"))
(div ((class "row"))
(div ((class "form-group col-sm-6"))
(label ((for "name")) "Package Name")
(input ((class "form-control")
(type "text")
(name "name")
(id "name")
(value ,(~a (draft-package-name draft))))))
(div ((class "form-group col-sm-6"))
(label ((for "tags")) "Package Tags (space-separated)")
(input ((class "form-control")
(type "text")
(tags "tags")
(id "tags")
(value ,(string-join
(draft-package-tags draft)))))))
(div ((class "row"))
(div ((class "form-group col-sm-6"))
(label ((for "description")) "Package Description")
(textarea ((class "form-control")
(name "description")
(id "description"))
,(draft-package-description draft)))
(div ((class "form-group col-sm-6"))
(label ((for "authors"))
"Author email addresses (one per line)")
(textarea ((class "form-control")
(name "authors")
(id "authors"))
,(string-join (draft-package-authors draft)
"\n"))))
(div ((class "row"))
(div ((class "form-group col-sm-12"))
(label "Package Versions & Sources")
,(build-versions-table)))
(div ((class "row"))
(div ((class "form-group col-sm-12"))
(button ((type "submit")
(class "btn btn-primary")
(name "save_changes"))
(span ((class "glyphicon glyphicon-save")))
" Save changes")))))
)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (search-page request)
(define-form-bindings request ([search-text q ""]
[tags-input tags ""]))
(define tags (for/list ((t (string-split tags-input)))
(match t
[(pregexp #px"!(.*)" (list _ tag)) (list (string->symbol tag) #f)]
[tag (list (string->symbol tag) #t)])))
(bootstrap-response "Search Racket Package Index"
`(form ((class "form-horizontal")
(role "form"))
(div ((class "form-group"))
(label ((class "col-sm-2 control-label")
(for "q")) "Search terms")
(div ((class "col-sm-10"))
(input ((class "form-control")
(type "text")
(placeholder "Enter free-form text to match here")
(name "q")
(value ,search-text)
(id "q")))))
(div ((class "form-group"))
(label ((class "col-sm-2 control-label")
(for "tags")) "Tags")
(div ((class "col-sm-10"))
(input ((class "form-control")
(type "text")
(placeholder "tag1 tag2 tag3 ...")
(name "tags")
(value ,tags-input)
(id "tags")))))
(div ((class "form-group"))
(div ((class "col-sm-offset-2 col-sm-10"))
(button ((type "submit")
(class "btn btn-primary"))
(span ((class "glyphicon glyphicon-search")))
" Search")))
(div ((class "search-results"))
,@(maybe-splice
(or (pair? tags) (not (equal? search-text "")))
(package-summary-table (package-search search-text tags)))))))
(authentication-wrap
#:request request
(define-form-bindings request ([search-text q ""]
[tags-input tags ""]))
(define tags (for/list ((t (string-split tags-input)))
(match t
[(pregexp #px"!(.*)" (list _ tag)) (list (string->symbol tag) #f)]
[tag (list (string->symbol tag) #t)])))
(bootstrap-response "Search Racket Package Index"
`(form ((class "form-horizontal")
(role "form"))
(div ((class "form-group"))
(label ((class "col-sm-2 control-label")
(for "q")) "Search terms")
(div ((class "col-sm-10"))
(input ((class "form-control")
(type "text")
(placeholder "Enter free-form text to match here")
(name "q")
(value ,search-text)
(id "q")))))
(div ((class "form-group"))
(label ((class "col-sm-2 control-label")
(for "tags")) "Tags")
(div ((class "col-sm-10"))
(input ((class "form-control")
(type "text")
(placeholder "tag1 tag2 tag3 ...")
(name "tags")
(value ,tags-input)
(id "tags")))))
(div ((class "form-group"))
(div ((class "col-sm-offset-2 col-sm-10"))
(button ((type "submit")
(class "btn btn-primary"))
(span ((class "glyphicon glyphicon-search")))
" Search")))
(div ((class "search-results"))
,@(maybe-splice
(or (pair? tags) (not (equal? search-text "")))
(package-summary-table (package-search search-text tags))))))))

14
src/randomness.rkt Normal file
View File

@ -0,0 +1,14 @@
#lang racket/base
(provide random-bytes
random-bytes/base64)
(require net/base64)
(define (random-bytes n)
(with-input-from-file "/dev/urandom"
(lambda ()
(read-bytes n))))
(define (random-bytes/base64 n)
(base64-encode (random-bytes n) #""))

43
src/sessions.rkt Normal file
View File

@ -0,0 +1,43 @@
#lang racket/base
(provide session-lifetime
(struct-out session)
create-session!
lookup-session/touch!
lookup-session)
(require "randomness.rkt")
(define session-lifetime (make-parameter (* 7 24 60 60 1000))) ;; one week in milliseconds
(struct session (expiry email password) #:transparent)
(define sessions (make-hash))
(define (expire-sessions!)
(define now (current-inexact-milliseconds))
(for ((session-key (hash-keys sessions)))
(define s (hash-ref sessions session-key (lambda () #f)))
(when (and s (<= (session-expiry s) now))
(hash-remove! sessions session-key))))
(define (create-session! email password)
(expire-sessions!)
(define session-key (bytes->string/utf-8 (random-bytes/base64 32)))
(hash-set! sessions
session-key
(session (+ (current-inexact-milliseconds) (session-lifetime))
email
password))
session-key)
(define (lookup-session/touch! session-key)
(define s (hash-ref sessions session-key (lambda () #f)))
(and s
(let ((s1 (struct-copy session s [expiry (+ (current-inexact-milliseconds)
(session-lifetime))])))
(hash-set! sessions session-key s1)
s1)))
(define (lookup-session session-key)
(hash-ref sessions session-key (lambda () #f)))

48
static/editpackage.js Normal file
View File

@ -0,0 +1,48 @@
function preenSourceTypes() {
$(".package-version-source-type").each(function (index, e) {
preenSourceType(e);
});
}
function preenSourceType(e) {
function controlId(name) {
return "#version__" + e.dataset.packageversion + "__" + name;
}
function showhide1(n, v) {
var c = $(controlId(n));
if (v) {
c.show();
} else {
c.hide();
}
}
function showhide(s, gh, gu, gp, gb) {
showhide1("simple_url", s);
showhide1("g_host", gh);
showhide1("g_user", gu);
showhide1("g_project", gp);
showhide1("g_branch", gb);
}
console.log(e.dataset.packageversion);
switch (e.value) {
case "github":
showhide(false, false, true, true, true);
break;
case "git":
showhide(false, true, true, true, true);
break;
case "simple":
default:
showhide(true, false, false, false, false);
break;
}
}
$(document).ready(function () {
$(".package-version-source-type").each(function (index, e) {
$(e).change(function () {
preenSourceType(e);
});
});
preenSourceTypes();
});

0
static/site.js Normal file
View File