Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
Dan Ballard | 9476bf1e74 | |
Dan Ballard | 94221611c1 | |
Dan Ballard | b45d101657 | |
Dan Ballard | 62366427fb | |
Dan Ballard | e45be9fbd3 | |
Dan Ballard | 765659b3ee | |
Dan Ballard | de09e150b2 | |
Dan Ballard | 015d6fa86d | |
Dan Ballard | 9f5cd7e5d8 | |
Dan Ballard | cbc4dc40bf | |
Dan Ballard | 2b77215d05 | |
Dan Ballard | 7e16f7431c | |
Dan Ballard | 3f963a066f | |
Dan Ballard | 674c44032b | |
Dan Ballard | 0dc40f0a96 |
|
@ -1,6 +1,7 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
transmet
|
||||
db/dbconf.yml
|
||||
csrf-secret.txt
|
||||
.Rhistory
|
||||
.project
|
||||
*.iml
|
||||
|
|
65
README.md
65
README.md
|
@ -1,4 +1,4 @@
|
|||
# transmet
|
||||
# transmet ![thrree eyed smiley face](https://raw.githubusercontent.com/dballard/transmet/master/favicon.ico)
|
||||
Quick fast personal link store that exports to a HTML template for quick posting to a blog
|
||||
|
||||
Usecase: storing interesting news articles you come across during a week with at the moment notes/commentary and
|
||||
|
@ -6,8 +6,8 @@ Usecase: storing interesting news articles you come across during a week with at
|
|||
|
||||
## Note
|
||||
|
||||
As this is a personal project, some of the niceities like user managment and category managment
|
||||
(that are one time tasks) are left to be done in SQL. I needed a tool to store links and export
|
||||
As this is a personal project, some of the niceities like user managment
|
||||
are left to be done in SQL. I needed a tool to store links and export
|
||||
to html so that's what I've focused on.
|
||||
|
||||
# Install
|
||||
|
@ -18,56 +18,55 @@ go get github.com/dballard/transmet
|
|||
|
||||
sudo apt-get install postgres postgresql-contrib
|
||||
|
||||
Setup postgres to hadle a local connection for transmet in pg_hba.conf
|
||||
Setup postgres to handle a local connection for transmet in pg_hba.conf
|
||||
either:
|
||||
'''host transmet transmet 127.0.0.1/32 md5'''
|
||||
host transmet transmet 127.0.0.1/32 md5
|
||||
or a more liberal:
|
||||
'''host all all 127.0.0.1/32 md5 '''
|
||||
host all all 127.0.0.1/32 md5
|
||||
and do the same for
|
||||
host all all ::1/128 md5
|
||||
ipv6
|
||||
|
||||
create ssl certs and put them somewhere
|
||||
|
||||
enable SSL in postgresql.conf
|
||||
'''
|
||||
ssl = true
|
||||
ssl_cert_file = 'WHER_YOU_PUT/server.crt'
|
||||
ssl_key_file = 'WHERE_YOU_PUT/server.key'
|
||||
'''
|
||||
|
||||
ssl = true
|
||||
ssl_cert_file = 'WHER_YOU_PUT/server.crt'
|
||||
ssl_key_file = 'WHERE_YOU_PUT/server.key'
|
||||
|
||||
|
||||
Create postgress DB and user
|
||||
|
||||
'''sh
|
||||
sudo -u postgres
|
||||
createuser -S -P -E transmet
|
||||
createdb --owner transmet --encoding utf8 transmet
|
||||
psql
|
||||
\c transmet
|
||||
CREATE EXTENSION pgcrypto;
|
||||
'''
|
||||
sh
|
||||
sudo -u postgres --or-- sudo su - postgres
|
||||
createuser -S -P -E transmet
|
||||
createdb --owner transmet --encoding utf8 transmet
|
||||
psql
|
||||
\c transmet
|
||||
CREATE EXTENSION pgcrypto;
|
||||
|
||||
put DB details in
|
||||
db/dbconf.yml (copied from db/dbconf.EXAMPLE)
|
||||
config/prod.json (copied from config/local.json)
|
||||
|
||||
go get bitbucket.org/liamstask/goose/cmd/goose
|
||||
put DB details in:
|
||||
* db/dbconf.yml (copied from db/dbconf.EXAMPLE)
|
||||
* config/prod.json (copied from config/local.json)
|
||||
|
||||
goose up
|
||||
go get bitbucket.org/liamstask/goose/cmd/goose
|
||||
|
||||
goose up
|
||||
|
||||
## Run
|
||||
|
||||
Assumed GOPATH=/opt/go
|
||||
Assumed `GOPATH=/opt/go`
|
||||
|
||||
edit transmet.conf to point to correct location
|
||||
|
||||
sudo cp transmet.conf /etc/init
|
||||
|
||||
sudo service transmet start
|
||||
sudo cp transmet.conf /etc/init
|
||||
./gen-csrf.sh
|
||||
sudo service transmet start
|
||||
|
||||
## Setup environment
|
||||
|
||||
### Adding a user
|
||||
|
||||
INSERT INTO users (username, password) VALUES('USERNAME', crypt('PASSWORD', gen_salt('bf')));
|
||||
|
||||
### Adding Categories
|
||||
|
||||
INSERT INTO categories (name, parent_id) VALUES ('NAME', [null or PARENT_ID]);
|
||||
INSERT INTO users (username, password) VALUES('USERNAME', crypt('PASSWORD', gen_salt('bf')));
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
RUN GEN-CSRF.SH TO INIT THIS FI
|
|
@ -0,0 +1,39 @@
|
|||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
body {
|
||||
/* Margin bottom by footer height */
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
/* Set the fixed height of the footer here */
|
||||
height: 60px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
|
||||
/* Custom page CSS
|
||||
-------------------------------------------------- */
|
||||
/* Not required for template or sticky footer method. */
|
||||
|
||||
body > .container {
|
||||
padding: 0px 15px 0;
|
||||
}
|
||||
.container .text-muted {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.footer > .container {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 80%;
|
||||
}
|
|
@ -38,10 +38,27 @@ body {
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.post-preview {
|
||||
.post-content {
|
||||
font-size: 12px;
|
||||
max-height: 5em;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.post-content blockquote {
|
||||
font-size: 12px;
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.content-slider {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-radius: 3px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
.post-delete, .post-edit {
|
||||
font-size: 12px;
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
dd if=/dev/random of=csrf-secret.txt count=32 bs=1
|
||||
dd if=/dev/urandom of=csrf-secret.txt count=32 bs=1
|
||||
|
|
12
js/funcs.js
12
js/funcs.js
|
@ -42,4 +42,16 @@ $(document).ready( function () {
|
|||
form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
$(".content-slider").click(function (e) {
|
||||
var contentDiv = $(this).parents('.news-row').find('.post-content');
|
||||
if (contentDiv.hasClass("state-up")) {
|
||||
contentDiv.removeClass('state-up').addClass('state-down').animate({'max-height': '100%', 'height': '100%'});
|
||||
$(this).html('^');
|
||||
} else {
|
||||
contentDiv.removeClass('state-down').addClass('state-up').animate({height: '5em'});
|
||||
$(this).html('v');
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
|
8
main.go
8
main.go
|
@ -107,11 +107,15 @@ func main() {
|
|||
//errHandler := csrf.ErrorHandler( CSRFErrorHandler{} )
|
||||
|
||||
// Terrible. TODO: Get SSL for prod, and then wrap in if(dev) { {
|
||||
csrfSecurityOption := csrf.Secure(false)
|
||||
//csrfSecurityOption := csrf.Secure(false)
|
||||
//csrfMaxTimeOption := csrf.MaxAge(3600 * 24 * 3) // 3 Days - a little more wiggle room
|
||||
|
||||
fmt.Println("Listening on", config.Port, "...")
|
||||
|
||||
err := http.ListenAndServe(":"+config.Port, csrf.Protect([]byte(csrfSecret()) /*errHandler,*/, csrfSecurityOption)(muxRouter))
|
||||
// Disabled CSRF until SSL (and sorting why the popup is throwing CSRF errs
|
||||
// for tor and FF with ublock + https everywhere)
|
||||
//err := http.ListenAndServe(":"+config.Port, csrf.Protect([]byte(csrfSecret()), errHandler, csrfSecurityOption, csrfMaxTimeOption)(muxRouter))
|
||||
err := http.ListenAndServe(":"+config.Port, muxRouter)
|
||||
if err != nil {
|
||||
fmt.Println("Fatal Error: ", err)
|
||||
}
|
||||
|
|
12
news/news.go
12
news/news.go
|
@ -8,6 +8,8 @@ import (
|
|||
_ "github.com/lib/pq"
|
||||
"strconv"
|
||||
"time"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type News struct {
|
||||
|
@ -17,6 +19,7 @@ type News struct {
|
|||
Category_id int
|
||||
Date time.Time
|
||||
Notes string
|
||||
htmlNotes template.HTML
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -94,7 +97,7 @@ func Get(db *sql.DB, id int) (*News, error) {
|
|||
func LoadPage(db *sql.DB, offset, amount int) ([]*News, int, error) {
|
||||
categories.LoadCategories(db) // required by addContainer
|
||||
|
||||
rows, err := db.Query("SELECT " + SQL_NEWS_FIELDS + " FROM news order by timestamp DESC")
|
||||
rows, err := db.Query("SELECT " + SQL_NEWS_FIELDS + " FROM news ORDER BY timestamp DESC LIMIT $1 OFFSET $2", amount, offset)
|
||||
if err != nil {
|
||||
fmt.Println("DB errpr reading LoadPage news: ", err)
|
||||
return nil, 0, err
|
||||
|
@ -138,6 +141,10 @@ func (news *News) Id() int {
|
|||
return news.id
|
||||
}
|
||||
|
||||
func (news *News) HTMLNotes() template.HTML {
|
||||
return news.htmlNotes
|
||||
}
|
||||
|
||||
func nullStringToString(str *sql.NullString) string {
|
||||
if str.Valid {
|
||||
return str.String
|
||||
|
@ -182,6 +189,9 @@ func scanNews(rows *sql.Rows) (*News, error) {
|
|||
news.Url = nullStringToString(&url)
|
||||
news.Title = nullStringToString(&title)
|
||||
news.Notes = nullStringToString(¬es)
|
||||
// support wordpress style <quote> tags
|
||||
news.Notes = strings.Replace(news.Notes, "quote>", "blockquote>", -1)
|
||||
news.htmlNotes = template.HTML(news.Notes)
|
||||
|
||||
if category_id.Valid {
|
||||
news.Category_id = int(category_id.Int64)
|
||||
|
|
|
@ -144,10 +144,17 @@ func addFormHandler(w http.ResponseWriter, r *http.Request, user *user.User, ses
|
|||
if title == "" && url != "" {
|
||||
title = getUrlTitle(url)
|
||||
}
|
||||
|
||||
var notes = ""
|
||||
selection := r.URL.Query().Get("selection")
|
||||
if selection != "" {
|
||||
notes = "<quote>" + selection + "</quote>"
|
||||
}
|
||||
|
||||
|
||||
popup := r.URL.Query().Get("popup")
|
||||
|
||||
ShowTemplate("post", w, r, map[string]interface{}{"mode": "add", "user": user, "flashes": flashes, "link": url, "categories": categories.CategoriesTree, "title": title, "popup": popup, "category_id": -1})
|
||||
ShowTemplate("post", w, r, map[string]interface{}{"mode": "add", "user": user, "flashes": flashes, "link": url, "categories": categories.CategoriesTree, "title": title, "popup": popup, "category_id": -1, "notes": notes})
|
||||
}
|
||||
|
||||
func addPostHandler(w http.ResponseWriter, r *http.Request, user *user.User, session *sessions.Session) {
|
||||
|
@ -411,7 +418,7 @@ func newsFormHandler(w http.ResponseWriter, r *http.Request, user *user.User, se
|
|||
session.AddFlash("Error loading news", flash_err)
|
||||
}
|
||||
|
||||
ShowTemplate("news", w, r, map[string]interface{}{"user": user, "flashes": flashes, "news": news, "count": count, "categories": categories.CategoriesFlat})
|
||||
ShowTemplate("news", w, r, map[string]interface{}{"user": user, "flashes": flashes, "news": news, "count": count, "offset": argOffset, "amount": amount, "categories": categories.CategoriesFlat, "url": config.Url})
|
||||
}
|
||||
|
||||
func ServeFileHandler(res http.ResponseWriter, req *http.Request) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"github.com/dballard/transmet/categories"
|
||||
"github.com/gorilla/csrf"
|
||||
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
<!-- Bootstrap -->
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/css/sticky-footer-navbar.css" rel="stylesheet">
|
||||
<link href="/css/jquery-ui.min.css" rel="stylesheet">
|
||||
<link href="/css/template.css" rel="stylesheet">
|
||||
<link href="/css/signin.css" rel="stylesheet">
|
||||
|
@ -17,11 +18,10 @@
|
|||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
|
@ -32,27 +32,34 @@
|
|||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/news/add">add</a></li>
|
||||
<li><a href="/news/export">export</a></li>
|
||||
<li><a href="/categories">categories</a></li>
|
||||
|
||||
{{if .user}}
|
||||
<li><a href="/news/add">add</a></li>
|
||||
<li><a href="/news/export">export</a></li>
|
||||
<li><a href="/categories">categories</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{if .user}}
|
||||
<li>{{.user.Username}}</li>
|
||||
<li><form method="POST" action="/logout">{{ .csrfField }}<input type="submit" value="Logout" class="btn btn-sm btn-primary btn-block" /></form></li>
|
||||
<li><a href="">{{.user.Username}}</a></li>
|
||||
<li><form method="POST" action="/logout">{{ .csrfField }}<input type="submit" value="Logout" class="btn-sm btn-primary btn-block" /></form></li>
|
||||
{{else}}
|
||||
<li><a href="/login">Log in</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="text-muted">Powered by <a href="https://github.com/dballard/transmet">transmet</a> <img src="/favicon.ico" />
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
|
|
|
@ -35,7 +35,7 @@ new category select
|
|||
</div>
|
||||
<div class="col-xs-1">
|
||||
<form method="POST" action="/categories/{{.category.Id}}/delete" class="cat-delete">{{ $.csrfField }}
|
||||
<input type="submit" class="btn btn-sm btn-block" value="Delete" />
|
||||
<input type="submit" class="btn btn-default btn-sm btn-block" value="Delete" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
|
|
|
@ -7,16 +7,24 @@
|
|||
</div>
|
||||
|
||||
<div class="col-xs-6">
|
||||
Drag this link to bookmark bar and click anywhere to add a link
|
||||
Drag this bookmarklet to bookmark bar and click anywhere to add a link
|
||||
</div>
|
||||
<div class="col-xs-4"></div>
|
||||
<div class="col-xs-4"></div>
|
||||
|
||||
{{template "pager" .}}
|
||||
|
||||
<div class="col-xs-12"> </div>
|
||||
|
||||
</div>
|
||||
<div class="news-posts">
|
||||
{{range $news_post := .news}}
|
||||
{{template "row-news" dict "post" $news_post "categories" $.categories "csrfField" $.csrfField}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{{template "pager" .}}
|
||||
</div>
|
||||
|
||||
{{end}}
|
||||
|
||||
|
@ -39,21 +47,39 @@
|
|||
<div class="col-xs-8 post-url">
|
||||
<a href="{{.post.Url}}">{{truncate .post.Url 100}}</a>
|
||||
</div>
|
||||
<div class="col-xs-2 post-edit"><a href="/news/{{.post.Id}}/edit" class="btn btn-sm btn-block">Edit</a></div>
|
||||
<div class="col-xs-2 post-edit"><a href="/news/{{.post.Id}}/edit" class="btn btn-default btn-sm btn-block">Edit</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2"> </div>
|
||||
<div class="col-xs-8 post-preview">{{truncate .post.Notes 500}}</div>
|
||||
<div class="col-xs-8 post-content state-up"> {{ .post.HTMLNotes }}</div>
|
||||
<div class="col-xs-2 post-delete">
|
||||
<form method="POST" action="/news/{{.post.Id}}/delete" class="confirm-news-delete">{{ .csrfField }}
|
||||
<input type="submit" class="btn btn-sm btn-block" value="Delete" />
|
||||
<input type="submit" class="btn btn-default btn-sm btn-block" value="Delete" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2"> </div>
|
||||
<div class="col-xs-8 btn-default btn content-slider state-up">v</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "pager"}}
|
||||
<div class="col-xs-12"> </div>
|
||||
|
||||
<div class="col-xs-2">
|
||||
{{if ge .count .amount }}
|
||||
<a href="?offset={{ add .offset 1}}">< Prev</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-xs-8"> </div>
|
||||
<div class="col-xs-2">
|
||||
{{ if ne .offset 0 }}
|
||||
<a href="?offset={{ minus .offset 1 }}">Next ></a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- JS for the launcher of the add bookmarklet -->
|
||||
{{define "launch-add"}}
|
||||
javascript:var d=document,w=window,e=w.getSelection,k=d.getSelection,x=d.selection,s=(e?e():(k)?k():(x?x.createRange().text:0)),f='{{.url}}news/add',l=d.location,e=encodeURIComponent,u=f+'?popup=1&url='+e(l.href)+'&title='+e(d.title);a=function(){if(!w.open(u,'t','toolbar=0,resizable=1,scrollbars=1,status=1,width=720,height=410'))l.href=u;};if (/Firefox/.test(navigator.userAgent)) setTimeout(a, 0); else a();void(0)
|
||||
{{end}}
|
||||
{{define "launch-add"}}javascript:(function() { var d=document,w=window,e=w.getSelection,k=d.getSelection,x=d.selection,s=(e?e():(k)?k():(x?x.createRange().text:0)),f='{{.url}}/news/add',l=d.location,e=encodeURIComponent,u=f+'?popup=1&url='+e(l.href)+'&title='+e(d.title)+'&selection='+e(s);a=function(){if(!w.open(u,'t','toolbar=0,resizable=1,scrollbars=1,status=1,width=720,height=480'))l.href=u;};if (/Firefox/.test(navigator.userAgent)) setTimeout(a, 0); else a();void(0) })();{{end}}
|
Loading…
Reference in New Issue