1
0
Fork 0
mirror of https://github.com/YunoHost/apps.git synced 2024-09-03 20:06:07 +02:00

Initial commit for new app store

This commit is contained in:
Alexandre Aubin 2023-08-15 18:49:45 +02:00
parent 53027ff76a
commit a057aab198
8 changed files with 511 additions and 0 deletions

1
.gitignore vendored
View file

@ -15,3 +15,4 @@ tools/autopatches/token
__pycache__
app_list_auto_update.log
venv

121
store/app.py Normal file
View file

@ -0,0 +1,121 @@
from flask import Flask, send_from_directory, render_template, session, redirect, request
import base64
import hashlib
import hmac
import os
import random
import urllib
import json
from settings import DISCOURSE_SSO_SECRET, DISCOURSE_SSO_ENDPOINT, CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE
app = Flask(__name__)
app.debug = True
app.config["DEBUG"] = True
app.config['TEMPLATES_AUTO_RELOAD'] = True
catalog = json.load(open("apps.json"))
catalog['categories'] = {c['id']:c for c in catalog['categories']}
category_color = {
"synchronization": "sky",
"publishing": "yellow",
"communication": "amber",
"office": "lime",
"productivity_and_management": "purple",
"small_utilities": "",
"reading": "emerald",
"multimedia": "fuchsia",
"social_media": "rose",
"games": "violet",
"dev": "stone",
"system_tools": "white",
"iot": "orange",
"wat": "teal",
}
for id_, category in catalog['categories'].items():
category["color"] = category_color[id_]
wishlist = json.load(open("wishlist.json"))
# This is the secret key used for session signing
app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)])
@app.route('/login_using_discourse')
def login_using_discourse():
"""
Send auth request to Discourse:
"""
nonce, url = create_nonce_and_build_url_to_login_on_discourse_sso()
session.clear()
session["nonce"] = nonce
return redirect(url)
@app.route('/sso_login_callback')
def sso_login_callback():
response = base64.b64decode(request.args['sso'].encode()).decode()
user_data = urllib.parse.parse_qs(response)
if user_data['nonce'][0] != session.get("nonce"):
return "Invalid nonce", 401
else:
session.clear()
session['user'] = {
"id": user_data["external_id"][0],
"username": user_data["username"][0],
"avatar_url": user_data["avatar_url"][0],
}
return redirect("/")
@app.route('/logout')
def logout():
session.clear()
return redirect("/")
@app.route('/')
def index():
return render_template("index.html", user=session.get('user', {}), catalog=catalog)
@app.route('/catalog')
def browse_catalog(category_filter=None):
return render_template("catalog.html", user=session.get('user', {}), catalog=catalog)
@app.route('/app/<app_id>')
def app_info(app_id):
infos = catalog["apps"].get(app_id)
if not infos:
return f"App {app_id} not found", 404
return render_template("app.html", user=session.get('user', {}), app_id=app_id, infos=infos)
@app.route('/wishlist')
def browse_wishlist():
return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist)
################################################
def create_nonce_and_build_url_to_login_on_discourse_sso():
"""
Redirect the user to DISCOURSE_ROOT_URL/session/sso_provider?sso=URL_ENCODED_PAYLOAD&sig=HEX_SIGNATURE
"""
nonce = ''.join([str(random.randint(0, 9)) for i in range(99)])
url_data = {"nonce": nonce, "return_sso_url": CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE}
url_encoded = urllib.parse.urlencode(url_data)
payload = base64.b64encode(url_encoded.encode()).decode()
sig = hmac.new(DISCOURSE_SSO_SECRET.encode(), msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
data = {"sig": sig, "sso": payload}
url = f"{DISCOURSE_SSO_ENDPOINT}?{urllib.parse.urlencode(data)}"
return nonce, url

1
store/requirements.txt Normal file
View file

@ -0,0 +1 @@
Flask==2.3.2

9
store/templates/app.html Normal file
View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block main %}
<div class="max-w-screen-lg mx-auto pt-5">
<h2>{{ app_id }}</h2>
<p>{{ infos }}</p>
</div>
{% endblock %}

