mirror of
https://github.com/miniflux/v2.git
synced 2025-06-27 16:36:00 +00:00
Add "Share article" feature
A new "shareCode" field is generated for each entry, and allows unlogged users to access the entry through the /shared endpoint. This feature is particularly useful to share articles from miniflux to third-party users without having them to visit the original source. The image proxy is disabled and special cache headers are proposed in the shared page to avoid denial of service.
This commit is contained in:
parent
1b86913c00
commit
41a2b7e58e
24 changed files with 243 additions and 26 deletions
|
@ -133,6 +133,7 @@ type Entry struct {
|
||||||
Date time.Time `json:"published_at"`
|
Date time.Time `json:"published_at"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
|
ShareCode string `json:"share_code"`
|
||||||
Starred bool `json:"starred"`
|
Starred bool `json:"starred"`
|
||||||
Enclosures Enclosures `json:"enclosures,omitempty"`
|
Enclosures Enclosures `json:"enclosures,omitempty"`
|
||||||
Feed *Feed `json:"feed,omitempty"`
|
Feed *Feed `json:"feed,omitempty"`
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,3 +37,8 @@ func GenerateRandomBytes(size int) []byte {
|
||||||
func GenerateRandomString(size int) string {
|
func GenerateRandomString(size int) string {
|
||||||
return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
|
return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateRandomStringHex returns a random hexadecimal string.
|
||||||
|
func GenerateRandomStringHex(size int) string {
|
||||||
|
return hex.EncodeToString(GenerateRandomBytes(size))
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"miniflux.app/logger"
|
"miniflux.app/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const schemaVersion = 27
|
const schemaVersion = 28
|
||||||
|
|
||||||
// Migrate executes database migrations.
|
// Migrate executes database migrations.
|
||||||
func Migrate(db *sql.DB) {
|
func Migrate(db *sql.DB) {
|
||||||
|
|
|
@ -167,6 +167,9 @@ alter table entries alter column changed_at set not null;
|
||||||
primary key(id),
|
primary key(id),
|
||||||
unique (user_id, description)
|
unique (user_id, description)
|
||||||
);
|
);
|
||||||
|
`,
|
||||||
|
"schema_version_28": `alter table entries add column share_code text not null default '';
|
||||||
|
create unique index entries_share_code_idx on entries using btree(share_code) where share_code <> '';
|
||||||
`,
|
`,
|
||||||
"schema_version_3": `create table tokens (
|
"schema_version_3": `create table tokens (
|
||||||
id text not null,
|
id text not null,
|
||||||
|
@ -223,6 +226,7 @@ var SqlMapChecksums = map[string]string{
|
||||||
"schema_version_25": "5262d2d4c88d637b6603a1fcd4f68ad257bd59bd1adf89c58a18ee87b12050d7",
|
"schema_version_25": "5262d2d4c88d637b6603a1fcd4f68ad257bd59bd1adf89c58a18ee87b12050d7",
|
||||||
"schema_version_26": "64f14add40691f18f514ac0eed10cd9b19c83a35e5c3d8e0bce667e0ceca9094",
|
"schema_version_26": "64f14add40691f18f514ac0eed10cd9b19c83a35e5c3d8e0bce667e0ceca9094",
|
||||||
"schema_version_27": "4235396b37fd7f52ff6f7526416042bb1649701233e2d99f0bcd583834a0a967",
|
"schema_version_27": "4235396b37fd7f52ff6f7526416042bb1649701233e2d99f0bcd583834a0a967",
|
||||||
|
"schema_version_28": "a64b5ba0b37fe3f209617b7d0e4dd05018d2b8362d2c9c528ba8cce19b77e326",
|
||||||
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
|
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
|
||||||
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
|
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
|
||||||
"schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
|
"schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
|
||||||
|
|
2
database/sql/schema_version_28.sql
Normal file
2
database/sql/schema_version_28.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
alter table entries add column share_code text not null default '';
|
||||||
|
create unique index entries_share_code_idx on entries using btree(share_code) where share_code <> '';
|
|
@ -76,6 +76,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Original-Artikel",
|
"entry.original.label": "Original-Artikel",
|
||||||
"entry.comments.label": "Kommentare",
|
"entry.comments.label": "Kommentare",
|
||||||
"entry.comments.title": "Kommentare anzeigen",
|
"entry.comments.title": "Kommentare anzeigen",
|
||||||
|
"entry.share.label": "Teilen",
|
||||||
|
"entry.share.title": "Diesen Artikel teilen",
|
||||||
"page.unread.title": "Ungelesen",
|
"page.unread.title": "Ungelesen",
|
||||||
"page.starred.title": "Lesezeichen",
|
"page.starred.title": "Lesezeichen",
|
||||||
"page.categories.title": "Kategorien",
|
"page.categories.title": "Kategorien",
|
||||||
|
@ -402,6 +404,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Original",
|
"entry.original.label": "Original",
|
||||||
"entry.comments.label": "Comments",
|
"entry.comments.label": "Comments",
|
||||||
"entry.comments.title": "View Comments",
|
"entry.comments.title": "View Comments",
|
||||||
|
"entry.share.label": "Share",
|
||||||
|
"entry.share.title": "Share this article",
|
||||||
"page.unread.title": "Unread",
|
"page.unread.title": "Unread",
|
||||||
"page.starred.title": "Starred",
|
"page.starred.title": "Starred",
|
||||||
"page.categories.title": "Categories",
|
"page.categories.title": "Categories",
|
||||||
|
@ -708,6 +712,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Original",
|
"entry.original.label": "Original",
|
||||||
"entry.comments.label": "Comentarios",
|
"entry.comments.label": "Comentarios",
|
||||||
"entry.comments.title": "Ver comentarios",
|
"entry.comments.title": "Ver comentarios",
|
||||||
|
"entry.share.label": "Comparta",
|
||||||
|
"entry.share.title": "Comparta este articulo",
|
||||||
"page.unread.title": "No leídos",
|
"page.unread.title": "No leídos",
|
||||||
"page.starred.title": "Marcadores",
|
"page.starred.title": "Marcadores",
|
||||||
"page.categories.title": "Categorias",
|
"page.categories.title": "Categorias",
|
||||||
|
@ -1014,6 +1020,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Original",
|
"entry.original.label": "Original",
|
||||||
"entry.comments.label": "Commentaires",
|
"entry.comments.label": "Commentaires",
|
||||||
"entry.comments.title": "Voir les commentaires",
|
"entry.comments.title": "Voir les commentaires",
|
||||||
|
"entry.share.label": "Partager",
|
||||||
|
"entry.share.title": "Partager cet article",
|
||||||
"page.unread.title": "Non lus",
|
"page.unread.title": "Non lus",
|
||||||
"page.starred.title": "Favoris",
|
"page.starred.title": "Favoris",
|
||||||
"page.categories.title": "Catégories",
|
"page.categories.title": "Catégories",
|
||||||
|
@ -1340,6 +1348,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Contenuto originale",
|
"entry.original.label": "Contenuto originale",
|
||||||
"entry.comments.label": "Commenti",
|
"entry.comments.label": "Commenti",
|
||||||
"entry.comments.title": "Mostra i commenti",
|
"entry.comments.title": "Mostra i commenti",
|
||||||
|
"entry.share.label": "Condividi",
|
||||||
|
"entry.share.title": "Condividi questo articolo",
|
||||||
"page.unread.title": "Da leggere",
|
"page.unread.title": "Da leggere",
|
||||||
"page.starred.title": "Preferiti",
|
"page.starred.title": "Preferiti",
|
||||||
"page.categories.title": "Categorie",
|
"page.categories.title": "Categorie",
|
||||||
|
@ -1646,6 +1656,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "オリジナル",
|
"entry.original.label": "オリジナル",
|
||||||
"entry.comments.label": "コメント",
|
"entry.comments.label": "コメント",
|
||||||
"entry.comments.title": "コメントを見る",
|
"entry.comments.title": "コメントを見る",
|
||||||
|
"entry.share.label": "共有",
|
||||||
|
"entry.share.title": "この記事を共有する",
|
||||||
"page.unread.title": "未読",
|
"page.unread.title": "未読",
|
||||||
"page.starred.title": "星付き",
|
"page.starred.title": "星付き",
|
||||||
"page.categories.title": "カテゴリ",
|
"page.categories.title": "カテゴリ",
|
||||||
|
@ -1952,6 +1964,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Origineel",
|
"entry.original.label": "Origineel",
|
||||||
"entry.comments.label": "Comments",
|
"entry.comments.label": "Comments",
|
||||||
"entry.comments.title": "Bekijk de reacties",
|
"entry.comments.title": "Bekijk de reacties",
|
||||||
|
"entry.share.label": "Deel",
|
||||||
|
"entry.share.title": "Deel dit artikel",
|
||||||
"page.unread.title": "Ongelezen",
|
"page.unread.title": "Ongelezen",
|
||||||
"page.starred.title": "Favorieten",
|
"page.starred.title": "Favorieten",
|
||||||
"page.categories.title": "Categorieën",
|
"page.categories.title": "Categorieën",
|
||||||
|
@ -2276,6 +2290,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Oryginalny artykuł",
|
"entry.original.label": "Oryginalny artykuł",
|
||||||
"entry.comments.label": "Komentarze",
|
"entry.comments.label": "Komentarze",
|
||||||
"entry.comments.title": "Zobacz komentarze",
|
"entry.comments.title": "Zobacz komentarze",
|
||||||
|
"entry.share.label": "Podzielić się",
|
||||||
|
"entry.share.title": "Podzielić się ten artykuł",
|
||||||
"page.unread.title": "Nieprzeczytane",
|
"page.unread.title": "Nieprzeczytane",
|
||||||
"page.starred.title": "Oznaczone gwiazdką",
|
"page.starred.title": "Oznaczone gwiazdką",
|
||||||
"page.categories.title": "Kategorie",
|
"page.categories.title": "Kategorie",
|
||||||
|
@ -2608,6 +2624,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "Оригинал",
|
"entry.original.label": "Оригинал",
|
||||||
"entry.comments.label": "Комментарии",
|
"entry.comments.label": "Комментарии",
|
||||||
"entry.comments.title": "Показать комментарии",
|
"entry.comments.title": "Показать комментарии",
|
||||||
|
"entry.share.label": "поделиться",
|
||||||
|
"entry.share.title": "поделиться эту статью",
|
||||||
"page.unread.title": "Непрочитанное",
|
"page.unread.title": "Непрочитанное",
|
||||||
"page.starred.title": "Избранное",
|
"page.starred.title": "Избранное",
|
||||||
"page.categories.title": "Категории",
|
"page.categories.title": "Категории",
|
||||||
|
@ -2922,6 +2940,8 @@ var translations = map[string]string{
|
||||||
"entry.original.label": "原始内容",
|
"entry.original.label": "原始内容",
|
||||||
"entry.comments.label": "评论",
|
"entry.comments.label": "评论",
|
||||||
"entry.comments.title": "查看评论",
|
"entry.comments.title": "查看评论",
|
||||||
|
"entry.share.label": "分享",
|
||||||
|
"entry.share.title": "分享这篇文章",
|
||||||
"page.unread.title": "未读",
|
"page.unread.title": "未读",
|
||||||
"page.starred.title": "星标",
|
"page.starred.title": "星标",
|
||||||
"page.categories.title": "分类",
|
"page.categories.title": "分类",
|
||||||
|
@ -3169,14 +3189,14 @@ var translations = map[string]string{
|
||||||
}
|
}
|
||||||
|
|
||||||
var translationsChecksums = map[string]string{
|
var translationsChecksums = map[string]string{
|
||||||
"de_DE": "cc826a57cf4bf789df38db4f50626ad8c1c2b84ce34075c2c04de3d1f0dcd2d5",
|
"de_DE": "7360a69e038d71e00f64c03891401cd517779687d46a907688f4a9a7b6205146",
|
||||||
"en_US": "f7e6db53cdbc2c0d959ac231dbacf0ef4d0ed81248944c4a4f8b83ef000f5349",
|
"en_US": "92dda79899a673652a43fd8d61c893749713af09909ca03ac6fea06ac617d361",
|
||||||
"es_ES": "cc727f62eef3a6cba51b65253d70a50161af35bf9c5366281b7984b2fc189961",
|
"es_ES": "813b8cd42907dfbc19ff51f3367e0dbb013d373b013d7854df512e846652ff21",
|
||||||
"fr_FR": "d3d1a4bf9aa8e4e24bae2f117507dcfc3cf00660a73b44a6c42356e8dbab8ae8",
|
"fr_FR": "279c52bbf682949cf8782e7e81f2bf5cfd300cebf577d51ce9436d44aaaf6323",
|
||||||
"it_IT": "5ded991f2c70ec2268e6053bd84a77cf4136ebaea42013d3e79d594f38abb1b3",
|
"it_IT": "5e8408e9aee142e1bd7e73f2a91ae96bc9ad0ab61c20416ad9e93b6fe505e8a9",
|
||||||
"ja_JP": "110d7a7b1c888282b031de340e3318a62cdd62076b05a7fb49759f554c6dbe76",
|
"ja_JP": "508025c0c7e7f57195ae011c4499ab58a85d043c828565c1740df879fb2376c1",
|
||||||
"nl_NL": "a934ab4b1eff85580425a5859c31fcb227ae8926deba74df4e42b5d4feb67826",
|
"nl_NL": "e621a5e7408928624a060a832d9fc36b74026221bd7b07894a4cce267be3cdd1",
|
||||||
"pl_PL": "6e80c36788723b9a7ff3f372e13a55c68d153727ec0abb56663cadbf6d6e1d9f",
|
"pl_PL": "2383c1a9be451557fe601f346e30bb165a88d9c00d17909a9c747d64864a423d",
|
||||||
"ru_RU": "d56f9e31f63731d23ce1ea2a8a4cb019f3ab282b23a1f494c47061daea523587",
|
"ru_RU": "d7ad59bbd7a150af9d476c4c3034eb85762de7381e2925d75e373584ed45c725",
|
||||||
"zh_CN": "4a5ca40790fceab88257f6742dc05294b79142bee8aad6fc87fbd479d1941292",
|
"zh_CN": "e5f169a3c83c9bd7a41e9737e001e58fec243eee7aa23a71d37bfa8e05d92860",
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Original-Artikel",
|
"entry.original.label": "Original-Artikel",
|
||||||
"entry.comments.label": "Kommentare",
|
"entry.comments.label": "Kommentare",
|
||||||
"entry.comments.title": "Kommentare anzeigen",
|
"entry.comments.title": "Kommentare anzeigen",
|
||||||
|
"entry.share.label": "Teilen",
|
||||||
|
"entry.share.title": "Diesen Artikel teilen",
|
||||||
"page.unread.title": "Ungelesen",
|
"page.unread.title": "Ungelesen",
|
||||||
"page.starred.title": "Lesezeichen",
|
"page.starred.title": "Lesezeichen",
|
||||||
"page.categories.title": "Kategorien",
|
"page.categories.title": "Kategorien",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Original",
|
"entry.original.label": "Original",
|
||||||
"entry.comments.label": "Comments",
|
"entry.comments.label": "Comments",
|
||||||
"entry.comments.title": "View Comments",
|
"entry.comments.title": "View Comments",
|
||||||
|
"entry.share.label": "Share",
|
||||||
|
"entry.share.title": "Share this article",
|
||||||
"page.unread.title": "Unread",
|
"page.unread.title": "Unread",
|
||||||
"page.starred.title": "Starred",
|
"page.starred.title": "Starred",
|
||||||
"page.categories.title": "Categories",
|
"page.categories.title": "Categories",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Original",
|
"entry.original.label": "Original",
|
||||||
"entry.comments.label": "Comentarios",
|
"entry.comments.label": "Comentarios",
|
||||||
"entry.comments.title": "Ver comentarios",
|
"entry.comments.title": "Ver comentarios",
|
||||||
|
"entry.share.label": "Comparta",
|
||||||
|
"entry.share.title": "Comparta este articulo",
|
||||||
"page.unread.title": "No leídos",
|
"page.unread.title": "No leídos",
|
||||||
"page.starred.title": "Marcadores",
|
"page.starred.title": "Marcadores",
|
||||||
"page.categories.title": "Categorias",
|
"page.categories.title": "Categorias",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Original",
|
"entry.original.label": "Original",
|
||||||
"entry.comments.label": "Commentaires",
|
"entry.comments.label": "Commentaires",
|
||||||
"entry.comments.title": "Voir les commentaires",
|
"entry.comments.title": "Voir les commentaires",
|
||||||
|
"entry.share.label": "Partager",
|
||||||
|
"entry.share.title": "Partager cet article",
|
||||||
"page.unread.title": "Non lus",
|
"page.unread.title": "Non lus",
|
||||||
"page.starred.title": "Favoris",
|
"page.starred.title": "Favoris",
|
||||||
"page.categories.title": "Catégories",
|
"page.categories.title": "Catégories",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Contenuto originale",
|
"entry.original.label": "Contenuto originale",
|
||||||
"entry.comments.label": "Commenti",
|
"entry.comments.label": "Commenti",
|
||||||
"entry.comments.title": "Mostra i commenti",
|
"entry.comments.title": "Mostra i commenti",
|
||||||
|
"entry.share.label": "Condividi",
|
||||||
|
"entry.share.title": "Condividi questo articolo",
|
||||||
"page.unread.title": "Da leggere",
|
"page.unread.title": "Da leggere",
|
||||||
"page.starred.title": "Preferiti",
|
"page.starred.title": "Preferiti",
|
||||||
"page.categories.title": "Categorie",
|
"page.categories.title": "Categorie",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "オリジナル",
|
"entry.original.label": "オリジナル",
|
||||||
"entry.comments.label": "コメント",
|
"entry.comments.label": "コメント",
|
||||||
"entry.comments.title": "コメントを見る",
|
"entry.comments.title": "コメントを見る",
|
||||||
|
"entry.share.label": "共有",
|
||||||
|
"entry.share.title": "この記事を共有する",
|
||||||
"page.unread.title": "未読",
|
"page.unread.title": "未読",
|
||||||
"page.starred.title": "星付き",
|
"page.starred.title": "星付き",
|
||||||
"page.categories.title": "カテゴリ",
|
"page.categories.title": "カテゴリ",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Origineel",
|
"entry.original.label": "Origineel",
|
||||||
"entry.comments.label": "Comments",
|
"entry.comments.label": "Comments",
|
||||||
"entry.comments.title": "Bekijk de reacties",
|
"entry.comments.title": "Bekijk de reacties",
|
||||||
|
"entry.share.label": "Deel",
|
||||||
|
"entry.share.title": "Deel dit artikel",
|
||||||
"page.unread.title": "Ongelezen",
|
"page.unread.title": "Ongelezen",
|
||||||
"page.starred.title": "Favorieten",
|
"page.starred.title": "Favorieten",
|
||||||
"page.categories.title": "Categorieën",
|
"page.categories.title": "Categorieën",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Oryginalny artykuł",
|
"entry.original.label": "Oryginalny artykuł",
|
||||||
"entry.comments.label": "Komentarze",
|
"entry.comments.label": "Komentarze",
|
||||||
"entry.comments.title": "Zobacz komentarze",
|
"entry.comments.title": "Zobacz komentarze",
|
||||||
|
"entry.share.label": "Podzielić się",
|
||||||
|
"entry.share.title": "Podzielić się ten artykuł",
|
||||||
"page.unread.title": "Nieprzeczytane",
|
"page.unread.title": "Nieprzeczytane",
|
||||||
"page.starred.title": "Oznaczone gwiazdką",
|
"page.starred.title": "Oznaczone gwiazdką",
|
||||||
"page.categories.title": "Kategorie",
|
"page.categories.title": "Kategorie",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "Оригинал",
|
"entry.original.label": "Оригинал",
|
||||||
"entry.comments.label": "Комментарии",
|
"entry.comments.label": "Комментарии",
|
||||||
"entry.comments.title": "Показать комментарии",
|
"entry.comments.title": "Показать комментарии",
|
||||||
|
"entry.share.label": "поделиться",
|
||||||
|
"entry.share.title": "поделиться эту статью",
|
||||||
"page.unread.title": "Непрочитанное",
|
"page.unread.title": "Непрочитанное",
|
||||||
"page.starred.title": "Избранное",
|
"page.starred.title": "Избранное",
|
||||||
"page.categories.title": "Категории",
|
"page.categories.title": "Категории",
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"entry.original.label": "原始内容",
|
"entry.original.label": "原始内容",
|
||||||
"entry.comments.label": "评论",
|
"entry.comments.label": "评论",
|
||||||
"entry.comments.title": "查看评论",
|
"entry.comments.title": "查看评论",
|
||||||
|
"entry.share.label": "分享",
|
||||||
|
"entry.share.title": "分享这篇文章",
|
||||||
"page.unread.title": "未读",
|
"page.unread.title": "未读",
|
||||||
"page.starred.title": "星标",
|
"page.starred.title": "星标",
|
||||||
"page.categories.title": "分类",
|
"page.categories.title": "分类",
|
||||||
|
|
|
@ -31,6 +31,7 @@ type Entry struct {
|
||||||
Date time.Time `json:"published_at"`
|
Date time.Time `json:"published_at"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
|
ShareCode string `json:"share_code"`
|
||||||
Starred bool `json:"starred"`
|
Starred bool `json:"starred"`
|
||||||
Enclosures EnclosureList `json:"enclosures,omitempty"`
|
Enclosures EnclosureList `json:"enclosures,omitempty"`
|
||||||
Feed *Feed `json:"feed,omitempty"`
|
Feed *Feed `json:"feed,omitempty"`
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"miniflux.app/crypto"
|
||||||
"miniflux.app/logger"
|
"miniflux.app/logger"
|
||||||
"miniflux.app/model"
|
"miniflux.app/model"
|
||||||
|
|
||||||
|
@ -351,3 +352,35 @@ func (s *Storage) EntryURLExists(feedID int64, entryURL string) bool {
|
||||||
s.db.QueryRow(query, feedID, entryURL).Scan(&result)
|
s.db.QueryRow(query, feedID, entryURL).Scan(&result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEntryShareCode returns the share code of the provided entry.
|
||||||
|
// It generates a new one if not already defined.
|
||||||
|
func (s *Storage) GetEntryShareCode(userID int64, entryID int64) (shareCode string, err error) {
|
||||||
|
query := `SELECT share_code FROM entries WHERE user_id=$1 AND id=$2`
|
||||||
|
err = s.db.QueryRow(query, userID, entryID).Scan(&shareCode)
|
||||||
|
|
||||||
|
if err != nil || shareCode != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shareCode = crypto.GenerateRandomStringHex(16)
|
||||||
|
|
||||||
|
query = `UPDATE entries SET share_code = $1 WHERE user_id=$2 AND id=$3`
|
||||||
|
result, err := s.db.Exec(query, shareCode, userID, entryID)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
err = errors.New(`store: nothing has been updated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -128,6 +128,13 @@ func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithShareCode set the entry hash.
|
||||||
|
func (e *EntryQueryBuilder) WithShareCode(shareCode string) *EntryQueryBuilder {
|
||||||
|
e.conditions = append(e.conditions, fmt.Sprintf("e.share_code = $%d", len(e.args)+1))
|
||||||
|
e.args = append(e.args, shareCode)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
// WithOrder set the sorting order.
|
// WithOrder set the sorting order.
|
||||||
func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
|
func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
|
||||||
e.order = order
|
e.order = order
|
||||||
|
@ -198,6 +205,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||||
e.url,
|
e.url,
|
||||||
e.comments_url,
|
e.comments_url,
|
||||||
e.author,
|
e.author,
|
||||||
|
e.share_code,
|
||||||
e.content,
|
e.content,
|
||||||
e.status,
|
e.status,
|
||||||
e.starred,
|
e.starred,
|
||||||
|
@ -255,6 +263,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||||
&entry.URL,
|
&entry.URL,
|
||||||
&entry.CommentsURL,
|
&entry.CommentsURL,
|
||||||
&entry.Author,
|
&entry.Author,
|
||||||
|
&entry.ShareCode,
|
||||||
&entry.Content,
|
&entry.Content,
|
||||||
&entry.Status,
|
&entry.Status,
|
||||||
&entry.Starred,
|
&entry.Starred,
|
||||||
|
@ -358,3 +367,10 @@ func NewEntryQueryBuilder(store *Storage, userID int64) *EntryQueryBuilder {
|
||||||
conditions: []string{"e.user_id = $1"},
|
conditions: []string{"e.user_id = $1"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAnonymousQueryBuilder returns a new EntryQueryBuilder suitable for anonymous users.
|
||||||
|
func NewAnonymousQueryBuilder(store *Storage) *EntryQueryBuilder {
|
||||||
|
return &EntryQueryBuilder{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<h1>
|
<h1>
|
||||||
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
{{ if .user }}
|
||||||
<div class="entry-actions">
|
<div class="entry-actions">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -43,6 +44,12 @@
|
||||||
>{{ t "entry.save.title" }}</a>
|
>{{ t "entry.save.title" }}</a>
|
||||||
</li>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<li>
|
||||||
|
<a href="{{ route "shareGenerate" "entryID" .entry.ID }}"
|
||||||
|
title="{{ t "entry.share.title" }}"
|
||||||
|
target="_blank"
|
||||||
|
>{{ t "entry.share.label" }}</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#"
|
<a href="#"
|
||||||
title="{{ t "entry.scraper.title" }}"
|
title="{{ t "entry.scraper.title" }}"
|
||||||
|
@ -59,9 +66,10 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{{ end }}
|
||||||
<div class="entry-meta">
|
<div class="entry-meta">
|
||||||
<span class="entry-website">
|
<span class="entry-website">
|
||||||
{{ if ne .entry.Feed.Icon.IconID 0 }}
|
{{ if and .user (ne .entry.Feed.Icon.IconID 0) }}
|
||||||
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .entry.Feed.Title }}">
|
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .entry.Feed.Title }}">
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
|
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
|
||||||
|
@ -75,21 +83,33 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<span class="category">
|
{{ if .user }}
|
||||||
<a href="{{ route "categoryEntries" "categoryID" .entry.Feed.Category.ID }}">{{ .entry.Feed.Category.Title }}</a>
|
<span class="category">
|
||||||
</span>
|
<a href="{{ route "categoryEntries" "categoryID" .entry.Feed.Category.ID }}">{{ .entry.Feed.Category.Title }}</a>
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-date">
|
<div class="entry-date">
|
||||||
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed $.user.Timezone .entry.Date }}</time>
|
{{ if .user }}
|
||||||
|
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed $.user.Timezone .entry.Date }}</time>
|
||||||
|
{{ else }}
|
||||||
|
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed "UTC" .entry.Date }}</time>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{{ if gt (len .entry.Content) 120 }}
|
{{ if gt (len .entry.Content) 120 }}
|
||||||
|
{{ if .user }}
|
||||||
<div class="pagination-top">
|
<div class="pagination-top">
|
||||||
{{ template "entry_pagination" . }}
|
{{ template "entry_pagination" . }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
<article class="entry-content">
|
<article class="entry-content">
|
||||||
{{ noescape (proxyFilter .entry.Content) }}
|
{{ if .user }}
|
||||||
|
{{ noescape (proxyFilter .entry.Content) }}
|
||||||
|
{{ else }}
|
||||||
|
{{ noescape .entry.Content }}
|
||||||
|
{{ end }}
|
||||||
</article>
|
</article>
|
||||||
{{ if .entry.Enclosures }}
|
{{ if .entry.Enclosures }}
|
||||||
<details class="entry-enclosures">
|
<details class="entry-enclosures">
|
||||||
|
@ -111,7 +131,11 @@
|
||||||
</div>
|
</div>
|
||||||
{{ else if hasPrefix .MimeType "image/" }}
|
{{ else if hasPrefix .MimeType "image/" }}
|
||||||
<div class="enclosure-image">
|
<div class="enclosure-image">
|
||||||
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
{{ if .user }}
|
||||||
|
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
||||||
|
{{ else }}
|
||||||
|
<img src="{{ .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
@ -126,7 +150,9 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{ if .user }}
|
||||||
<div class="pagination-bottom">
|
<div class="pagination-bottom">
|
||||||
{{ template "entry_pagination" . }}
|
{{ template "entry_pagination" . }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
|
@ -653,6 +653,7 @@ var templateViewsMap = map[string]string{
|
||||||
<h1>
|
<h1>
|
||||||
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
{{ if .user }}
|
||||||
<div class="entry-actions">
|
<div class="entry-actions">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -690,6 +691,12 @@ var templateViewsMap = map[string]string{
|
||||||
>{{ t "entry.save.title" }}</a>
|
>{{ t "entry.save.title" }}</a>
|
||||||
</li>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<li>
|
||||||
|
<a href="{{ route "shareGenerate" "entryID" .entry.ID }}"
|
||||||
|
title="{{ t "entry.share.title" }}"
|
||||||
|
target="_blank"
|
||||||
|
>{{ t "entry.share.label" }}</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#"
|
<a href="#"
|
||||||
title="{{ t "entry.scraper.title" }}"
|
title="{{ t "entry.scraper.title" }}"
|
||||||
|
@ -706,9 +713,10 @@ var templateViewsMap = map[string]string{
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{{ end }}
|
||||||
<div class="entry-meta">
|
<div class="entry-meta">
|
||||||
<span class="entry-website">
|
<span class="entry-website">
|
||||||
{{ if ne .entry.Feed.Icon.IconID 0 }}
|
{{ if and .user (ne .entry.Feed.Icon.IconID 0) }}
|
||||||
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .entry.Feed.Title }}">
|
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .entry.Feed.Title }}">
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
|
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
|
||||||
|
@ -722,21 +730,33 @@ var templateViewsMap = map[string]string{
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<span class="category">
|
{{ if .user }}
|
||||||
<a href="{{ route "categoryEntries" "categoryID" .entry.Feed.Category.ID }}">{{ .entry.Feed.Category.Title }}</a>
|
<span class="category">
|
||||||
</span>
|
<a href="{{ route "categoryEntries" "categoryID" .entry.Feed.Category.ID }}">{{ .entry.Feed.Category.Title }}</a>
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-date">
|
<div class="entry-date">
|
||||||
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed $.user.Timezone .entry.Date }}</time>
|
{{ if .user }}
|
||||||
|
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed $.user.Timezone .entry.Date }}</time>
|
||||||
|
{{ else }}
|
||||||
|
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed "UTC" .entry.Date }}</time>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{{ if gt (len .entry.Content) 120 }}
|
{{ if gt (len .entry.Content) 120 }}
|
||||||
|
{{ if .user }}
|
||||||
<div class="pagination-top">
|
<div class="pagination-top">
|
||||||
{{ template "entry_pagination" . }}
|
{{ template "entry_pagination" . }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
<article class="entry-content">
|
<article class="entry-content">
|
||||||
{{ noescape (proxyFilter .entry.Content) }}
|
{{ if .user }}
|
||||||
|
{{ noescape (proxyFilter .entry.Content) }}
|
||||||
|
{{ else }}
|
||||||
|
{{ noescape .entry.Content }}
|
||||||
|
{{ end }}
|
||||||
</article>
|
</article>
|
||||||
{{ if .entry.Enclosures }}
|
{{ if .entry.Enclosures }}
|
||||||
<details class="entry-enclosures">
|
<details class="entry-enclosures">
|
||||||
|
@ -758,7 +778,11 @@ var templateViewsMap = map[string]string{
|
||||||
</div>
|
</div>
|
||||||
{{ else if hasPrefix .MimeType "image/" }}
|
{{ else if hasPrefix .MimeType "image/" }}
|
||||||
<div class="enclosure-image">
|
<div class="enclosure-image">
|
||||||
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
{{ if .user }}
|
||||||
|
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
||||||
|
{{ else }}
|
||||||
|
<img src="{{ .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
@ -773,10 +797,12 @@ var templateViewsMap = map[string]string{
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{ if .user }}
|
||||||
<div class="pagination-bottom">
|
<div class="pagination-bottom">
|
||||||
{{ template "entry_pagination" . }}
|
{{ template "entry_pagination" . }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
`,
|
`,
|
||||||
"feed_entries": `{{ define "title"}}{{ .feed.Title }} ({{ .total }}){{ end }}
|
"feed_entries": `{{ define "title"}}{{ .feed.Title }} ({{ .total }}){{ end }}
|
||||||
|
|
||||||
|
@ -1422,7 +1448,7 @@ var templateViewsMapChecksums = map[string]string{
|
||||||
"edit_category": "b1c0b38f1b714c5d884edcd61e5b5295a5f1c8b71c469b35391e4dcc97cc6d36",
|
"edit_category": "b1c0b38f1b714c5d884edcd61e5b5295a5f1c8b71c469b35391e4dcc97cc6d36",
|
||||||
"edit_feed": "cc0b5dbb73f81398410958b41771ed38246bc7ae4bd548228f0d48c49a598c2a",
|
"edit_feed": "cc0b5dbb73f81398410958b41771ed38246bc7ae4bd548228f0d48c49a598c2a",
|
||||||
"edit_user": "c692db9de1a084c57b93e95a14b041d39bf489846cbb91fc982a62b72b77062a",
|
"edit_user": "c692db9de1a084c57b93e95a14b041d39bf489846cbb91fc982a62b72b77062a",
|
||||||
"entry": "513183f0f0b11a199630562f5a85eb9a5646051aae278cbc682bac13d62e65cc",
|
"entry": "ef9cd8bb99c561023c1dcea1dbd7f90c4cdc195ed70e2ed9c88213fec875d770",
|
||||||
"feed_entries": "9c70b82f55e4b311eff20be1641733612e3c1b406ce8010861e4c417d97b6dcc",
|
"feed_entries": "9c70b82f55e4b311eff20be1641733612e3c1b406ce8010861e4c417d97b6dcc",
|
||||||
"feeds": "ec7d3fa96735bd8422ba69ef0927dcccddc1cc51327e0271f0312d3f881c64fd",
|
"feeds": "ec7d3fa96735bd8422ba69ef0927dcccddc1cc51327e0271f0312d3f881c64fd",
|
||||||
"history_entries": "87e17d39de70eb3fdbc4000326283be610928758eae7924e4b08dcb446f3b6a9",
|
"history_entries": "87e17d39de70eb3fdbc4000326283be610928758eae7924e4b08dcb446f3b6a9",
|
||||||
|
|
|
@ -135,6 +135,7 @@ func (m *middleware) isPublicRoute(r *http.Request) bool {
|
||||||
"favicon",
|
"favicon",
|
||||||
"webManifest",
|
"webManifest",
|
||||||
"robots",
|
"robots",
|
||||||
|
"share",
|
||||||
"healthcheck":
|
"healthcheck":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
|
|
57
ui/share.go
Normal file
57
ui/share.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Apache 2.0
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ui // import "miniflux.app/ui"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"miniflux.app/http/request"
|
||||||
|
"miniflux.app/http/response"
|
||||||
|
"miniflux.app/http/response/html"
|
||||||
|
"miniflux.app/http/route"
|
||||||
|
"miniflux.app/storage"
|
||||||
|
"miniflux.app/ui/session"
|
||||||
|
"miniflux.app/ui/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *handler) shareGenerate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
entryID := request.RouteInt64Param(r, "entryID")
|
||||||
|
shareCode, err := h.store.GetEntryShareCode(request.UserID(r), entryID)
|
||||||
|
if err != nil {
|
||||||
|
html.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
html.Redirect(w, r, route.Path(h.router, "share", "shareCode", shareCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) sharePage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
shareCode := request.RouteStringParam(r, "shareCode")
|
||||||
|
if shareCode == "" {
|
||||||
|
html.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
etag := shareCode
|
||||||
|
response.New(w, r).WithCaching(etag, 72*time.Hour, func(b *response.Builder) {
|
||||||
|
builder := storage.NewAnonymousQueryBuilder(h.store)
|
||||||
|
builder.WithShareCode(shareCode)
|
||||||
|
|
||||||
|
entry, err := builder.GetEntry()
|
||||||
|
if err != nil || entry == nil {
|
||||||
|
html.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(h.store, request.SessionID(r))
|
||||||
|
view := view.New(h.tpl, r, sess)
|
||||||
|
view.Set("entry", entry)
|
||||||
|
|
||||||
|
b.WithHeader("Content-Type", "text/html; charset=utf-8")
|
||||||
|
b.WithBody(view.Render("entry"))
|
||||||
|
b.Write()
|
||||||
|
})
|
||||||
|
}
|
4
ui/ui.go
4
ui/ui.go
|
@ -88,6 +88,10 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
|
||||||
uiRouter.HandleFunc("/proxy/{encodedURL}", handler.imageProxy).Name("proxy").Methods("GET")
|
uiRouter.HandleFunc("/proxy/{encodedURL}", handler.imageProxy).Name("proxy").Methods("GET")
|
||||||
uiRouter.HandleFunc("/entry/bookmark/{entryID}", handler.toggleBookmark).Name("toggleBookmark").Methods("POST")
|
uiRouter.HandleFunc("/entry/bookmark/{entryID}", handler.toggleBookmark).Name("toggleBookmark").Methods("POST")
|
||||||
|
|
||||||
|
// Share pages.
|
||||||
|
uiRouter.HandleFunc("/entry/share/{entryID}", handler.shareGenerate).Name("shareGenerate").Methods("GET")
|
||||||
|
uiRouter.HandleFunc("/shared/{shareCode}", handler.sharePage).Name("share").Methods("GET")
|
||||||
|
|
||||||
// User pages.
|
// User pages.
|
||||||
uiRouter.HandleFunc("/users", handler.showUsersPage).Name("users").Methods("GET")
|
uiRouter.HandleFunc("/users", handler.showUsersPage).Name("users").Methods("GET")
|
||||||
uiRouter.HandleFunc("/user/create", handler.showCreateUserPage).Name("createUser").Methods("GET")
|
uiRouter.HandleFunc("/user/create", handler.showCreateUserPage).Name("createUser").Methods("GET")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue