Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions announcements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Announcements

## Announcement 2

Hello announcements!

- This is the first announcement.
- Stay tuned for more updates.

[https://github.com/ivpn/mailx/releases/tag/0.2.4](https://github.com/ivpn/mailx/releases/tag/0.2.4)

## Announcement 1

Hello announcements!

- This is the first announcement.
- Stay tuned for more updates.

[https://github.com/ivpn/mailx/releases/tag/0.2.1](https://github.com/ivpn/mailx/releases/tag/0.2.1)
1 change: 1 addition & 0 deletions api/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ SIGNUP_WEBHOOK_PSK=
PREAUTH_URL=
PREAUTH_PSK=
PREAUTH_TTL=60m
ANNOUNCEMENTS_URL=

APP_PORT=3001

Expand Down
2 changes: 2 additions & 0 deletions api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type APIConfig struct {
PreauthURL string
PreauthPSK string
PreauthTTL time.Duration
AnnouncementsURL string
}

type DBConfig struct {
Expand Down Expand Up @@ -169,6 +170,7 @@ func New() (Config, error) {
PreauthURL: os.Getenv("PREAUTH_URL"),
PreauthPSK: os.Getenv("PREAUTH_PSK"),
PreauthTTL: preauthTTL,
AnnouncementsURL: os.Getenv("ANNOUNCEMENTS_URL"),
},
DB: DBConfig{
Hosts: dbHosts,
Expand Down
136 changes: 136 additions & 0 deletions api/internal/transport/api/announcements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package api

import (
"fmt"
"io"
"net/http"
"regexp"
"strings"

"github.com/gofiber/fiber/v2"
)

type Announcement struct {
Title string `json:"title"`
Body string `json:"body"`
Link string `json:"link"`
}

// @Summary Get announcements
// @Description Get list of announcements from the public announcements file
// @Produce json
// @Success 200 {array} Announcement
// @Failure 500 {object} ErrorRes
// @Router /announcements [get]
func (h *Handler) GetAnnouncements(c *fiber.Ctx) error {
resp, err := http.Get(h.Cfg.AnnouncementsURL) //nolint:noctx
if err != nil {
return c.Status(500).JSON(ErrorRes{Error: "failed to fetch announcements"})
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return c.Status(500).JSON(ErrorRes{Error: fmt.Sprintf("unexpected status fetching announcements: %d", resp.StatusCode)})
}

data, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(500).JSON(ErrorRes{Error: "failed to read announcements"})
}

announcements := parseAnnouncements(string(data))
return c.JSON(announcements)
}

var linkPattern = regexp.MustCompile(`^\[.*?\]\((https?://[^)]+)\)$`)

func parseAnnouncements(content string) []Announcement {
lines := strings.Split(content, "\n")
var announcements []Announcement
var current *Announcement
var bodyLines []string

flush := func() {
if current == nil {
return
}
current.Body = bodyLinesToHTML(bodyLines)
announcements = append(announcements, *current)
current = nil
bodyLines = nil
}

for _, line := range lines {
line = strings.TrimRight(line, "\r")

if strings.HasPrefix(line, "## ") {
flush()
current = &Announcement{Title: strings.TrimPrefix(line, "## ")}
continue
}

if current == nil {
continue
}

if m := linkPattern.FindStringSubmatch(strings.TrimSpace(line)); m != nil {
current.Link = m[1]
continue
}

bodyLines = append(bodyLines, line)
}

flush()
return announcements
}

func bodyLinesToHTML(lines []string) string {
var sb strings.Builder
var listItems []string
var paraLines []string

flushList := func() {
if len(listItems) == 0 {
return
}
sb.WriteString("<ul>")
for _, item := range listItems {
sb.WriteString("<li>")
sb.WriteString(item)
sb.WriteString("</li>")
}
sb.WriteString("</ul>")
listItems = nil
}

flushPara := func() {
text := strings.TrimSpace(strings.Join(paraLines, " "))
if text != "" {
sb.WriteString("<p>")
sb.WriteString(text)
sb.WriteString("</p>")
}
paraLines = nil
}

for _, line := range lines {
if strings.TrimSpace(line) == "" {
flushPara()
flushList()
continue
}
if strings.HasPrefix(line, "- ") {
flushPara()
listItems = append(listItems, strings.TrimPrefix(line, "- "))
continue
}
flushList()
paraLines = append(paraLines, line)
}

flushPara()
flushList()

return sb.String()
}
1 change: 1 addition & 0 deletions api/internal/transport/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func (h *Handler) SetupRoutes(cfg config.APIConfig) {
h.Server.Use(helmet.New())
h.Server.Use(healthcheck.New())

h.Server.Get("/v1/announcements", limiter.New(), h.GetAnnouncements)
h.Server.Post("/v1/register", limiter.New(), h.Register)
h.Server.Post("/v1/login", limit.New(5, 10*time.Minute), h.Login)
h.Server.Post("/v1/initiatepasswordreset", limiter.New(), h.InitiatePasswordReset)
Expand Down
5 changes: 5 additions & 0 deletions app/src/api/announcements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { api } from './api.ts'

export const announcementsApi = {
getList: () => api.get('/announcements'),
}
76 changes: 76 additions & 0 deletions app/src/components/Announcements.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<div class="container mx-auto max-w-screen-lg sm:p-10 p-5">
<p class="py-5">
<button @click="goBack" class="flex items-center gap-2">
<i class="icon arrow-left-line icon-accent"></i>
Back
</button>
</p>
<h1>News</h1>
<hr>

<div v-if="!loaded && !error">
<p>Loading...</p>
</div>

<div v-if="error">
<p class="text-red-500">{{ error }}</p>
</div>

<div v-if="loaded && list.length === 0">
<p>No news.</p>
</div>

<template v-if="loaded">
<div v-for="(item, index) in list" :key="index">
<h2>{{ item.title }}</h2>
<div v-html="item.body"></div>
<p v-if="item.link">
<a :href="item.link" target="_blank" rel="noopener noreferrer">{{ item.link }}</a>
</p>
<hr v-if="index < list.length - 1">
</div>
</template>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
import { announcementsApi } from '../api/announcements.ts'

interface Announcement {
title: string
body: string
link: string
}

const router = useRouter()
const goBack = () => router.back()

const list = ref<Announcement[]>([])
const error = ref('')
const loaded = ref(false)

onMounted(() => {
getList()
})

const getList = async () => {
try {
const response = await announcementsApi.getList()
list.value = response.data
loaded.value = true
error.value = ''
} catch (err) {
if (axios.isAxiosError(err)) {
error.value = err.response?.data.error || err.message

if (err.response?.status === 429) {
error.value = 'Too many requests, please try again later.'
}
}
}
}
</script>
6 changes: 3 additions & 3 deletions app/src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
</div>
</nav>
<p class="px-5 mt-0 pl-6 text-sm">
Support:
<a href="mailto:mailx@ivpn.net">Email</a> /
<a href="/faq">FAQ</a>
<a href="mailto:mailx@ivpn.net">Support</a> /
<a href="/faq">FAQ</a> /
<a href="/news">News</a>
</p>
</div>
</header>
Expand Down
6 changes: 6 additions & 0 deletions app/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ResetPassword from './components/ResetPassword.vue'
import Terms from './components/Terms.vue'
import Privacy from './components/Privacy.vue'
import Faq from './components/Faq.vue'
import Announcements from './components/Announcements.vue'
import NotFound from './components/NotFound.vue'
import Landing from './components/Landing.vue'
import { type IStaticMethods } from 'preline/preline'
Expand Down Expand Up @@ -129,6 +130,11 @@ const routes: RouteRecordRaw[] = [
name: `${AppName}: FAQ`,
component: Faq
},
{
path: '/news',
name: `${AppName}: News`,
component: Announcements
},
{
path: '/:pathMatch(.*)*',
name: '404: Not Found',
Expand Down
Loading