123
store/templates/base.html Normal file
View file

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>YunoHost app store</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/css/fork-awesome.min.css" integrity="sha256-XoaMnoYC5TH6/+ihMEnospgm0J1PM/nioxbOUdnM8HY=" crossorigin="anonymous">
</head>
<body>
<header class="pb-2">
<div
class="flex h-16 items-center gap-8 px-4 sm:px-6 lg:px-8"
>
<a class="block text-teal-600" href="/">
<span class="sr-only">Home</span>
<img src="https://raw.githubusercontent.com/YunoHost/doc/master/images/logo_roundcorner.png" style="height: 3em;" />
</a>
<div class="flex flex-1 items-center justify-end md:justify-between">
<nav aria-label="Global" class="hidden md:block">
<ul class="flex items-center gap-6 text-sm">
<li>
<a class="text-gray-800 font-bold transition hover:text-gray-500/75" href="{{ url_for('browse_catalog') }}">
Catalog
</a>
</li>
<li>
<a class="text-gray-800 font-bold transition hover:text-gray-500/75" href="{{ url_for('browse_wishlist') }}">
Wishlist
</a>
</li>
</ul>
</nav>
<div class="flex items-center gap-4">
<div class="sm:flex sm:gap-4">
<a
class="hidden rounded-md bg-gray-100 px-5 py-2.5 text-sm font-medium text-teal-600 transition hover:text-teal-600/75 sm:block"
href="https://yunohost.org/docs/"
>
<i class="fa fa-external-link fa-fw" aria-hidden="true"></i>
YunoHost documentation
</a>
{% if not user %}
<a
class="block rounded-md bg-teal-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-teal-700"
href="{{ url_for('login_using_discourse') }}"
>
Login using YunoHost's forum
</a>
{% else %}
<button
type="button"
class="group flex shrink-0 items-center rounded-lg transition"
>
<span class="sr-only">Menu</span>
<img
alt="Man"
src="{{ user['avatar_url'] }}"
class="h-10 w-10 rounded-full object-cover"
/>
<p class="ms-2 hidden text-left text-xs sm:block">
<strong class="block font-medium">{{ user['username'] }}</strong>
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
class="ms-4 hidden h-5 w-5 text-gray-500 transition group-hover:text-gray-700 sm:block"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
<!--
<a
class="block rounded-md bg-teal-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-teal-700"
href="{{ url_for('logout') }}"
>
Logout
</a>
-->
{% endif %}
</div>
<button
class="block rounded bg-gray-100 p-2.5 text-gray-600 transition hover:text-gray-600/75 md:hidden"
>
<span class="sr-only">Toggle menu</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div>
</div>
</header>
{% block main %}
{% endblock %}
<footer class="text-center"><hr/>TODO : add a proper footer</footer>
</body>
</html>

View file

