UX: allow to share login tokens via QR-code for educational context

This commit is contained in:
Pieter Vander Vennet 2025-05-29 23:26:59 +02:00
parent 93c613aa89
commit a90387c4f3
6 changed files with 123 additions and 48 deletions

View file

@ -1531,6 +1531,48 @@
}
}
},
{
"id": "share-login-title",
"render": {
"en": "<h3>Login via QR code</h3>"
}
},
{
"id": "share-login-explanation",
"render": {
"en": "With the below QR-code, you can login on another device without having to share your password"
}
},
{
"id": "share-login-group",
"render": {
"special": {
"type": "group",
"header": "share-login-group-title",
"labels": "share-login-qr"
}
}
},
{
"id": "share-login-group-title",
"labels": [
"hidden"
],
"render": {
"en": "Allow to log in and act as <b>{_name}</b>"
}
},
{
"id": "share-login-qr",
"labels": [
"hidden"
],
"render": {
"special": {
"type": "qr_login"
}
}
},
{
"id": "debug-title",
"render": {

View file

@ -154,7 +154,8 @@ export class OsmConnection {
constructor(options?: {
dryRun?: Store<boolean>
fakeUser?: false | boolean
oauth_token?: UIEventSource<string>
oauth_token?: UIEventSource<string>,
shared_cookie?: string,
// Used to keep multiple changesets open and to write to the correct changeset
singlePage?: boolean
attemptLogin?: boolean
@ -205,6 +206,10 @@ export class OsmConnection {
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
if (options?.shared_cookie) {
this.setToken(options?.shared_cookie)
}
this.updateAuthObject(false)
AndroidPolyfill.inAndroid.addCallback(() => {
this.updateAuthObject(false)
@ -600,6 +605,9 @@ export class OsmConnection {
})
}
/**
* Gets the login token. Sharing this will allow to mimic the user session on another device
*/
public getToken(): string {
// https://www.openstreetmap.orgoauth2_access_token
let prefix = this.Backend()
@ -608,12 +616,20 @@ export class OsmConnection {
}
return (
QueryParameters.GetQueryParameter(prefix + "oauth_token", undefined).data ??
window.localStorage.getItem(this._oauth_config.url + "oauth2_access_token")
window.localStorage.getItem(this.getLoginCookieName())
)
}
public setToken(token: string) {
window.localStorage.setItem(this.getLoginCookieName(), token)
}
private getLoginCookieName() {
return this._oauth_config.url + "oauth2_access_token"
}
private async loginAndroidPolyfill() {
const key = "https://www.openstreetmap.orgoauth2_access_token"
const key = this.getLoginCookieName()
if (localStorage.getItem(key)) {
// We are probably already logged in
return
@ -629,6 +645,7 @@ export class OsmConnection {
}
await this.loadUserInfo()
}
private updateAuthObject(autoLogin: boolean) {
let redirect_uri = Utils.runningFromConsole
? "https://mapcomplete.org/land.html"

View file

@ -1,42 +1,14 @@
import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
feat.properties['__current_backgroun'] = 'initial_value'
}
}

View file

@ -37,9 +37,11 @@ export class WithUserRelatedState {
}
this.theme = theme
this.featureSwitches = new FeatureSwitchState(theme)
this.osmConnection = new OsmConnection({
dryRun: this.featureSwitches.featureSwitchIsTesting,
fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
shared_cookie: QueryParameters.GetQueryParameter("shared_oauth_cookie", undefined, "Used to share a session with another device - this saves logging in at another device").data,
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,

View file

@ -13,16 +13,38 @@
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
export let extraUrlParams: Record<string, string> = {}
const includeLayout = window.location.pathname.split("/").at(-1).startsWith("theme")
const layout = includeLayout ? "layout=" + state.theme.id + "&" : ""
let id: Store<string> = tags.mapD((tags) => tags.id)
let url = id.mapD(
extraUrlParams["z"] ??= 15
if (includeLayout) {
extraUrlParams["layout"] ??= state.theme.id
}
if (feature) {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
extraUrlParams["lon"] ??= "" + lon
extraUrlParams["lat"] ??= "" + lat
} else if (state?.mapProperties?.location?.data) {
const l = state?.mapProperties?.location?.data
extraUrlParams["lon"] ??= "" + l.lon
extraUrlParams["lat"] ??= "" + l.lat
}
const params = []
for (const key in extraUrlParams) {
console.log(key, "-->", extraUrlParams[key])
params.push(key + "=" + encodeURIComponent(extraUrlParams[key]))
}
let url = id.map((id) => {
if (id) {
return "#" + id
} else {
return ""
}
}).map(
(id) =>
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
`#${id}`
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${params.join("&")}${id}`
)
function toggleSize() {
@ -32,9 +54,11 @@
size.setData(smallSize)
}
}
url.addCallbackAndRunD(url => console.log("URL IS", url))
</script>
{#if $id.startsWith("node/-")}
{#if $id?.startsWith("node/-")}
<!-- Not yet uploaded, doesn't have a fixed ID -->
<Loading />
{:else}

View file

@ -14,6 +14,9 @@ import LanguageUtils from "../../Utils/LanguageUtils"
import LanguagePicker from "../InputElement/LanguagePicker.svelte"
import PendingChangesIndicator from "../BigComponents/PendingChangesIndicator.svelte"
import { Utils } from "../../Utils"
import { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import QrCode from "../Popup/QrCode.svelte"
export class SettingsVisualisations {
public static initList(): SpecialVisualizationSvelte[] {
@ -146,6 +149,21 @@ export class SettingsVisualisations {
})
},
},
{
funcName: "qr_login",
args: [],
docs: "A QR-code which shares the current URL and adds the login token. Anyone with this login token will have the same permissions as you currently have. Logging out from this session will also log them out",
group: "settings",
constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): SvelteUIElement {
const shared_oauth_cookie = state.osmConnection.getToken()
return new SvelteUIElement(QrCode, {
state,
tags,
feature,
extraUrlParams: { shared_oauth_cookie }
})
}
},
{
funcName: "logout",