@ -0,0 +1,113 @@
{% extends "base.html" %}
{% block main %}
{% set locale = 'en' %}
<div class="mt-5 text-center">
<h2 class="text-2xl font-bold text-gray-900">
Application Catalog
</h2>
</div>
<div class="max-w-screen-md mx-auto mt-3 mb-3">
<div class="flex flex-row">
<div class="px-2 inline-block relative basis-2/3 text-gray-700">
<label for="search" class="sr-only"> Search </label>
<input
type="text"
id="search"
placeholder="Search for..."
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2.5 pe-10"
/>
<span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4">
<i class="fa fa-search" aria-hidden="true"></i>
</span>
</div>
<select
name="selectcategory"
id="selectcategory"
class="w-full rounded-md border-gray-200 shadow-sm sm:test-sm px-2 basis-1/3 "
>
<option value="">All apps</option>
{% for id, category in catalog['categories'].items() %}
{{ category['title'][locale] }}
<option value="{{ id }}">{{ category['title'][locale] }}</option>
{% endfor %}
</select>
</div>
<div class=" mx-auto pt-2 text-center">
<div class="inline-block px-2">
<label for="sortbynew" class="inline-block relative h-4 w-7 cursor-pointer">
<input type="checkbox" id="sortbynew" class="peer sr-only" />
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500">
</span>
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
</span>
</label>
Sort by newest
</div>
<div class="inline-block px-2">
<label for="onlyfav" class="inline-block relative h-4 w-7 cursor-pointer">
<input type="checkbox" id="onlyfav" class="peer sr-only" />
<span class="absolute inset-0 rounded-full bg-gray-300 transition peer-checked:bg-green-500">
</span>
<span class="absolute inset-y-0 start-0 m-1 h-2 w-2 rounded-full bg-white transition-all peer-checked:start-3">
</span>
</label>
Show only your bookmarks
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 max-w-screen-lg mx-auto pt-10">
{% for app, infos in catalog['apps'].items() %}
<div>
<a
href="{{ url_for('app_info', app_id=app) }}"
class="relative block overflow-hidden rounded-lg p-2 hover:bg-gray-200 "
>
<div class="sm:flex sm:justify-between sm:gap-4">
<div class="sm:shrink-0">
<img
src="https://app.yunohost.org/default/v3/logos/{{ infos['logo_hash'] }}.png"
class="h-16 w-16 rounded-lg object-cover shadow-sm mt-1"
/>
</div>
<div class="w-full">
<span class="flex">
<h3 class="grow text-md font-bold text-gray-900">
{{ infos['manifest']['name'] }}
</h3>
<span class="pt-1 pr-2 text-xs">
{% if infos['level'] == 0 %}
<i class="fa fa-exclamation-circle text-red-500 opacity-50" aria-hidden="true"></i>
{% elif infos['level']|int <= 4 %}
<i class="fa fa-exclamation-triangle text-orange-500 opacity-50" aria-hidden="true"></i>
{% elif infos['level'] == 8 %}
<i class="fa fa-star text-yellow-500 opacity-50" aria-hidden="true"></i>
{% endif %}
<span class="text-purple-500">
123 <i class="fa fa-bookmark opacity-50" aria-hidden="true"></i>
</span>
</span>
</span>
<p class="max-w-[40ch] text-xs text-gray-500">
{{ infos['manifest']['description']['en'] }}
</p>
{% if infos['category'] %}
<span class="rounded-full bg-{{ catalog['categories'][infos['category']]['color'] }}-500 px-2.5 py-0.5 text-[10px] {% if catalog['categories'][infos['category']]['color'] != 'white' %} text-white {% else %} border border-gray-400 {% endif %} ">
{{ catalog['categories'][infos['category']]['title'][locale].lower() }}
</span>
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block main %}
{% set locale = 'en' %}
<div class="mx-auto w-full text-center p-8">
<img src="https://raw.githubusercontent.com/YunoHost/doc/master/images/ynh_logo_black.svg" class="w-32 mx-auto" />
<h2 class="text-2xl font-bold text-gray-900">
Application Store
</h2>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 max-w-screen-lg mx-auto pt-5">
<div class="text-center border rounded-lg h-32">
<a
href="{{ url_for('browse_catalog') }}"
class="h-full relative block overflow-hidden hover:bg-gray-200 pt-12"
>
<h3 class="text-md font-bold text-gray-900">
Browse all applications
</h3>
</a>
</div>
{% for id, category in catalog['categories'].items() %}
<div class="text-center border rounded-lg h-32">
<a
href="{{ url_for('browse_catalog', category_filter=id) }}"
class="h-full relative block overflow-hidden hover:bg-gray-200 pt-10"
>
<h3 class="text-md font-bold text-gray-900">
<i class="fa fa-{{ category['icon'] }}" aria-hidden="true"></i>
{{ category['title'][locale] }}
</h3>
<p class="mx-auto max-w-[40ch] text-xs text-gray-500">
{{ category['description'][locale] }}
</p>
</a>
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,103 @@
{% extends "base.html" %}
{% block main %}
<div class="mt-5 text-center">
<h2 class="text-2xl font-bold text-gray-900">
Application Wishlist
</h2>
</div>
<div class="max-w-screen-md mx-auto mt-3 mb-3">
<div class="flex flex-row">
<div class="px-2 inline-block relative basis-2/3 text-gray-700">
<label for="search" class="sr-only"> Search </label>
<input
type="text"
id="search"
placeholder="Search for..."
class="w-full rounded-md border-gray-200 shadow-sm sm:text-sm py-2.5 pe-10"
/>
<span class="absolute inset-y-0 end-0 grid w-10 place-content-center pr-4">
<i class="fa fa-search" aria-hidden="true"></i>
</span>
</div>
<a
class="inline-block rounded border text-blue-600 border-blue-500 px-4 pt-3 text-sm font-medium hover:bg-blue-500 hover:text-white"
href="#"
>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
Add an app to the wishlist
</a>
</div>
</div>
</div>
<div class="overflow-x-auto max-w-screen-lg mx-auto pt-5">
<table class="min-w-full divide-y-2 divide-gray-200 bg-white text-sm">
<thead>
<tr>
<th class="whitespace-nowrap px-4 py-2 font-medium text-gray-900">
Name
</th>
<th class="whitespace-nowrap px-4 py-2 font-medium text-gray-900">
Description
</th>
<th class="px-1 py-2"></th>
<th class="px-1 py-2"></th>
<th class="px-1 py-2"></th>
<th class="px-1 py-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for infos in wishlist %}
<tr>
<td class="px-4 py-2 font-bold text-gray-900 max-w-[10em]">
{{ infos['name'] }}
</td>
<td class="px-4 py-2 text-gray-700 max-w-md">{{ infos['description'] }}</td>
<td class="px-1 py-2">
{% if infos['website'] %}
<a
href="{{ infos['website'] }}"
class="inline-block"
>
<i class="fa fa-globe rounded border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
</a>
{% endif %}
</td>
<td class="px-1 py-2">
{% if infos['upstream'] %}
<a
href="{{ infos['upstream'] }}"
class="inline-block"
>
<i class="fa fa-code rounded border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
</a>
{% endif %}
</td>
<td class="px-1 py-2">
<a
href="#"
class="inline-block rounded border text-blue-600 border-blue-500 px-4 py-2 text-xs font-medium hover:bg-blue-500 hover:text-white"
>
<i class="fa fa-bookmark fa-fw" aria-hidden="true"></i>
Vote
</a>
</td>
<td class="px-1 py-2">
<span class="text-blue-500">
123 <i class="fa fa-bookmark" aria-hidden="true"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}